@dingpenghui/dsh-obsidian-sync 3.4.2
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/README.md +131 -0
- package/cordis.patch.yml +23 -0
- package/lib/client.js +372 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +41 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +810 -0
- package/lib/index.js.map +1 -0
- package/package.json +91 -0
- package/src/client/PluginDetailPage.module.css +81 -0
- package/src/client/css-module.d.ts +9 -0
- package/src/client/index.ts +109 -0
- package/src/client/plugin-detail-controller.ts +137 -0
- package/src/client/plugin-detail-page.tsx +106 -0
- package/src/index.ts +897 -0
- package/src/locales/index.ts +40 -0
- package/src/shared.ts +24 -0
- package/src/slot-contract.d.ts +42 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
/** 把工具返回值断言为 JsonValue(0.2.0-rc.1 的 execute 要求返回 InferValue,
|
|
5
|
+
* 即 output.schema 推断出的 JsonValue;对象 union 的 undefined 分支在此收敛)。 */
|
|
6
|
+
function asJsonValue(v) {
|
|
7
|
+
return v;
|
|
8
|
+
}
|
|
9
|
+
/** Cordis 插件名。 */
|
|
10
|
+
const name = "obsidian-sync";
|
|
11
|
+
/** 本插件需要的 Service(`tools`/`fs` 为硬依赖,缺失时等待 Cordis 重激活)。
|
|
12
|
+
* 仅保留真实消费的 Service:settings 是可选增强(未组合时降级为默认值)。 */
|
|
13
|
+
const inject = ["tools", "fs"];
|
|
14
|
+
/**
|
|
15
|
+
* Schemastery 配置 schema:加载器用它解析行 `config` 并补默认值。
|
|
16
|
+
*
|
|
17
|
+
* 注意 Schemastery 的 API 与 zod 不同:没有 `.optional()` / `.int()`。
|
|
18
|
+
* 对象属性默认即可选(不调用 `.required()` 时,缺失键不会写入解析结果),
|
|
19
|
+
* 整数约束通过 `.step(1)` 表达。
|
|
20
|
+
*/
|
|
21
|
+
/** 运行时 schema:volatile 标记 + 默认值。Loader 据此解析行 config 并补默认。 */
|
|
22
|
+
const Config = z.object({
|
|
23
|
+
vaultPath: z.string().volatile().default("E:/dsh-workspace/obsidian-vault"),
|
|
24
|
+
searchEnabled: z.boolean().default(true).volatile(),
|
|
25
|
+
indexRefreshMs: z.number().step(1).min(5e3).default(6e4).volatile()
|
|
26
|
+
});
|
|
27
|
+
function apply(ctx) {
|
|
28
|
+
/** settings 服务是可选增强:未组合时降级为默认值。
|
|
29
|
+
* 按 SettingsDescriptor 契约读命名空间行(ns + value 由 Loader 按
|
|
30
|
+
* 本文件导出的 Config schema 解析并补默认值;volatile HMR 保存后即时刷新)。 */
|
|
31
|
+
const settings = ctx.get("settings");
|
|
32
|
+
/** 读命名空间行 value。 */
|
|
33
|
+
const readSection = () => {
|
|
34
|
+
if (settings === void 0) return void 0;
|
|
35
|
+
try {
|
|
36
|
+
const v = settings.describe().find((r) => r.ns === "dsh-obsidian-sync")?.value;
|
|
37
|
+
return v !== null && typeof v === "object" ? v : void 0;
|
|
38
|
+
} catch (_e) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const DEFAULT_VAULT_PATH = "E:/dsh-workspace/obsidian-vault";
|
|
43
|
+
const currentVaultPath = () => {
|
|
44
|
+
const v = readSection()?.vaultPath;
|
|
45
|
+
return typeof v === "string" && v.length > 0 ? v : DEFAULT_VAULT_PATH;
|
|
46
|
+
};
|
|
47
|
+
const readSearchEnabled = () => {
|
|
48
|
+
const v = readSection()?.searchEnabled;
|
|
49
|
+
return typeof v === "boolean" ? v : true;
|
|
50
|
+
};
|
|
51
|
+
const readIndexRefreshMs = () => {
|
|
52
|
+
const v = readSection()?.indexRefreshMs;
|
|
53
|
+
return typeof v === "number" && v >= 5e3 ? v : 6e4;
|
|
54
|
+
};
|
|
55
|
+
let searchEnabled = readSearchEnabled();
|
|
56
|
+
const refreshMs = Math.max(readIndexRefreshMs(), 5e3);
|
|
57
|
+
const fs = ctx.fs;
|
|
58
|
+
const index = /* @__PURE__ */ new Map();
|
|
59
|
+
let dirty = true;
|
|
60
|
+
let refreshing = false;
|
|
61
|
+
let observedVaultPath = currentVaultPath();
|
|
62
|
+
function markDirtyOnVaultChange() {
|
|
63
|
+
const p = currentVaultPath();
|
|
64
|
+
if (p !== observedVaultPath) {
|
|
65
|
+
observedVaultPath = p;
|
|
66
|
+
dirty = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* vault 外部修改感知(stat 比对 version token):
|
|
71
|
+
* 快速 stat 每篇已索引文件的 version(不透明新鲜度令牌),发现外部变更
|
|
72
|
+
* (用户手动编辑 / 其他工具写入 / git pull)即置 dirty。
|
|
73
|
+
* 0.2.0-rc.1 的 FsInfo 不再暴露 mtimeMs,改以 opaque version token 比对;
|
|
74
|
+
* 同内容文件的 version 相同,任何写入/删除都会改变它。stat 代价极小(~60 篇 × ms 级)。
|
|
75
|
+
*/
|
|
76
|
+
async function detectExternalChanges() {
|
|
77
|
+
if (index.size === 0) return;
|
|
78
|
+
for (const entry of index.values()) try {
|
|
79
|
+
const target = await fs.resolve(entry.displayPath);
|
|
80
|
+
const stat = await fs.stat(target);
|
|
81
|
+
if (stat !== void 0) {
|
|
82
|
+
if (String(stat.version) !== String(entry.version)) {
|
|
83
|
+
dirty = true;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
dirty = true;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
} catch (_e) {
|
|
91
|
+
dirty = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
for (const sub of [
|
|
95
|
+
"04-Archive",
|
|
96
|
+
"02-Projects",
|
|
97
|
+
"03-Areas",
|
|
98
|
+
"05-Resources"
|
|
99
|
+
]) try {
|
|
100
|
+
const dir = await fs.resolve(currentVaultPath() + "/" + sub);
|
|
101
|
+
const entries = await fs.listDir(dir);
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
if (e.type !== "file" || e.name === void 0 || !e.name.endsWith(".md")) continue;
|
|
104
|
+
if (![...index.keys()].some((k) => k.endsWith("/" + e.name) || k === sub + "/" + e.name)) {
|
|
105
|
+
dirty = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (_e) {}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 分词:ASCII 重叠三元组 + 中文滑动二元组(bigram)。
|
|
113
|
+
*
|
|
114
|
+
* - ASCII 字母数字:长度 ≥2 直接加入;长度 ≥4 额外加 i+=1 重叠三元组
|
|
115
|
+
* - 中文:按 Unicode CJK 字符切分,长度 1 直接加入,≥2 加滑动 bigram
|
|
116
|
+
* 这样「知识库」/「知识」可以互相命中,「连不上」/「连接不上」同理
|
|
117
|
+
*/
|
|
118
|
+
function tokenize(text) {
|
|
119
|
+
const lower = String(text).toLowerCase();
|
|
120
|
+
const seen = /* @__PURE__ */ new Set();
|
|
121
|
+
const asciiRe = /[a-z0-9]+/g;
|
|
122
|
+
let m;
|
|
123
|
+
while ((m = asciiRe.exec(lower)) !== null) {
|
|
124
|
+
const p = m[0];
|
|
125
|
+
if (p.length < 2) continue;
|
|
126
|
+
seen.add(p);
|
|
127
|
+
if (p.length >= 4) for (let i = 0; i + 3 <= p.length; i++) seen.add(p.slice(i, i + 3));
|
|
128
|
+
}
|
|
129
|
+
const cjkRe = /[\u4e00-\u9fff]+/g;
|
|
130
|
+
while ((m = cjkRe.exec(lower)) !== null) {
|
|
131
|
+
const p = m[0];
|
|
132
|
+
if (p.length === 1) seen.add(p);
|
|
133
|
+
else {
|
|
134
|
+
for (let i = 0; i + 2 <= p.length; i++) seen.add(p.slice(i, i + 2));
|
|
135
|
+
if (p.length >= 3) seen.add(p.slice(p.length - 3));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return seen;
|
|
139
|
+
}
|
|
140
|
+
/** 为索引 entry 计算 IDF 权重表(token → 1/log(1+df))。 */
|
|
141
|
+
function buildIdf(entries) {
|
|
142
|
+
const df = /* @__PURE__ */ new Map();
|
|
143
|
+
for (const entry of entries) for (const t of entry.tokens) df.set(t, (df.get(t) ?? 0) + 1);
|
|
144
|
+
const total = entries.length || 1;
|
|
145
|
+
const idf = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const [t, count] of df) idf.set(t, Math.log(total / (1 + count)));
|
|
147
|
+
return idf;
|
|
148
|
+
}
|
|
149
|
+
async function rebuildIndex() {
|
|
150
|
+
refreshing = true;
|
|
151
|
+
try {
|
|
152
|
+
const next = new Map(index);
|
|
153
|
+
const idfEntries = [];
|
|
154
|
+
try {
|
|
155
|
+
const root = await fs.resolve(currentVaultPath());
|
|
156
|
+
const statRoot = await fs.stat(root);
|
|
157
|
+
if (statRoot !== void 0 && statRoot.type === "directory") {
|
|
158
|
+
const queue = [root];
|
|
159
|
+
while (queue.length > 0) {
|
|
160
|
+
const dir = queue.shift();
|
|
161
|
+
let entries;
|
|
162
|
+
try {
|
|
163
|
+
entries = await fs.listDir(dir);
|
|
164
|
+
} catch (_e) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
if (entry.type === "directory") {
|
|
169
|
+
const entryName = entry.name || "";
|
|
170
|
+
if (entryName === ".obsidian" || entryName === ".git") continue;
|
|
171
|
+
queue.push(entry.target);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (entry.type !== "file" || entry.name === void 0 || !entry.name.endsWith(".md") || entry.name.startsWith(".")) continue;
|
|
175
|
+
let text;
|
|
176
|
+
try {
|
|
177
|
+
text = await fs.readText(entry.target);
|
|
178
|
+
} catch (_e) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const tokens = tokenize("\n" + entry.name + "\n" + text);
|
|
182
|
+
const displayPath = entry.target && typeof entry.target === "object" ? entry.target.displayPath : String(entry.target ?? "");
|
|
183
|
+
const stat = await fs.stat(entry.target);
|
|
184
|
+
next.set(displayPath, {
|
|
185
|
+
displayPath,
|
|
186
|
+
tokens,
|
|
187
|
+
snippet: text.slice(0, 240),
|
|
188
|
+
size: entry.size ?? text.length,
|
|
189
|
+
version: stat !== void 0 ? String(stat.version) : ""
|
|
190
|
+
});
|
|
191
|
+
idfEntries.push({ tokens });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch (_e) {}
|
|
196
|
+
index.clear();
|
|
197
|
+
for (const kv of next) index.set(kv[0], kv[1]);
|
|
198
|
+
idfTable = buildIdf(idfEntries);
|
|
199
|
+
dirty = false;
|
|
200
|
+
} finally {
|
|
201
|
+
refreshing = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
let idfTable = /* @__PURE__ */ new Map();
|
|
205
|
+
async function ensureIndex() {
|
|
206
|
+
markDirtyOnVaultChange();
|
|
207
|
+
if (dirty) await rebuildIndex();
|
|
208
|
+
else while (refreshing) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
209
|
+
}
|
|
210
|
+
if (searchEnabled) ctx.effect(() => {
|
|
211
|
+
const handle = setInterval(() => {
|
|
212
|
+
markDirtyOnVaultChange();
|
|
213
|
+
if (dirty) {
|
|
214
|
+
rebuildIndex();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
detectExternalChanges().then(() => {
|
|
218
|
+
if (dirty) rebuildIndex();
|
|
219
|
+
});
|
|
220
|
+
}, refreshMs);
|
|
221
|
+
return () => {
|
|
222
|
+
clearInterval(handle);
|
|
223
|
+
};
|
|
224
|
+
}, "obsidian-sync: index refresh");
|
|
225
|
+
function searchMatches(topic, limit) {
|
|
226
|
+
const topicTokens = tokenize(topic);
|
|
227
|
+
const results = [];
|
|
228
|
+
for (const entry of index.values()) {
|
|
229
|
+
let score = 0;
|
|
230
|
+
const matchedKeywords = [];
|
|
231
|
+
for (const t of topicTokens) if (entry.tokens.has(t)) {
|
|
232
|
+
score += idfTable.get(t) ?? 1;
|
|
233
|
+
matchedKeywords.push(t);
|
|
234
|
+
}
|
|
235
|
+
if (score === 0) continue;
|
|
236
|
+
results.push({
|
|
237
|
+
file: entry.displayPath,
|
|
238
|
+
score: Math.round(score * 100) / 100,
|
|
239
|
+
snippet: entry.snippet,
|
|
240
|
+
size: entry.size,
|
|
241
|
+
matchReason: matchedKeywords.slice(0, 5).join(" ")
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
results.sort((a, b) => b.score - a.score);
|
|
245
|
+
return results.slice(0, limit);
|
|
246
|
+
}
|
|
247
|
+
function sanitizeTitleForFilename(title) {
|
|
248
|
+
const base = String(title || "").replace(/[\\/:*?"<>|()()]/g, " ").replace(/\s+/g, " ").trim();
|
|
249
|
+
return base.length > 0 ? base.slice(0, 40) : "会话";
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 噪声会话判定:身份测试类("你是谁" / "你是什么模型" / "介绍一下自己" / 冒烟测试)
|
|
253
|
+
* 且摘要很短(≤ 摘要阈值字,近似单轮短会话)→ 不写笔记不进索引。
|
|
254
|
+
*
|
|
255
|
+
* 双重条件避免误杀:标题命中噪声模式 且 摘要长度 ≤ 阈值。
|
|
256
|
+
* 摘要长的"你是谁"类会话(用户有实质讨论)仍正常归档。
|
|
257
|
+
*/
|
|
258
|
+
const NOISE_TITLE_PATTERNS = [
|
|
259
|
+
/^你是谁$/,
|
|
260
|
+
/^你是什么(大)?模型$/,
|
|
261
|
+
/^介绍(一下)?自己$/,
|
|
262
|
+
/^自我(介绍|认识)?$/,
|
|
263
|
+
/^echo\s+\S+$/i,
|
|
264
|
+
/^测试$/
|
|
265
|
+
];
|
|
266
|
+
const NOISE_SUMMARY_MAX_CHARS = 200;
|
|
267
|
+
function isNoiseSession(title, summary) {
|
|
268
|
+
const t = String(title || "").trim();
|
|
269
|
+
if (!NOISE_TITLE_PATTERNS.some((re) => re.test(t))) return false;
|
|
270
|
+
return String(summary || "").trim().length <= NOISE_SUMMARY_MAX_CHARS;
|
|
271
|
+
}
|
|
272
|
+
/** 确保 shortId 恰好 8 位:不足时补 SHA-1 前缀 hash。 */
|
|
273
|
+
function normalizeShortId(sessionId) {
|
|
274
|
+
let s = sessionId.replace(/[^A-Za-z0-9]/g, "");
|
|
275
|
+
if (s.length >= 8) return s.slice(0, 8);
|
|
276
|
+
let hash = 0;
|
|
277
|
+
for (let i = 0; i < sessionId.length; i++) hash = (hash << 5) - hash + sessionId.charCodeAt(i) | 0;
|
|
278
|
+
s = s + Math.abs(hash).toString(16).slice(0, 8);
|
|
279
|
+
return s.slice(0, 8);
|
|
280
|
+
}
|
|
281
|
+
/** vault 在 workspace 之外,写入需按调用抬升到 danger-full-access。 */
|
|
282
|
+
const widePolicy = {
|
|
283
|
+
mode: "danger-full-access",
|
|
284
|
+
workspaceRoot: currentVaultPath()
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* 每日 22:00 复盘沉淀节律:
|
|
288
|
+
* - 汇总当天 04-Archive 归档(按日期前缀统计笔记数与主题关键词)
|
|
289
|
+
* - 把当天要点追加到 02-Projects/ 下匹配项目的"近期演进"段(幂等:当天已追加则跳过)
|
|
290
|
+
* - git commit + push
|
|
291
|
+
*
|
|
292
|
+
* 实现:60s 轮询,发现跨过当天 22:00 且尚未执行过 → 执行一次。
|
|
293
|
+
*/
|
|
294
|
+
let lastReviewDate = "";
|
|
295
|
+
async function dailyReview() {
|
|
296
|
+
const now = /* @__PURE__ */ new Date();
|
|
297
|
+
const dateStr = now.toISOString().slice(0, 10);
|
|
298
|
+
if (now.getHours() < 22 || lastReviewDate === dateStr) return;
|
|
299
|
+
lastReviewDate = dateStr;
|
|
300
|
+
try {
|
|
301
|
+
await ensureIndex();
|
|
302
|
+
const todayFiles = [];
|
|
303
|
+
for (const entry of index.values()) {
|
|
304
|
+
if (!entry.displayPath.includes("04-Archive/")) continue;
|
|
305
|
+
const m = entry.displayPath.match(/* @__PURE__ */ new RegExp("^.*?(\\d{4}-\\d{2}-\\d{2})"));
|
|
306
|
+
if (m && m[1] === dateStr) todayFiles.push({
|
|
307
|
+
file: entry.displayPath,
|
|
308
|
+
date: dateStr,
|
|
309
|
+
snippet: entry.snippet
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (todayFiles.length === 0) return;
|
|
313
|
+
for (const proj of [
|
|
314
|
+
"02-Projects/dsh-obsidian-sync.md",
|
|
315
|
+
"02-Projects/dsh-tool-agnes.md",
|
|
316
|
+
"02-Projects/dsh-plugin-manager.md",
|
|
317
|
+
"02-Projects/dsh-conversation-language.md"
|
|
318
|
+
]) try {
|
|
319
|
+
const target = await fs.resolve(currentVaultPath() + "/" + proj);
|
|
320
|
+
let content = String(await fs.readText(target));
|
|
321
|
+
const markerPos = content.indexOf("## 近期演进");
|
|
322
|
+
if (markerPos === -1) continue;
|
|
323
|
+
if (content.indexOf(dateStr) !== -1 && content.indexOf(dateStr) > markerPos) continue;
|
|
324
|
+
const todayLines = todayFiles.map((f) => "- [[04-Archive/" + f.file.replace(/^04-Archive\//, "") + "]] — " + f.snippet.slice(0, 80).replace(/\n/g, " ")).join("\n");
|
|
325
|
+
content = content.slice(0, content.length) + "\n" + dateStr + " 归档:\n" + todayLines + "\n";
|
|
326
|
+
await fs.writeText(target, content, void 0, void 0, widePolicy);
|
|
327
|
+
} catch (_e) {}
|
|
328
|
+
dirty = true;
|
|
329
|
+
try {
|
|
330
|
+
const { execSync } = await import("node:child_process");
|
|
331
|
+
if (execSync("git add -A && git status --porcelain", {
|
|
332
|
+
cwd: currentVaultPath(),
|
|
333
|
+
timeout: 15e3,
|
|
334
|
+
encoding: "utf-8"
|
|
335
|
+
}).trim().length > 0) execSync("git commit -m \"daily-review: " + dateStr + " 归档沉淀(" + todayFiles.length + " 篇)\" && git push", {
|
|
336
|
+
cwd: currentVaultPath(),
|
|
337
|
+
timeout: 6e4,
|
|
338
|
+
encoding: "utf-8"
|
|
339
|
+
});
|
|
340
|
+
} catch (_e) {}
|
|
341
|
+
} catch (_e) {}
|
|
342
|
+
}
|
|
343
|
+
ctx.effect(() => {
|
|
344
|
+
const handle = setInterval(() => {
|
|
345
|
+
dailyReview();
|
|
346
|
+
}, 6e4);
|
|
347
|
+
return () => {
|
|
348
|
+
clearInterval(handle);
|
|
349
|
+
};
|
|
350
|
+
}, "obsidian-sync: daily review at 22:00");
|
|
351
|
+
/**
|
|
352
|
+
* 每周复盘沉淀节律(补齐决策习惯页"周复盘尚未建立"的开放问题):
|
|
353
|
+
* - 每周日 22:30 触发(本地时区),汇总本 ISO 周(周一~周日)04-Archive 归档
|
|
354
|
+
* - 写入/更新 03-Areas/周复盘-<ISO 周>.md(主题分布 + 归档清单 + 待收敛项)
|
|
355
|
+
* - 幂等:同 ISO 周已生成则跳过;git commit + push
|
|
356
|
+
*/
|
|
357
|
+
let lastWeeklyKey = "";
|
|
358
|
+
function isoWeekKey(d) {
|
|
359
|
+
const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
360
|
+
const day = t.getUTCDay() || 7;
|
|
361
|
+
t.setUTCDate(t.getUTCDate() + 4 - day);
|
|
362
|
+
const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
|
|
363
|
+
const week = Math.ceil(((+t - +yearStart) / 864e5 + 1) / 7);
|
|
364
|
+
return t.getUTCFullYear() + "-W" + String(week).padStart(2, "0");
|
|
365
|
+
}
|
|
366
|
+
async function weeklyReview() {
|
|
367
|
+
const now = /* @__PURE__ */ new Date();
|
|
368
|
+
if (now.getDay() !== 0 || now.getHours() < 22) return;
|
|
369
|
+
const weekKey = isoWeekKey(now);
|
|
370
|
+
if (lastWeeklyKey === weekKey) return;
|
|
371
|
+
lastWeeklyKey = weekKey;
|
|
372
|
+
try {
|
|
373
|
+
await ensureIndex();
|
|
374
|
+
const weekDate = new Date(now);
|
|
375
|
+
weekDate.setDate(now.getDate() - 6);
|
|
376
|
+
const monday = weekDate.toISOString().slice(0, 10);
|
|
377
|
+
const weekFiles = [];
|
|
378
|
+
for (const entry of index.values()) {
|
|
379
|
+
if (!entry.displayPath.includes("04-Archive/")) continue;
|
|
380
|
+
const m = entry.displayPath.match(/(\d{4}-\d{2}-\d{2})/);
|
|
381
|
+
if (m && m[1] >= monday && m[1] <= now.toISOString().slice(0, 10)) weekFiles.push({
|
|
382
|
+
file: entry.displayPath,
|
|
383
|
+
date: m[1],
|
|
384
|
+
snippet: entry.snippet
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
weekFiles.sort((a, b) => a.date.localeCompare(b.date));
|
|
388
|
+
if (weekFiles.length === 0) return;
|
|
389
|
+
const lines = weekFiles.map((f) => "- [[04-Archive/" + f.file.replace(/^04-Archive\//, "") + "]](" + f.date + ")— " + f.snippet.slice(0, 60).replace(/\n/g, " ")).join("\n");
|
|
390
|
+
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";
|
|
391
|
+
const target = await fs.resolve(currentVaultPath() + "/03-Areas/周复盘-" + weekKey + ".md");
|
|
392
|
+
let skipped = false;
|
|
393
|
+
try {
|
|
394
|
+
if (String(await fs.readText(target)).includes(weekKey)) skipped = true;
|
|
395
|
+
} catch (_e) {}
|
|
396
|
+
if (!skipped) await fs.writeText(target, note, void 0, void 0, widePolicy);
|
|
397
|
+
dirty = true;
|
|
398
|
+
try {
|
|
399
|
+
const { execSync } = await import("node:child_process");
|
|
400
|
+
if (execSync("git add -A && git status --porcelain", {
|
|
401
|
+
cwd: currentVaultPath(),
|
|
402
|
+
timeout: 15e3,
|
|
403
|
+
encoding: "utf-8"
|
|
404
|
+
}).trim().length > 0) execSync("git commit -m \"weekly-review: " + weekKey + " 归档沉淀(" + weekFiles.length + " 篇)\" && git push", {
|
|
405
|
+
cwd: currentVaultPath(),
|
|
406
|
+
timeout: 6e4,
|
|
407
|
+
encoding: "utf-8"
|
|
408
|
+
});
|
|
409
|
+
} catch (_e) {}
|
|
410
|
+
} catch (_e) {}
|
|
411
|
+
}
|
|
412
|
+
ctx.effect(() => {
|
|
413
|
+
const handle = setInterval(() => {
|
|
414
|
+
weeklyReview();
|
|
415
|
+
}, 6e4);
|
|
416
|
+
return () => {
|
|
417
|
+
clearInterval(handle);
|
|
418
|
+
};
|
|
419
|
+
}, "obsidian-sync: weekly review at Sunday 22:30");
|
|
420
|
+
/**
|
|
421
|
+
* 更新索引文件(DSH-会话归档-索引.md)的按日期段。
|
|
422
|
+
* - 找到当天段落(### YYYY-MM-DD)
|
|
423
|
+
* - 边界:下一个 `### ` 或 `## `(两个井号终止条件)
|
|
424
|
+
* - 同 shortId 已有条目 → 替换该行(幂等去重)
|
|
425
|
+
* - 否则追加到当天段落末尾
|
|
426
|
+
*/
|
|
427
|
+
async function updateIndexFile(entryRelPath, title, sessionId, dateStr) {
|
|
428
|
+
try {
|
|
429
|
+
const idxTarget = await fs.resolve(currentVaultPath() + "/DSH-会话归档-索引.md");
|
|
430
|
+
const idxContent = String(await fs.readText(idxTarget));
|
|
431
|
+
const dayAnchor = "### " + dateStr;
|
|
432
|
+
const line = "\n- [[" + entryRelPath + "|" + title + "]] (" + dateStr + ", " + sessionId + ")\n";
|
|
433
|
+
const dayPos = idxContent.indexOf(dayAnchor);
|
|
434
|
+
if (dayPos === -1) {
|
|
435
|
+
const dayHeader = "\n### " + dateStr + "\n" + line;
|
|
436
|
+
const tailPos = idxContent.indexOf("\n## 主题分类");
|
|
437
|
+
const updated = tailPos === -1 ? idxContent + dayHeader : idxContent.slice(0, tailPos) + dayHeader + idxContent.slice(tailPos);
|
|
438
|
+
await fs.writeText(idxTarget, updated, void 0, void 0, widePolicy);
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
const afterAnchor = dayPos + dayAnchor.length;
|
|
442
|
+
const nextH3 = idxContent.indexOf("\n### ", afterAnchor);
|
|
443
|
+
const nextH2 = idxContent.indexOf("\n## ", afterAnchor);
|
|
444
|
+
let dayEnd = -1;
|
|
445
|
+
if (nextH3 !== -1 && nextH2 !== -1) dayEnd = Math.min(nextH3, nextH2);
|
|
446
|
+
else if (nextH3 !== -1) dayEnd = nextH3;
|
|
447
|
+
else if (nextH2 !== -1) dayEnd = nextH2;
|
|
448
|
+
else dayEnd = idxContent.length;
|
|
449
|
+
const daySection = idxContent.slice(afterAnchor, dayEnd);
|
|
450
|
+
const shortId = normalizeShortId(sessionId);
|
|
451
|
+
const dedupePattern = new RegExp("^- \\[\\[[^\\]]*" + dateStr + "-" + shortId + "-[^\\]]*\\]\\]\\s*\\([^\\)]*\\)$", "m");
|
|
452
|
+
const existingMatch = daySection.match(dedupePattern);
|
|
453
|
+
let updated;
|
|
454
|
+
if (existingMatch !== null) {
|
|
455
|
+
const absPos = afterAnchor + daySection.indexOf(existingMatch[0]);
|
|
456
|
+
updated = idxContent.slice(0, absPos) + line.trimStart() + "\n" + idxContent.slice(absPos + existingMatch[0].length);
|
|
457
|
+
} else updated = idxContent.slice(0, dayEnd) + line + idxContent.slice(dayEnd);
|
|
458
|
+
await fs.writeText(idxTarget, updated, void 0, void 0, widePolicy);
|
|
459
|
+
return true;
|
|
460
|
+
} catch (_e) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/** 从索引中按目录前缀过滤 + 按 task 相关性取 top N 提炼页。 */
|
|
465
|
+
function topDistilled(task, limit) {
|
|
466
|
+
const distilledDirs = [
|
|
467
|
+
"02-Projects/",
|
|
468
|
+
"03-Areas/",
|
|
469
|
+
"05-Resources/"
|
|
470
|
+
];
|
|
471
|
+
const taskTokens = task.trim().length > 0 ? tokenize(task) : /* @__PURE__ */ new Set();
|
|
472
|
+
const results = [];
|
|
473
|
+
for (const entry of index.values()) {
|
|
474
|
+
if (!distilledDirs.some((d) => entry.displayPath.includes(d))) continue;
|
|
475
|
+
let score = 0;
|
|
476
|
+
for (const t of taskTokens) if (entry.tokens.has(t)) score += idfTable.get(t) ?? 1;
|
|
477
|
+
const title = entry.displayPath.split("/").pop() ?? entry.displayPath;
|
|
478
|
+
results.push({
|
|
479
|
+
file: entry.displayPath,
|
|
480
|
+
title,
|
|
481
|
+
score: taskTokens.size > 0 ? Math.round(score * 100) / 100 : 0,
|
|
482
|
+
snippet: entry.snippet
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
results.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title));
|
|
486
|
+
return results.slice(0, limit);
|
|
487
|
+
}
|
|
488
|
+
/** 从 04-Archive/ 取最近 N 篇(按文件名日期倒序)。 */
|
|
489
|
+
function recentArchives(limit) {
|
|
490
|
+
const results = [];
|
|
491
|
+
for (const entry of index.values()) {
|
|
492
|
+
if (!entry.displayPath.includes("04-Archive/")) continue;
|
|
493
|
+
const dateMatch = entry.displayPath.match(/(\d{4}-\d{2}-\d{2})/);
|
|
494
|
+
results.push({
|
|
495
|
+
file: entry.displayPath,
|
|
496
|
+
date: dateMatch ? dateMatch[1] : "unknown",
|
|
497
|
+
snippet: entry.snippet
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
results.sort((a, b) => b.date.localeCompare(a.date));
|
|
501
|
+
return results.slice(0, limit);
|
|
502
|
+
}
|
|
503
|
+
/** 读取指定相对路径的文件内容。 */
|
|
504
|
+
async function readFileContent(relPath) {
|
|
505
|
+
try {
|
|
506
|
+
const target = await fs.resolve(currentVaultPath() + "/" + relPath);
|
|
507
|
+
return String(await fs.readText(target));
|
|
508
|
+
} catch (_e) {
|
|
509
|
+
return "";
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (searchEnabled) {
|
|
513
|
+
ctx.tools.register(defineTool({
|
|
514
|
+
name: "obsidian.search",
|
|
515
|
+
description: "在 Obsidian 知识库中按关键词搜索相关笔记,返回最多 5 条匹配(文件相对路径、命中片段、命中关键词)。按需调用,平时不产生任何 token 成本。",
|
|
516
|
+
parameters: {
|
|
517
|
+
topic: {
|
|
518
|
+
type: "string",
|
|
519
|
+
required: true,
|
|
520
|
+
description: "搜索关键词,可含多个词(中英文均可,自动做 CJK bigram 分词)"
|
|
521
|
+
},
|
|
522
|
+
limit: {
|
|
523
|
+
type: "number",
|
|
524
|
+
description: "返回结果上限,默认 3,最大 5"
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
output: {
|
|
528
|
+
schema: { type: "json" },
|
|
529
|
+
render: (_args, value) => [{
|
|
530
|
+
type: "text",
|
|
531
|
+
text: JSON.stringify(value, null, 2)
|
|
532
|
+
}]
|
|
533
|
+
},
|
|
534
|
+
isConcurrencySafe: () => true,
|
|
535
|
+
async execute(args) {
|
|
536
|
+
const topic = String(args.topic ?? "");
|
|
537
|
+
if (topic.trim().length === 0) return asJsonValue({
|
|
538
|
+
ok: false,
|
|
539
|
+
error: "topic must not be empty"
|
|
540
|
+
});
|
|
541
|
+
const limit = Math.min(Math.max(1, Number(args.limit) || 3), 5);
|
|
542
|
+
await ensureIndex();
|
|
543
|
+
const matches = searchMatches(topic, limit);
|
|
544
|
+
return asJsonValue({
|
|
545
|
+
ok: true,
|
|
546
|
+
matches,
|
|
547
|
+
count: matches.length,
|
|
548
|
+
vaultPath: currentVaultPath()
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}));
|
|
552
|
+
ctx.tools.register(defineTool({
|
|
553
|
+
name: "obsidian.brief",
|
|
554
|
+
description: "每日简报:返回与当前任务最相关的提炼页(02-Projects/03-Areas/05-Resources)top 3 + 最近 3 条归档摘要 + 用户决策习惯与偏好页内容。低成本\"开机记忆\"入口,建议任务开始前调用一次。",
|
|
555
|
+
parameters: { task: {
|
|
556
|
+
type: "string",
|
|
557
|
+
description: "当前任务描述(可选),用于相关性排序;省略则返回全部提炼页按名称排序的 top 3"
|
|
558
|
+
} },
|
|
559
|
+
output: {
|
|
560
|
+
schema: { type: "json" },
|
|
561
|
+
render: (_args, value) => [{
|
|
562
|
+
type: "text",
|
|
563
|
+
text: JSON.stringify(value, null, 2)
|
|
564
|
+
}]
|
|
565
|
+
},
|
|
566
|
+
isConcurrencySafe: () => true,
|
|
567
|
+
async execute(args) {
|
|
568
|
+
const task = String(args.task ?? "").trim();
|
|
569
|
+
await ensureIndex();
|
|
570
|
+
const distilled = topDistilled(task, 3);
|
|
571
|
+
const recent = recentArchives(3);
|
|
572
|
+
let userProfile = "";
|
|
573
|
+
const profileRelPath = "03-Areas/决策习惯与偏好.md";
|
|
574
|
+
if ([...index.values()].some((e) => e.displayPath === profileRelPath)) userProfile = await readFileContent(profileRelPath);
|
|
575
|
+
return asJsonValue({
|
|
576
|
+
ok: true,
|
|
577
|
+
task: task || "(未指定)",
|
|
578
|
+
distilled,
|
|
579
|
+
recent,
|
|
580
|
+
userProfile: userProfile.length > 0 ? userProfile.slice(0, 2e3) : "",
|
|
581
|
+
vaultPath: currentVaultPath()
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}));
|
|
585
|
+
ctx.tools.register(defineTool({
|
|
586
|
+
name: "obsidian.read_note",
|
|
587
|
+
description: "按相对路径读取 Obsidian 笔记的 frontmatter 与正文,补全 obsidian.search 的\"搜索→阅读\"闭环。路径相对 vault 根(如 02-Projects/dsh-obsidian-sync.md),自动解析 YAML frontmatter(key: value / [list] / 数值 / 布尔)。按需调用,零 token 成本。",
|
|
588
|
+
parameters: { path: {
|
|
589
|
+
type: "string",
|
|
590
|
+
required: true,
|
|
591
|
+
description: "笔记相对 vault 的路径(如 02-Projects/dsh-obsidian-sync.md 或 04-Archive/xxx.md)"
|
|
592
|
+
} },
|
|
593
|
+
output: {
|
|
594
|
+
schema: { type: "json" },
|
|
595
|
+
render: (_args, value) => [{
|
|
596
|
+
type: "text",
|
|
597
|
+
text: JSON.stringify(value, null, 2)
|
|
598
|
+
}]
|
|
599
|
+
},
|
|
600
|
+
isConcurrencySafe: () => true,
|
|
601
|
+
async execute(args) {
|
|
602
|
+
const rel = String(args.path ?? "").replace(/^\/+/, "").replace(/\\/g, "/").trim();
|
|
603
|
+
if (rel.length === 0) return asJsonValue({
|
|
604
|
+
ok: false,
|
|
605
|
+
error: "path must not be empty",
|
|
606
|
+
code: "EMPTY_PATH"
|
|
607
|
+
});
|
|
608
|
+
const content = await readFileContent(rel);
|
|
609
|
+
if (content.length === 0) return asJsonValue({
|
|
610
|
+
ok: false,
|
|
611
|
+
error: "note not found or unreadable: " + rel,
|
|
612
|
+
code: "NOT_FOUND"
|
|
613
|
+
});
|
|
614
|
+
const frontmatter = {};
|
|
615
|
+
let body = content;
|
|
616
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
617
|
+
if (fmMatch !== null) {
|
|
618
|
+
body = content.slice(fmMatch[0].length);
|
|
619
|
+
for (const line of fmMatch[1].split("\n")) {
|
|
620
|
+
const kv = line.match(/^([A-Za-z0-9_\-]+):\s*(.*)$/);
|
|
621
|
+
if (kv === null) continue;
|
|
622
|
+
const key = kv[1];
|
|
623
|
+
let raw = kv[2].trim();
|
|
624
|
+
let val;
|
|
625
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
626
|
+
const inner = raw.slice(1, -1).trim();
|
|
627
|
+
val = inner.length === 0 ? [] : inner.split(",").map((s) => s.trim().replace(/^["']|["']$/g, ""));
|
|
628
|
+
} else if (raw === "null") val = null;
|
|
629
|
+
else if (raw === "true") val = true;
|
|
630
|
+
else if (raw === "false") val = false;
|
|
631
|
+
else if (/^\d+$/.test(raw)) val = Number(raw);
|
|
632
|
+
else val = raw.replace(/^["']|["']$/g, "");
|
|
633
|
+
frontmatter[key] = val;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const titleLine = body.match(/^#\s+(.+)$/m);
|
|
637
|
+
return asJsonValue({
|
|
638
|
+
ok: true,
|
|
639
|
+
path: rel,
|
|
640
|
+
frontmatter,
|
|
641
|
+
title: titleLine ? titleLine[1].trim() : rel.split("/").pop() ?? rel,
|
|
642
|
+
body
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
ctx.tools.register(defineTool({
|
|
648
|
+
name: "obsidian.sync_session",
|
|
649
|
+
description: "把当前 DSH 会话按 Obsidian vault 的 PARA 结构与用户既有命名/索引规则,以 Markdown 笔记形式幂等写入 vault/04-Archive/,并自动把新条目挂到 DSH-会话归档-索引.md 的按日期段。摘要未变则跳过。幂等键 = date + shortId(标题变更不影响去重)。",
|
|
650
|
+
parameters: {
|
|
651
|
+
session_id: {
|
|
652
|
+
type: "string",
|
|
653
|
+
required: true,
|
|
654
|
+
description: "当前会话的完整 SessionId(UUID 或短 ID),原样存入 frontmatter 与正文"
|
|
655
|
+
},
|
|
656
|
+
title: {
|
|
657
|
+
type: "string",
|
|
658
|
+
required: true,
|
|
659
|
+
description: "会话主题标题(写入 frontmatter 与正文 # 标题;文件名仅取前 40 字符做安全截断)"
|
|
660
|
+
},
|
|
661
|
+
summary: {
|
|
662
|
+
type: "string",
|
|
663
|
+
required: true,
|
|
664
|
+
description: "会话结论/摘要(纯文本,建议 200-800 字,包含用户需求、过程要点、交付物)"
|
|
665
|
+
},
|
|
666
|
+
tags: {
|
|
667
|
+
type: "array",
|
|
668
|
+
items: { type: "string" },
|
|
669
|
+
description: "主题标签数组,最多 10 个,将写入 frontmatter tags 字段(Obsidian 原生标签)"
|
|
670
|
+
},
|
|
671
|
+
related: {
|
|
672
|
+
type: "array",
|
|
673
|
+
items: { type: "string" },
|
|
674
|
+
description: "关联笔记完整文件名(含 .md 扩展名,如 2026-09-11-a9095879-xxx.md),将生成为 [[全路径双链]]"
|
|
675
|
+
},
|
|
676
|
+
raw_log: {
|
|
677
|
+
type: "string",
|
|
678
|
+
description: "原始会话日志绝对路径,格式 ~/.dsh/sessions/<project-dir>/session-<uuid>/session.v3.jsonl.zstd,将写入落款以便追溯"
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
output: {
|
|
682
|
+
schema: { type: "json" },
|
|
683
|
+
render: (_args, value) => [{
|
|
684
|
+
type: "text",
|
|
685
|
+
text: JSON.stringify(value, null, 2)
|
|
686
|
+
}]
|
|
687
|
+
},
|
|
688
|
+
async execute(args) {
|
|
689
|
+
const sessionId = String(args.session_id ?? "unknown");
|
|
690
|
+
const title = String(args.title ?? "");
|
|
691
|
+
const safeTitle = sanitizeTitleForFilename(title);
|
|
692
|
+
const summary = String(args.summary ?? "");
|
|
693
|
+
const tags = Array.isArray(args.tags) ? args.tags.map(String).slice(0, 10) : [];
|
|
694
|
+
const related = Array.isArray(args.related) ? args.related.map(String).slice(0, 10) : [];
|
|
695
|
+
const rawLog = String(args.raw_log ?? "");
|
|
696
|
+
if (isNoiseSession(title, summary)) return asJsonValue({
|
|
697
|
+
ok: true,
|
|
698
|
+
skipped: true,
|
|
699
|
+
noise: true,
|
|
700
|
+
reason: "identity-test noise session filtered out (short title + short summary)",
|
|
701
|
+
index: "unchanged"
|
|
702
|
+
});
|
|
703
|
+
const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
704
|
+
const shortId = normalizeShortId(sessionId);
|
|
705
|
+
const entryPrefix = dateStr + "-" + shortId + "-";
|
|
706
|
+
let entryRelPath;
|
|
707
|
+
try {
|
|
708
|
+
const archiveDir = await fs.resolve(currentVaultPath() + "/04-Archive");
|
|
709
|
+
const existing = (await fs.listDir(archiveDir)).find((e) => e.type === "file" && e.name?.startsWith(entryPrefix));
|
|
710
|
+
if (existing !== void 0 && existing.name !== void 0) entryRelPath = "04-Archive/" + existing.name;
|
|
711
|
+
else entryRelPath = "04-Archive/" + entryPrefix + safeTitle + ".md";
|
|
712
|
+
} catch (_e) {
|
|
713
|
+
entryRelPath = "04-Archive/" + entryPrefix + safeTitle + ".md";
|
|
714
|
+
}
|
|
715
|
+
const relatedLines = [];
|
|
716
|
+
for (const r of related) {
|
|
717
|
+
const rFull = r.endsWith(".md") ? r : r + ".md";
|
|
718
|
+
const rRel = "04-Archive/" + rFull;
|
|
719
|
+
try {
|
|
720
|
+
await fs.stat(await fs.resolve(currentVaultPath() + "/" + rRel));
|
|
721
|
+
relatedLines.push("- [[" + rRel + "|" + rFull.replace(/^.*\//, "").replace(/\.md$/, "") + "]]");
|
|
722
|
+
} catch (_e) {
|
|
723
|
+
relatedLines.push("- " + rFull + "(未找到,降级为文字引用)");
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const fmDate = dateStr;
|
|
727
|
+
const fmSessionId = sessionId.replace(/[^A-Za-z0-9-]/g, "").slice(0, 8);
|
|
728
|
+
const fmTags = tags.length > 0 ? tags : [];
|
|
729
|
+
const frontmatter = [
|
|
730
|
+
"---",
|
|
731
|
+
"session_id: " + fmSessionId,
|
|
732
|
+
"full_session_id: " + sessionId,
|
|
733
|
+
"date: " + fmDate,
|
|
734
|
+
"tags: [" + fmTags.join(", ") + "]",
|
|
735
|
+
"---",
|
|
736
|
+
""
|
|
737
|
+
].join("\n");
|
|
738
|
+
const logLine = rawLog.trim().length > 0 ? "> 📎 原始日志: `" + rawLog.trim() + "`\n\n> 由 DSH Obsidian Sync 自动同步\n" : "> 由 DSH Obsidian Sync 自动同步\n";
|
|
739
|
+
const content = frontmatter + "# DSH 会话: " + title + "\n\n## 基本信息\n\n| 属性 | 值 |\n|------|-----|\n| **日期** | " + dateStr + " |\n| **会话ID** | `" + sessionId + "` |\n| **状态** | ✅ 已归档 |\n| **分类** | " + (tags.length > 0 ? tags.join(" ") : "通用") + " |\n\n---\n\n## 摘要\n\n" + summary + "\n\n" + (relatedLines.length > 0 ? "## 关联\n\n" + relatedLines.join("\n") + "\n\n---\n" : "") + logLine;
|
|
740
|
+
let target;
|
|
741
|
+
try {
|
|
742
|
+
target = await fs.resolve(currentVaultPath() + "/" + entryRelPath);
|
|
743
|
+
} catch (_e) {
|
|
744
|
+
const winPath = currentVaultPath() + "\\" + entryRelPath.split("/").join("\\");
|
|
745
|
+
try {
|
|
746
|
+
target = await fs.resolve(winPath);
|
|
747
|
+
} catch (_e2) {
|
|
748
|
+
return asJsonValue({
|
|
749
|
+
ok: false,
|
|
750
|
+
error: "cannot resolve vault entry path",
|
|
751
|
+
code: "RESOLVE_FAILED"
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
let skipped = false;
|
|
756
|
+
try {
|
|
757
|
+
const prev = String(await fs.readText(target));
|
|
758
|
+
const pos = prev.indexOf("## 摘要");
|
|
759
|
+
if (pos >= 0 && prev.slice(pos + 5).trim() === summary.trim()) skipped = true;
|
|
760
|
+
} catch (_e) {}
|
|
761
|
+
if (skipped) return asJsonValue({
|
|
762
|
+
ok: true,
|
|
763
|
+
skipped: true,
|
|
764
|
+
file: entryRelPath,
|
|
765
|
+
reason: "summary unchanged",
|
|
766
|
+
index: "unchanged"
|
|
767
|
+
});
|
|
768
|
+
try {
|
|
769
|
+
await fs.writeText(target, content, void 0, void 0, widePolicy);
|
|
770
|
+
} catch (writeErr) {
|
|
771
|
+
return asJsonValue({
|
|
772
|
+
ok: false,
|
|
773
|
+
error: "writeText failed: " + String(writeErr instanceof Error ? writeErr.message : writeErr),
|
|
774
|
+
code: "WRITE_FAILED"
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
const indexOk = await updateIndexFile(entryRelPath, title, sessionId, dateStr);
|
|
778
|
+
dirty = true;
|
|
779
|
+
let gitStatus = "skipped";
|
|
780
|
+
try {
|
|
781
|
+
const { execSync } = await import("node:child_process");
|
|
782
|
+
if (execSync("git add -A && git status --porcelain", {
|
|
783
|
+
cwd: currentVaultPath(),
|
|
784
|
+
timeout: 15e3,
|
|
785
|
+
encoding: "utf-8"
|
|
786
|
+
}).trim().length > 0) {
|
|
787
|
+
execSync("git commit -m \"" + ("archive: " + entryRelPath.replace(/^04-Archive\//, "")).replace(/"/g, "\\\"") + "\" && git push", {
|
|
788
|
+
cwd: currentVaultPath(),
|
|
789
|
+
timeout: 6e4,
|
|
790
|
+
encoding: "utf-8"
|
|
791
|
+
});
|
|
792
|
+
gitStatus = "pushed";
|
|
793
|
+
} else gitStatus = "no changes";
|
|
794
|
+
} catch (gitErr) {
|
|
795
|
+
gitStatus = "failed: " + String(gitErr instanceof Error ? gitErr.message : gitErr);
|
|
796
|
+
}
|
|
797
|
+
return asJsonValue({
|
|
798
|
+
ok: true,
|
|
799
|
+
skipped: false,
|
|
800
|
+
file: entryRelPath,
|
|
801
|
+
index: indexOk ? "updated" : "skipped",
|
|
802
|
+
git: gitStatus
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
807
|
+
//#endregion
|
|
808
|
+
export { Config, apply, inject, name };
|
|
809
|
+
|
|
810
|
+
//# sourceMappingURL=index.js.map
|