在Obsidian 里触发 properties,DeepSeek Harness 用 TaoToken 跑复盘
2026/9/18 23:28:15 网站建设 项目流程

把 Obsidian 笔记 frontmatter 里的review_triggernone改成milestone,DeepSeek Harness 上跑着的知识库插件就会自己去凑上下文、调模型、写回一份复盘草稿。这一步之所以能稳定复现,关键不在 Harness 的编排能力,而在模型调用有没有一个可靠的出口:先在 TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=obsidian-properties-intro )拿到 Key,再把插件里的 Base URL 固定成https://taotoken.net/api,剩下的才是属性设计、判级规则和回写策略。

不少人在 Harness + Obsidian 这条链路上卡住,并不是插件写不出来,而是三件事没对齐:Obsidian 的 properties 到底以什么形态被插件读到、Harness 里插件什么时候消耗 Token、以及模型请求打到哪个地址。第一件事决定「改属性能不能触发」,第二件事决定「跑一次复盘要花多少 Token」,第三件事决定「会不会 401/404」。这篇按落地顺序把它们拆开,全部给到可直接复制的配置和代码骨架,最后你能得到一个最小闭环:改一篇笔记的属性,跑出一条复盘命令,并在review/目录里看到结构化输出。

在整个链路里,Token 的消耗方只有一个,就是 Harness 内插件调用模型那一下。Obsidian 本身、文件监听、YAML 解析、写回 Markdown 都不烧 Token。所以优化思路也很清晰:把大文件切碎、把无关目录挡在门外、把低风险更新的判级交给规则而不是交给模型,模型只负责「归纳、找缺口、写报告」这类它真正擅长的事。

1. 一条 properties 变更,是怎么走到模型调用的

先把链路说清楚,后面排查问题时才不会乱。

Obsidian 的 properties 本质上是 Markdown 文件顶部的 YAML frontmatter,经过metadataCache解析后变成结构化对象。插件监听metadataCachechanged事件,就能在属性被修改后拿到文件对象和解析好的属性字典。注意是「属性被修改」,不是「文件内容任意变化」,这两者在实现上是同一个事件源,但插件里可以用字段判断把范围收窄。

一条完整的触发链大概是这样:

  1. 用户在 Obsidian 里打开项目/项目A.md,把属性review_triggernone改成milestone
  2. Obsidian 保存文件,metadataCache重新解析 frontmatter,抛出changed事件;
  3. Harness 内的知识库插件收到事件,读取属性,判断review_trigger命中约定值;
  4. 插件收集上下文:这篇笔记的正文、关联的候选知识、方法目录里的既有规则;
  5. 插件向https://taotoken.net/api发一次对话请求,模型返回结构化复盘结果;
  6. 插件解析结果,判断风险等级:低风险直接写入方法目录,高风险落到待审批目录;
  7. 写回完成后,把源笔记的review_statuspending改成done

第 7 步要特别小心:写回本身就是一次文件修改,如果插件不设防重入和忽略目录,就会变成「写回触发监听、监听触发写回」的循环。后面第 4 节的骨架里会用一个busy集合加忽略目录列表把这个口子堵上。

另一个容易忽略的点是属性值的类型。Obsidian 会对属性做类型推断,2024-01-01这种值会被当成日期类型,插件读到的可能是时间戳而不是字符串。所以触发字段建议只用纯文本枚举值,比如none/scan/milestone/health,不要用日期或布尔值去兼职做触发器。

如果你还没拿到可用的 Key,先到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=obsidian-properties-key 完成注册和创建,再去 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=obsidian-properties-keys 生成一个 API Key,本文统一用YOUR_API_KEY占位。Key 只在本地环境变量或客户端配置里出现,不要写进 Obsidian 笔记 frontmatter,那样等于把凭据同步进了知识库。

2. 准备清单:Key、Base URL 与三种客户端配置

这一节的目标是把「模型从哪来」这件事一次性钉死。Harness 插件、Claude Code、Codex、CC Switch 只是不同入口,底层都要指向同一个 Base URL。

基础信息只有三条:

  • Base URL:https://taotoken.net/api
  • API Key:YOUR_API_KEY
  • 模型 ID:以控制台模型列表里实际可用的为准,本文用YOUR_MODEL_ID占位

2.1 Claude Code:settings.json 写法

Claude Code 走的是ANTHROPIC_*系列环境变量。配置文件一般放在~/.claude/settings.json

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "YOUR_MODEL_ID" } }

如果你习惯用 shell 变量而不是配置文件,效果等价:

export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="YOUR_API_KEY" export ANTHROPIC_MODEL="YOUR_MODEL_ID"

改完配置后新开一个终端会话,让环境变量重新加载。验证方式是随便发起一次对话,看返回里有没有鉴权错误。详细的 Claude Code 接入说明可以对照 https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=obsidian-properties-doc 。

2.2 Codex:config.toml 写法

Codex 完全不走ANTHROPIC_*,它读的是 TOML 配置。把下面这段放进~/.codex/config.toml

model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"

然后确保环境里有这个 Key:

export TAOTOKEN_API_KEY="YOUR_API_KEY"

这里的env_key是告诉 Codex 去环境变量里找哪个名字,不要把 Key 明文写进 TOML。wire_apimodel的具体取值请以 Codex 当前版本和 TaoToken 文档为准,不同版本字段可能有差异。

2.3 CC Switch:三件套一次填完

如果你同时用多个客户端,用 CC Switch 做配置切换最省事。新建一个供应商条目,填三件套:

字段
Base URLhttps://taotoken.net/api
API KeyYOUR_API_KEY
模型名YOUR_MODEL_ID

命名建议写清楚用途,比如taotoken-kb,以后一键切换即可。切换完成后,回到终端重启一次客户端进程,避免旧配置残留在内存里。

2.4 Harness 插件侧的环境变量

Harness 内的插件是独立进程逻辑,它读的是运行 Harness 的那个 shell 的环境。所以先把变量导出去再启动:

export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_MODEL="YOUR_MODEL_ID"

这三条是后面所有代码的前置条件。少任何一条,插件那边就会拿到空值,表现出来通常就是 401 或者请求发到错误的地址上。

3. Obsidian 侧:properties 字段怎么设计才不会被误触发

属性设计的目的不是好看,而是让「人改属性」和「插件做动作」之间有一份明确契约。契约越清楚,模型被无意义调用的次数越少。

一篇项目笔记的属性建议长这样:

--- title: 项目A 里程碑复盘 type: project status: active review_trigger: milestone review_scope: methods review_risk: auto review_status: idle tags: - project - review --- ## 项目背景 这里是项目背景描述…… ## 目标 - 目标一 - 目标二 ---

字段含义可以固定成下面这张表,插件按表实现,人按表填写:

属性作用建议取值
review_trigger唯一触发器,值变化即视为一次请求none/scan/milestone/health
review_scope这次复盘扫哪些目录methods/workflow/all
review_risk是否允许自动写入auto/manual
review_status插件回写的状态,人不要手改idle/pending/done/rejected

有两条纪律要守住。

第一,review_trigger只在需要触发的那一刻改,触发结束后由插件把它改回none或保持现状但把review_status置为done,避免下次保存笔记时又被判定为一次新触发。

第二,人工维护的规则文件单独放,不参与自动重写。建议在知识库根目录放一份《知识自动更新规则.md》,用自然语言写清楚风险边界:

# 知识自动更新规则 ## 低风险(可直接写入) - 补充来源项目链接 - 追加实践记录、验证案例 - 修正错别字与格式 ## 高风险(必须人工审批) - 修改既有方法论的结论 - 变更工作流程步骤 - 删除或合并既有知识条目 ## 禁止自动执行 - 修改本文件自身 - 修改 review/ 目录以外的任何脚本

插件在拼 prompt 时把这份规则一并带上,判级就有了统一口径。规则文件是人工维护的,保证长期可控;模型只是执行者,不是规则制定者。

4. Harness 插件骨架:监听、判级、调用、回写

下面是一个最小可跑骨架。插件名kb-autoupdate只是示例,你可以自己命名,结构也可以按 Harness 的插件规范拆分。

// 示例骨架:Obsidian 侧知识库自动更新插件 const { Plugin, TFile, normalizePath } = require("obsidian"); const TRIGGER_FIELD = "review_trigger"; const STATUS_FIELD = "review_status"; const IGNORE_DIRS = ["review/", "pending/", "candidate/", "templates/"]; module.exports = class KbAutoUpdate extends Plugin { async onload() { this.busy = new Set(); this.registerEvent( this.app.metadataCache.on("changed", (file, data) => { this.handleChange(file, data).catch((err) => { console.error("[kb-autoupdate] 处理失败:", err); }); }) ); } async handleChange(file, frontmatter) { const path = file.path; // 1) 忽略自动写入目录,避免写回再次触发监听 if (IGNORE_DIRS.some((dir) => path.startsWith(dir))) return; // 2) 防重入:同一文件同一时间只处理一次 if (this.busy.has(path)) return; // 3) 只有触发器有值时才继续,其余属性变更一律放过 const trigger = frontmatter?.[TRIGGER_FIELD]; if (!trigger || trigger === "none") return; this.busy.add(path); try { await this.patchStatus(file, "pending"); const noteText = await this.app.vault.read(file); const rules = await this.readRules(); const result = await this.callModel(trigger, path, noteText, rules); await this.dispatch(file, frontmatter, result); await this.patchStatus(file, "done"); } catch (err) { console.error("[kb-autoupdate] 复盘失败:", err); await this.patchStatus(file, "idle"); } finally { this.busy.delete(path); } } async readRules() { const ruleFile = this.app.vault.getAbstractFileByPath("知识自动更新规则.md"); if (ruleFile instanceof TFile) { return await this.app.vault.read(ruleFile); } return "无规则文件,默认按高风险处理。"; } async callModel(trigger, path, content, rules) { const base = process.env.TAOTOKEN_BASE_URL || "https://taotoken.net/api"; const key = process.env.TAOTOKEN_API_KEY; const model = process.env.TAOTOKEN_MODEL; if (!key || !model) { throw new Error("缺少 TAOTOKEN_API_KEY 或 TAOTOKEN_MODEL 环境变量"); } const prompt = [ `触发类型:${trigger}`, `源文件:${path}`, "请按下面的规则判定风险等级,并输出 JSON。", "字段要求:risk(low/high)、summary(一句话)、findings(数组)、patch(低风险时的建议内容)。", "规则文件内容如下:", rules, "笔记正文如下:", content.slice(0, 6000) ].join("\n"); const resp = await fetch(`${base}/v1/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` }, body: JSON.stringify({ model, messages: [ { role: "system", content: "你是知识库复盘助手,只输出合法 JSON,不要解释。" }, { role: "user", content: prompt } ], response_format: { type: "json_object" }, temperature: 0.2 }) }); if (!resp.ok) { const text = await resp.text(); throw new Error(`模型请求失败 ${resp.status}: ${text.slice(0, 300)}`); } const data = await resp.json(); const raw = data?.choices?.[0]?.message?.content || "{}"; return JSON.parse(raw); } async dispatch(file, frontmatter, result) { const scope = frontmatter?.review_scope || "methods"; const risk = result?.risk === "low" ? "low" : "high"; const targetDir = risk === "low" ? "methods" : "pending"; const report = [ "---", `source_note: "[[${file.path}]]"`, `scope: ${scope}`, `risk: ${risk}`, `generated_at: ${new Date().toISOString()}`, `review_status: ${risk === "low" ? "applied" : "waiting_approval"}`, "---", "", `## 复盘摘要`, "", result?.summary || "(模型未返回 summary)", "", "## 发现", "", ...(Array.isArray(result?.findings) ? result.findings.map((f) => `- ${f}`) : ["- 无"]), "", "## 建议变更", "", result?.patch || "(无)" ].join("\n"); await this.writeFile(`${targetDir}/${file.basename}-复盘.md`, report); } async writeFile(path, content) { const normalized = normalizePath(path); const existing = this.app.vault.getAbstractFileByPath(normalized); if (existing instanceof TFile) { await this.app.vault.modify(existing, content); } else { const folder = normalized.split("/").slice(0, -1).join("/"); if (folder && !this.app.vault.getAbstractFileByPath(folder)) { await this.app.vault.createFolder(folder); } await this.app.vault.create(normalized, content); } } async patchStatus(file, status) { await this.app.fileManager.processFrontMatter(file, (fm) => { fm[STATUS_FIELD] = status; if (status === "done") fm[TRIGGER_FIELD] = "none"; }); } };

几个设计点值得单独说。

上下文裁剪。content.slice(0, 6000)是硬截断,目的是控制单次请求的 Token 消耗。真实项目里更稳的做法是按标题切块,只把「目标」「结论」「变更记录」这几段送进去,其余正文留着本地检索。

判级交给规则,不交给感觉。prompt 里明确要求输出risk字段,并且把《知识自动更新规则.md》原文带进去。模型只需要做一次二分类,出错概率远低于让它自由决定「该不该改」。

状态回写用processFrontMatter这个 API 会保留原有 YAML 结构,不会把整份 frontmatter 重排成不可读的样子。手写正则去改 YAML,遇到多行数组或注释就会出事。

写回目录必须在忽略列表里。review/pending/candidate/三个目录一旦被监听,插件就会对自己的输出再触发一次复盘,Token 消耗会呈倍数上涨。

5. properties 触发复盘:一次可复现的完整操作

前面都是准备,这一节给出从改属性到看到输出的完整步骤。

第一步:确认环境变量已生效。

echo "$TAOTOKEN_BASE_URL" echo "$TAOTOKEN_MODEL" test -n "$TAOTOKEN_API_KEY" && echo "key ok"

期望输出里 Base URL 是https://taotoken.net/api,模型 ID 非空,最后打印key ok

第二步:确认插件已加载。在 Obsidian 里打开命令面板,执行一次「重新加载插件」,然后在插件目录里确认kb-autoupdate处于启用状态。

第三步:修改属性。打开项目/项目A.md,把:

review_trigger: none

改成:

review_trigger: milestone

保存文件。

第四步:观察状态流转。属性review_status应该先从idle变成pending,几秒到几十秒后变成done,同时review_trigger被自动改回none。如果停在pending不动,说明模型请求没回来,去看控制台日志里的 HTTP 状态码。

第五步:查看输出。review/项目A-复盘.mdpending/项目A-复盘.md会出现一份结构化报告,长这样:

--- source_note: "[[项目/项目A.md]]" scope: methods risk: high generated_at: 2025-01-15T09:24:11.000Z review_status: waiting_approval --- ## 复盘摘要 本次里程碑涉及工作流步骤调整,判定为高风险,已转入待审批。 ## 发现 - 方法目录中「需求评审」条目与本次实践流程不一致 - 缺少异常回滚的验证案例 - 候选知识里有两条未归档记录 ## 建议变更 - 更新 methods/需求评审.md 的第 3 步 - 新增 methods/异常回滚验证.md - 将 candidate/ 下两条记录合并进对应方法条目

第六步:验证风险分流是否正确。把同一篇笔记的review_trigger改成scan(假设日志类触发),如果这次只涉及「补充来源项目、追加实践记录」,输出应落在methods/目录且risk: low;如果涉及流程变更,应落在pending/。两种结果都符合预期,说明判级逻辑在正常工作。

这条闭环跑通之后,复盘就不再依赖你记性好,而是依赖属性值的变化。你需要的只是「想起来的时候就改一下属性」。

6. 两条写入路径:低风险直写与高风险审批

低风险和高风险走不同目录,是整个系统能长期跑下去的前提。如果全自动写入,一次错误的方法论变更可能污染整个知识库;如果全部人工审批,那自动化就没有意义。分流的意义在于:把审批成本压到只占少数的高风险变更上。

推荐的目录结构:

知识库/ ├── 知识自动更新规则.md ├── 项目/ │ └── 项目A.md ├── methods/ # 低风险直写目标 ├── workflow/ # 流程类知识,通常高风险 ├── candidate/ # 候选知识,等待归档 ├── pending/ # 高风险复盘报告,等待人工审批 └── review/ # 例行复盘与健康检查报告

审批动作本身也做成属性变更,保持交互一致。在pending/里的报告,把属性review_statuswaiting_approval改成approved,插件收到事件后把变更合入目标目录,并把报告标记为merged;改成rejected则原样归档,不再处理。

这里要提醒一句:审批合入涉及改写methods/workflow/下的既有知识条目,动手前先确认知识库有版本管理(比如整个 vault 用 Git 管理),这样任何一次误合入都可以回退。

低风险直写要控制爆炸半径。插件写入时带上source_notegenerated_at,任何一条自动写入的内容都能追溯到源头笔记。发现某条知识不对劲时,顺着source_note就能找到是哪次复盘带进来的。

7. 月度健康检查:只读模式怎么写

健康检查和阶段复盘是两种不同动作。复盘会改知识库,健康检查只看不改,产出一份报告,用来回答「知识库有没有变胖、有没有孤儿条目、有没有长期没被引用的方法」。

触发方式同样走属性,只是这类检查通常挂在月度索引笔记上:

--- title: 2025-01 知识库月度检查 type: index review_trigger: health review_scope: all review_risk: manual review_status: idle ---

review_risk: manual是给插件的一个硬信号:无论模型返回什么风险等级,都不写入methods/workflow/,报告统一落到review/目录。

健康检查里可以复用的三个本地指标,全部在本地算,不消耗 Token:

# 统计候选知识条数 ls candidate/*.md 2>/dev/null | wc -l # 找出 90 天未修改的方法文件 find methods -name "*.md" -mtime +90 # 统计没有被任何笔记引用的条目(简化写法) grep -L "\[\[" methods/*.md

这些指标跑完后,把结果拼进 prompt,让模型做归纳和排序。这样 Token 花在「总结」而不是「统计」上,单次成本会低很多。

插件里的只读分支实现很短:

async dispatchReadonly(file, result) { const report = [ "---", `check_type: health`, `generated_at: ${new Date().toISOString()}`, `review_status: report_only`, "---", "", result?.summary || "(无摘要)" ].join("\n"); await this.writeFile(`review/健康检查-${Date.now()}.md`, report); }

写完就结束,不碰methods/workflow/candidate/任何一个目录。月度检查的价值不在于它改了什么,而在于它定期把「知识库现在长什么样」摆到你面前。

8. 高频报错与排障清单

这部分按报错现象归类,遇到问题直接对号入座。

401 / Unauthorized。三种可能:环境变量没导出、Key 已失效、请求头拼错。先跑一遍环境变量检查命令,再看请求头是不是标准的Authorization: Bearer YOUR_API_KEY。Harness 插件和终端不在同一个 shell 环境里,是这里最常见的坑,导入变量后必须重启 Harness 进程。

404 / Not Found。绝大多数是 Base URL 或路径拼接问题。Base URL 固定写https://taotoken.net/api,对话路径在代码里拼/v1/chat/completions,不要在配置文件里手动补斜杠,也不要把 Base URL 写成带/v1的形式再叠加一层/v1

403 或模型不可用。通常是YOUR_MODEL_ID填了一个当前账号不可用的模型名。以自己的控制台模型列表为准,先跑通一个最基础的对话,再换到复盘用的模型。

改了属性但插件没反应。依次检查:插件是否启用;属性名大小写是否一致(建议统一小写下划线);属性值是不是被 Obsidian 推断成了日期或布尔类型;review_trigger是不是已经等于none;文件是不是落在忽略目录里。

复盘跑了一半,review_status卡在pending说明请求发出去了但没拿到合法 JSON。在插件里把原始返回体打出来,常见原因是模型带了 Markdown 代码围栏,JSON.parse直接失败。处理办法是在解析前剥掉围栏,或者用更严格的response_format约束。

Token 消耗比预期高。三个方向的排查:一是写回目录没进忽略列表,导致自触发循环;二是每次复盘都把整篇笔记原文塞进 prompt,没有做分段裁剪;三是低风险和高风险的判级被交给了模型自由发挥,导致每次都要带上大量上下文。把忽略目录、正文截断、规则判级这三件事做掉,消耗通常会明显下降。

自动写入的内容格式乱了。不要用正则去改 frontmatter,用processFrontMatter。手写解析在遇到多行数组、嵌套对象、注释时几乎一定会出错。

多人协作时反复触发。知识库同步工具(网盘、Git 自动拉取)会让文件批量变更,间接带动属性变化。给插件加一个基于文件修改时间和内容哈希的去重判断,同一个内容哈希只处理一次。

9. 落地顺序:三天把一个最小闭环跑起来

不要一上来就把召回、扫描、复盘、健康检查全做完,那样很难定位问题出在哪一层。按下面顺序推进,每一步都是可验证的。

第一天:把模型出口打通。到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=obsidian-properties-plan 了解接入方式,在控制台创建 Key,配好环境变量,用命令行发一次最简对话请求,确认能拿到返回。这一步不过,后面所有代码都是白写。

第二天:把 Obsidian 侧的触发跑通。写一个最小插件,只做一件事:监听到review_trigger变成指定值后,在review/目录写一个带时间戳的文件。不调模型,纯本地。触发链路确认无误后,再把模型调用接进去。

第三天:接判级与分流。把《知识自动更新规则.md》加进来,让模型输出risk字段,低风险写methods/,高风险写pending/。再手动走一遍审批流程,确认属性改成approved后变更能正确合入。

三天之后,你手里就有了一套能自跑的复盘流程:改一次属性,换一份结构化复盘报告。后续的召回、扫描、健康检查都是在同一个骨架上加分支,成本会低得多。

更进一步,如果你的复盘频率上来了,按量计费的模型调用会变成一笔需要考虑的开销,可以看看 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=obsidian-properties-coding 里的套餐是否更合适;已经跑通流程、准备把插件用到多个项目上的,直接去 https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=obsidian-properties-chat 验证模型可用性,再去 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=obsidian-properties-keys-final 创建一把新的 Key 专门给 Harness 用,把权限和环境隔离清楚。

回到最初那个动作:在 Obsidian 里改一下review_trigger,剩下的交给插件和模型。知识库能不能自我进化,靠的不是某一次灵感,而是这条链路每周都能稳定跑一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询