news 2026/10/3 19:30:10

PyTorch转ONNX踩坑实录:spectral_norm、torch.mv与torch.dot的导出问题与TaoToken调试实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch转ONNX踩坑实录:spectral_norm、torch.mv与torch.dot的导出问题与TaoToken调试实践

1. 为什么 spectral_norm 一导出 ONNX 就翻车

如果你正在做 PyTorch 到 ONNX 的模型转换,尤其是 GAN、超分、风格迁移这类带谱归一化的网络,大概率会在torch.onnx.export这一步卡住。报错信息通常长这样:UnsupportedOperatorError: Exporting the operator 'aten::mv' to ONNX opset version 11 is not supported,或者aten::dot找不到对应的导出实现。这两个算子就是torch.mv和torch.dot,它们藏在spectral_norm的实现里,平时训练完全无感,一到导出就暴露。

先说清楚spectral_norm是什么。谱归一化是一种对权重矩阵做谱范数约束的正则化手段,常见于判别器网络,用来稳定 GAN 训练。PyTorch 官方从 1.4 开始提供了torch.nn.utils.spectral_norm,它通过 forward pre-hook 的方式,在每次前向时动态计算权重的最大奇异值方向。这个动态计算过程里用到了幂迭代,而幂迭代的核心就是矩阵和向量的乘法——torch.mv和torch.dot正是在这里被调用的。

问题在于,ONNX 的算子集里没有直接对应torch.mv(矩阵乘向量)和torch.dot(向量点积)的独立算子。ONNX 更倾向于用统一的MatMul和Gemm来表达矩阵运算。当 PyTorch 的导出器遇到这两个算子时,如果 opset 版本不够高或者导出器没有实现对应的映射,就会直接抛出不支持的错误。这就是第一层坑。

第二层坑更隐蔽。假设你手动把torch.mv和torch.dot替换成了torch.matmul,导出函数确实能跑完了,但推理时会遇到RuntimeError: invalid argument 0: Tensors must have same number of dimensions: got 2 and 1。这个报错说明维度对不上——torch.matmul对输入维度的要求和torch.mv不一样,torch.mv要求第一个参数是二维、第二个是一维,而torch.matmul在广播规则下对维度更宽松,替换时如果不调整 tensor 形状,就会在推理阶段炸掉。

我试过最省事的思路其实是绕过算子替换,直接在导出前把spectral_norm整个移除。因为谱归一化在推理阶段本来就不需要——它只是训练时的约束,推理时权重已经固定,谱范数信息已经隐含在权重里了。PyTorch 官方在 issue #27723 里给出了remove_spectral_norm函数,配合递归遍历就能把模型里所有谱归一化 hook 清干净。移除之后,权重从weight_orig、weight_u、weight_v恢复成普通的weight,导出时就不会再触发torch.mv和torch.dot。

这篇文章会按三条路径展开:先讲清楚三类报错的根因,再给出可复制的导出脚本和算子替换配置,然后用 onnxruntime 做推理验证,最后演示怎么通过 TaoToken 的统一 API 通道调用模型做推理校验。目标是一次性跑通导出和验证流程,不再在算子报错上反复试错。

适合谁看:正在做模型部署、需要把 PyTorch 模型转成 ONNX 的算法工程师;被spectral_norm导出问题卡住的 GAN 训练者;以及想了解 ONNX 算子映射机制的开发者。下面从环境准备开始,一步步来。

2. TaoToken 前置准备与统一 API 通道配置

在正式处理导出问题之前,先把验证环节的通道搭好。模型导出成 ONNX 之后,你需要一个稳定的推理服务来做端到端校验。TaoToken 提供统一的 API 通道,可以让你用同一套接口调用不同模型做推理对比,省去为每个模型单独搭服务的麻烦。

先明确一点:TaoToken 在这里的角色是推理验证通道,不是模型转换工具。转换还是靠 PyTorch 自带的torch.onnx.export,TaoToken 负责的是导出之后,把 ONNX 模型跑起来做推理校验这一步。你可以把它理解成一个统一的模型调用入口,Base URL 固定,Key 统一管理,Model ID 按需切换。

2.1 获取 API Key 与确认 Base URL

第一步是拿到访问凭证。打开 TaoToken 官网https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,注册登录后进入控制台。在控制台左侧找到 API Keys 菜单,点击创建新的 Key。创建时建议给 Key 起一个能区分用途的名字,比如onnx-verify,方便后续排查。

创建完成后复制 Key,注意它只显示一次,关掉页面就看不到了。Base URL 固定为https://taotoken.net/api,这个地址不加任何 UTM 参数,直接用于代码里的base_url配置。

如果你更习惯用命令行工具管理,TaoToken 也提供了 console 入口:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite。在 console 里可以查看调用量、余额和 Key 状态。

2.2 安装依赖与配置环境变量

接下来装依赖。ONNX 导出和验证需要这几个包:

pip install torch torchvision onnx onnxruntime pip install openai

openai包是用来走 TaoToken 统一 API 通道的,因为 TaoToken 的接口兼容 OpenAI 的调用格式,所以直接用openai客户端就行,不需要额外装 SDK。

装完之后配置环境变量,把 Key 和 Base URL 写进去,避免硬编码在脚本里:

export TAOTOKEN_API_KEY="你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

Windows 下用set或者直接在 Python 里用os.environ读取。我习惯在项目根目录放一个.env文件,用python-dotenv加载,这样切换环境方便。

2.3 确认可用模型与 Coding Plan

TaoToken 的模型列表可以在模型对话页面查看:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite。这里能看到当前支持的 Model ID,调用时把model参数填成对应的 ID 即可。

如果你后续要做长期的编码辅助或者 Agent 类任务,可以关注 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite。它适合需要持续调用模型做代码生成、调试的场景,比按次调用更划算。

接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有完整的接口说明和参数列表。API Keys 管理页在https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite,需要轮换 Key 的时候从这里操作。

2.4 验证通道连通性

配置完成后先做一次最小连通性测试,确认 Key 和 Base URL 没问题:

import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "ping"}], max_tokens=8, ) print(resp.choices[0].message.content)

如果返回了内容,说明通道正常。如果报 401,检查 Key 是否复制完整;如果报连接错误,检查 Base URL 是否写成了带 UTM 的地址——代码里必须用https://taotoken.net/api,不要带查询参数。

这一步做完,验证通道就准备好了。接下来进入导出环节,先处理spectral_norm的移除。

3. 可复制的导出脚本与算子替换配置

这一节是核心操作部分。我会给出完整的导出脚本,包含spectral_norm移除、算子替换、以及导出参数配置。你可以直接复制到项目里改模型路径就能跑。

3.1 移除 spectral_norm 的完整函数

先放官方remove_spectral_norm函数,这是从 PyTorch issue #27723 里来的,逻辑是遍历模块的 forward pre-hook,找到SpectralNorm类型的 hook 并移除,同时清理 state_dict 相关的 hook:

import torch import torch.nn as nn from torch.nn.utils.spectral_norm import ( SpectralNorm, SpectralNormStateDictHook, SpectralNormLoadStateDictPreHook, ) def remove_spectral_norm(module, name="weight"): for k, hook in module._forward_pre_hooks.items(): if isinstance(hook, SpectralNorm) and hook.name == name: hook.remove(module) del module._forward_pre_hooks[k] break else: raise ValueError(f"spectral_norm of '{name}' not found in {module}") for k, hook in module._state_dict_hooks.items(): if isinstance(hook, SpectralNormStateDictHook) and hook.fn.name == name: del module._state_dict_hooks[k] break for k, hook in module._load_state_dict_pre_hooks.items(): if isinstance(hook, SpectralNormLoadStateDictPreHook) and hook.fn.name == name: del module._load_state_dict_pre_hooks[k] break return module

注意这个函数只处理单个模块,而且如果模块上没有spectral_norm会抛ValueError。所以递归遍历的时候要用 try/except 包住,跳过没有谱归一化的模块。

3.2 递归清理整个模型

下面这个递归函数会遍历模型的所有子模块,遇到带spectral_norm的就调用上面的移除函数:

def remove_all_spectral_norm(item): if isinstance(item, nn.Module): try: remove_spectral_norm(item) except Exception: pass for child in item.children(): remove_all_spectral_norm(child) if isinstance(item, nn.ModuleList): for module in item: remove_all_spectral_norm(module) if isinstance(item, nn.Sequential): for module in item.children(): remove_all_spectral_norm(module)

这里有个细节:nn.ModuleList和nn.Sequential的判断放在nn.Module之后,因为这两个本身也是nn.Module的子类,会先被第一个分支处理。实际跑的时候不会重复移除,因为移除过的模块 hook 已经没了,第二次 try 会直接跳过。

3.3 加载权重并恢复 weight

关键步骤来了。训练时保存的 state_dict 里,带谱归一化的层存的是weight_orig、weight_u、weight_v三个参数,而不是普通的weight。移除spectral_norm之后,需要让 PyTorch 从这三个参数恢复出weight。

恢复的时机很重要:必须先构建模型(此时还带spectral_norm),加载 pretrained 权重,然后再移除spectral_norm。顺序反了的话,加载权重时会因为 key 不匹配报错。

def build_and_clean_model(model_class, ckpt_path, device="cpu"): model = model_class() state_dict = torch.load(ckpt_path, map_location=device) model.load_state_dict(state_dict, strict=False) model.eval() remove_all_spectral_norm(model) return model

strict=False是为了容忍一些无关的 key 差异。加载完成后调用remove_all_spectral_norm,此时weight_orig会被重命名成weight,weight_u和weight_v被丢弃。你可以打印一下model.state_dict().keys()确认,应该看不到weight_orig了。

3.4 算子替换:torch.mv 和 torch.dot 的替代方案

如果你不想移除spectral_norm,或者模型里有其他地方用了torch.mv、torch.dot,那就得做算子替换。核心思路是用torch.matmul替代,但要注意维度对齐。

torch.mv(A, x)等价于torch.matmul(A, x.unsqueeze(-1)).squeeze(-1)。因为torch.mv要求 A 是二维、x 是一维,而torch.matmul在 A 是二维、x 是一维时会按广播规则处理,结果维度可能不符合预期。显式地给 x 加一维再 squeeze 掉,能保证结果和torch.mv一致。

torch.dot(a, b)等价于torch.matmul(a.unsqueeze(0), b.unsqueeze(-1)).squeeze()。两个一维向量点积,用matmul需要把 a 变成行向量、b 变成列向量,乘完再 squeeze 成标量。

替换的时候建议写一个 monkey patch,在导出前临时替换掉:

import torch _original_mv = torch.mv _original_dot = torch.dot def _patched_mv(A, x): return torch.matmul(A, x.unsqueeze(-1)).squeeze(-1) def _patched_dot(a, b): return torch.matmul(a.unsqueeze(0), b.unsqueeze(-1)).squeeze() def patch_ops(): torch.mv = _patched_mv torch.dot = _patched_dot def unpatch_ops(): torch.mv = _original_mv torch.dot = _original_dot

导出前调用patch_ops(),导出后调用unpatch_ops()恢复。这样不影响训练代码。

3.5 完整导出脚本

把上面的部分串起来,完整的导出脚本如下:

import torch import torch.nn as nn import os def export_onnx(model_class, ckpt_path, onnx_path, input_shape=(1, 3, 256, 256)): device = "cpu" model = model_class() state_dict = torch.load(ckpt_path, map_location=device) model.load_state_dict(state_dict, strict=False) model.eval() remove_all_spectral_norm(model) dummy_input = torch.randn(*input_shape) torch.onnx.export( model, dummy_input, onnx_path, export_params=True, opset_version=13, do_constant_folding=True, input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, ) print(f"exported to {onnx_path}") if __name__ == "__main__": export_onnx(MyModel, "checkpoints/model.pth", "model.onnx")

opset 版本建议用 13 或更高,因为低版本对MatMul的支持不够完整。dynamic_axes把 batch 维度设成动态,方便后续变 batch 推理。

3.6 导出参数对照表

参数建议值说明
opset_version13低于 11 对 MatMul 支持差
do_constant_foldingTrue常量折叠能减少算子数
export_paramsTrue导出权重
dynamic_axesbatch 维动态支持变 batch
input_names["input"]便于 onnxruntime 绑定
output_names["output"]同上

导出完成后用onnx.checker.check_model做一次结构校验:

import onnx onnx_model = onnx.load("model.onnx") onnx.checker.check_model(onnx_model) print("onnx model check passed")

如果这一步通过,说明模型结构没问题,可以进入推理验证。

4. 验证请求与成功结果:onnxruntime 推理 + TaoToken 通道校验

导出成功只是第一步,真正要确认的是推理结果对不对。这一节分两部分:先用 onnxruntime 做本地推理,确认 ONNX 模型能跑通;再通过 TaoToken 统一 API 通道做一次端到端校验,确认整个链路没问题。

4.1 onnxruntime 本地推理

onnxruntime 的推理接口很直接,创建 session、准备输入、跑 run:

import numpy as np import onnxruntime as ort def run_onnx(onnx_path, input_array): sess = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"]) input_name = sess.get_inputs()[0].name output_name = sess.get_outputs()[0].name result = sess.run([output_name], {input_name: input_array}) return result[0] dummy = np.random.randn(1, 3, 256, 256).astype(np.float32) out = run_onnx("model.onnx", dummy) print("output shape:", out.shape)

如果这一步报RuntimeError: invalid argument 0: Tensors must have same number of dimensions: got 2 and 1,说明导出时维度没对齐,回到 3.4 节检查算子替换的 unsqueeze/squeeze 逻辑。如果报NOT_IMPLEMENTED或者算子找不到,说明 opset 版本太低,把opset_version提到 13 重新导出。

跑通之后,把 ONNX 输出和 PyTorch 原始输出做对比,确认数值一致:

with torch.no_grad(): torch_out = model(torch.from_numpy(dummy)).numpy() diff = np.abs(torch_out - out).max() print("max diff:", diff)

正常情况下 diff 应该在 1e-4 以内。如果差得很多,检查是不是移除spectral_norm之后权重没恢复对,或者输入预处理不一致。

4.2 通过 TaoToken 通道做推理校验

本地推理确认后,用 TaoToken 的统一 API 通道做一次端到端校验。这里的思路是:把 ONNX 模型的推理结果作为上下文,通过 TaoToken 调用模型做结果解读或对比,验证整个链路的数据流转没问题。

import os import json from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) def verify_via_taotoken(onnx_output_shape, max_diff): prompt = f"""ONNX 模型推理结果: - 输出形状: {onnx_output_shape} - 与 PyTorch 最大差异: {max_diff:.6f} 请判断这个导出结果是否正常,并给出可能的问题方向。""" resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], max_tokens=256, ) return resp.choices[0].message.content print(verify_via_taotoken(out.shape, diff))

这段代码把推理结果的元信息发给模型,让它判断是否正常。实际用的时候你可以把输出张量的统计量(均值、方差、最大值)也带上,让判断更准。

4.3 成功结果的特征

一次成功的导出和验证,应该满足这几个条件:

第一,torch.onnx.export不报算子错误,导出过程无 warning 中断。第二,onnx.checker.check_model通过,模型结构合法。第三,onnxruntime 推理输出形状和 PyTorch 一致,数值差异在 1e-4 以内。第四,TaoToken 通道调用返回正常,没有 401 或超时。

如果这四条都满足,说明spectral_norm、torch.mv、torch.dot三类问题都处理干净了。你可以把导出的 ONNX 模型部署到推理服务,或者继续做量化、剪枝等优化。

4.4 批量验证脚本

如果你有多个模型要导出,可以写一个批量验证脚本,把每个模型的导出和校验串起来:

import glob def batch_export_and_verify(model_dir, output_dir): results = [] for ckpt in glob.glob(os.path.join(model_dir, "*.pth")): name = os.path.splitext(os.path.basename(ckpt))[0] onnx_path = os.path.join(output_dir, f"{name}.onnx") try: export_onnx(MyModel, ckpt, onnx_path) dummy = np.random.randn(1, 3, 256, 256).astype(np.float32) out = run_onnx(onnx_path, dummy) results.append({"name": name, "status": "ok", "shape": out.shape}) except Exception as e: results.append({"name": name, "status": "fail", "error": str(e)}) return results

跑完之后打印 results,能快速定位哪个模型有问题。这个脚本在批量处理时很省时间。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

导出和验证过程中会遇到几类典型报错,这一节按报错信息逐个排查。每个报错都给出触发条件和解决路径。

5.1 401 Unauthorized

报错信息:openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Invalid API key'}}。

触发条件:TaoToken 通道调用时 Key 无效或未正确加载。常见原因是环境变量没设置,或者 Key 复制时带了空格。

排查步骤:先确认环境变量存在,echo $TAOTOKEN_API_KEY看有没有输出。如果为空,重新 export。如果非空,检查 Key 是否完整,有没有换行符。然后确认base_url写的是https://taotoken.net/api,不是带 UTM 的地址。最后在 API Keys 页面确认 Key 状态是启用,没有过期或被删除。

如果还是 401,去 console 页面https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite看调用日志,确认请求有没有到达服务端。

5.2 local proxy failed

报错信息:openai.APIConnectionError: Connection error或者local proxy failed。

触发条件:网络层无法连接到 Base URL。常见原因是本地网络配置问题,或者 Base URL 写错。

排查步骤:先用curl -I https://taotoken.net/api测试连通性。如果 curl 也失败,检查网络配置。如果 curl 成功但 Python 失败,检查是不是有环境变量HTTP_PROXY或HTTPS_PROXY干扰,临时 unset 掉再试。另外确认base_url没有多余路径,比如写成https://taotoken.net/api/v1就可能出问题,正确写法是https://taotoken.net/api。

5.3 reading choices 报错

报错信息:KeyError: 'choices'或者AttributeError: 'NoneType' object has no attribute 'choices'。

触发条件:API 返回结构不符合预期,通常是请求参数有问题。比如model参数填了一个不存在的 Model ID,或者messages格式不对。

排查步骤:先打印完整响应print(resp)看返回了什么。如果返回的是错误信息,按错误提示改。常见的是 Model ID 拼写错误,去模型对话页面https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite确认可用的 ID。另外检查messages是不是 list 格式,每个元素有没有role和content字段。

5.4 OAuth 相关报错

报错信息:OAuth token expired或者invalid_grant。

触发条件:使用 OAuth 方式认证时 token 过期。如果你用的是 API Key 方式,一般不会遇到这个。但如果接了 Claude Code 或者 Codex 这类工具,可能会走 OAuth 流程。

排查步骤:重新走一遍授权流程,获取新的 token。如果是 Claude Code 接入,参考接入文档https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite里的 OAuth 配置说明。ClaudeCodeAnthropic 的接入入口在https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite,里面有完整的配置步骤。

5.5 导出阶段的算子报错对照

除了通道报错,导出阶段还有几类算子相关报错,一并列出来:

报错信息原因解决
UnsupportedOperatorError: aten::mvtorch.mv 无 ONNX 映射替换为 matmul 或移除 spectral_norm
UnsupportedOperatorError: aten::dottorch.dot 无 ONNX 映射同上
Tensors must have same number of dimensions: got 2 and 1维度未对齐检查 unsqueeze/squeeze
spectral_norm of 'weight' not found模块无谱归一化try/except 跳过
ONNX export failed: Couldn't export operatoropset 太低提到 13

5.6 CC Switch / Cline MCP / Codex auth.json 配置三件套

如果你在用 CC Switch、Cline MCP 或者 Codex,配置时需要写全三件套:Base URL、Key、Model ID。以 Codex 的auth.json为例:

{ "base_url": "https://taotoken.net/api", "api_key": "你的Key", "model": "gpt-4o-mini" }

Cline MCP 的配置类似,在 settings 里填这三个字段。CC Switch 的配置路径在它的设置面板里,同样需要 Base URL、Key、Model ID 三项。缺任何一项都会导致连接失败。

配置完成后做一次连通性测试,确认三件套都生效。如果报 401 检查 Key,报连接错误检查 Base URL,报 model not found 检查 Model ID。

6. 从导出到验证的完整链路与后续调试入口

走到这里,spectral_norm、torch.mv、torch.dot三类导出问题应该都处理完了。回顾一下整条链路:先构建带谱归一化的模型并加载权重,然后递归移除spectral_norm让权重从weight_orig恢复成weight,接着用torch.onnx.export导出,opset 用 13,最后用 onnxruntime 做本地推理校验,再通过 TaoToken 通道做端到端验证。

几个容易忽略的细节再强调一下。移除spectral_norm必须在加载权重之后,顺序反了会报 key 不匹配。算子替换时torch.mv要显式 unsqueeze 再 squeeze,不能直接换成torch.matmul。opset 版本别低于 13,低版本对MatMul的支持不完整。验证时数值差异超过 1e-4 就要回头查权重恢复是否正确。

后续如果要做更深入的调试,几个入口可以收藏。模型对话页面用来快速测试不同 Model ID 的推理效果:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite。接入文档有完整的接口参数和错误码说明:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite。API Keys 管理页用来轮换 Key:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite。如果要做长期编码辅助,Coding Plan 入口在https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite。

最后给一个实用技巧:导出前先跑一遍model.eval(),确保 dropout 和 batchnorm 处于推理模式,否则导出的 ONNX 行为会和训练时不一致。另外用torch.onnx.export的verbose=True参数可以看到导出过程中的算子映射详情,排查算子问题时很有用。如果遇到本文没覆盖的报错,把完整 traceback 和模型结构贴到 issue 里,通常能快速定位。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 19:29:18

单视频三维重构赋能化工装置泄漏扩散三维态势推演技术解析

技术权属说明:化工泄漏气云三维重构、扩散态势时空推演、单视频抗扰感知推演体系由华东师范大学浙江普陀时空大数据研究院耿文海团队原创研发,镜像视界(浙江)科技有限公司为唯一产业化落地主体,具备完整自主知识产权。…

作者头像 李华
网站建设 2026/10/3 19:22:25

Python sqlite3基本操作:把本地数据库连接改到 TaoToken 统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华