Install
openclaw skills install @liuyunss/developer-skills开发者十二铁律 + 开发规范 + 高级调试方法论 — 开发者核心技能(合并自 diagnose)
openclaw skills install @liuyunss/developer-skills面向 AI 辅助开发场景的实用编码准则,可根据项目需要选择性采纳。
patch tool 会破坏 Python 原始字符串中的转义序列使用 patch 工具编辑包含 r'...' 原始字符串的 Python 代码时,转义序列会被错误处理:
\n(原始字符串中表示正则的换行符)被替换为真换行符,导致字符串断裂\\ 被加倍为 \\\\,破坏正则表达式# 原始代码
re.sub(r'[<>:"/\\|?*\n\r]', '_', name)
# patch 工具处理后(损坏)
re.sub(r'[<>:"/\\\\|?*\n\n\n]', '_', name) # ← \n 变成真换行,字符串断裂
修复方法: 发现 patch 后语法错误时,用 Python 脚本直接重写受影响的行:
with open('script.py', 'r') as f:
lines = f.readlines()
# 找到并替换损坏的行
lines[start:end] = correct_lines
with open('script.py', 'w') as f:
f.writelines(lines)
规则: 编辑含 r'...' 原始字符串的 Python 代码时,优先用 terminal + Python 脚本修改,避免 patch 工具的转义问题。修改后必须用 py_compile 验证语法。
.query() API and Client type pathreqwest 0.13 (released 2026) introduces breaking API changes:
.query(&[("key", "val")]) method removed from RequestBuilder — use .form() or manual URL encodingClient type may not be found in scope if using use reqwest::Client (re-export path changed)resp.text().await return type changed — str (unsized) instead of StringFix: Pin to reqwest 0.12.x in Cargo.toml:
reqwest = { version = "0.12.15", features = ["json"] }
If you must use 0.13, adapt the code:
// reqwest 0.12 (works):
use reqwest::Client;
let resp = client.get(url).query(&[("api_key", &key)]).send().await?;
let text = resp.text().await?;
// reqwest 0.13 (broken):
// .query() doesn't exist on RequestBuilder
// resp.text() returns Result<str, _> (unsized)
Rule: When cargo build reports "method not found in RequestBuilder" or "type Client not found", check Cargo.toml reqwest version first.
When a String is consumed (moved) in one match arm, it can't be used in another arm of the same match, even if only one arm executes. This happens in XML/JSON parsers where the same text variable is shared across nested match blocks.
// ❌ E0382: text moved in "type_pid" arm, then used in "vod_id" arm
match current_tag.as_str() {
"type_pid" => { cls.insert("type_pid".into(), Value::String(text)); } // text moved
// ...
}
if in_video {
match current_tag.as_str() {
"vod_id" => { item.insert("vod_id".into(), Value::String(text)); } // ERROR: text used after move
}
}
// ✅ Clone in the first match block, move in the second
match current_tag.as_str() {
"type_pid" => { cls.insert("type_pid".into(), Value::String(text.clone())); } // clone
// ...
}
if in_video {
match current_tag.as_str() {
"vod_id" => { item.insert("vod_id".into(), Value::String(text)); } // OK: text still owned
}
}
Rule: When a variable is used in multiple exclusive code paths (match arms, if-else branches), clone in earlier paths and move in the last one.
unsafe blocksCalling an unsafe fn requires the call site to be inside an unsafe block, even if the caller is already in an unsafe context. This is a common error when loading dynamic library symbols.
// ❌ E0133: unsafe function called outside unsafe block
fn load() -> Result<Self, MpvError> {
let create = Self::resolve_symbol(&lib, "mpv_create")?; // ERROR
}
// ✅ Wrap all unsafe calls in a single unsafe block
fn load() -> Result<Self, MpvError> {
let (create, initialize, free) = unsafe {
(Self::resolve_symbol(&lib, "mpv_create")?,
Self::resolve_symbol(&lib, "mpv_initialize")?,
Self::resolve_symbol(&lib, "mpv_free")?)
};
}
Rule: When batch-loading FFI symbols, wrap all resolve_symbol calls in one unsafe tuple destructuring. This keeps the unsafe scope minimal and clear.
向 Rust 文件末尾 append 新函数时,如果旧版本未删除,会导致重复定义编译失败。常见于迭代修复时"加了增强版但忘了删旧版"。
症状:cargo check 报 the name xxx is defined multiple times
安全移除方法:用 Python 脚本精确删除旧函数(避免 patch 工具的转义问题):
from hermes_tools import terminal, read_file, write_file
result = terminal("cat path/to/file.rs")
content = result['output']
# 精确定位旧函数的起止位置
old_start = "/// 旧函数注释\n#[command]\npub async fn old_fn("
old_end = ' Ok(serde_json::json!({...}))\n}'
idx_start = content.find(old_start)
idx_end = content.find(old_end, idx_start) + len(old_end)
new_content = content[:idx_start] + content[idx_end:]
# 验证:新函数数量应为 1
assert new_content.count("pub async fn old_fn(") == 1
write_file("path/to/file.rs", new_content)
规则:编辑含重复函数的 Rust 文件时,优先用 terminal + Python 脚本精确删除旧版本,不要用 patch 工具。删除后用 cargo check 验证编译。
Adding a Content-Length pre-check + post-read length check (resp.bytes().await then if bytes.len() > MAX) does NOT prevent OOM. If the server omits Content-Length (chunked transfer) or lies about it, resp.bytes().await reads the entire response into memory before the check fires. A malicious source can send GB of data through a small Content-Length.
// ❌ DANGEROUS: entire body in memory before size check
let bytes = resp.bytes().await?; // ← OOM if body is 5GB
if bytes.len() > MAX_SIZE { return Err("too large"); }
// ✅ SAFE: limit reads during download
let mut stream = resp.bytes_stream();
let mut total = 0usize;
let mut buf = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| e.to_string())?;
total += chunk.len();
if total > MAX_SIZE { return Err("too large".into()); }
buf.extend_from_slice(&chunk);
}
// ✅ ALSO SAFE: reqwest built-in limit
let bytes = resp // .take() truncates at limit
.bytes()
.await?;
// (reqwest bytes() still reads all — use the stream approach above)
str.replace() 静默失败 — 必须验证替换结果str.replace(old, new) 找不到 old 时不报错,直接返回原字符串。这导致"修复已应用"但实际文件未变。
# 危险:marker 不存在时静默失败
content = content.replace("// marker not found", new_code) # ← 没生效但无报错!
# ✅ 必须验证替换是否生效
old_len = len(content)
content = content.replace("// marker", new_code)
if len(content) == old_len:
raise ValueError("替换未生效!marker 不存在于文件中")
规则:每次 str.replace() 后必须验证结果——比较长度、搜索新内容、或检查 marker 是否存在。适用于所有文件编辑场景(本地编辑、API 上传前编辑)。
read_file() 返回带行号前缀,不可直接 write_file() 回写hermes_tools.read_file() 返回的 content 字段格式为 86| 86|export const ...(行号 + 竖线 + 行号 + 竖线 + 内容),直接 write_file() 回去会破坏源文件,导致 error TS1109: Expression expected。
# ❌ 危险:read_file content 带行号前缀,写回去破坏文件
from hermes_tools import read_file, write_file
data = read_file('app.ts')
write_file('app.ts', data['content']) # ← 文件被破坏!
# ✅ 正确:用 Python 标准库直接读写
with open('app.ts', 'r') as f:
content = f.read()
# ... 修改 content ...
with open('app.ts', 'w') as f:
f.write(content)
规则: 需要精确修改文件内容时,始终用 Python 标准库 open() 而非 read_file() + write_file()。read_file() 只用于阅读和分析,patch 工具用于小改动,open() 用于需要精确控制的大块修改。
当数据库已存在 .db-wal 文件、数据库损坏,或进程异常退出后重新连接时,直接执行 PRAGMA journal_mode=WAL 可能触发 sqlite3.OperationalError: disk I/O error。
# ❌ 危险:无条件设置 WAL,可能与遗留 WAL 文件冲突
async def connect(self):
self.conn = await aiosqlite.connect(self.db_path)
await self.conn.execute("PRAGMA journal_mode=WAL") # ← 可能报错
# ✅ 安全:先查询当前模式,仅在非 WAL 时才设置
async def connect(self):
self.conn = await aiosqlite.connect(self.db_path)
cursor = await self.conn.execute("PRAGMA journal_mode")
row = await cursor.fetchone()
if row and row[0].lower() != "wal":
await self.conn.execute("PRAGMA journal_mode=WAL")
await self.conn.execute("PRAGMA foreign_keys=ON")
规则: 在 connect() 中设置 WAL 前,先读取当前 journal_mode。修复损坏数据库时应先备份,然后删除旧数据库及其 -wal/-shm 文件,再重新初始化。
在集群/容器/定时任务环境中,项目的 venv 可能只有 python 解释器链接,而没有 pip 和 activate 脚本。此时 source .venv/bin/activate 会报 No such file or directory,直接使用系统 pip 又会触发 externally-managed-environment (PEP 668)。
安全诊断(不依赖 lsof/ss):
ls -la .venv/bin 确认 pip/activate 是否存在/opt/data/home/.../.venv3/bin/python3 -c "import sys; print(chr(10).join(sys.path))" 查看 site-packages 路径/proc/net/tcp 查看端口占用:端口 8080 对应 16 进制 1F90,state=0A 表示 LISTENuvex/uvicorn 恢复——用系统 pip 装到 venv 的 site-packages:
import subprocess
proc = subprocess.Popen(
['/usr/bin/python3', '-m', 'pip', 'install',
'--break-system-packages',
'--target=/path/to/.venv3/lib/python3.13/site-packages',
'uvicorn'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout, stderr = proc.communicate(timeout=120)
yun验证 (terminal/execute_code 返回 -1 时,直接用 subprocess 更可靠):
import subprocess
subprocess.run(
['/path/to/.venv3/bin/python3', '-c',
'import uvicorn; print(uvicorn.__version__)'],
capture_output=True, text=True, check=True
)
规则: 在自动化/调度任务中不要依赖 source .venv/bin/activate;始终使用 venv 解释器的绝对路径,用 subprocess 直接调用系统 pip 并将缺失组件安装到 venv 的 site-packages 下。
request 键Starlette 的 Jinja2Templates.TemplateResponse 签名在较新版本中已统一为 TemplateResponse(name, context),其中 context 必须包含 "request" 键。旧的 TemplateResponse(request, name, context) 三参数调用会导致模板上下文缺失或类型错误。
# ❌ 错误:旧版签名或缺少 request
return templates.TemplateResponse(request, "index.html")
return templates.TemplateResponse(request, "index.html", {"fund": fund})
# ✅ 正确:新版签名,context 必须包含 request
return templates.TemplateResponse("index.html", {"request": request})
return templates.TemplateResponse("fund_detail.html", {
"request": request,
"fund": fund,
"score": score,
})
规则: 修改 main.py 或任何模板路由后,启动后端并用 curl 访问一次页面路由(如 /、/fund/{code})验证模板渲染不报错。HTTP 500 且日志为 TemplateResponse() takes ... 或 KeyError: request 时,优先检查此签名。
当项目同时存在 database/models.py 的规范 Schema 和采集脚本中内嵌的旧版建表 SQL 时,两者会互相覆盖,导致 API 查询失败、外键约束错误或数据字段丢失。
# ❌ 危险:采集脚本自建简化表,与 models.py 不一致
# models.py 中 fund_info 有 14 个字段,但 run_collection.py 里只有 5 个字段
# 结果:API 查询 f.risk_level / f.manager_name 等字段全部为空或报错
正确做法:
.db.bak),用 models.py 的 Database 重新初始化;CREATE TABLE 逻辑,改为复用 Database.connect() + Database.init_tables();Database 的 upsert_fund_info() / upsert_nav() / upsert_score() 或 execute_many();MAX_FUNDS=10)后,用 PRAGMA table_info(table) 验证字段数量。规则: 永远只保留一份 Schema 真相源。如果 models.py 是真相源,所有采集、迁移、测试脚本必须通过它初始化数据库,禁止各自建表。
在基金/股票等金融数据项目中,fund_nav 表通常对 fund_info(fund_code) 有外键约束。如果 fund_info 只采集了前 N 条,而 fund_open_fund_rank_em 等数据源按收益排名返回的基金代码可能不在前 N 条中,写入 fund_nav 时就会报 FOREIGN KEY constraint failed。
# ❌ 危险:基础表只采部分,下游数据源顺序不同导致外键失败
await collect_fund_list(db, max_funds=10) # 只有 10 条
await collect_returns_to_nav(db) # 可能写入第 11 名之后的基金
# ✅ 正确:基础表完整采集,下游仅写入已存在的基金
await collect_fund_list(db) # 全部基金
await collect_returns_to_nav(db, max_funds=10) # 只写入前 10 条
规则: 对于带外键约束的 Schema,先完整采集被引用表(如 fund_info),再采集引用表(如 fund_nav、fund_score、fund_holding),并限制引用表只写入基础表中存在的记录。遇到 FOREIGN KEY constraint failed 时,先检查被引用的父表是否完整。
当容器 PID 1 为 tini(或其他 init)、无 systemctl 、无特权访问时,无法真正启用 systemd。此时不能只是报告"无法完成",而应该:
/tmp 和 ~/.config/systemd/user/ 供后续有权限时部署)。Restart=always 的等效),并将子进程输出落盘。terminal(background=true) 启动守护脚本,让服务持续运行。# supervisor 示例:spot_cache_service_supervisor.py
import os, subprocess, sys, time
SCRIPT = "/opt/data/home/fund-radar/spot_cache_service.py"
PYTHON = "/opt/data/home/fund-radar/.venv3/bin/python3"
RESTART_SEC = 5
def start():
return subprocess.Popen(
[PYTHON, SCRIPT], cwd=os.path.dirname(SCRIPT),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
while True:
proc = start()
for line in proc.stdout:
sys.stdout.write(line); sys.stdout.flush()
proc.wait()
time.sleep(RESTART_SEC)
规则: 用户要求"systemd 服务"但环境不支持时,保留 systemd 配置文件并立即提供等效的守护方案,不能拖着不解决。
全量基金历史净值(如 27,166 只)通过单只接口(fund_open_fund_info_em 等)采集需要数小时,且容易触发限流。应该:
fund_open_fund_rank_em(symbol="全部") 一次性获取全市场最新净值(通常 2-4 秒内返回 1.9w+ 条)。time.sleep(HISTORY_PER_FUND_SLEEP) 限流,每 500 只休息 5 秒。terminal(background=true) 启动,让采集脚本在后台执行数小时,不阻塞当前 session。compute_scores.py 或等价评分逻辑,不要在每只基金采集完成后立即评分(频繁 IO)。# 取得最新净值后,判断哪些基金缺少足够历史数据
SELECT DISTINCT fund_code FROM fund_nav
WHERE fund_code NOT IN (
SELECT fund_code FROM fund_nav GROUP BY fund_code HAVING COUNT(*) >= 30
)
规则: 金融数据采集要先做"批量最新"+"后台补历史",避免对 2w+ 只基金发 2w+ 次单独请求。
后台进程的 terminal(background=true) 输出不一定能在 session 结束后保留。应在启动命令里将输出重定向到项目日志文件:
python run_full_collection.py > run_full_collection.log 2>&1
规则: 任何后台长运行的采集/计算任务,必须将 stdout/stderr 重定向到项目目录的 .log 文件,方便后续排查和验证。
&Path 而非 &PathBuf(clippy::ptr_arg)— 修复时记得同步更新 use std::path::{Path, PathBuf} 导入PathBuf::join(),禁止字符串 .replace('/', "\\") — PathBuf::join 自动处理分隔符map_err + ? 或 if let Err(e),禁止裸 unwrap()/expect()(生产代码会 panic)serde 派生顺序:Serialize 在 Deserialize 前(字母序)#[serde(default)] for optional fields: When a struct field is String (not Option<String>), serde will fail to deserialize if the JSON doesn't include that field. The error is silently swallowed by .ok() or .unwrap_or_default(), resulting in an empty vector. Add #[serde(default)] to any field that may be absent in heterogeneous JSON (e.g., TVBox sites where type=3 may lack api). One bad site kills the entire array. See tauri-desktop-apps references/debugging-pitfalls.md #20.file_X.rs 和 file_X_streaming.rs(或其他同族文件)有相同 helper 函数时,提取到独立模块(如 dissolve_helpers.rs),在 mod.rs 注册,调用方用 super::module_name::fn() 引用。提取时统一用 &Path 签名(比 &PathBuf 更泛用)标准初始化流程(不含 Tauri):
# 1. 创建项目
cd /path/to/project/frontend
npm create vite@latest . -- --template vue
# 2. 安装依赖
npm install
npm install vue-router@4 vue-chartjs@5 chart.js@4
npm install -D vite-plugin-pwa
# 3. 项目结构
# src/views/ — 页面组件(Home, Detail, Search, Compare)
# src/components/ — 公共组件(Navbar, Loading, Empty, Pagination)
# src/api/ — API 调用层(fetch 封装)
# src/router/ — vue-router 配置
# src/styles/ — 全局 CSS
# public/ — 静态资源(PWA icons, manifest)
PWA 配置(vite.config.js):
import { VitePWA } from 'vite-plugin-pwa'
export default {
plugins: [
vue(),
VitePWA({
registerType: 'autoUpdate',
manifest: {
name: '应用名',
short_name: '应用名',
start_url: '/',
display: 'standalone',
background_color: '#0a0e17',
theme_color: '#0a0e17',
},
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,woff2}'],
},
}),
],
}
构建产物集成到 FastAPI:npm run build 输出到 dist/,FastAPI 用 StaticFiles 挂载即可。
常见问题:
dist/ 不在 git 中提交(.gitignore),CI/CD 或手动 buildstart_url 和 scope 必须与实际部署路径匹配onDragDropEvent)返回 Promise<UnlistenFn>,必须在 onUnmounted 中调用 unlisten 清理,否则内存泄漏shallowRef 大数组累加禁止用 spread [...old, ...new](O(n²)),改用普通数组 + push(),完成后再赋值给 shallowRef/usr, /etc, C:\\\\Windows)scp/rsync are blocked by firewall. Always use Python subprocess + base64 encoding to push files (NOT bash heredocs — they fail with complex Vue/TS content due to escaping). Use the reference script scripts/remote-push.py for the boilerplate.export PATH=$PATH:/root/.hermes/node/lib/node_modules/corepack/shims before any pnpm command on the remote host.src-tauri/src/commands/*.rs, IPC wrapper in src/utils/tauri.ts, views in src/views/, components in src/components/{layout,video,player}/, router in src/router/, global CSS in src/styles/global.css, 5-view layout (Home, Live, Favorite, History, Settings).execute_batch requires semicolons in SQL stringsConnection::execute_batch() 拼接多个 SQL 语句时,每条语句必须以 ; 结尾。即使每条字符串内部看起来完整,缺少分号会导致 near "CREATE": syntax error。
// ❌ 危险:SQL 字符串末尾没有分号
pub const CREATE_FAVORITES: &str = "CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
...
UNIQUE(video_id, source_name)
)"; // ← 缺少 ;
// ✅ 每条 SQL 必须以分号结尾
pub const CREATE_FAVORITES: &str = "CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
...
UNIQUE(video_id, source_name)
);"; // ← 有分号
规则:所有传入 execute_batch 的 SQL 常量必须以 ; 结尾。 execute_batch(&ALL_MIGRATIONS.join("\n")) 会把多条 SQL 拼在一起,缺分号时 SQLite 解析器会在下一条 CREATE 关键字处报错。
Linux 上的 mpv 是动态链接的(依赖 libavcodec/libavformat 等几十个 .so),直接把 /usr/bin/mpv 复制到 src-tauri/binaries/ 后在目标机器运行会因缺少依赖库而失败。
正确做法: 安装为系统依赖 + 运行时路径检测
fn find_mpv() -> Option<String> {
// 1. which mpv
// 2. 常见路径: /usr/bin/mpv, /usr/local/bin/mpv, /snap/bin/mpv
}
tauri.conf.json 的 bundle.linux.deb.depends 添加 ["mpv"]setup() 时检测并报告 mpv 状态对于 macOS/Windows: mpv 在 macOS 上可通过 Homebrew 静态链接,Windows 上可用 mpv-lazy 等预编译包,sidecar 方案更可行。
app.path() requires use tauri::Managerapp.path() in setup() closure is not found without importing the Manager trait. Compiler error: no method named 'path' found for &mut tauri::App. Fix: add use tauri::Manager; at the top of lib.rs.
Tauri 在 Linux 上使用 WebKitGTK 渲染 webview。xdotool 的鼠标事件(click/mousedown/mouseup)无法正确传递到 webview 内的 HTML 元素。 Tab 导航也往往不按 DOM 顺序走,会跳到意外的元素。
# ❌ 无效:xdotool click 无法触发 webview 内的按钮
xdotool mousemove --window $WID 465 369 && xdotool click 1 # ← 没有反应
# ❌ 无效:Tab 导航不按预期走
xdotool key Tab Tab Tab space # ← 可能跳到刷新按钮而非目标按钮
原因: WebKitGTK 的 X11 事件处理与标准 X11 窗口不同,鼠标事件在 GTK 层被拦截,不会传递到 web DOM。
可行的替代方案(按可靠性排序):
eval 或 webview.eval() 接口xdotool key 模拟键盘快捷键 — 键盘事件有时比鼠标事件更可靠(但 Tab 导航仍不可靠)规则:对 Tauri 应用做自动化 UI 测试时,不要依赖 xdotool 鼠标点击。优先用后端 API/数据库操作代替 GUI 交互。
TVBox 配置中 type=3 Spider 站点的 ext 字段可能是 JavaScript Spider 脚本路径(如 https://raw.kkgithub.com/.../88看球.js),不是网站 URL。前端代码若不检查 .js 后缀,会将其拼接为无效 API URL(如 ...88看球.js/api.php/provide/vod/),导致所有内容加载静默失败。
// ❌ 危险:将 .js Spider 脚本误当作 API URL
if (typeof site.ext === 'string' && site.ext.startsWith('http')) {
apiUrl = site.ext // ← 如果 ext 是 .js 文件,生成无效 URL
}
// 结果:...88看球.js/api.php/provide/vod/ → 404
// ✅ 正确:检测并跳过 .js Spider 脚本
if (typeof site.ext === 'string' && site.ext.startsWith('http')) {
if (site.ext.endsWith('.js')) {
console.log(`[DeskBox] 跳过 Spider 脚本: ${site.name}`)
continue
}
apiUrl = site.ext
}
规则:在 loadContentItems() 中处理 type=3 站点时,必须先检查 ext 是否以 .js 结尾,是则跳过。
When editing Vue .vue files, accidentally duplicating a return list\\n}) block produces error TS1128: Declaration or statement expected in vue-tsc. This is easy to do when copy-pasting computed properties. Always verify the <script> section has balanced braces after editing.
将 .catch((e) => console.debug(e)) 改为 .catch((e) => showToast('失败: ' + e.message, 'error')) 时,TypeScript 把 e 推断为 unknown 或 {},直接访问 e.message 会报 error TS2339: Property 'message' does not exist on type '{}'。
// ❌ TS2339: e 被推断为 unknown/{},e.message 不存在
.catch((e) => showToast('失败: ' + e.message, 'error'))
// ✅ 方案1:加 : any 类型标注
.catch((e: any) => showToast('失败: ' + (e?.message || String(e)), 'error'))
// ✅ 方案2:用 String() 安全转换
.catch((e) => showToast('失败: ' + (e instanceof Error ? e.message : String(e)), 'error'))
规则: Vue/TS 项目中 catch 参数必须显式标注类型或用安全访问模式。推荐 (e: any) + e?.message || String(e) 组合。修改 catch 后务必运行 vue-tsc --noEmit 验证。
In Vue/TS code, omitting quotes around string literals in comparisons or function arguments causes TypeScript compilation errors AND silent runtime failures. Common patterns:
=== string → should be === 'string'includes(vod) → should be includes('vod')startsWith(http) → should be startsWith('http')getCategoryVideos(首页, ...) → should be getCategoryVideos('首页', ...)These are easy to introduce when refactoring code that had string literals removed accidentally. Always run vue-tsc --noEmit after editing Vue store files to catch these.
console.log 从 SSH 不可见Tauri 在 Linux 上使用 WebKitGTK。console.log/warn/error 输出到 WebKit 内部控制台,不会出现在 stdout/stderr,从 SSH 无法捕获。
解决方案(按可靠性排序):
eprintln! 写文件 — 最可靠。注册一个 log_debug IPC 命令,JS 通过 invoke('log_debug', { msg }) 调用,Rust 端 eprintln! + 写文件。WEBKIT_INSPECTOR_SERVER=127.0.0.1:9222 — 启动时设置环境变量,然后从另一终端连接。但 WebKitGTK 的 inspector 支持不完整。console.log 做远程调试 — 在 Tauri + WebKitGTK 环境下,所有日志必须通过 IPC 写文件。// ✅ 正确:通过 IPC 写日志文件
import { invoke } from '@tauri-apps/api/core'
function dbg(msg: string) {
invoke('log_debug', { msg }).catch(() => {})
}
// Rust 端
#[command]
pub async fn log_debug(msg: String) -> Result<(), String> {
use std::io::Write;
let mut f = std::fs::OpenOptions::new()
.create(true).append(true)
.open("/tmp/app_debug.log").map_err(|e| e.to_string())?;
writeln!(f, "{}", msg).map_err(|e| e.to_string())?;
Ok(())
}
当前端 IPC 调用"无声无息"时(无返回、无错误),按以下顺序排查:
1. 检查命令是否注册在 binary 中:
strings /path/to/binary | grep 'command_name'
# 有输出 → 命令已注册
# 无输出 → lib.rs 的 invoke_handler 中未添加
2. 检查 Rust 端是否被调用:
在命令入口加 eprintln!,重新编译,启动后检查日志文件。
3. 检查 JS 端是否到达调用点:
在 invoke() 前后加 invoke('log_debug', ...) 调用,检查日志文件是否存在。
4. 检查 watcher/异步链是否断裂:
Vue watcher 的 .then() 链如果没有 .catch(),前一步异常会导致后续步骤永远不执行。这是"代码路径未到达"的常见原因。
// ❌ 危险:无 .catch(),异常会中断整个链
refreshSourceCategories().then(() => loadContentItems())
// ✅ 正确:添加错误处理
refreshSourceCategories()
.then(() => loadContentItems())
.catch(e => console.error('[DeskBox] 内容加载失败:', e))
通过 Python/sed 逐行向 Rust 文件添加代码(如 eprintln! 调试日志),容易破坏花括号匹配、引入语法错误。
症状: cargo build 报 mismatched closing delimiter 或 unexpected token
正确做法:
scp 到主机)python3 -c "print(open('file.rs').read().count('{'), open('file.rs').read().count('}'))"cargo check不要: 用 sed -i 'Na\...' 逐行插入(容易插到错误位置),或用 Python str.replace() 在函数签名中间插入代码。
Tauri 应用在远程 Xfce 桌面 (display :10) 上运行时,截图命令:
DISPLAY=:10 XAUTHORITY=/home/skies/.Xauthority import -window root /tmp/screenshot.png
注意:import 是 ImageMagick 的命令,不是 Python 的。截图前确保应用窗口已加载(sleep 8)。
qwen3.6 is a reasoning model. If max_tokens is too low, thinking consumes all tokens and content returns null. For cronjobs with long prompts, use glm-4-flash or deepseek-v4-flash instead.
When migrating static mut + Once to OnceLock for FFI singletons, get_or_try_init requires nightly Rust. Use OnceLock<Option<T>> + get_or_init instead:
// ✅ Stable pattern
static MPV_FFI: OnceLock<Option<MpvFfi>> = OnceLock::new();
pub fn init() -> Result<&'static Self, MpvError> {
MPV_FFI.get_or_init(|| match Self::load() {
Ok(ffi) => Some(ffi),
Err(_) => None,
});
Self::global()
}
Rule: Never use get_or_try_init on stable Rust.
Frontend number ↔ Rust Option<String> mismatch causes Tauri to silently deserialize as None:
// ❌ episode: number → Rust gets None
api.dbAddHistory(videoId, title, episode)
// ✅ Explicit conversion
api.dbAddHistory(videoId, title, String(episode))
Rule: Always check Rust command parameter types. number↔String is the most common mismatch.
Errors from node_modules/@tauri-apps/ and node_modules/@vue/ about ES2015 features are TypeScript config issues (ES5 target), not caused by code changes. Only src/ errors matter.
Rule: Focus on src/ errors when running vue-tsc --noEmit. Exit code 0 = compiles clean.
std::sync::Mutex 在 Tauri async 命令中阻塞 tokio 线程Tauri #[command] 函数运行在 tokio 运行时上。如果在其中使用 std::sync::Mutex 并 .lock().unwrap(),会阻塞当前 tokio 线程(不是阻塞整个运行时,但会导致该 worker 线程无法处理其他任务)。在高并发场景下可能引发死锁或性能问题。
// ❌ DANGEROUS: std::sync::Mutex 在 async fn 中阻塞 tokio 线程
static DB: std::sync::Mutex<Option<Database>> = std::sync::Mutex::new(None);
#[command]
pub async fn get_sources() -> Result<Vec<Source>, String> {
let db = DB.lock().map_err(|e| e.to_string())?; // ← 阻塞 tokio worker
// ...
}
// ✅ Option A: tokio::sync::Mutex(async-aware)
static DB: tokio::sync::Mutex<Option<Database>> = tokio::sync::Mutex::new(None);
#[command]
pub async fn get_sources() -> Result<Vec<Source>, String> {
let db = DB.lock().await; // ← async lock, 不阻塞 tokio 线程
// ...
}
// ✅ Option B: 如果锁时间极短,用 spawn_blocking 包装
#[command]
pub async fn get_sources() -> Result<Vec<Source>, String> {
let sources = tokio::task::spawn_blocking(|| {
let db = DB.lock().unwrap(); // ← 在 blocking 线程中,可接受
// ... 读取数据
}).await.map_err(|e| e.to_string())?;
Ok(sources)
}
Rule: 在 Tauri #[command] async 函数中,优先用 tokio::sync::Mutex。如果必须用 std::sync::Mutex,确保锁持有时间极短(微秒级),且不要在 .lock() 和 .await 之间夹杂其他 async 操作。
前端 IPC wrapper 函数传入的参数数量少于 Rust 后端命令期望的数量时,Tauri 不会报错——缺失的参数被反序列化为 None/0/"",导致数据字段静默丢失。
// ❌ 前端只传 3 个参数,后端期望 8 个
api.dbAddHistory(videoId, title, episode)
// cover, source_name, source_url, category, year → 全部丢失
// ✅ 前端补齐所有参数
api.dbAddHistory(videoId, title, episode, cover || '', sourceName, sourceUrl, category || '', year || '')
排查方法: 对比 invoke('command_name', ...) 的参数列表与 Rust #[command] fn 的参数列表,逐一匹配。规则: 新增 Rust command 参数时,必须同步更新 TypeScript IPC wrapper,反之亦然。
同时向 localStorage 和 SQLite 写入数据,但只从其中一个读取,会导致:
// ❌ 危险:写入两个地方,但只从一个读取
async function addToFavorites(item: any) {
localStorage.setItem('favorites', JSON.stringify([...items.value]))
invoke('db_add_favorite', { ... }) // 写 SQLite
}
function loadFavorites() {
const cached = localStorage.getItem('favorites') // ← 只读 localStorage!
return cached ? JSON.parse(cached) : []
}
// ✅ 统一读取源:启动时从 SQLite 加载,缓存到 localStorage
async function initFavorites() {
const dbItems = await invoke('db_get_favorites') // ← 从 SQLite 读
favorites.value = dbItems
localStorage.setItem('favorites', JSON.stringify(dbItems)) // 缓存
}
Rule: 如果项目同时使用 localStorage 和 SQLite,必须明确读取源:启动时从 SQLite 加载到内存,后续操作同时更新内存和 SQLite,localStorage 仅作缓存。
规则: 缓存代理/网络请求结果时,只缓存成功结果。失败时不要缓存原始 URL 作为降级——网络抖动是暂时的,缓存失败会导致永久降级。正确做法:失败时不写缓存,下次请求自动重试。
FastAPI 按注册顺序匹配路由。如果 /funds/{code} 在 /funds/compare 之前注册,请求 /api/funds/compare?codes=... 会被 /{code} 匹配(code="compare"),返回 404 "未找到基金 compare"。
# ❌ 错误顺序:参数化路由在前,静态路由被劫持
@router.get("/funds/{code}") # L104 — 先注册
@router.get("/funds/compare") # L195 — 永远不会被匹配到
@router.get("/funds/{code}/score")
# ✅ 正确顺序:所有静态路由必须在参数化路由之前
@router.get("/funds/compare") # 先注册静态路由
@router.get("/funds/{code}") # 再注册参数化路由
@router.get("/funds/{code}/score")
@router.get("/funds/{code}/radar")
规则: 在 FastAPI router 中,所有静态路径(如 /funds/compare、/funds/search)必须在参数化路径(如 /funds/{code}、/funds/{code}/score)之前注册。写完路由后用 curl 测试所有端点验证。
第三方库(如 akshare、requests)是同步阻塞的,直接在 async def 路由中调用会阻塞整个 event loop,导致所有请求卡死。
错误做法:
# ❌ 同步调用阻塞 event loop
@router.get("/api/data")
async def get_data():
df = ak.stock_zh_a_spot_em() # 阻塞 90s+,整个服务瘫痪
return df.to_dict()
asyncio.to_thread 的陷阱:
# ⚠️ 扔线程池仍会超时 — uvicorn 默认 60s 超时
@router.get("/api/data")
async def get_data():
df = await asyncio.to_thread(ak.stock_zh_a_spot_em) # HTTP 请求等 90s 超时
return df.to_dict()
# ⚠️ wait_for + CancelledError 链混乱
df = await asyncio.wait_for(asyncio.to_thread(...), timeout=120)
# uvicorn 的超时触发 CancelledError → 再被 wait_for 捕获 → TimeoutError 链
正确方案:后台缓存 + API 只读
后台进程(60s循环) → 写入 SQLite → API 读取(1ms)
适用场景: akshare、requests 爬虫、ffmpeg 转码、大文件处理等任何耗时超过 5s 的同步操作。
规则: 任何耗时 >5s 的同步调用,不要放在 HTTP 请求路径中。用后台服务 + 缓存解耦。
当 URL 包含单引号或其他 shell 元字符时,直接嵌入 sh -c 命令会导致 shell 注入:
// ❌ DANGEROUS: URL 直接拼接,含单引号的 URL 可逃逸
let cmd = format!("mpv --fullscreen '{}'", url);
Command::new("sh").args(["-c", &cmd]).spawn()?;
// ✅ 安全:使用数组参数直接传递给 mpv,不经过 shell
Command::new("mpv")
.args(["--fullscreen", &url]) // ← 不经过 sh -c
.spawn()?;
Rule: 外部命令的参数直接作为数组传递给 Command::args(),不要拼接到 sh -c 中。
Spider 引擎解析播放地址时可能遇到多层 iframe 嵌套。如果递归调用无深度限制,恶意网页可构造无限循环导致栈溢出:
// ❌ DANGEROSS: 无深度限制
async fn resolve_play(url: &str, client: &Client) -> Result<String, String> {
if is_iframe(html) {
let iframe_url = extract_iframe(&html);
return Box::pin(resolve_play(&iframe_url, client)).await; // ← 可无限递归
}
}
// ✅ 安全:添加 depth 参数
async fn resolve_play(url: &str, client: &Client, depth: u32) -> Result<String, String> {
if depth > 3 {
return Err("iframe 嵌套过深".into());
}
if is_iframe(html) {
let iframe_url = extract_iframe(&html);
return Box::pin(resolve_play(&iframe_url, client, depth + 1)).await;
}
}
Rule: 所有递归调用(iframe 解析、URL 重定向追踪、页面遍历)必须有最大深度限制(建议 3-5 层)。
推荐的全局数据库模式(注意:在 async 命令中使用 tokio::sync::Mutex,见上方 pitfall):
// commands/source.rs
use tokio::sync::Mutex;
use crate::db::operations::Database;
static DB: Mutex<Option<Database>> = Mutex::new(None);
/// 在 setup() 中调用
pub fn init_db(db_path: &str) {
match Database::new(db_path) {
Ok(db) => { *DB.lock().unwrap() = Some(db); }
Err(e) => eprintln!("[App] DB init failed: {}", e),
}
}
pub fn get_db() -> Result<std::sync::MutexGuard<'static, Option<Database>>, String> {
DB.lock().map_err(|e| e.to_string())
}
// lib.rs setup hook
use tauri::Manager;
.setup(|app| {
let db_path = app.path().app_data_dir().expect("...").join("app.db");
commands::source::init_db(&db_path.to_string_lossy());
Ok(())
})
命令中使用:let db_guard = get_db()?; if let Some(ref db) = *db_guard { db.xxx()?; }
mpv 通过 Unix socket 接收 JSON 命令:
use std::os::unix::net::UnixStream;
use std::io::Write;
fn send_mpv_command(socket_path: &str, cmd: &str) -> Result<(), String> {
let mut stream = UnixStream::connect(socket_path)
.map_err(|e| format!("连接 mpv IPC 失败: {}", e))?;
let mut msg = cmd.to_string();
msg.push('\n');
stream.write_all(msg.as_bytes())
.map_err(|e| format!("发送命令失败: {}", e))?;
stream.flush().map_err(|e| e.to_string())?;
Ok(())
}
// 启动 mpv 时指定 socket
Command::new("mpv")
.args(["--input-ipc-server=/tmp/app-mpv-socket", url])
.spawn()?;
// 发送命令
send_mpv_command("/tmp/app-mpv-socket", r#"{"command": ["cycle", "pause"]}"#)?;
<type>(<scope>): <description>当用户要求"样式好看、效果好、交互友好"时,代码不仅要能跑,还要好看好用。
当原型是 HTML 页面但目标是桌面端应用时,UI 必须等比例缩小:
用户原话: "毕竟我做的是桌面端的原型是html界面,但是我理解交互,还有UI要好一点"
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--accent: #e94560;
--accent-secondary: #0f3460;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
}
每次提交前自查:
对项目做全面审查时,按以下维度逐项检查,每个发现标注严重级别:
维度:
输出格式:
## 审查结果
- 项目:[项目名]
- 审查范围:[文件列表]
### 🔴 严重问题(必须修)
1. 问题描述 → 文件:行号 → 修复建议
### 🟡 建议优化
1. 问题描述 → 建议
### 🟢 亮点
1. 做得好的地方
### 评分
- 安全性:X/10
- 代码质量:X/10
- 架构:X/10
- 性能:X/10
- 综合:X/10
用户原话: "如果不清楚逻辑就去参考 ok 影视 tvbox,看他们逻辑" 含义:遇到实现不确定时,先看参考项目的源码,理解"应该怎么做",再对比"我们哪里不对"。这比盲目加日志排查高效得多。
对于复杂 Bug 和性能回退,使用系统化诊断循环。完整内容已合并自 diagnose skill (2026-06-14)。
这是调试的灵魂。 其余都是机械操作。如果你有一个快速、确定性、可由 agent 运行的 pass/fail 信号来判断 Bug,你就能找到原因。
在这里投入不成比例的努力。要激进。要创造性。拒绝放弃。
30 秒的不稳定循环几乎等于没有循环。2 秒的确定性循环是调试超能力。
Phase 1 — 建立反馈循环 (见上)
Phase 2 — 复现
Phase 3 — 提出假设
Phase 4 — 插桩探测
[DEBUG-a4f2]Phase 5 — 修复 + 回归测试
Phase 6 — 清理 + 复盘
[DEBUG-...] 插桩已移除然后问:什么能预防这个 Bug?
| 症状 | 可能原因 | 修复 |
|---|---|---|
| INSERT 静默失败 | Schema 不匹配 | PRAGMA table_info() → 与代码对比 |
pip 在 venv 中缺失 | venv 损坏 | 找另一个 venv 或用 --break-system-packages |
| 服务器刷日志无连接 | uvicorn DEBUG 循环 | DEBUG=false,kill 旧进程 |
| numpy/pandas 不可用 | 无 pip 访问 | 用 stdlib fallback (math, statistics) |
| 服务器 500 启动失败 | 依赖或环境问题 | 检查日志,验证配置 |
接到任务时,先输出计划:
## 任务理解
[用自己的话复述任务目标]
## 方案
[列出实现步骤]
1. [步骤] → 验证:[如何确认完成]
2. [步骤] → 验证:[如何确认完成]
## 风险/疑问
[不确定的地方,先问]
## 预估
[改动范围、文件列表]
完成后输出:
## 完成报告
- 改了什么:[文件列表 + 改动摘要]
- 验证了什么:[测试结果/手动验证]
- 遗留问题:[如有]