@dingpenghui/dsh-obsidian-sync 3.4.5 → 3.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +6 -6
- package/lib/index.js +3 -2
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +8 -4
package/lib/client.js
CHANGED
|
@@ -128,15 +128,15 @@ window.__ModuleLoader__.load({
|
|
|
128
128
|
document.head.appendChild(tag);
|
|
129
129
|
}
|
|
130
130
|
var PluginDetailPage_module_css_default = {
|
|
131
|
-
"gitHint": "jWPSbW_gitHint",
|
|
132
|
-
"input": "jWPSbW_input",
|
|
133
131
|
"subHint": "jWPSbW_subHint",
|
|
134
|
-
"field": "jWPSbW_field",
|
|
135
|
-
"fieldText": "jWPSbW_fieldText",
|
|
136
132
|
"title": "jWPSbW_title",
|
|
137
|
-
"
|
|
133
|
+
"fieldText": "jWPSbW_fieldText",
|
|
138
134
|
"page": "jWPSbW_page",
|
|
139
|
-
"hint": "jWPSbW_hint"
|
|
135
|
+
"hint": "jWPSbW_hint",
|
|
136
|
+
"gitHint": "jWPSbW_gitHint",
|
|
137
|
+
"inputSmall": "jWPSbW_inputSmall",
|
|
138
|
+
"field": "jWPSbW_field",
|
|
139
|
+
"input": "jWPSbW_input"
|
|
140
140
|
};
|
|
141
141
|
//#endregion
|
|
142
142
|
//#region src/client/plugin-detail-page.tsx
|
package/lib/index.js
CHANGED
|
@@ -271,10 +271,11 @@ function apply(ctx) {
|
|
|
271
271
|
}
|
|
272
272
|
/** 确保 shortId 恰好 8 位:不足时补 SHA-1 前缀 hash。 */
|
|
273
273
|
function normalizeShortId(sessionId) {
|
|
274
|
-
|
|
274
|
+
const stripped = sessionId.replace(/^session-/i, "");
|
|
275
|
+
let s = stripped.replace(/[^A-Za-z0-9]/g, "");
|
|
275
276
|
if (s.length >= 8) return s.slice(0, 8);
|
|
276
277
|
let hash = 0;
|
|
277
|
-
for (let i = 0; i <
|
|
278
|
+
for (let i = 0; i < stripped.length; i++) hash = (hash << 5) - hash + stripped.charCodeAt(i) | 0;
|
|
278
279
|
s = s + Math.abs(hash).toString(16).slice(0, 8);
|
|
279
280
|
return s.slice(0, 8);
|
|
280
281
|
}
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * DSH Obsidian Sync — 按需搜索 Obsidian 知识库 + 按 vault 既有 PARA 规则归档 DSH 会话。\r\n *\r\n * 设计理念(零 token / 高性能):\r\n * - 纯模型 Tool 按需调用,不注入系统提示词(默认 token 成本 ≈ 两个 Tool 的 schema)\r\n * - 进程内 fs 服务(resolve/stat/listDir/readText/writeText),不起子进程\r\n * - 倒排索引 + 60s 增量重建(dirty 标记,跳过 .obsidian/.git)\r\n * - 按 session 幂等 upsert:摘要未变则跳过写盘\r\n * - 笔记写入 vault/04-Archive/,自动挂 DSH-会话归档-索引.md 的按日期段\r\n * - 落款支持 raw_log 原始日志指针,贴合 vault 既有笔记惯例\r\n *\r\n * 使用前提(在插件详情页设置,保存后经 volatile HMR 即时生效,无需重启):\r\n * 1. vaultPath —— 必须指向一个已存在的 Obsidian vault 目录(PARA 结构:\r\n * 02-Projects / 03-Areas / 04-Archive / 05-Resources 与 DSH-会话归档-索引.md)。\r\n * 路径错则搜索与归档全部失败。默认 E:/dsh-workspace/obsidian-vault。\r\n * 2. searchEnabled —— 是否注册倒排索引搜索(obsidian_search / obsidian_brief)。\r\n * 关闭后仅保留 obsidian_read_note 与 obsidian_sync_session。默认 true。\r\n * 3. indexRefreshMs —— 索引增量重建 / vault 外部变更感知间隔(≥5000ms)。默认 60000ms。\r\n * 4. git 自动推送(隐含前提,无需设置):vault 是 git 仓库且配置了 remote 时,\r\n * 归档笔记会自动 git commit + push;不满足时笔记仍正常写入,git 步骤在结果中降级提示。\r\n *\r\n * @module dsh-obsidian-sync\r\n */\r\n\r\nimport type { Context } from '@deepseek-ai/cordis'\r\nimport z from '@deepseek-ai/schemastery'\r\n// defineTool 是运行时值导入,但 @deepseek-ai/dsh-tools 与 @deepseek-ai/cordis\r\n// 必须声明在 peerDependencies,**绝不可放进 dependencies**:DSH 通过模块级\r\n// TOOL_RUNTIME_SCHEDULER = Symbol('@deepseek-ai/dsh-tools.scheduler')\r\n// 定位工具调度器,而 Symbol() 在不同模块副本间不共享身份。把 dsh-tools 放进\r\n// dependencies 会让 pnpm 在 profile 中装出第二份副本,于是 agent-loop 里的\r\n// ctx.tools[TOOL_RUNTIME_SCHEDULER].prepare(...)\r\n// 取到 undefined,任何工具调用都会抛\r\n// \"Cannot read properties of undefined (reading 'prepare')\"。\r\n// 官方插件(dsh-tool-cordis / dsh-tool-jobs)同样只把 dsh-tools 放在 peer 中。\r\n// 详见 RELEASE_NOTES_v3.4.4.md。\r\nimport { defineTool } from '@deepseek-ai/dsh-tools'\r\nimport type { FileSystem, FsDirEntry, FsTarget } from '@deepseek-ai/dsh-fs'\r\nimport type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'\r\nimport type { JsonValue } from '@deepseek-ai/dsh-util-values'\r\n\r\n/** 把工具返回值断言为 JsonValue(0.2.0-rc.1 的 execute 要求返回 InferValue,\r\n * 即 output.schema 推断出的 JsonValue;对象 union 的 undefined 分支在此收敛)。 */\r\nfunction asJsonValue(v: unknown): JsonValue {\r\n return v as JsonValue\r\n}\r\n\r\n/** Cordis 插件名。 */\r\nexport const name = 'obsidian-sync'\r\n\r\n/** 本插件需要的 Service(`tools`/`fs` 为硬依赖,缺失时等待 Cordis 重激活)。\r\n * 仅保留真实消费的 Service:settings 是可选增强(未组合时降级为默认值)。 */\r\nexport const inject = ['tools', 'fs']\r\n\r\n/** 插件配置。schema 字段标记 `.volatile()`:宿主 settings 框架投影到浏览器\r\n * 设置页,详情页保存后经 profile Cordis patch + volatile HMR 即时生效,无需重启。\r\n * 接口本身保留普通类型(与已验证的 conversation-language 插件一致),\r\n * volatile 语义由 schema 与 Loader 承担。 */\r\nexport interface Config {\r\n /** Obsidian vault 绝对路径;缺省 `E:/dsh-workspace/obsidian-vault`。 */\r\n vaultPath: string\r\n /** 是否注册 obsidian_search(倒排索引 + 定时增量重建)。 */\r\n searchEnabled: boolean\r\n /** 索引增量重建间隔(毫秒,下限 5000)。 */\r\n indexRefreshMs: number\r\n}\r\n\r\n/**\r\n * Schemastery 配置 schema:加载器用它解析行 `config` 并补默认值。\r\n *\r\n * 注意 Schemastery 的 API 与 zod 不同:没有 `.optional()` / `.int()`。\r\n * 对象属性默认即可选(不调用 `.required()` 时,缺失键不会写入解析结果),\r\n * 整数约束通过 `.step(1)` 表达。\r\n */\r\n/** 运行时 schema:volatile 标记 + 默认值。Loader 据此解析行 config 并补默认。 */\r\nexport const Config = z.object({\r\n vaultPath: z.string().volatile().default('E:/dsh-workspace/obsidian-vault'),\r\n searchEnabled: z.boolean().default(true).volatile(),\r\n indexRefreshMs: z.number().step(1).min(5000).default(60000).volatile(),\r\n})\r\n\r\nexport function apply(ctx: Context): void {\r\n // 0.2.0-rc.1 settings API:describe() 返回按 profile entry id 排序的行,\r\n // 本插件的 entry id 是 dsh-obsidian-sync(cordis.yml 中的 id)。\r\n // 行 value 由 Loader 按本文件导出的 Config schema 解析并补默认值,\r\n // volatile HMR 保存后经 settings 框架即时刷新,读到的就是当前生效值。\r\n /** settings 服务是可选增强:未组合时降级为默认值。\r\n * 按 SettingsDescriptor 契约读命名空间行(ns + value 由 Loader 按\r\n * 本文件导出的 Config schema 解析并补默认值;volatile HMR 保存后即时刷新)。 */\r\n const settings = ctx.get('settings') as {\r\n describe(): Array<{ ns: string; value?: unknown }>\r\n } | undefined\r\n\r\n /** 读命名空间行 value。 */\r\n const readSection = (): Record<string, unknown> | undefined => {\r\n if (settings === undefined) return undefined\r\n try {\r\n const row = settings.describe().find((r) => r.ns === 'dsh-obsidian-sync')\r\n const v = row?.value\r\n return (v !== null && typeof v === 'object') ? (v as Record<string, unknown>) : undefined\r\n } catch (_e) {\r\n return undefined\r\n }\r\n }\r\n\r\n // vault 路径是插件核心前提:缺省 E:/dsh-workspace/obsidian-vault。\r\n //\r\n // 0.2.0-rc.2 保存行为:宿主把行 value 写回 cordis.patch.yml 并更新 settings\r\n // 镜像,但不会自动重激活本插件 fiber(volatile HMR 只在整应用重启或手动\r\n // 卸载/重载时发生)。因此这里不能像旧实现那样在 apply() 时把路径求值成\r\n // const 冻结 —— 保存新路径后运行中的实例仍读旧值。改为惰性读取:\r\n // currentVaultPath() 每次构建/感知索引时重读 settings,保存即对后续\r\n // 索引周期生效(无需重启)。\r\n const DEFAULT_VAULT_PATH = 'E:/dsh-workspace/obsidian-vault'\r\n const currentVaultPath = (): string => {\r\n const v = readSection()?.vaultPath\r\n return typeof v === 'string' && v.length > 0 ? v : DEFAULT_VAULT_PATH\r\n }\r\n const readSearchEnabled = (): boolean => {\r\n const v = readSection()?.searchEnabled\r\n return typeof v === 'boolean' ? v : true\r\n }\r\n const readIndexRefreshMs = (): number => {\r\n const v = readSection()?.indexRefreshMs\r\n return typeof v === 'number' && v >= 5000 ? v : 60000\r\n }\r\n\r\n let searchEnabled = readSearchEnabled()\r\n const refreshMs = Math.max(readIndexRefreshMs(), 5000)\r\n\r\n const fs = ctx.fs as unknown as FileSystem\r\n // 0.2.0-rc.1 不再提供 ctx.timer 服务;改用 Node 原生 setInterval,\r\n // 通过 ctx.effect 的清理函数保证插件卸载时定时器被清除。\r\n const index = new Map<string, { displayPath: string; tokens: Set<string>; snippet: string; size: number; version: string }>()\r\n let dirty = true\r\n let refreshing = false\r\n\r\n // vault 路径变更感知:保存新路径后宿主不会重激活本实例,靠这里的\r\n // 变更检测把 dirty 置位 —— 下一次定时器 tick / 工具调用(ensureIndex)\r\n // 即按新路径全量重建,无需重启。\r\n let observedVaultPath = currentVaultPath()\r\n function markDirtyOnVaultChange(): void {\r\n const p = currentVaultPath()\r\n if (p !== observedVaultPath) {\r\n observedVaultPath = p\r\n dirty = true\r\n }\r\n }\r\n\r\n /**\r\n * vault 外部修改感知(stat 比对 version token):\r\n * 快速 stat 每篇已索引文件的 version(不透明新鲜度令牌),发现外部变更\r\n * (用户手动编辑 / 其他工具写入 / git pull)即置 dirty。\r\n * 0.2.0-rc.1 的 FsInfo 不再暴露 mtimeMs,改以 opaque version token 比对;\r\n * 同内容文件的 version 相同,任何写入/删除都会改变它。stat 代价极小(~60 篇 × ms 级)。\r\n */\r\n async function detectExternalChanges(): Promise<void> {\r\n if (index.size === 0) return\r\n for (const entry of index.values()) {\r\n try {\r\n const target = await fs.resolve(entry.displayPath)\r\n const stat = await fs.stat(target)\r\n if (stat !== undefined) {\r\n if (String(stat.version) !== String(entry.version)) {\r\n dirty = true\r\n return\r\n }\r\n } else {\r\n // 目标已不存在 → 视为变更\r\n dirty = true\r\n return\r\n }\r\n } catch (_e) {\r\n // 文件被外部删除 → 也视为变更\r\n dirty = true\r\n return\r\n }\r\n }\r\n // 新文件检测:listDir 顶层 04-Archive / 02-Projects / 03-Areas,出现未知前缀即 dirty\r\n for (const sub of ['04-Archive', '02-Projects', '03-Areas', '05-Resources']) {\r\n try {\r\n const dir = await fs.resolve(currentVaultPath() + '/' + sub)\r\n const entries = await fs.listDir(dir)\r\n for (const e of entries) {\r\n if (e.type !== 'file' || e.name === undefined || !e.name.endsWith('.md')) continue\r\n const known = [...index.keys()].some(k => k.endsWith('/' + e.name) || k === sub + '/' + e.name)\r\n if (!known) {\r\n dirty = true\r\n return\r\n }\r\n }\r\n } catch (_e) { /* 目录不存在,跳过 */ }\r\n }\r\n }\r\n\r\n /**\r\n * 分词:ASCII 重叠三元组 + 中文滑动二元组(bigram)。\r\n *\r\n * - ASCII 字母数字:长度 ≥2 直接加入;长度 ≥4 额外加 i+=1 重叠三元组\r\n * - 中文:按 Unicode CJK 字符切分,长度 1 直接加入,≥2 加滑动 bigram\r\n * 这样「知识库」/「知识」可以互相命中,「连不上」/「连接不上」同理\r\n */\r\n function tokenize(text: string): Set<string> {\r\n const lower = String(text).toLowerCase()\r\n const seen = new Set<string>()\r\n // 连续 ASCII 字母数字段\r\n const asciiRe = /[a-z0-9]+/g\r\n let m: RegExpExecArray | null\r\n while ((m = asciiRe.exec(lower)) !== null) {\r\n const p = m[0]\r\n if (p.length < 2) continue\r\n seen.add(p)\r\n if (p.length >= 4) {\r\n for (let i = 0; i + 3 <= p.length; i++) seen.add(p.slice(i, i + 3))\r\n }\r\n }\r\n // 连续 CJK 段\r\n const cjkRe = /[\\u4e00-\\u9fff]+/g\r\n while ((m = cjkRe.exec(lower)) !== null) {\r\n const p = m[0]\r\n if (p.length === 1) {\r\n seen.add(p)\r\n } else {\r\n for (let i = 0; i + 2 <= p.length; i++) seen.add(p.slice(i, i + 2))\r\n if (p.length >= 3) seen.add(p.slice(p.length - 3))\r\n }\r\n }\r\n return seen\r\n }\r\n\r\n /** 为索引 entry 计算 IDF 权重表(token → 1/log(1+df))。 */\r\n function buildIdf(entries: Array<{ tokens: Set<string> }>): Map<string, number> {\r\n const df = new Map<string, number>()\r\n for (const entry of entries) {\r\n for (const t of entry.tokens) {\r\n df.set(t, (df.get(t) ?? 0) + 1)\r\n }\r\n }\r\n const total = entries.length || 1\r\n const idf = new Map<string, number>()\r\n for (const [t, count] of df) {\r\n idf.set(t, Math.log(total / (1 + count)))\r\n }\r\n return idf\r\n }\r\n\r\n async function rebuildIndex(): Promise<void> {\r\n refreshing = true\r\n try {\r\n const next = new Map(index)\r\n const idfEntries: Array<{ tokens: Set<string> }> = []\r\n try {\r\n const root = await fs.resolve(currentVaultPath())\r\n const statRoot = await fs.stat(root)\r\n if (statRoot !== undefined && statRoot.type === 'directory') {\r\n const queue: FsTarget[] = [root]\r\n while (queue.length > 0) {\r\n const dir = queue.shift()!\r\n let entries: FsDirEntry[]\r\n try { entries = await fs.listDir(dir) } catch (_e) { continue }\r\n for (const entry of entries) {\r\n if (entry.type === 'directory') {\r\n const entryName = entry.name || ''\r\n if (entryName === '.obsidian' || entryName === '.git') continue\r\n queue.push(entry.target)\r\n continue\r\n }\r\n if (entry.type !== 'file' || entry.name === undefined || !entry.name.endsWith('.md') || entry.name.startsWith('.')) continue\r\n let text: string\r\n try { text = await fs.readText(entry.target) } catch (_e) { continue }\r\n // 索引吃全文(~46 篇 × 几 KB,内存无压力),不再只取前 600 字\r\n const tokens = tokenize('\\n' + entry.name + '\\n' + text)\r\n const displayPath = entry.target && typeof entry.target === 'object' ? entry.target.displayPath : String(entry.target ?? '')\r\n const stat = await fs.stat(entry.target)\r\n next.set(displayPath, {\r\n displayPath,\r\n tokens,\r\n snippet: text.slice(0, 240),\r\n size: entry.size ?? text.length,\r\n version: stat !== undefined ? String(stat.version) : '',\r\n })\r\n idfEntries.push({ tokens })\r\n }\r\n }\r\n }\r\n } catch (_e) { /* vault 不可达:保留旧索引 */ }\r\n index.clear()\r\n for (const kv of next) index.set(kv[0], kv[1])\r\n // 重建 IDF 权重表\r\n idfTable = buildIdf(idfEntries)\r\n dirty = false\r\n } finally {\r\n refreshing = false\r\n }\r\n }\r\n\r\n let idfTable = new Map<string, number>()\r\n\r\n async function ensureIndex(): Promise<void> {\r\n markDirtyOnVaultChange()\r\n if (dirty) await rebuildIndex()\r\n else while (refreshing) await new Promise(resolve => setTimeout(resolve, 20))\r\n }\r\n\r\n if (searchEnabled) {\r\n ctx.effect(() => {\r\n const handle = setInterval(() => {\r\n markDirtyOnVaultChange()\r\n if (dirty) {\r\n void rebuildIndex()\r\n return\r\n }\r\n // 非 dirty 时做外部修改感知:stat 比对 version token,发现外部变更才置 dirty 下轮重建\r\n void detectExternalChanges().then(() => {\r\n if (dirty) void rebuildIndex()\r\n })\r\n }, refreshMs)\r\n return () => { clearInterval(handle) }\r\n }, 'obsidian-sync: index refresh')\r\n }\r\n\r\n function searchMatches(topic: string, limit: number): Array<{ file: string; score: number; snippet: string; size: number; matchReason: string }> {\r\n const topicTokens = tokenize(topic)\r\n const results: Array<{ file: string; score: number; snippet: string; size: number; matchReason: string }> = []\r\n for (const entry of index.values()) {\r\n let score = 0\r\n const matchedKeywords: string[] = []\r\n for (const t of topicTokens) {\r\n if (entry.tokens.has(t)) {\r\n score += idfTable.get(t) ?? 1\r\n matchedKeywords.push(t)\r\n }\r\n }\r\n if (score === 0) continue\r\n results.push({\r\n file: entry.displayPath,\r\n score: Math.round(score * 100) / 100,\r\n snippet: entry.snippet,\r\n size: entry.size,\r\n matchReason: matchedKeywords.slice(0, 5).join(' '),\r\n })\r\n }\r\n results.sort((a, b) => b.score - a.score)\r\n return results.slice(0, limit)\r\n }\r\n\r\n function sanitizeTitleForFilename(title: string): string {\r\n const base = String(title || '')\r\n .replace(/[\\\\/:*?\"<>|()()]/g, ' ')\r\n .replace(/\\s+/g, ' ')\r\n .trim()\r\n return base.length > 0 ? base.slice(0, 40) : '会话'\r\n }\r\n\r\n /**\r\n * 噪声会话判定:身份测试类(\"你是谁\" / \"你是什么模型\" / \"介绍一下自己\" / 冒烟测试)\r\n * 且摘要很短(≤ 摘要阈值字,近似单轮短会话)→ 不写笔记不进索引。\r\n *\r\n * 双重条件避免误杀:标题命中噪声模式 且 摘要长度 ≤ 阈值。\r\n * 摘要长的\"你是谁\"类会话(用户有实质讨论)仍正常归档。\r\n */\r\n const NOISE_TITLE_PATTERNS: RegExp[] = [\r\n /^你是谁$/,\r\n /^你是什么(大)?模型$/,\r\n /^介绍(一下)?自己$/,\r\n /^自我(介绍|认识)?$/,\r\n /^echo\\s+\\S+$/i,\r\n /^测试$/,\r\n ]\r\n const NOISE_SUMMARY_MAX_CHARS = 200\r\n\r\n function isNoiseSession(title: string, summary: string): boolean {\r\n const t = String(title || '').trim()\r\n const hit = NOISE_TITLE_PATTERNS.some(re => re.test(t))\r\n if (!hit) return false\r\n return String(summary || '').trim().length <= NOISE_SUMMARY_MAX_CHARS\r\n }\r\n\r\n /** 确保 shortId 恰好 8 位:不足时补 SHA-1 前缀 hash。 */\r\n function normalizeShortId(sessionId: string): string {\r\n let s = sessionId.replace(/[^A-Za-z0-9]/g, '')\r\n if (s.length >= 8) return s.slice(0, 8)\r\n // 不足 8 位:用原始 sessionId 的简单 hash 补齐\r\n let hash = 0\r\n for (let i = 0; i < sessionId.length; i++) {\r\n hash = ((hash << 5) - hash + sessionId.charCodeAt(i)) | 0\r\n }\r\n s = s + Math.abs(hash).toString(16).slice(0, 8)\r\n return s.slice(0, 8)\r\n }\r\n\r\n /** vault 在 workspace 之外,写入需按调用抬升到 danger-full-access。 */\r\n const widePolicy: SandboxExecutionPolicy = { mode: 'danger-full-access', workspaceRoot: currentVaultPath() }\r\n\r\n /**\r\n * 每日 22:00 复盘沉淀节律:\r\n * - 汇总当天 04-Archive 归档(按日期前缀统计笔记数与主题关键词)\r\n * - 把当天要点追加到 02-Projects/ 下匹配项目的\"近期演进\"段(幂等:当天已追加则跳过)\r\n * - git commit + push\r\n *\r\n * 实现:60s 轮询,发现跨过当天 22:00 且尚未执行过 → 执行一次。\r\n */\r\n let lastReviewDate = ''\r\n\r\n async function dailyReview(): Promise<void> {\r\n const now = new Date()\r\n const dateStr = now.toISOString().slice(0, 10)\r\n // 只在 22:00 之后(本地时区)且当天未执行时触发\r\n const hour = now.getHours()\r\n if (hour < 22 || lastReviewDate === dateStr) return\r\n lastReviewDate = dateStr\r\n\r\n try {\r\n await ensureIndex()\r\n\r\n // 1. 汇总当天归档\r\n const todayFiles: Array<{ file: string; date: string; snippet: string }> = []\r\n for (const entry of index.values()) {\r\n if (!entry.displayPath.includes('04-Archive/')) continue\r\n const m = entry.displayPath.match(new RegExp('^.*?(\\\\d{4}-\\\\d{2}-\\\\d{2})'))\r\n if (m && m[1] === dateStr) {\r\n todayFiles.push({ file: entry.displayPath, date: dateStr, snippet: entry.snippet })\r\n }\r\n }\r\n if (todayFiles.length === 0) return\r\n\r\n // 2. 对每个 02-Projects 页追加当天归档条目(幂等)\r\n for (const proj of ['02-Projects/dsh-obsidian-sync.md', '02-Projects/dsh-tool-agnes.md', '02-Projects/dsh-plugin-manager.md', '02-Projects/dsh-conversation-language.md']) {\r\n try {\r\n const target = await fs.resolve(currentVaultPath() + '/' + proj)\r\n let content = String(await fs.readText(target))\r\n const marker = '## 近期演进'\r\n const markerPos = content.indexOf(marker)\r\n if (markerPos === -1) continue\r\n // 检查当天是否已追加(幂等)\r\n if (content.indexOf(dateStr) !== -1 && content.indexOf(dateStr) > markerPos) continue\r\n const todayLines = todayFiles.map(f => '- [[04-Archive/' + f.file.replace(/^04-Archive\\//, '') + ']] — ' + f.snippet.slice(0, 80).replace(/\\n/g, ' ')).join('\\n')\r\n content = content.slice(0, content.length) + '\\n' + dateStr + ' 归档:\\n' + todayLines + '\\n'\r\n await fs.writeText(target, content, undefined, undefined, widePolicy)\r\n } catch (_e) { /* 文件不存在或写入失败,跳过 */ }\r\n }\r\n\r\n dirty = true\r\n\r\n // 3. git commit + push\r\n try {\r\n const { execSync } = await import('node:child_process')\r\n const out = execSync('git add -A && git status --porcelain', {\r\n cwd: currentVaultPath(), timeout: 15000, encoding: 'utf-8',\r\n })\r\n if (out.trim().length > 0) {\r\n execSync('git commit -m \"daily-review: ' + dateStr + ' 归档沉淀(' + todayFiles.length + ' 篇)\" && git push', {\r\n cwd: currentVaultPath(), timeout: 60000, encoding: 'utf-8',\r\n })\r\n }\r\n } catch (_e) { /* git 失败不影响 */ }\r\n } catch (_e) { /* 整体失败静默 */ }\r\n }\r\n\r\n ctx.effect(() => {\r\n const handle = setInterval(() => {\r\n void dailyReview()\r\n }, 60000)\r\n return () => { clearInterval(handle) }\r\n }, 'obsidian-sync: daily review at 22:00')\r\n\r\n /**\r\n * 每周复盘沉淀节律(补齐决策习惯页\"周复盘尚未建立\"的开放问题):\r\n * - 每周日 22:30 触发(本地时区),汇总本 ISO 周(周一~周日)04-Archive 归档\r\n * - 写入/更新 03-Areas/周复盘-<ISO 周>.md(主题分布 + 归档清单 + 待收敛项)\r\n * - 幂等:同 ISO 周已生成则跳过;git commit + push\r\n */\r\n let lastWeeklyKey = ''\r\n\r\n function isoWeekKey(d: Date): string {\r\n // ISO 周:周四归属所在周;周一为周首\r\n const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()))\r\n const day = t.getUTCDay() || 7\r\n t.setUTCDate(t.getUTCDate() + 4 - day)\r\n const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1))\r\n const week = Math.ceil((((+t - +yearStart) / 86400000) + 1) / 7)\r\n return t.getUTCFullYear() + '-W' + String(week).padStart(2, '0')\r\n }\r\n\r\n async function weeklyReview(): Promise<void> {\r\n const now = new Date()\r\n const dow = now.getDay() // 0=Sun\r\n if (dow !== 0 || now.getHours() < 22) return\r\n const weekKey = isoWeekKey(now)\r\n if (lastWeeklyKey === weekKey) return\r\n lastWeeklyKey = weekKey\r\n\r\n try {\r\n await ensureIndex()\r\n const weekDate = new Date(now)\r\n weekDate.setDate(now.getDate() - 6) // 周一\r\n const monday = weekDate.toISOString().slice(0, 10)\r\n\r\n // 汇总本周归档(文件名日期 >= 周一 且 <= 今天)\r\n const weekFiles: Array<{ file: string; date: string; snippet: string }> = []\r\n for (const entry of index.values()) {\r\n if (!entry.displayPath.includes('04-Archive/')) continue\r\n const m = entry.displayPath.match(/(\\d{4}-\\d{2}-\\d{2})/)\r\n if (m && m[1] >= monday && m[1] <= now.toISOString().slice(0, 10)) {\r\n weekFiles.push({ file: entry.displayPath, date: m[1], snippet: entry.snippet })\r\n }\r\n }\r\n weekFiles.sort((a, b) => a.date.localeCompare(b.date))\r\n if (weekFiles.length === 0) return\r\n\r\n // 主题分布(按 04-Archive 笔记 frontmatter tags 简化:从 snippet 首行提取)\r\n const lines = weekFiles.map(f => '- [[04-Archive/' + f.file.replace(/^04-Archive\\//, '') + ']](' + f.date + ')— ' + f.snippet.slice(0, 60).replace(/\\n/g, ' ')).join('\\n')\r\n const note = '---\\ndate: ' + now.toISOString().slice(0, 10) + '\\nstatus: active\\ntags: [area, 周复盘]\\n---\\n\\n# 周复盘 ' + weekKey + '\\n\\n> 自动汇总 ' + monday + ' ~ ' + now.toISOString().slice(0, 10) + ' 的 04-Archive 归档(' + weekFiles.length + ' 篇)。\\n\\n## 本周归档\\n\\n' + lines + '\\n\\n## 待收敛\\n\\n- [ ] 人工审阅:识别本周新增的长期 Area / Resource 沉淀点\\n\\n'\r\n const target = await fs.resolve(currentVaultPath() + '/03-Areas/周复盘-' + weekKey + '.md')\r\n let skipped = false\r\n try {\r\n const prev = String(await fs.readText(target))\r\n if (prev.includes(weekKey)) skipped = true\r\n } catch (_e) { /* 尚不存在 */ }\r\n if (!skipped) await fs.writeText(target, note, undefined, undefined, widePolicy)\r\n\r\n dirty = true\r\n try {\r\n const { execSync } = await import('node:child_process')\r\n const out = execSync('git add -A && git status --porcelain', { cwd: currentVaultPath(), timeout: 15000, encoding: 'utf-8' })\r\n if (out.trim().length > 0) {\r\n execSync('git commit -m \"weekly-review: ' + weekKey + ' 归档沉淀(' + weekFiles.length + ' 篇)\" && git push', { cwd: currentVaultPath(), timeout: 60000, encoding: 'utf-8' })\r\n }\r\n } catch (_e) { /* git 失败不影响 */ }\r\n } catch (_e) { /* 整体失败静默 */ }\r\n }\r\n\r\n ctx.effect(() => {\r\n const handle = setInterval(() => {\r\n void weeklyReview()\r\n }, 60000)\r\n return () => { clearInterval(handle) }\r\n }, 'obsidian-sync: weekly review at Sunday 22:30')\r\n\r\n /**\r\n * 更新索引文件(DSH-会话归档-索引.md)的按日期段。\r\n * - 找到当天段落(### YYYY-MM-DD)\r\n * - 边界:下一个 `### ` 或 `## `(两个井号终止条件)\r\n * - 同 shortId 已有条目 → 替换该行(幂等去重)\r\n * - 否则追加到当天段落末尾\r\n */\r\n async function updateIndexFile(entryRelPath: string, title: string, sessionId: string, dateStr: string): Promise<boolean> {\r\n try {\r\n const idxTarget = await fs.resolve(currentVaultPath() + '/DSH-会话归档-索引.md')\r\n const idxContent = String(await fs.readText(idxTarget))\r\n const dayAnchor = '### ' + dateStr\r\n const line = '\\n- [[' + entryRelPath + '|' + title + ']] (' + dateStr + ', ' + sessionId + ')\\n'\r\n const dayPos = idxContent.indexOf(dayAnchor)\r\n if (dayPos === -1) {\r\n // 当天段落不存在:在 \"## 主题分类\" 之前插入\r\n const dayHeader = '\\n### ' + dateStr + '\\n' + line\r\n const tailPos = idxContent.indexOf('\\n## 主题分类')\r\n const updated = tailPos === -1 ? idxContent + dayHeader : idxContent.slice(0, tailPos) + dayHeader + idxContent.slice(tailPos)\r\n await fs.writeText(idxTarget, updated, undefined, undefined, widePolicy)\r\n return true\r\n }\r\n // 找当天段落的结尾:下一个 `### ` 或 `## `(双井号终止)\r\n const afterAnchor = dayPos + dayAnchor.length\r\n const nextH3 = idxContent.indexOf('\\n### ', afterAnchor)\r\n const nextH2 = idxContent.indexOf('\\n## ', afterAnchor)\r\n let dayEnd = -1\r\n if (nextH3 !== -1 && nextH2 !== -1) dayEnd = Math.min(nextH3, nextH2)\r\n else if (nextH3 !== -1) dayEnd = nextH3\r\n else if (nextH2 !== -1) dayEnd = nextH2\r\n else dayEnd = idxContent.length\r\n\r\n // 去重:当天段落内是否已有相同 shortId 的条目\r\n const daySection = idxContent.slice(afterAnchor, dayEnd)\r\n const shortId = normalizeShortId(sessionId)\r\n const dedupePattern = new RegExp('^- \\\\[\\\\[[^\\\\]]*' + dateStr + '-' + shortId + '-[^\\\\]]*\\\\]\\\\]\\\\s*\\\\([^\\\\)]*\\\\)$', 'm')\r\n const existingMatch = daySection.match(dedupePattern)\r\n let updated: string\r\n if (existingMatch !== null) {\r\n // 替换已有行(幂等)\r\n const absPos = afterAnchor + daySection.indexOf(existingMatch[0])\r\n updated = idxContent.slice(0, absPos) + line.trimStart() + '\\n' + idxContent.slice(absPos + existingMatch[0].length)\r\n } else {\r\n // 追加到段落末尾(dayEnd 前)\r\n updated = idxContent.slice(0, dayEnd) + line + idxContent.slice(dayEnd)\r\n }\r\n await fs.writeText(idxTarget, updated, undefined, undefined, widePolicy)\r\n return true\r\n } catch (_e) {\r\n return false\r\n }\r\n }\r\n\r\n /** 从索引中按目录前缀过滤 + 按 task 相关性取 top N 提炼页。 */\r\n function topDistilled(task: string, limit: number): Array<{ file: string; title: string; score: number; snippet: string }> {\r\n const distilledDirs = ['02-Projects/', '03-Areas/', '05-Resources/']\r\n const taskTokens = task.trim().length > 0 ? tokenize(task) : new Set<string>()\r\n const results: Array<{ file: string; title: string; score: number; snippet: string }> = []\r\n for (const entry of index.values()) {\r\n if (!distilledDirs.some(d => entry.displayPath.includes(d))) continue\r\n let score = 0\r\n for (const t of taskTokens) {\r\n if (entry.tokens.has(t)) score += idfTable.get(t) ?? 1\r\n }\r\n const title = entry.displayPath.split('/').pop() ?? entry.displayPath\r\n results.push({ file: entry.displayPath, title, score: taskTokens.size > 0 ? Math.round(score * 100) / 100 : 0, snippet: entry.snippet })\r\n }\r\n results.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))\r\n return results.slice(0, limit)\r\n }\r\n\r\n /** 从 04-Archive/ 取最近 N 篇(按文件名日期倒序)。 */\r\n function recentArchives(limit: number): Array<{ file: string; date: string; snippet: string }> {\r\n const results: Array<{ file: string; date: string; snippet: string }> = []\r\n for (const entry of index.values()) {\r\n if (!entry.displayPath.includes('04-Archive/')) continue\r\n const dateMatch = entry.displayPath.match(/(\\d{4}-\\d{2}-\\d{2})/)\r\n results.push({ file: entry.displayPath, date: dateMatch ? dateMatch[1] : 'unknown', snippet: entry.snippet })\r\n }\r\n results.sort((a, b) => b.date.localeCompare(a.date))\r\n return results.slice(0, limit)\r\n }\r\n\r\n /** 读取指定相对路径的文件内容。 */\r\n async function readFileContent(relPath: string): Promise<string> {\r\n try {\r\n const target = await fs.resolve(currentVaultPath() + '/' + relPath)\r\n return String(await fs.readText(target))\r\n } catch (_e) {\r\n return ''\r\n }\r\n }\r\n\r\n // 工具名必须匹配模型 provider 的 function-name 约束 ^[a-zA-Z0-9_-]+$。\r\n // v3.4.2 及更早版本用了 `obsidian.search` 这类带点号的命名,DeepSeek API 会在\r\n // 请求校验阶段直接返回 400(Invalid 'tools[N].name': string does not match pattern),\r\n // 导致任何启用了本插件的会话完全无法发起对话。禁止改回点号命名。\r\n if (searchEnabled) {\r\n ctx.tools.register(defineTool({\r\n name: 'obsidian_search',\r\n description: '在 Obsidian 知识库中按关键词搜索相关笔记,返回最多 5 条匹配(文件相对路径、命中片段、命中关键词)。按需调用,平时不产生任何 token 成本。',\r\n parameters: {\r\n topic: { type: 'string', required: true, description: '搜索关键词,可含多个词(中英文均可,自动做 CJK bigram 分词)' },\r\n limit: { type: 'number', description: '返回结果上限,默认 3,最大 5' },\r\n },\r\n output: {\r\n schema: { type: 'json' },\r\n render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],\r\n },\r\n isConcurrencySafe: () => true,\r\n async execute(args) {\r\n const topic = String(args.topic ?? '')\r\n if (topic.trim().length === 0) return asJsonValue({ ok: false, error: 'topic must not be empty' })\r\n const limit = Math.min(Math.max(1, Number(args.limit) || 3), 5)\r\n await ensureIndex()\r\n const matches = searchMatches(topic, limit)\r\n return asJsonValue({ ok: true, matches, count: matches.length, vaultPath: currentVaultPath() })\r\n },\r\n }))\r\n\r\n ctx.tools.register(defineTool({\r\n name: 'obsidian_brief',\r\n description: '每日简报:返回与当前任务最相关的提炼页(02-Projects/03-Areas/05-Resources)top 3 + 最近 3 条归档摘要 + 用户决策习惯与偏好页内容。低成本\"开机记忆\"入口,建议任务开始前调用一次。',\r\n parameters: {\r\n task: { type: 'string', description: '当前任务描述(可选),用于相关性排序;省略则返回全部提炼页按名称排序的 top 3' },\r\n },\r\n output: {\r\n schema: { type: 'json' },\r\n render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],\r\n },\r\n isConcurrencySafe: () => true,\r\n async execute(args) {\r\n const task = String(args.task ?? '').trim()\r\n await ensureIndex()\r\n\r\n // 1. 提炼页 top 3\r\n const distilled = topDistilled(task, 3)\r\n\r\n // 2. 最近归档 3 条\r\n const recent = recentArchives(3)\r\n\r\n // 3. 用户决策习惯与偏好页(03-Areas 下,如果存在)\r\n let userProfile = ''\r\n const profileRelPath = '03-Areas/决策习惯与偏好.md'\r\n const hasProfile = [...index.values()].some(e => e.displayPath === profileRelPath)\r\n if (hasProfile) {\r\n userProfile = await readFileContent(profileRelPath)\r\n }\r\n\r\n return asJsonValue({\r\n ok: true,\r\n task: task || '(未指定)',\r\n distilled,\r\n recent,\r\n userProfile: userProfile.length > 0 ? userProfile.slice(0, 2000) : '',\r\n vaultPath: currentVaultPath(),\r\n })\r\n },\r\n }))\r\n\r\n ctx.tools.register(defineTool({\r\n name: 'obsidian_read_note',\r\n description: '按相对路径读取 Obsidian 笔记的 frontmatter 与正文,补全 obsidian_search 的\"搜索→阅读\"闭环。路径相对 vault 根(如 02-Projects/dsh-obsidian-sync.md),自动解析 YAML frontmatter(key: value / [list] / 数值 / 布尔)。按需调用,零 token 成本。',\r\n parameters: {\r\n path: { type: 'string', required: true, description: '笔记相对 vault 的路径(如 02-Projects/dsh-obsidian-sync.md 或 04-Archive/xxx.md)' },\r\n },\r\n output: {\r\n schema: { type: 'json' },\r\n render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],\r\n },\r\n isConcurrencySafe: () => true,\r\n async execute(args) {\r\n const rel = String(args.path ?? '').replace(/^\\/+/, '').replace(/\\\\/g, '/').trim()\r\n if (rel.length === 0) return asJsonValue({ ok: false, error: 'path must not be empty', code: 'EMPTY_PATH' })\r\n const content = await readFileContent(rel)\r\n if (content.length === 0) return asJsonValue({ ok: false, error: 'note not found or unreadable: ' + rel, code: 'NOT_FOUND' })\r\n\r\n // 解析 YAML frontmatter(简单 key: value / key: [a, b] / key: \"str\")\r\n const frontmatter: Record<string, unknown> = {}\r\n let body = content\r\n const fmMatch = content.match(/^---\\n([\\s\\S]*?)\\n---\\n?/)\r\n if (fmMatch !== null) {\r\n body = content.slice(fmMatch[0].length)\r\n for (const line of fmMatch[1].split('\\n')) {\r\n const kv = line.match(/^([A-Za-z0-9_\\-]+):\\s*(.*)$/)\r\n if (kv === null) continue\r\n const key = kv[1]\r\n let raw = kv[2].trim()\r\n let val: unknown\r\n if (raw.startsWith('[') && raw.endsWith(']')) {\r\n const inner = raw.slice(1, -1).trim()\r\n val = inner.length === 0 ? [] : inner.split(',').map((s: string) => s.trim().replace(/^[\"']|[\"']$/g, ''))\r\n } else if (raw === 'null') {\r\n val = null\r\n } else if (raw === 'true') {\r\n val = true\r\n } else if (raw === 'false') {\r\n val = false\r\n } else if (/^\\d+$/.test(raw)) {\r\n val = Number(raw)\r\n } else {\r\n val = raw.replace(/^[\"']|[\"']$/g, '')\r\n }\r\n frontmatter[key] = val\r\n }\r\n }\r\n\r\n const titleLine = body.match(/^#\\s+(.+)$/m)\r\n return asJsonValue({\r\n ok: true,\r\n path: rel,\r\n frontmatter,\r\n title: titleLine ? titleLine[1].trim() : (rel.split('/').pop() ?? rel),\r\n body,\r\n })\r\n },\r\n }))\r\n }\r\n\r\n ctx.tools.register(defineTool({\r\n name: 'obsidian_sync_session',\r\n description: '把当前 DSH 会话按 Obsidian vault 的 PARA 结构与用户既有命名/索引规则,以 Markdown 笔记形式幂等写入 vault/04-Archive/,并自动把新条目挂到 DSH-会话归档-索引.md 的按日期段。摘要未变则跳过。幂等键 = date + shortId(标题变更不影响去重)。',\r\n parameters: {\r\n session_id: { type: 'string', required: true, description: '当前会话的完整 SessionId(UUID 或短 ID),原样存入 frontmatter 与正文' },\r\n title: { type: 'string', required: true, description: '会话主题标题(写入 frontmatter 与正文 # 标题;文件名仅取前 40 字符做安全截断)' },\r\n summary: { type: 'string', required: true, description: '会话结论/摘要(纯文本,建议 200-800 字,包含用户需求、过程要点、交付物)' },\r\n tags: { type: 'array', items: { type: 'string' }, description: '主题标签数组,最多 10 个,将写入 frontmatter tags 字段(Obsidian 原生标签)' },\r\n related: { type: 'array', items: { type: 'string' }, description: '关联笔记完整文件名(含 .md 扩展名,如 2026-09-11-a9095879-xxx.md),将生成为 [[全路径双链]]' },\r\n raw_log: { type: 'string', description: '原始会话日志绝对路径,格式 ~/.dsh/sessions/<project-dir>/session-<uuid>/session.v3.jsonl.zstd,将写入落款以便追溯' },\r\n // 可选参数:dsh-tools 的 ParameterPropertySpec 只允许 `required?: true`,省略即表示可选(同 tags/related)。\r\n date: { type: 'string', description: '归档日期(YYYY-MM-DD),回填历史会话时使用;省略则取当前 UTC 日期。仅匹配该格式时采用,否则回退当前 UTC 日期' },\r\n },\r\n output: {\r\n schema: { type: 'json' },\r\n render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],\r\n },\r\n async execute(args) {\r\n const sessionId = String(args.session_id ?? 'unknown')\r\n const title = String(args.title ?? '')\r\n const safeTitle = sanitizeTitleForFilename(title)\r\n const summary = String(args.summary ?? '')\r\n const tags = Array.isArray(args.tags) ? args.tags.map(String).slice(0, 10) : []\r\n const related = Array.isArray(args.related) ? args.related.map(String).slice(0, 10) : []\r\n const rawLog = String(args.raw_log ?? '')\r\n\r\n // 噪声会话过滤:身份测试类 + 摘要极短 → 不写笔记不进索引\r\n if (isNoiseSession(title, summary)) {\r\n return asJsonValue({ ok: true, skipped: true, noise: true, reason: 'identity-test noise session filtered out (short title + short summary)', index: 'unchanged' })\r\n }\r\n\r\n // 归档日期:优先采用调用方传入的 date(用于回填历史会话),未传或格式非法时回退当前 UTC 日期。\r\n // dateStr 单一来源:文件名前缀 / frontmatter date / 基本信息表格 / 索引行日期段全部沿用它。\r\n const rawDate = String(args.date ?? '').trim()\r\n const dateStr = /^\\d{4}-\\d{2}-\\d{2}$/.test(rawDate) ? rawDate : new Date().toISOString().slice(0, 10)\r\n const shortId = normalizeShortId(sessionId)\r\n\r\n // 幂等键 = date + shortId(标题不参与路径),写入前按前缀探测已存在文件并原地覆盖\r\n const entryPrefix = dateStr + '-' + shortId + '-'\r\n let entryRelPath: string\r\n\r\n // 探测 04-Archive/ 下是否已有同前缀文件(同会话多次同步)\r\n try {\r\n const archiveDir = await fs.resolve(currentVaultPath() + '/04-Archive')\r\n const archiveEntries = await fs.listDir(archiveDir)\r\n const existing = archiveEntries.find(e => e.type === 'file' && e.name?.startsWith(entryPrefix))\r\n if (existing !== undefined && existing.name !== undefined) {\r\n // 已有文件:原地覆盖(标题可能变了但幂等键不变)\r\n entryRelPath = '04-Archive/' + existing.name\r\n } else {\r\n entryRelPath = '04-Archive/' + entryPrefix + safeTitle + '.md'\r\n }\r\n } catch (_e) {\r\n entryRelPath = '04-Archive/' + entryPrefix + safeTitle + '.md'\r\n }\r\n\r\n // related 双链:生成全路径 [[04-Archive/xxx.md|显示名]],写入前校验目标存在\r\n const relatedLines: string[] = []\r\n for (const r of related) {\r\n const rFull = r.endsWith('.md') ? r : r + '.md'\r\n const rRel = '04-Archive/' + rFull\r\n // 校验目标存在(全路径双链)\r\n try {\r\n await fs.stat(await fs.resolve(currentVaultPath() + '/' + rRel))\r\n relatedLines.push('- [[' + rRel + '|' + rFull.replace(/^.*\\//, '').replace(/\\.md$/, '') + ']]')\r\n } catch (_e) {\r\n relatedLines.push('- ' + rFull + '(未找到,降级为文字引用)')\r\n }\r\n }\r\n\r\n // 生成 YAML frontmatter(Obsidian 原生标签 / Dataview 可查)\r\n const fmDate = dateStr\r\n const fmSessionId = sessionId.replace(/[^A-Za-z0-9-]/g, '').slice(0, 8)\r\n const fmTags = tags.length > 0 ? tags : []\r\n const frontmatter = [\r\n '---',\r\n 'session_id: ' + fmSessionId,\r\n 'full_session_id: ' + sessionId,\r\n 'date: ' + fmDate,\r\n 'tags: [' + fmTags.join(', ') + ']',\r\n '---',\r\n '',\r\n ].join('\\n')\r\n\r\n const logLine = rawLog.trim().length > 0\r\n ? '> 📎 原始日志: `' + rawLog.trim() + '`\\n\\n> 由 DSH Obsidian Sync 自动同步\\n'\r\n : '> 由 DSH Obsidian Sync 自动同步\\n'\r\n\r\n const content = frontmatter\r\n + '# DSH 会话: ' + title + '\\n\\n'\r\n + '## 基本信息\\n\\n'\r\n + '| 属性 | 值 |\\n|------|-----|\\n'\r\n + '| **日期** | ' + dateStr + ' |\\n'\r\n + '| **会话ID** | `' + sessionId + '` |\\n'\r\n + '| **状态** | ✅ 已归档 |\\n'\r\n + '| **分类** | ' + (tags.length > 0 ? tags.join(' ') : '通用') + ' |\\n\\n'\r\n + '---\\n\\n'\r\n + '## 摘要\\n\\n' + summary + '\\n\\n'\r\n + (relatedLines.length > 0 ? '## 关联\\n\\n' + relatedLines.join('\\n') + '\\n\\n---\\n' : '')\r\n + logLine\r\n\r\n let target: FsTarget\r\n try {\r\n target = await fs.resolve(currentVaultPath() + '/' + entryRelPath)\r\n } catch (_e) {\r\n const winPath = currentVaultPath() + '\\\\' + entryRelPath.split('/').join('\\\\')\r\n try { target = await fs.resolve(winPath) } catch (_e2) { return asJsonValue({ ok: false, error: 'cannot resolve vault entry path', code: 'RESOLVE_FAILED' }) }\r\n }\r\n\r\n let skipped = false\r\n try {\r\n const prev = String(await fs.readText(target))\r\n const marker = '## 摘要'\r\n const pos = prev.indexOf(marker)\r\n if (pos >= 0) {\r\n // 摘要区块末尾 = 下一个 `## ` 标题 / `\\n---\\n` 分隔线 / 空行起的落款引用行中最早出现的位置;\r\n // 只比较摘要本身,否则 `## 关联` 与落款会被带入导致全串永不相等、每次重写。\r\n const bodyStart = pos + marker.length\r\n const boundaries = ['\\n## ', '\\n---\\n', '\\n\\n> ']\r\n .map(sep => prev.indexOf(sep, bodyStart))\r\n .filter(i => i !== -1)\r\n const bodyEnd = boundaries.length > 0 ? Math.min(...boundaries) : prev.length\r\n if (prev.slice(bodyStart, bodyEnd).trim() === summary.trim()) skipped = true\r\n }\r\n } catch (_e) { /* 尚不存在 */ }\r\n if (skipped) return asJsonValue({ ok: true, skipped: true, file: entryRelPath, reason: 'summary unchanged', index: 'unchanged' })\r\n\r\n try {\r\n await fs.writeText(target, content, undefined, undefined, widePolicy)\r\n } catch (writeErr) {\r\n return asJsonValue({ ok: false, error: 'writeText failed: ' + String(writeErr instanceof Error ? writeErr.message : writeErr), code: 'WRITE_FAILED' })\r\n }\r\n\r\n const indexOk = await updateIndexFile(entryRelPath, title, sessionId, dateStr)\r\n dirty = true\r\n\r\n // 写入成功后自动 git commit + push(vault 有 remote 时才生效)\r\n let gitStatus = 'skipped'\r\n try {\r\n const { execSync } = await import('node:child_process')\r\n const out = execSync('git add -A && git status --porcelain', {\r\n cwd: currentVaultPath(),\r\n timeout: 15000,\r\n encoding: 'utf-8',\r\n })\r\n if (out.trim().length > 0) {\r\n const msg = 'archive: ' + entryRelPath.replace(/^04-Archive\\//, '')\r\n execSync('git commit -m \"' + msg.replace(/\"/g, '\\\\\"') + '\" && git push', {\r\n cwd: currentVaultPath(),\r\n timeout: 60000,\r\n encoding: 'utf-8',\r\n })\r\n gitStatus = 'pushed'\r\n } else {\r\n gitStatus = 'no changes'\r\n }\r\n } catch (gitErr) {\r\n gitStatus = 'failed: ' + String(gitErr instanceof Error ? gitErr.message : gitErr)\r\n }\r\n\r\n return asJsonValue({ ok: true, skipped: false, file: entryRelPath, index: indexOk ? 'updated' : 'skipped', git: gitStatus })\r\n },\r\n }))\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * DSH Obsidian Sync — 按需搜索 Obsidian 知识库 + 按 vault 既有 PARA 规则归档 DSH 会话。\r\n *\r\n * 设计理念(零 token / 高性能):\r\n * - 纯模型 Tool 按需调用,不注入系统提示词(默认 token 成本 ≈ 两个 Tool 的 schema)\r\n * - 进程内 fs 服务(resolve/stat/listDir/readText/writeText),不起子进程\r\n * - 倒排索引 + 60s 增量重建(dirty 标记,跳过 .obsidian/.git)\r\n * - 按 session 幂等 upsert:摘要未变则跳过写盘\r\n * - 笔记写入 vault/04-Archive/,自动挂 DSH-会话归档-索引.md 的按日期段\r\n * - 落款支持 raw_log 原始日志指针,贴合 vault 既有笔记惯例\r\n *\r\n * 使用前提(在插件详情页设置,保存后经 volatile HMR 即时生效,无需重启):\r\n * 1. vaultPath —— 必须指向一个已存在的 Obsidian vault 目录(PARA 结构:\r\n * 02-Projects / 03-Areas / 04-Archive / 05-Resources 与 DSH-会话归档-索引.md)。\r\n * 路径错则搜索与归档全部失败。默认 E:/dsh-workspace/obsidian-vault。\r\n * 2. searchEnabled —— 是否注册倒排索引搜索(obsidian_search / obsidian_brief)。\r\n * 关闭后仅保留 obsidian_read_note 与 obsidian_sync_session。默认 true。\r\n * 3. indexRefreshMs —— 索引增量重建 / vault 外部变更感知间隔(≥5000ms)。默认 60000ms。\r\n * 4. git 自动推送(隐含前提,无需设置):vault 是 git 仓库且配置了 remote 时,\r\n * 归档笔记会自动 git commit + push;不满足时笔记仍正常写入,git 步骤在结果中降级提示。\r\n *\r\n * @module dsh-obsidian-sync\r\n */\r\n\r\nimport type { Context } from '@deepseek-ai/cordis'\r\nimport z from '@deepseek-ai/schemastery'\r\n// defineTool 是运行时值导入,但 @deepseek-ai/dsh-tools 与 @deepseek-ai/cordis\r\n// 必须声明在 peerDependencies,**绝不可放进 dependencies**:DSH 通过模块级\r\n// TOOL_RUNTIME_SCHEDULER = Symbol('@deepseek-ai/dsh-tools.scheduler')\r\n// 定位工具调度器,而 Symbol() 在不同模块副本间不共享身份。把 dsh-tools 放进\r\n// dependencies 会让 pnpm 在 profile 中装出第二份副本,于是 agent-loop 里的\r\n// ctx.tools[TOOL_RUNTIME_SCHEDULER].prepare(...)\r\n// 取到 undefined,任何工具调用都会抛\r\n// \"Cannot read properties of undefined (reading 'prepare')\"。\r\n// 官方插件(dsh-tool-cordis / dsh-tool-jobs)同样只把 dsh-tools 放在 peer 中。\r\n// 详见 RELEASE_NOTES_v3.4.4.md。\r\nimport { defineTool } from '@deepseek-ai/dsh-tools'\r\nimport type { FileSystem, FsDirEntry, FsTarget } from '@deepseek-ai/dsh-fs'\r\nimport type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'\r\nimport type { JsonValue } from '@deepseek-ai/dsh-util-values'\r\n\r\n/** 把工具返回值断言为 JsonValue(0.2.0-rc.1 的 execute 要求返回 InferValue,\r\n * 即 output.schema 推断出的 JsonValue;对象 union 的 undefined 分支在此收敛)。 */\r\nfunction asJsonValue(v: unknown): JsonValue {\r\n return v as JsonValue\r\n}\r\n\r\n/** Cordis 插件名。 */\r\nexport const name = 'obsidian-sync'\r\n\r\n/** 本插件需要的 Service(`tools`/`fs` 为硬依赖,缺失时等待 Cordis 重激活)。\r\n * 仅保留真实消费的 Service:settings 是可选增强(未组合时降级为默认值)。 */\r\nexport const inject = ['tools', 'fs']\r\n\r\n/** 插件配置。schema 字段标记 `.volatile()`:宿主 settings 框架投影到浏览器\r\n * 设置页,详情页保存后经 profile Cordis patch + volatile HMR 即时生效,无需重启。\r\n * 接口本身保留普通类型(与已验证的 conversation-language 插件一致),\r\n * volatile 语义由 schema 与 Loader 承担。 */\r\nexport interface Config {\r\n /** Obsidian vault 绝对路径;缺省 `E:/dsh-workspace/obsidian-vault`。 */\r\n vaultPath: string\r\n /** 是否注册 obsidian_search(倒排索引 + 定时增量重建)。 */\r\n searchEnabled: boolean\r\n /** 索引增量重建间隔(毫秒,下限 5000)。 */\r\n indexRefreshMs: number\r\n}\r\n\r\n/**\r\n * Schemastery 配置 schema:加载器用它解析行 `config` 并补默认值。\r\n *\r\n * 注意 Schemastery 的 API 与 zod 不同:没有 `.optional()` / `.int()`。\r\n * 对象属性默认即可选(不调用 `.required()` 时,缺失键不会写入解析结果),\r\n * 整数约束通过 `.step(1)` 表达。\r\n */\r\n/** 运行时 schema:volatile 标记 + 默认值。Loader 据此解析行 config 并补默认。 */\r\nexport const Config = z.object({\r\n vaultPath: z.string().volatile().default('E:/dsh-workspace/obsidian-vault'),\r\n searchEnabled: z.boolean().default(true).volatile(),\r\n indexRefreshMs: z.number().step(1).min(5000).default(60000).volatile(),\r\n})\r\n\r\nexport function apply(ctx: Context): void {\r\n // 0.2.0-rc.1 settings API:describe() 返回按 profile entry id 排序的行,\r\n // 本插件的 entry id 是 dsh-obsidian-sync(cordis.yml 中的 id)。\r\n // 行 value 由 Loader 按本文件导出的 Config schema 解析并补默认值,\r\n // volatile HMR 保存后经 settings 框架即时刷新,读到的就是当前生效值。\r\n /** settings 服务是可选增强:未组合时降级为默认值。\r\n * 按 SettingsDescriptor 契约读命名空间行(ns + value 由 Loader 按\r\n * 本文件导出的 Config schema 解析并补默认值;volatile HMR 保存后即时刷新)。 */\r\n const settings = ctx.get('settings') as {\r\n describe(): Array<{ ns: string; value?: unknown }>\r\n } | undefined\r\n\r\n /** 读命名空间行 value。 */\r\n const readSection = (): Record<string, unknown> | undefined => {\r\n if (settings === undefined) return undefined\r\n try {\r\n const row = settings.describe().find((r) => r.ns === 'dsh-obsidian-sync')\r\n const v = row?.value\r\n return (v !== null && typeof v === 'object') ? (v as Record<string, unknown>) : undefined\r\n } catch (_e) {\r\n return undefined\r\n }\r\n }\r\n\r\n // vault 路径是插件核心前提:缺省 E:/dsh-workspace/obsidian-vault。\r\n //\r\n // 0.2.0-rc.2 保存行为:宿主把行 value 写回 cordis.patch.yml 并更新 settings\r\n // 镜像,但不会自动重激活本插件 fiber(volatile HMR 只在整应用重启或手动\r\n // 卸载/重载时发生)。因此这里不能像旧实现那样在 apply() 时把路径求值成\r\n // const 冻结 —— 保存新路径后运行中的实例仍读旧值。改为惰性读取:\r\n // currentVaultPath() 每次构建/感知索引时重读 settings,保存即对后续\r\n // 索引周期生效(无需重启)。\r\n const DEFAULT_VAULT_PATH = 'E:/dsh-workspace/obsidian-vault'\r\n const currentVaultPath = (): string => {\r\n const v = readSection()?.vaultPath\r\n return typeof v === 'string' && v.length > 0 ? v : DEFAULT_VAULT_PATH\r\n }\r\n const readSearchEnabled = (): boolean => {\r\n const v = readSection()?.searchEnabled\r\n return typeof v === 'boolean' ? v : true\r\n }\r\n const readIndexRefreshMs = (): number => {\r\n const v = readSection()?.indexRefreshMs\r\n return typeof v === 'number' && v >= 5000 ? v : 60000\r\n }\r\n\r\n let searchEnabled = readSearchEnabled()\r\n const refreshMs = Math.max(readIndexRefreshMs(), 5000)\r\n\r\n const fs = ctx.fs as unknown as FileSystem\r\n // 0.2.0-rc.1 不再提供 ctx.timer 服务;改用 Node 原生 setInterval,\r\n // 通过 ctx.effect 的清理函数保证插件卸载时定时器被清除。\r\n const index = new Map<string, { displayPath: string; tokens: Set<string>; snippet: string; size: number; version: string }>()\r\n let dirty = true\r\n let refreshing = false\r\n\r\n // vault 路径变更感知:保存新路径后宿主不会重激活本实例,靠这里的\r\n // 变更检测把 dirty 置位 —— 下一次定时器 tick / 工具调用(ensureIndex)\r\n // 即按新路径全量重建,无需重启。\r\n let observedVaultPath = currentVaultPath()\r\n function markDirtyOnVaultChange(): void {\r\n const p = currentVaultPath()\r\n if (p !== observedVaultPath) {\r\n observedVaultPath = p\r\n dirty = true\r\n }\r\n }\r\n\r\n /**\r\n * vault 外部修改感知(stat 比对 version token):\r\n * 快速 stat 每篇已索引文件的 version(不透明新鲜度令牌),发现外部变更\r\n * (用户手动编辑 / 其他工具写入 / git pull)即置 dirty。\r\n * 0.2.0-rc.1 的 FsInfo 不再暴露 mtimeMs,改以 opaque version token 比对;\r\n * 同内容文件的 version 相同,任何写入/删除都会改变它。stat 代价极小(~60 篇 × ms 级)。\r\n */\r\n async function detectExternalChanges(): Promise<void> {\r\n if (index.size === 0) return\r\n for (const entry of index.values()) {\r\n try {\r\n const target = await fs.resolve(entry.displayPath)\r\n const stat = await fs.stat(target)\r\n if (stat !== undefined) {\r\n if (String(stat.version) !== String(entry.version)) {\r\n dirty = true\r\n return\r\n }\r\n } else {\r\n // 目标已不存在 → 视为变更\r\n dirty = true\r\n return\r\n }\r\n } catch (_e) {\r\n // 文件被外部删除 → 也视为变更\r\n dirty = true\r\n return\r\n }\r\n }\r\n // 新文件检测:listDir 顶层 04-Archive / 02-Projects / 03-Areas,出现未知前缀即 dirty\r\n for (const sub of ['04-Archive', '02-Projects', '03-Areas', '05-Resources']) {\r\n try {\r\n const dir = await fs.resolve(currentVaultPath() + '/' + sub)\r\n const entries = await fs.listDir(dir)\r\n for (const e of entries) {\r\n if (e.type !== 'file' || e.name === undefined || !e.name.endsWith('.md')) continue\r\n const known = [...index.keys()].some(k => k.endsWith('/' + e.name) || k === sub + '/' + e.name)\r\n if (!known) {\r\n dirty = true\r\n return\r\n }\r\n }\r\n } catch (_e) { /* 目录不存在,跳过 */ }\r\n }\r\n }\r\n\r\n /**\r\n * 分词:ASCII 重叠三元组 + 中文滑动二元组(bigram)。\r\n *\r\n * - ASCII 字母数字:长度 ≥2 直接加入;长度 ≥4 额外加 i+=1 重叠三元组\r\n * - 中文:按 Unicode CJK 字符切分,长度 1 直接加入,≥2 加滑动 bigram\r\n * 这样「知识库」/「知识」可以互相命中,「连不上」/「连接不上」同理\r\n */\r\n function tokenize(text: string): Set<string> {\r\n const lower = String(text).toLowerCase()\r\n const seen = new Set<string>()\r\n // 连续 ASCII 字母数字段\r\n const asciiRe = /[a-z0-9]+/g\r\n let m: RegExpExecArray | null\r\n while ((m = asciiRe.exec(lower)) !== null) {\r\n const p = m[0]\r\n if (p.length < 2) continue\r\n seen.add(p)\r\n if (p.length >= 4) {\r\n for (let i = 0; i + 3 <= p.length; i++) seen.add(p.slice(i, i + 3))\r\n }\r\n }\r\n // 连续 CJK 段\r\n const cjkRe = /[\\u4e00-\\u9fff]+/g\r\n while ((m = cjkRe.exec(lower)) !== null) {\r\n const p = m[0]\r\n if (p.length === 1) {\r\n seen.add(p)\r\n } else {\r\n for (let i = 0; i + 2 <= p.length; i++) seen.add(p.slice(i, i + 2))\r\n if (p.length >= 3) seen.add(p.slice(p.length - 3))\r\n }\r\n }\r\n return seen\r\n }\r\n\r\n /** 为索引 entry 计算 IDF 权重表(token → 1/log(1+df))。 */\r\n function buildIdf(entries: Array<{ tokens: Set<string> }>): Map<string, number> {\r\n const df = new Map<string, number>()\r\n for (const entry of entries) {\r\n for (const t of entry.tokens) {\r\n df.set(t, (df.get(t) ?? 0) + 1)\r\n }\r\n }\r\n const total = entries.length || 1\r\n const idf = new Map<string, number>()\r\n for (const [t, count] of df) {\r\n idf.set(t, Math.log(total / (1 + count)))\r\n }\r\n return idf\r\n }\r\n\r\n async function rebuildIndex(): Promise<void> {\r\n refreshing = true\r\n try {\r\n const next = new Map(index)\r\n const idfEntries: Array<{ tokens: Set<string> }> = []\r\n try {\r\n const root = await fs.resolve(currentVaultPath())\r\n const statRoot = await fs.stat(root)\r\n if (statRoot !== undefined && statRoot.type === 'directory') {\r\n const queue: FsTarget[] = [root]\r\n while (queue.length > 0) {\r\n const dir = queue.shift()!\r\n let entries: FsDirEntry[]\r\n try { entries = await fs.listDir(dir) } catch (_e) { continue }\r\n for (const entry of entries) {\r\n if (entry.type === 'directory') {\r\n const entryName = entry.name || ''\r\n if (entryName === '.obsidian' || entryName === '.git') continue\r\n queue.push(entry.target)\r\n continue\r\n }\r\n if (entry.type !== 'file' || entry.name === undefined || !entry.name.endsWith('.md') || entry.name.startsWith('.')) continue\r\n let text: string\r\n try { text = await fs.readText(entry.target) } catch (_e) { continue }\r\n // 索引吃全文(~46 篇 × 几 KB,内存无压力),不再只取前 600 字\r\n const tokens = tokenize('\\n' + entry.name + '\\n' + text)\r\n const displayPath = entry.target && typeof entry.target === 'object' ? entry.target.displayPath : String(entry.target ?? '')\r\n const stat = await fs.stat(entry.target)\r\n next.set(displayPath, {\r\n displayPath,\r\n tokens,\r\n snippet: text.slice(0, 240),\r\n size: entry.size ?? text.length,\r\n version: stat !== undefined ? String(stat.version) : '',\r\n })\r\n idfEntries.push({ tokens })\r\n }\r\n }\r\n }\r\n } catch (_e) { /* vault 不可达:保留旧索引 */ }\r\n index.clear()\r\n for (const kv of next) index.set(kv[0], kv[1])\r\n // 重建 IDF 权重表\r\n idfTable = buildIdf(idfEntries)\r\n dirty = false\r\n } finally {\r\n refreshing = false\r\n }\r\n }\r\n\r\n let idfTable = new Map<string, number>()\r\n\r\n async function ensureIndex(): Promise<void> {\r\n markDirtyOnVaultChange()\r\n if (dirty) await rebuildIndex()\r\n else while (refreshing) await new Promise(resolve => setTimeout(resolve, 20))\r\n }\r\n\r\n if (searchEnabled) {\r\n ctx.effect(() => {\r\n const handle = setInterval(() => {\r\n markDirtyOnVaultChange()\r\n if (dirty) {\r\n void rebuildIndex()\r\n return\r\n }\r\n // 非 dirty 时做外部修改感知:stat 比对 version token,发现外部变更才置 dirty 下轮重建\r\n void detectExternalChanges().then(() => {\r\n if (dirty) void rebuildIndex()\r\n })\r\n }, refreshMs)\r\n return () => { clearInterval(handle) }\r\n }, 'obsidian-sync: index refresh')\r\n }\r\n\r\n function searchMatches(topic: string, limit: number): Array<{ file: string; score: number; snippet: string; size: number; matchReason: string }> {\r\n const topicTokens = tokenize(topic)\r\n const results: Array<{ file: string; score: number; snippet: string; size: number; matchReason: string }> = []\r\n for (const entry of index.values()) {\r\n let score = 0\r\n const matchedKeywords: string[] = []\r\n for (const t of topicTokens) {\r\n if (entry.tokens.has(t)) {\r\n score += idfTable.get(t) ?? 1\r\n matchedKeywords.push(t)\r\n }\r\n }\r\n if (score === 0) continue\r\n results.push({\r\n file: entry.displayPath,\r\n score: Math.round(score * 100) / 100,\r\n snippet: entry.snippet,\r\n size: entry.size,\r\n matchReason: matchedKeywords.slice(0, 5).join(' '),\r\n })\r\n }\r\n results.sort((a, b) => b.score - a.score)\r\n return results.slice(0, limit)\r\n }\r\n\r\n function sanitizeTitleForFilename(title: string): string {\r\n const base = String(title || '')\r\n .replace(/[\\\\/:*?\"<>|()()]/g, ' ')\r\n .replace(/\\s+/g, ' ')\r\n .trim()\r\n return base.length > 0 ? base.slice(0, 40) : '会话'\r\n }\r\n\r\n /**\r\n * 噪声会话判定:身份测试类(\"你是谁\" / \"你是什么模型\" / \"介绍一下自己\" / 冒烟测试)\r\n * 且摘要很短(≤ 摘要阈值字,近似单轮短会话)→ 不写笔记不进索引。\r\n *\r\n * 双重条件避免误杀:标题命中噪声模式 且 摘要长度 ≤ 阈值。\r\n * 摘要长的\"你是谁\"类会话(用户有实质讨论)仍正常归档。\r\n */\r\n const NOISE_TITLE_PATTERNS: RegExp[] = [\r\n /^你是谁$/,\r\n /^你是什么(大)?模型$/,\r\n /^介绍(一下)?自己$/,\r\n /^自我(介绍|认识)?$/,\r\n /^echo\\s+\\S+$/i,\r\n /^测试$/,\r\n ]\r\n const NOISE_SUMMARY_MAX_CHARS = 200\r\n\r\n function isNoiseSession(title: string, summary: string): boolean {\r\n const t = String(title || '').trim()\r\n const hit = NOISE_TITLE_PATTERNS.some(re => re.test(t))\r\n if (!hit) return false\r\n return String(summary || '').trim().length <= NOISE_SUMMARY_MAX_CHARS\r\n }\r\n\r\n /** 确保 shortId 恰好 8 位:不足时补 SHA-1 前缀 hash。 */\r\n function normalizeShortId(sessionId: string): string {\r\n // 剥离约定的 \"session-\" 前缀:传 \"session-<uuid>\" 时短 ID 应取 uuid 首 8 位,\r\n // 而非把 \"session\" 字样计入(否则 \"session-9f10d319…\" 会误归一化为 \"session9\")。\r\n // 仅影响入参形态,幂等键与既有归档(裸 uuid / 8 位短 ID)保持一致。\r\n const stripped = sessionId.replace(/^session-/i, '')\r\n let s = stripped.replace(/[^A-Za-z0-9]/g, '')\r\n if (s.length >= 8) return s.slice(0, 8)\r\n // 不足 8 位:用剥离前缀后的 sessionId 的简单 hash 补齐\r\n let hash = 0\r\n for (let i = 0; i < stripped.length; i++) {\r\n hash = ((hash << 5) - hash + stripped.charCodeAt(i)) | 0\r\n }\r\n s = s + Math.abs(hash).toString(16).slice(0, 8)\r\n return s.slice(0, 8)\r\n }\r\n\r\n /** vault 在 workspace 之外,写入需按调用抬升到 danger-full-access。 */\r\n const widePolicy: SandboxExecutionPolicy = { mode: 'danger-full-access', workspaceRoot: currentVaultPath() }\r\n\r\n /**\r\n * 每日 22:00 复盘沉淀节律:\r\n * - 汇总当天 04-Archive 归档(按日期前缀统计笔记数与主题关键词)\r\n * - 把当天要点追加到 02-Projects/ 下匹配项目的\"近期演进\"段(幂等:当天已追加则跳过)\r\n * - git commit + push\r\n *\r\n * 实现:60s 轮询,发现跨过当天 22:00 且尚未执行过 → 执行一次。\r\n */\r\n let lastReviewDate = ''\r\n\r\n async function dailyReview(): Promise<void> {\r\n const now = new Date()\r\n const dateStr = now.toISOString().slice(0, 10)\r\n // 只在 22:00 之后(本地时区)且当天未执行时触发\r\n const hour = now.getHours()\r\n if (hour < 22 || lastReviewDate === dateStr) return\r\n lastReviewDate = dateStr\r\n\r\n try {\r\n await ensureIndex()\r\n\r\n // 1. 汇总当天归档\r\n const todayFiles: Array<{ file: string; date: string; snippet: string }> = []\r\n for (const entry of index.values()) {\r\n if (!entry.displayPath.includes('04-Archive/')) continue\r\n const m = entry.displayPath.match(new RegExp('^.*?(\\\\d{4}-\\\\d{2}-\\\\d{2})'))\r\n if (m && m[1] === dateStr) {\r\n todayFiles.push({ file: entry.displayPath, date: dateStr, snippet: entry.snippet })\r\n }\r\n }\r\n if (todayFiles.length === 0) return\r\n\r\n // 2. 对每个 02-Projects 页追加当天归档条目(幂等)\r\n for (const proj of ['02-Projects/dsh-obsidian-sync.md', '02-Projects/dsh-tool-agnes.md', '02-Projects/dsh-plugin-manager.md', '02-Projects/dsh-conversation-language.md']) {\r\n try {\r\n const target = await fs.resolve(currentVaultPath() + '/' + proj)\r\n let content = String(await fs.readText(target))\r\n const marker = '## 近期演进'\r\n const markerPos = content.indexOf(marker)\r\n if (markerPos === -1) continue\r\n // 检查当天是否已追加(幂等)\r\n if (content.indexOf(dateStr) !== -1 && content.indexOf(dateStr) > markerPos) continue\r\n const todayLines = todayFiles.map(f => '- [[04-Archive/' + f.file.replace(/^04-Archive\\//, '') + ']] — ' + f.snippet.slice(0, 80).replace(/\\n/g, ' ')).join('\\n')\r\n content = content.slice(0, content.length) + '\\n' + dateStr + ' 归档:\\n' + todayLines + '\\n'\r\n await fs.writeText(target, content, undefined, undefined, widePolicy)\r\n } catch (_e) { /* 文件不存在或写入失败,跳过 */ }\r\n }\r\n\r\n dirty = true\r\n\r\n // 3. git commit + push\r\n try {\r\n const { execSync } = await import('node:child_process')\r\n const out = execSync('git add -A && git status --porcelain', {\r\n cwd: currentVaultPath(), timeout: 15000, encoding: 'utf-8',\r\n })\r\n if (out.trim().length > 0) {\r\n execSync('git commit -m \"daily-review: ' + dateStr + ' 归档沉淀(' + todayFiles.length + ' 篇)\" && git push', {\r\n cwd: currentVaultPath(), timeout: 60000, encoding: 'utf-8',\r\n })\r\n }\r\n } catch (_e) { /* git 失败不影响 */ }\r\n } catch (_e) { /* 整体失败静默 */ }\r\n }\r\n\r\n ctx.effect(() => {\r\n const handle = setInterval(() => {\r\n void dailyReview()\r\n }, 60000)\r\n return () => { clearInterval(handle) }\r\n }, 'obsidian-sync: daily review at 22:00')\r\n\r\n /**\r\n * 每周复盘沉淀节律(补齐决策习惯页\"周复盘尚未建立\"的开放问题):\r\n * - 每周日 22:30 触发(本地时区),汇总本 ISO 周(周一~周日)04-Archive 归档\r\n * - 写入/更新 03-Areas/周复盘-<ISO 周>.md(主题分布 + 归档清单 + 待收敛项)\r\n * - 幂等:同 ISO 周已生成则跳过;git commit + push\r\n */\r\n let lastWeeklyKey = ''\r\n\r\n function isoWeekKey(d: Date): string {\r\n // ISO 周:周四归属所在周;周一为周首\r\n const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()))\r\n const day = t.getUTCDay() || 7\r\n t.setUTCDate(t.getUTCDate() + 4 - day)\r\n const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1))\r\n const week = Math.ceil((((+t - +yearStart) / 86400000) + 1) / 7)\r\n return t.getUTCFullYear() + '-W' + String(week).padStart(2, '0')\r\n }\r\n\r\n async function weeklyReview(): Promise<void> {\r\n const now = new Date()\r\n const dow = now.getDay() // 0=Sun\r\n if (dow !== 0 || now.getHours() < 22) return\r\n const weekKey = isoWeekKey(now)\r\n if (lastWeeklyKey === weekKey) return\r\n lastWeeklyKey = weekKey\r\n\r\n try {\r\n await ensureIndex()\r\n const weekDate = new Date(now)\r\n weekDate.setDate(now.getDate() - 6) // 周一\r\n const monday = weekDate.toISOString().slice(0, 10)\r\n\r\n // 汇总本周归档(文件名日期 >= 周一 且 <= 今天)\r\n const weekFiles: Array<{ file: string; date: string; snippet: string }> = []\r\n for (const entry of index.values()) {\r\n if (!entry.displayPath.includes('04-Archive/')) continue\r\n const m = entry.displayPath.match(/(\\d{4}-\\d{2}-\\d{2})/)\r\n if (m && m[1] >= monday && m[1] <= now.toISOString().slice(0, 10)) {\r\n weekFiles.push({ file: entry.displayPath, date: m[1], snippet: entry.snippet })\r\n }\r\n }\r\n weekFiles.sort((a, b) => a.date.localeCompare(b.date))\r\n if (weekFiles.length === 0) return\r\n\r\n // 主题分布(按 04-Archive 笔记 frontmatter tags 简化:从 snippet 首行提取)\r\n const lines = weekFiles.map(f => '- [[04-Archive/' + f.file.replace(/^04-Archive\\//, '') + ']](' + f.date + ')— ' + f.snippet.slice(0, 60).replace(/\\n/g, ' ')).join('\\n')\r\n const note = '---\\ndate: ' + now.toISOString().slice(0, 10) + '\\nstatus: active\\ntags: [area, 周复盘]\\n---\\n\\n# 周复盘 ' + weekKey + '\\n\\n> 自动汇总 ' + monday + ' ~ ' + now.toISOString().slice(0, 10) + ' 的 04-Archive 归档(' + weekFiles.length + ' 篇)。\\n\\n## 本周归档\\n\\n' + lines + '\\n\\n## 待收敛\\n\\n- [ ] 人工审阅:识别本周新增的长期 Area / Resource 沉淀点\\n\\n'\r\n const target = await fs.resolve(currentVaultPath() + '/03-Areas/周复盘-' + weekKey + '.md')\r\n let skipped = false\r\n try {\r\n const prev = String(await fs.readText(target))\r\n if (prev.includes(weekKey)) skipped = true\r\n } catch (_e) { /* 尚不存在 */ }\r\n if (!skipped) await fs.writeText(target, note, undefined, undefined, widePolicy)\r\n\r\n dirty = true\r\n try {\r\n const { execSync } = await import('node:child_process')\r\n const out = execSync('git add -A && git status --porcelain', { cwd: currentVaultPath(), timeout: 15000, encoding: 'utf-8' })\r\n if (out.trim().length > 0) {\r\n execSync('git commit -m \"weekly-review: ' + weekKey + ' 归档沉淀(' + weekFiles.length + ' 篇)\" && git push', { cwd: currentVaultPath(), timeout: 60000, encoding: 'utf-8' })\r\n }\r\n } catch (_e) { /* git 失败不影响 */ }\r\n } catch (_e) { /* 整体失败静默 */ }\r\n }\r\n\r\n ctx.effect(() => {\r\n const handle = setInterval(() => {\r\n void weeklyReview()\r\n }, 60000)\r\n return () => { clearInterval(handle) }\r\n }, 'obsidian-sync: weekly review at Sunday 22:30')\r\n\r\n /**\r\n * 更新索引文件(DSH-会话归档-索引.md)的按日期段。\r\n * - 找到当天段落(### YYYY-MM-DD)\r\n * - 边界:下一个 `### ` 或 `## `(两个井号终止条件)\r\n * - 同 shortId 已有条目 → 替换该行(幂等去重)\r\n * - 否则追加到当天段落末尾\r\n */\r\n async function updateIndexFile(entryRelPath: string, title: string, sessionId: string, dateStr: string): Promise<boolean> {\r\n try {\r\n const idxTarget = await fs.resolve(currentVaultPath() + '/DSH-会话归档-索引.md')\r\n const idxContent = String(await fs.readText(idxTarget))\r\n const dayAnchor = '### ' + dateStr\r\n const line = '\\n- [[' + entryRelPath + '|' + title + ']] (' + dateStr + ', ' + sessionId + ')\\n'\r\n const dayPos = idxContent.indexOf(dayAnchor)\r\n if (dayPos === -1) {\r\n // 当天段落不存在:在 \"## 主题分类\" 之前插入\r\n const dayHeader = '\\n### ' + dateStr + '\\n' + line\r\n const tailPos = idxContent.indexOf('\\n## 主题分类')\r\n const updated = tailPos === -1 ? idxContent + dayHeader : idxContent.slice(0, tailPos) + dayHeader + idxContent.slice(tailPos)\r\n await fs.writeText(idxTarget, updated, undefined, undefined, widePolicy)\r\n return true\r\n }\r\n // 找当天段落的结尾:下一个 `### ` 或 `## `(双井号终止)\r\n const afterAnchor = dayPos + dayAnchor.length\r\n const nextH3 = idxContent.indexOf('\\n### ', afterAnchor)\r\n const nextH2 = idxContent.indexOf('\\n## ', afterAnchor)\r\n let dayEnd = -1\r\n if (nextH3 !== -1 && nextH2 !== -1) dayEnd = Math.min(nextH3, nextH2)\r\n else if (nextH3 !== -1) dayEnd = nextH3\r\n else if (nextH2 !== -1) dayEnd = nextH2\r\n else dayEnd = idxContent.length\r\n\r\n // 去重:当天段落内是否已有相同 shortId 的条目\r\n const daySection = idxContent.slice(afterAnchor, dayEnd)\r\n const shortId = normalizeShortId(sessionId)\r\n const dedupePattern = new RegExp('^- \\\\[\\\\[[^\\\\]]*' + dateStr + '-' + shortId + '-[^\\\\]]*\\\\]\\\\]\\\\s*\\\\([^\\\\)]*\\\\)$', 'm')\r\n const existingMatch = daySection.match(dedupePattern)\r\n let updated: string\r\n if (existingMatch !== null) {\r\n // 替换已有行(幂等)\r\n const absPos = afterAnchor + daySection.indexOf(existingMatch[0])\r\n updated = idxContent.slice(0, absPos) + line.trimStart() + '\\n' + idxContent.slice(absPos + existingMatch[0].length)\r\n } else {\r\n // 追加到段落末尾(dayEnd 前)\r\n updated = idxContent.slice(0, dayEnd) + line + idxContent.slice(dayEnd)\r\n }\r\n await fs.writeText(idxTarget, updated, undefined, undefined, widePolicy)\r\n return true\r\n } catch (_e) {\r\n return false\r\n }\r\n }\r\n\r\n /** 从索引中按目录前缀过滤 + 按 task 相关性取 top N 提炼页。 */\r\n function topDistilled(task: string, limit: number): Array<{ file: string; title: string; score: number; snippet: string }> {\r\n const distilledDirs = ['02-Projects/', '03-Areas/', '05-Resources/']\r\n const taskTokens = task.trim().length > 0 ? tokenize(task) : new Set<string>()\r\n const results: Array<{ file: string; title: string; score: number; snippet: string }> = []\r\n for (const entry of index.values()) {\r\n if (!distilledDirs.some(d => entry.displayPath.includes(d))) continue\r\n let score = 0\r\n for (const t of taskTokens) {\r\n if (entry.tokens.has(t)) score += idfTable.get(t) ?? 1\r\n }\r\n const title = entry.displayPath.split('/').pop() ?? entry.displayPath\r\n results.push({ file: entry.displayPath, title, score: taskTokens.size > 0 ? Math.round(score * 100) / 100 : 0, snippet: entry.snippet })\r\n }\r\n results.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))\r\n return results.slice(0, limit)\r\n }\r\n\r\n /** 从 04-Archive/ 取最近 N 篇(按文件名日期倒序)。 */\r\n function recentArchives(limit: number): Array<{ file: string; date: string; snippet: string }> {\r\n const results: Array<{ file: string; date: string; snippet: string }> = []\r\n for (const entry of index.values()) {\r\n if (!entry.displayPath.includes('04-Archive/')) continue\r\n const dateMatch = entry.displayPath.match(/(\\d{4}-\\d{2}-\\d{2})/)\r\n results.push({ file: entry.displayPath, date: dateMatch ? dateMatch[1] : 'unknown', snippet: entry.snippet })\r\n }\r\n results.sort((a, b) => b.date.localeCompare(a.date))\r\n return results.slice(0, limit)\r\n }\r\n\r\n /** 读取指定相对路径的文件内容。 */\r\n async function readFileContent(relPath: string): Promise<string> {\r\n try {\r\n const target = await fs.resolve(currentVaultPath() + '/' + relPath)\r\n return String(await fs.readText(target))\r\n } catch (_e) {\r\n return ''\r\n }\r\n }\r\n\r\n // 工具名必须匹配模型 provider 的 function-name 约束 ^[a-zA-Z0-9_-]+$。\r\n // v3.4.2 及更早版本用了 `obsidian.search` 这类带点号的命名,DeepSeek API 会在\r\n // 请求校验阶段直接返回 400(Invalid 'tools[N].name': string does not match pattern),\r\n // 导致任何启用了本插件的会话完全无法发起对话。禁止改回点号命名。\r\n if (searchEnabled) {\r\n ctx.tools.register(defineTool({\r\n name: 'obsidian_search',\r\n description: '在 Obsidian 知识库中按关键词搜索相关笔记,返回最多 5 条匹配(文件相对路径、命中片段、命中关键词)。按需调用,平时不产生任何 token 成本。',\r\n parameters: {\r\n topic: { type: 'string', required: true, description: '搜索关键词,可含多个词(中英文均可,自动做 CJK bigram 分词)' },\r\n limit: { type: 'number', description: '返回结果上限,默认 3,最大 5' },\r\n },\r\n output: {\r\n schema: { type: 'json' },\r\n render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],\r\n },\r\n isConcurrencySafe: () => true,\r\n async execute(args) {\r\n const topic = String(args.topic ?? '')\r\n if (topic.trim().length === 0) return asJsonValue({ ok: false, error: 'topic must not be empty' })\r\n const limit = Math.min(Math.max(1, Number(args.limit) || 3), 5)\r\n await ensureIndex()\r\n const matches = searchMatches(topic, limit)\r\n return asJsonValue({ ok: true, matches, count: matches.length, vaultPath: currentVaultPath() })\r\n },\r\n }))\r\n\r\n ctx.tools.register(defineTool({\r\n name: 'obsidian_brief',\r\n description: '每日简报:返回与当前任务最相关的提炼页(02-Projects/03-Areas/05-Resources)top 3 + 最近 3 条归档摘要 + 用户决策习惯与偏好页内容。低成本\"开机记忆\"入口,建议任务开始前调用一次。',\r\n parameters: {\r\n task: { type: 'string', description: '当前任务描述(可选),用于相关性排序;省略则返回全部提炼页按名称排序的 top 3' },\r\n },\r\n output: {\r\n schema: { type: 'json' },\r\n render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],\r\n },\r\n isConcurrencySafe: () => true,\r\n async execute(args) {\r\n const task = String(args.task ?? '').trim()\r\n await ensureIndex()\r\n\r\n // 1. 提炼页 top 3\r\n const distilled = topDistilled(task, 3)\r\n\r\n // 2. 最近归档 3 条\r\n const recent = recentArchives(3)\r\n\r\n // 3. 用户决策习惯与偏好页(03-Areas 下,如果存在)\r\n let userProfile = ''\r\n const profileRelPath = '03-Areas/决策习惯与偏好.md'\r\n const hasProfile = [...index.values()].some(e => e.displayPath === profileRelPath)\r\n if (hasProfile) {\r\n userProfile = await readFileContent(profileRelPath)\r\n }\r\n\r\n return asJsonValue({\r\n ok: true,\r\n task: task || '(未指定)',\r\n distilled,\r\n recent,\r\n userProfile: userProfile.length > 0 ? userProfile.slice(0, 2000) : '',\r\n vaultPath: currentVaultPath(),\r\n })\r\n },\r\n }))\r\n\r\n ctx.tools.register(defineTool({\r\n name: 'obsidian_read_note',\r\n description: '按相对路径读取 Obsidian 笔记的 frontmatter 与正文,补全 obsidian_search 的\"搜索→阅读\"闭环。路径相对 vault 根(如 02-Projects/dsh-obsidian-sync.md),自动解析 YAML frontmatter(key: value / [list] / 数值 / 布尔)。按需调用,零 token 成本。',\r\n parameters: {\r\n path: { type: 'string', required: true, description: '笔记相对 vault 的路径(如 02-Projects/dsh-obsidian-sync.md 或 04-Archive/xxx.md)' },\r\n },\r\n output: {\r\n schema: { type: 'json' },\r\n render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],\r\n },\r\n isConcurrencySafe: () => true,\r\n async execute(args) {\r\n const rel = String(args.path ?? '').replace(/^\\/+/, '').replace(/\\\\/g, '/').trim()\r\n if (rel.length === 0) return asJsonValue({ ok: false, error: 'path must not be empty', code: 'EMPTY_PATH' })\r\n const content = await readFileContent(rel)\r\n if (content.length === 0) return asJsonValue({ ok: false, error: 'note not found or unreadable: ' + rel, code: 'NOT_FOUND' })\r\n\r\n // 解析 YAML frontmatter(简单 key: value / key: [a, b] / key: \"str\")\r\n const frontmatter: Record<string, unknown> = {}\r\n let body = content\r\n const fmMatch = content.match(/^---\\n([\\s\\S]*?)\\n---\\n?/)\r\n if (fmMatch !== null) {\r\n body = content.slice(fmMatch[0].length)\r\n for (const line of fmMatch[1].split('\\n')) {\r\n const kv = line.match(/^([A-Za-z0-9_\\-]+):\\s*(.*)$/)\r\n if (kv === null) continue\r\n const key = kv[1]\r\n let raw = kv[2].trim()\r\n let val: unknown\r\n if (raw.startsWith('[') && raw.endsWith(']')) {\r\n const inner = raw.slice(1, -1).trim()\r\n val = inner.length === 0 ? [] : inner.split(',').map((s: string) => s.trim().replace(/^[\"']|[\"']$/g, ''))\r\n } else if (raw === 'null') {\r\n val = null\r\n } else if (raw === 'true') {\r\n val = true\r\n } else if (raw === 'false') {\r\n val = false\r\n } else if (/^\\d+$/.test(raw)) {\r\n val = Number(raw)\r\n } else {\r\n val = raw.replace(/^[\"']|[\"']$/g, '')\r\n }\r\n frontmatter[key] = val\r\n }\r\n }\r\n\r\n const titleLine = body.match(/^#\\s+(.+)$/m)\r\n return asJsonValue({\r\n ok: true,\r\n path: rel,\r\n frontmatter,\r\n title: titleLine ? titleLine[1].trim() : (rel.split('/').pop() ?? rel),\r\n body,\r\n })\r\n },\r\n }))\r\n }\r\n\r\n ctx.tools.register(defineTool({\r\n name: 'obsidian_sync_session',\r\n description: '把当前 DSH 会话按 Obsidian vault 的 PARA 结构与用户既有命名/索引规则,以 Markdown 笔记形式幂等写入 vault/04-Archive/,并自动把新条目挂到 DSH-会话归档-索引.md 的按日期段。摘要未变则跳过。幂等键 = date + shortId(标题变更不影响去重)。',\r\n parameters: {\r\n session_id: { type: 'string', required: true, description: '当前会话的完整 SessionId(UUID 或短 ID),原样存入 frontmatter 与正文' },\r\n title: { type: 'string', required: true, description: '会话主题标题(写入 frontmatter 与正文 # 标题;文件名仅取前 40 字符做安全截断)' },\r\n summary: { type: 'string', required: true, description: '会话结论/摘要(纯文本,建议 200-800 字,包含用户需求、过程要点、交付物)' },\r\n tags: { type: 'array', items: { type: 'string' }, description: '主题标签数组,最多 10 个,将写入 frontmatter tags 字段(Obsidian 原生标签)' },\r\n related: { type: 'array', items: { type: 'string' }, description: '关联笔记完整文件名(含 .md 扩展名,如 2026-09-11-a9095879-xxx.md),将生成为 [[全路径双链]]' },\r\n raw_log: { type: 'string', description: '原始会话日志绝对路径,格式 ~/.dsh/sessions/<project-dir>/session-<uuid>/session.v3.jsonl.zstd,将写入落款以便追溯' },\r\n // 可选参数:dsh-tools 的 ParameterPropertySpec 只允许 `required?: true`,省略即表示可选(同 tags/related)。\r\n date: { type: 'string', description: '归档日期(YYYY-MM-DD),回填历史会话时使用;省略则取当前 UTC 日期。仅匹配该格式时采用,否则回退当前 UTC 日期' },\r\n },\r\n output: {\r\n schema: { type: 'json' },\r\n render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],\r\n },\r\n async execute(args) {\r\n const sessionId = String(args.session_id ?? 'unknown')\r\n const title = String(args.title ?? '')\r\n const safeTitle = sanitizeTitleForFilename(title)\r\n const summary = String(args.summary ?? '')\r\n const tags = Array.isArray(args.tags) ? args.tags.map(String).slice(0, 10) : []\r\n const related = Array.isArray(args.related) ? args.related.map(String).slice(0, 10) : []\r\n const rawLog = String(args.raw_log ?? '')\r\n\r\n // 噪声会话过滤:身份测试类 + 摘要极短 → 不写笔记不进索引\r\n if (isNoiseSession(title, summary)) {\r\n return asJsonValue({ ok: true, skipped: true, noise: true, reason: 'identity-test noise session filtered out (short title + short summary)', index: 'unchanged' })\r\n }\r\n\r\n // 归档日期:优先采用调用方传入的 date(用于回填历史会话),未传或格式非法时回退当前 UTC 日期。\r\n // dateStr 单一来源:文件名前缀 / frontmatter date / 基本信息表格 / 索引行日期段全部沿用它。\r\n const rawDate = String(args.date ?? '').trim()\r\n const dateStr = /^\\d{4}-\\d{2}-\\d{2}$/.test(rawDate) ? rawDate : new Date().toISOString().slice(0, 10)\r\n const shortId = normalizeShortId(sessionId)\r\n\r\n // 幂等键 = date + shortId(标题不参与路径),写入前按前缀探测已存在文件并原地覆盖\r\n const entryPrefix = dateStr + '-' + shortId + '-'\r\n let entryRelPath: string\r\n\r\n // 探测 04-Archive/ 下是否已有同前缀文件(同会话多次同步)\r\n try {\r\n const archiveDir = await fs.resolve(currentVaultPath() + '/04-Archive')\r\n const archiveEntries = await fs.listDir(archiveDir)\r\n const existing = archiveEntries.find(e => e.type === 'file' && e.name?.startsWith(entryPrefix))\r\n if (existing !== undefined && existing.name !== undefined) {\r\n // 已有文件:原地覆盖(标题可能变了但幂等键不变)\r\n entryRelPath = '04-Archive/' + existing.name\r\n } else {\r\n entryRelPath = '04-Archive/' + entryPrefix + safeTitle + '.md'\r\n }\r\n } catch (_e) {\r\n entryRelPath = '04-Archive/' + entryPrefix + safeTitle + '.md'\r\n }\r\n\r\n // related 双链:生成全路径 [[04-Archive/xxx.md|显示名]],写入前校验目标存在\r\n const relatedLines: string[] = []\r\n for (const r of related) {\r\n const rFull = r.endsWith('.md') ? r : r + '.md'\r\n const rRel = '04-Archive/' + rFull\r\n // 校验目标存在(全路径双链)\r\n try {\r\n await fs.stat(await fs.resolve(currentVaultPath() + '/' + rRel))\r\n relatedLines.push('- [[' + rRel + '|' + rFull.replace(/^.*\\//, '').replace(/\\.md$/, '') + ']]')\r\n } catch (_e) {\r\n relatedLines.push('- ' + rFull + '(未找到,降级为文字引用)')\r\n }\r\n }\r\n\r\n // 生成 YAML frontmatter(Obsidian 原生标签 / Dataview 可查)\r\n const fmDate = dateStr\r\n const fmSessionId = sessionId.replace(/[^A-Za-z0-9-]/g, '').slice(0, 8)\r\n const fmTags = tags.length > 0 ? tags : []\r\n const frontmatter = [\r\n '---',\r\n 'session_id: ' + fmSessionId,\r\n 'full_session_id: ' + sessionId,\r\n 'date: ' + fmDate,\r\n 'tags: [' + fmTags.join(', ') + ']',\r\n '---',\r\n '',\r\n ].join('\\n')\r\n\r\n const logLine = rawLog.trim().length > 0\r\n ? '> 📎 原始日志: `' + rawLog.trim() + '`\\n\\n> 由 DSH Obsidian Sync 自动同步\\n'\r\n : '> 由 DSH Obsidian Sync 自动同步\\n'\r\n\r\n const content = frontmatter\r\n + '# DSH 会话: ' + title + '\\n\\n'\r\n + '## 基本信息\\n\\n'\r\n + '| 属性 | 值 |\\n|------|-----|\\n'\r\n + '| **日期** | ' + dateStr + ' |\\n'\r\n + '| **会话ID** | `' + sessionId + '` |\\n'\r\n + '| **状态** | ✅ 已归档 |\\n'\r\n + '| **分类** | ' + (tags.length > 0 ? tags.join(' ') : '通用') + ' |\\n\\n'\r\n + '---\\n\\n'\r\n + '## 摘要\\n\\n' + summary + '\\n\\n'\r\n + (relatedLines.length > 0 ? '## 关联\\n\\n' + relatedLines.join('\\n') + '\\n\\n---\\n' : '')\r\n + logLine\r\n\r\n let target: FsTarget\r\n try {\r\n target = await fs.resolve(currentVaultPath() + '/' + entryRelPath)\r\n } catch (_e) {\r\n const winPath = currentVaultPath() + '\\\\' + entryRelPath.split('/').join('\\\\')\r\n try { target = await fs.resolve(winPath) } catch (_e2) { return asJsonValue({ ok: false, error: 'cannot resolve vault entry path', code: 'RESOLVE_FAILED' }) }\r\n }\r\n\r\n let skipped = false\r\n try {\r\n const prev = String(await fs.readText(target))\r\n const marker = '## 摘要'\r\n const pos = prev.indexOf(marker)\r\n if (pos >= 0) {\r\n // 摘要区块末尾 = 下一个 `## ` 标题 / `\\n---\\n` 分隔线 / 空行起的落款引用行中最早出现的位置;\r\n // 只比较摘要本身,否则 `## 关联` 与落款会被带入导致全串永不相等、每次重写。\r\n const bodyStart = pos + marker.length\r\n const boundaries = ['\\n## ', '\\n---\\n', '\\n\\n> ']\r\n .map(sep => prev.indexOf(sep, bodyStart))\r\n .filter(i => i !== -1)\r\n const bodyEnd = boundaries.length > 0 ? Math.min(...boundaries) : prev.length\r\n if (prev.slice(bodyStart, bodyEnd).trim() === summary.trim()) skipped = true\r\n }\r\n } catch (_e) { /* 尚不存在 */ }\r\n if (skipped) return asJsonValue({ ok: true, skipped: true, file: entryRelPath, reason: 'summary unchanged', index: 'unchanged' })\r\n\r\n try {\r\n await fs.writeText(target, content, undefined, undefined, widePolicy)\r\n } catch (writeErr) {\r\n return asJsonValue({ ok: false, error: 'writeText failed: ' + String(writeErr instanceof Error ? writeErr.message : writeErr), code: 'WRITE_FAILED' })\r\n }\r\n\r\n const indexOk = await updateIndexFile(entryRelPath, title, sessionId, dateStr)\r\n dirty = true\r\n\r\n // 写入成功后自动 git commit + push(vault 有 remote 时才生效)\r\n let gitStatus = 'skipped'\r\n try {\r\n const { execSync } = await import('node:child_process')\r\n const out = execSync('git add -A && git status --porcelain', {\r\n cwd: currentVaultPath(),\r\n timeout: 15000,\r\n encoding: 'utf-8',\r\n })\r\n if (out.trim().length > 0) {\r\n const msg = 'archive: ' + entryRelPath.replace(/^04-Archive\\//, '')\r\n execSync('git commit -m \"' + msg.replace(/\"/g, '\\\\\"') + '\" && git push', {\r\n cwd: currentVaultPath(),\r\n timeout: 60000,\r\n encoding: 'utf-8',\r\n })\r\n gitStatus = 'pushed'\r\n } else {\r\n gitStatus = 'no changes'\r\n }\r\n } catch (gitErr) {\r\n gitStatus = 'failed: ' + String(gitErr instanceof Error ? gitErr.message : gitErr)\r\n }\r\n\r\n return asJsonValue({ ok: true, skipped: false, file: entryRelPath, index: indexOk ? 'updated' : 'skipped', git: gitStatus })\r\n },\r\n }))\r\n}\r\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dingpenghui/dsh-obsidian-sync",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.6",
|
|
4
4
|
"description": "DSH Obsidian Sync - DSH Cordis plugin: on-demand Obsidian knowledge-base search + DSH session archiving following your vault's existing PARA rules (zero token injection, in-process fs, inverted index, idempotent upsert)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
package/src/index.ts
CHANGED
|
@@ -378,12 +378,16 @@ export function apply(ctx: Context): void {
|
|
|
378
378
|
|
|
379
379
|
/** 确保 shortId 恰好 8 位:不足时补 SHA-1 前缀 hash。 */
|
|
380
380
|
function normalizeShortId(sessionId: string): string {
|
|
381
|
-
|
|
381
|
+
// 剥离约定的 "session-" 前缀:传 "session-<uuid>" 时短 ID 应取 uuid 首 8 位,
|
|
382
|
+
// 而非把 "session" 字样计入(否则 "session-9f10d319…" 会误归一化为 "session9")。
|
|
383
|
+
// 仅影响入参形态,幂等键与既有归档(裸 uuid / 8 位短 ID)保持一致。
|
|
384
|
+
const stripped = sessionId.replace(/^session-/i, '')
|
|
385
|
+
let s = stripped.replace(/[^A-Za-z0-9]/g, '')
|
|
382
386
|
if (s.length >= 8) return s.slice(0, 8)
|
|
383
|
-
// 不足 8
|
|
387
|
+
// 不足 8 位:用剥离前缀后的 sessionId 的简单 hash 补齐
|
|
384
388
|
let hash = 0
|
|
385
|
-
for (let i = 0; i <
|
|
386
|
-
hash = ((hash << 5) - hash +
|
|
389
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
390
|
+
hash = ((hash << 5) - hash + stripped.charCodeAt(i)) | 0
|
|
387
391
|
}
|
|
388
392
|
s = s + Math.abs(hash).toString(16).slice(0, 8)
|
|
389
393
|
return s.slice(0, 8)
|