dsh-sun-space 0.2.8 → 0.3.0
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/index.mjs +72 -21
- package/lib/router-tools.mjs +46 -28
- package/lib/worker-vault.mjs +37 -19
- package/package.json +2 -2
- package/src/client.js +24 -11
package/lib/index.mjs
CHANGED
|
@@ -18,29 +18,35 @@ var __exportAll = (all, no_symbols) => {
|
|
|
18
18
|
/**
|
|
19
19
|
* vault 路径解析——唯一事实源,全插件共享(source/router-tools/worker-vault/worker-watchdog)。
|
|
20
20
|
*
|
|
21
|
-
*
|
|
21
|
+
* 解析优先级(vaultPath() 每次调用重读,运行期变更即时生效):
|
|
22
22
|
* ① 环境变量 SUN_VAULT(启动命令注入,随进程生效)
|
|
23
23
|
* ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写,持久化,不入库)
|
|
24
|
-
* ③
|
|
24
|
+
* ③ ~/.dsh/state/sun-space/state.json 的 vault 字段(Router 会话创建时插件自动登记 = 用户选定的工作区目录)
|
|
25
|
+
* ④ 会话工作目录(调用方从 exec 提取传入——用户在哪个工作区开会话,vault 就解析到哪)
|
|
26
|
+
* ⑤ 默认约定 ~/Workspace/sun-vault
|
|
25
27
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
28
|
+
* ③ 的设计动机:用户的 vault 就是他在 DSH 里选定的那个目录——Router 会话创建时
|
|
29
|
+
* 插件自动把该会话 cwd 登记为 vault 根(registerVaultRootFromSession),无需任何
|
|
30
|
+
* 手工配置。①② 供自定义/覆盖;⑤ 仅在全新裸环境兜底。
|
|
29
31
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* 解析结果写回 state.json,作为浏览器端(孙学空间)的发现通道——浏览器经
|
|
33
|
+
* workspaceFiles/read 以绝对路径读取该文件(工作区外绝对路径读取被宿主允许)。
|
|
34
|
+
*
|
|
35
|
+
* 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(session-superagent 同款)。
|
|
36
|
+
* 本文件不携带任何个人路径;个人 vault 位置只存在于①②③(均不入库)。
|
|
32
37
|
*/
|
|
33
38
|
const DEFAULT_VAULT_DIRNAME = "sun-vault";
|
|
34
39
|
/** 插件自有状态目录(<DSH_HOME 或 ~/>/.dsh/state/sun-space/),沿用官方 state/<插件名> 约定。
|
|
35
40
|
* 跟随 DSH_HOME:多实例(隔离 HOME 的测试环境)之间状态互不串扰。 */
|
|
36
41
|
const stateDir = () => join(process.env.DSH_HOME ?? homedir(), ".dsh", "state", "sun-space");
|
|
37
|
-
/**
|
|
38
|
-
function computeVaultPath(envValue, configJson, home = homedir()) {
|
|
42
|
+
/** 纯函数:五级优先解析(测试与 vaultPath 共用)。 */
|
|
43
|
+
function computeVaultPath(envValue, configJson, stateJson, sessionCwd, home = homedir()) {
|
|
39
44
|
if (envValue && envValue.trim() !== "") return envValue;
|
|
40
|
-
try {
|
|
41
|
-
const
|
|
42
|
-
if (typeof
|
|
45
|
+
for (const raw of [configJson, stateJson]) try {
|
|
46
|
+
const parsed = JSON.parse(raw ?? "");
|
|
47
|
+
if (typeof parsed?.vault === "string" && parsed.vault.startsWith("/")) return parsed.vault;
|
|
43
48
|
} catch {}
|
|
49
|
+
if (sessionCwd && sessionCwd.startsWith("/")) return sessionCwd;
|
|
44
50
|
return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
|
|
45
51
|
}
|
|
46
52
|
let cached = {
|
|
@@ -48,32 +54,68 @@ let cached = {
|
|
|
48
54
|
key: "",
|
|
49
55
|
value: ""
|
|
50
56
|
};
|
|
51
|
-
/**
|
|
52
|
-
|
|
57
|
+
/** 运行期解析:每次调用重读状态目录(60s 微缓存防抖),登记/手改后即时生效。
|
|
58
|
+
* sessionCwd:调用方从 exec 提取的会话工作目录(可选)——用户在哪个工作区开会话,vault 就解析到哪。 */
|
|
59
|
+
function vaultPath(sessionCwd, now = Date.now()) {
|
|
53
60
|
const dir = stateDir();
|
|
54
61
|
let configJson;
|
|
62
|
+
let stateJson;
|
|
55
63
|
try {
|
|
56
64
|
configJson = readFileSync(join(dir, "config.json"), "utf8");
|
|
57
65
|
} catch {}
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
try {
|
|
67
|
+
stateJson = readFileSync(join(dir, "state.json"), "utf8");
|
|
68
|
+
} catch {}
|
|
69
|
+
const key = `${sessionCwd ?? ""}|${configJson ?? ""}|${stateJson ?? ""}`;
|
|
70
|
+
if (now - cached.at < 6e4 && cached.key === key) return cached.value;
|
|
71
|
+
const value = computeVaultPath(process.env.SUN_VAULT, configJson, stateJson, sessionCwd);
|
|
60
72
|
cached = {
|
|
61
73
|
at: now,
|
|
62
|
-
key
|
|
74
|
+
key,
|
|
63
75
|
value
|
|
64
76
|
};
|
|
65
77
|
return value;
|
|
66
78
|
}
|
|
67
79
|
vaultPath();
|
|
80
|
+
/**
|
|
81
|
+
* Router 会话创建时登记 vault 根("当前目录作为 vault"语义的落点)。
|
|
82
|
+
* 仅在更高优先级来源(env/config/state)均未设置时才登记首个值——
|
|
83
|
+
* 多 Router 并存或重复创建时第一个登记者生效。
|
|
84
|
+
* 尽力而为:任何失败静默退场(最坏是回退到约定路径)。
|
|
85
|
+
*/
|
|
86
|
+
function registerVaultRootFromSession(cwd, now = /* @__PURE__ */ new Date()) {
|
|
87
|
+
if (!cwd || !cwd.startsWith("/")) return false;
|
|
88
|
+
if (process.env.SUN_VAULT?.trim()) return false;
|
|
89
|
+
try {
|
|
90
|
+
const dir = stateDir();
|
|
91
|
+
mkdirSync(dir, { recursive: true });
|
|
92
|
+
const p = join(dir, "state.json");
|
|
93
|
+
let state = {};
|
|
94
|
+
try {
|
|
95
|
+
state = JSON.parse(readFileSync(p, "utf8"));
|
|
96
|
+
} catch {}
|
|
97
|
+
if (state.vault && state.vault.startsWith("/")) return false;
|
|
98
|
+
state.vault = cwd;
|
|
99
|
+
state.updatedAt = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
100
|
+
writeFileSync(p, JSON.stringify(state, null, 2), "utf8");
|
|
101
|
+
return true;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
68
106
|
/** 把解析结果写回状态目录的 state.json(apply 时调用,尽力而为),供客户端发现。 */
|
|
69
107
|
function publishVaultState(now = /* @__PURE__ */ new Date()) {
|
|
70
108
|
try {
|
|
71
109
|
const dir = stateDir();
|
|
72
110
|
mkdirSync(dir, { recursive: true });
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
const p = join(dir, "state.json");
|
|
112
|
+
let state = {};
|
|
113
|
+
try {
|
|
114
|
+
state = JSON.parse(readFileSync(p, "utf8"));
|
|
115
|
+
} catch {}
|
|
116
|
+
state.vault = vaultPath(void 0, now.getTime());
|
|
117
|
+
state.updatedAt = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
118
|
+
writeFileSync(p, JSON.stringify(state, null, 2), "utf8");
|
|
77
119
|
} catch {}
|
|
78
120
|
}
|
|
79
121
|
// src/dws.ts
|
|
@@ -493,5 +535,14 @@ function apply(ctx) {
|
|
|
493
535
|
publishVaultState();
|
|
494
536
|
ctx.plugin(source_exports);
|
|
495
537
|
ctx.plugin(worker_watchdog_exports);
|
|
538
|
+
try {
|
|
539
|
+
ctx.on("agent/created", (...args) => {
|
|
540
|
+
const agent = args[0]?.agent;
|
|
541
|
+
const header = agent?.session?.header;
|
|
542
|
+
if (header?.agentPreset !== "sun-router") return;
|
|
543
|
+
const cwd = header.cwd ?? agent?.cwd;
|
|
544
|
+
registerVaultRootFromSession(typeof cwd === "string" ? cwd : void 0);
|
|
545
|
+
});
|
|
546
|
+
} catch {}
|
|
496
547
|
}
|
|
497
548
|
export { apply, inject, name };
|
package/lib/router-tools.mjs
CHANGED
|
@@ -189,29 +189,35 @@ async function dingSelf(userId, content, type = "app") {
|
|
|
189
189
|
/**
|
|
190
190
|
* vault 路径解析——唯一事实源,全插件共享(source/router-tools/worker-vault/worker-watchdog)。
|
|
191
191
|
*
|
|
192
|
-
*
|
|
192
|
+
* 解析优先级(vaultPath() 每次调用重读,运行期变更即时生效):
|
|
193
193
|
* ① 环境变量 SUN_VAULT(启动命令注入,随进程生效)
|
|
194
194
|
* ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写,持久化,不入库)
|
|
195
|
-
* ③
|
|
195
|
+
* ③ ~/.dsh/state/sun-space/state.json 的 vault 字段(Router 会话创建时插件自动登记 = 用户选定的工作区目录)
|
|
196
|
+
* ④ 会话工作目录(调用方从 exec 提取传入——用户在哪个工作区开会话,vault 就解析到哪)
|
|
197
|
+
* ⑤ 默认约定 ~/Workspace/sun-vault
|
|
196
198
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
199
|
+
* ③ 的设计动机:用户的 vault 就是他在 DSH 里选定的那个目录——Router 会话创建时
|
|
200
|
+
* 插件自动把该会话 cwd 登记为 vault 根(registerVaultRootFromSession),无需任何
|
|
201
|
+
* 手工配置。①② 供自定义/覆盖;⑤ 仅在全新裸环境兜底。
|
|
200
202
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
+
* 解析结果写回 state.json,作为浏览器端(孙学空间)的发现通道——浏览器经
|
|
204
|
+
* workspaceFiles/read 以绝对路径读取该文件(工作区外绝对路径读取被宿主允许)。
|
|
205
|
+
*
|
|
206
|
+
* 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(session-superagent 同款)。
|
|
207
|
+
* 本文件不携带任何个人路径;个人 vault 位置只存在于①②③(均不入库)。
|
|
203
208
|
*/
|
|
204
209
|
const DEFAULT_VAULT_DIRNAME = "sun-vault";
|
|
205
210
|
/** 插件自有状态目录(<DSH_HOME 或 ~/>/.dsh/state/sun-space/),沿用官方 state/<插件名> 约定。
|
|
206
211
|
* 跟随 DSH_HOME:多实例(隔离 HOME 的测试环境)之间状态互不串扰。 */
|
|
207
212
|
const stateDir = () => join(process.env.DSH_HOME ?? homedir(), ".dsh", "state", "sun-space");
|
|
208
|
-
/**
|
|
209
|
-
function computeVaultPath(envValue, configJson, home = homedir()) {
|
|
213
|
+
/** 纯函数:五级优先解析(测试与 vaultPath 共用)。 */
|
|
214
|
+
function computeVaultPath(envValue, configJson, stateJson, sessionCwd, home = homedir()) {
|
|
210
215
|
if (envValue && envValue.trim() !== "") return envValue;
|
|
211
|
-
try {
|
|
212
|
-
const
|
|
213
|
-
if (typeof
|
|
216
|
+
for (const raw of [configJson, stateJson]) try {
|
|
217
|
+
const parsed = JSON.parse(raw ?? "");
|
|
218
|
+
if (typeof parsed?.vault === "string" && parsed.vault.startsWith("/")) return parsed.vault;
|
|
214
219
|
} catch {}
|
|
220
|
+
if (sessionCwd && sessionCwd.startsWith("/")) return sessionCwd;
|
|
215
221
|
return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
|
|
216
222
|
}
|
|
217
223
|
let cached = {
|
|
@@ -219,18 +225,24 @@ let cached = {
|
|
|
219
225
|
key: "",
|
|
220
226
|
value: ""
|
|
221
227
|
};
|
|
222
|
-
/**
|
|
223
|
-
|
|
228
|
+
/** 运行期解析:每次调用重读状态目录(60s 微缓存防抖),登记/手改后即时生效。
|
|
229
|
+
* sessionCwd:调用方从 exec 提取的会话工作目录(可选)——用户在哪个工作区开会话,vault 就解析到哪。 */
|
|
230
|
+
function vaultPath(sessionCwd, now = Date.now()) {
|
|
224
231
|
const dir = stateDir();
|
|
225
232
|
let configJson;
|
|
233
|
+
let stateJson;
|
|
226
234
|
try {
|
|
227
235
|
configJson = readFileSync(join(dir, "config.json"), "utf8");
|
|
228
236
|
} catch {}
|
|
229
|
-
|
|
230
|
-
|
|
237
|
+
try {
|
|
238
|
+
stateJson = readFileSync(join(dir, "state.json"), "utf8");
|
|
239
|
+
} catch {}
|
|
240
|
+
const key = `${sessionCwd ?? ""}|${configJson ?? ""}|${stateJson ?? ""}`;
|
|
241
|
+
if (now - cached.at < 6e4 && cached.key === key) return cached.value;
|
|
242
|
+
const value = computeVaultPath(process.env.SUN_VAULT, configJson, stateJson, sessionCwd);
|
|
231
243
|
cached = {
|
|
232
244
|
at: now,
|
|
233
|
-
key
|
|
245
|
+
key,
|
|
234
246
|
value
|
|
235
247
|
};
|
|
236
248
|
return value;
|
|
@@ -238,6 +250,12 @@ function vaultPath(now = Date.now()) {
|
|
|
238
250
|
vaultPath();
|
|
239
251
|
// src/router-tools.ts
|
|
240
252
|
const inject = ["tools", "subagents"];
|
|
253
|
+
/** 从工具执行上下文提取会话工作目录(用户在哪个工作区开会话,vault 就解析到哪)。防御式:形状未知时返回 undefined。 */
|
|
254
|
+
function sessionCwdOf(exec) {
|
|
255
|
+
const e = exec;
|
|
256
|
+
const c = e?.agent?.session?.cwd ?? e?.agent?.session?.header?.cwd ?? e?.session?.cwd;
|
|
257
|
+
return typeof c === "string" && c.startsWith("/") ? c : void 0;
|
|
258
|
+
}
|
|
241
259
|
const text = (s) => [{
|
|
242
260
|
type: "text",
|
|
243
261
|
text: s
|
|
@@ -313,10 +331,10 @@ function apply(ctx) {
|
|
|
313
331
|
}
|
|
314
332
|
},
|
|
315
333
|
required: ["what"]
|
|
316
|
-
}, async (args) => {
|
|
334
|
+
}, async (args, exec) => {
|
|
317
335
|
try {
|
|
318
336
|
if (args.what === "triggers") {
|
|
319
|
-
const dir = resolveSafe(vaultPath(), "90-Meta/triggers");
|
|
337
|
+
const dir = resolveSafe(vaultPath(sessionCwdOf(exec)), "90-Meta/triggers");
|
|
320
338
|
try {
|
|
321
339
|
const files = (await readdir(dir)).filter((f) => f.endsWith(".json")).sort().reverse().slice(0, 10);
|
|
322
340
|
const triggers = [];
|
|
@@ -348,7 +366,7 @@ function apply(ctx) {
|
|
|
348
366
|
}
|
|
349
367
|
if (args.what === "journal") {
|
|
350
368
|
const d = args.date ?? localDate();
|
|
351
|
-
const p = resolveRead(vaultPath(), `40-Journal/${d}.md`);
|
|
369
|
+
const p = resolveRead(vaultPath(sessionCwdOf(exec)), `40-Journal/${d}.md`);
|
|
352
370
|
const raw = await readFile(p, "utf8");
|
|
353
371
|
if (args.full) return {
|
|
354
372
|
ok: true,
|
|
@@ -363,7 +381,7 @@ function apply(ctx) {
|
|
|
363
381
|
};
|
|
364
382
|
}
|
|
365
383
|
if (args.what === "tasks") {
|
|
366
|
-
const dir = resolveSafe(vaultPath(), "50-Tasks");
|
|
384
|
+
const dir = resolveSafe(vaultPath(sessionCwdOf(exec)), "50-Tasks");
|
|
367
385
|
const files = (await readdir(dir)).filter((f) => f.endsWith(".md"));
|
|
368
386
|
const tasks = [];
|
|
369
387
|
for (const f of files) {
|
|
@@ -386,7 +404,7 @@ function apply(ctx) {
|
|
|
386
404
|
};
|
|
387
405
|
}
|
|
388
406
|
if (args.what === "runbook" || args.what === "memory") {
|
|
389
|
-
const p = resolveSafe(vaultPath(), `90-Meta/${args.what}.md`);
|
|
407
|
+
const p = resolveSafe(vaultPath(sessionCwdOf(exec)), `90-Meta/${args.what}.md`);
|
|
390
408
|
return {
|
|
391
409
|
ok: true,
|
|
392
410
|
data: { text: (await readFile(p, "utf8")).slice(0, 8e3) }
|
|
@@ -397,7 +415,7 @@ function apply(ctx) {
|
|
|
397
415
|
ok: false,
|
|
398
416
|
note: "note 需要 path"
|
|
399
417
|
};
|
|
400
|
-
const raw = await readFile(resolveRead(vaultPath(), args.path), "utf8");
|
|
418
|
+
const raw = await readFile(resolveRead(vaultPath(sessionCwdOf(exec)), args.path), "utf8");
|
|
401
419
|
if (args.full) return {
|
|
402
420
|
ok: true,
|
|
403
421
|
data: { text: raw.slice(0, 8e3) }
|
|
@@ -439,9 +457,9 @@ function apply(ctx) {
|
|
|
439
457
|
}
|
|
440
458
|
},
|
|
441
459
|
required: ["path", "content"]
|
|
442
|
-
}, async (args) => {
|
|
460
|
+
}, async (args, exec) => {
|
|
443
461
|
try {
|
|
444
|
-
const p = resolveSafe(vaultPath(), args.path);
|
|
462
|
+
const p = resolveSafe(vaultPath(sessionCwdOf(exec)), args.path);
|
|
445
463
|
await mkdir(join(p, ".."), { recursive: true });
|
|
446
464
|
if (args.mode === "append") await appendFile(p, args.content, "utf8");
|
|
447
465
|
else await writeFile(p, args.content, "utf8");
|
|
@@ -535,9 +553,9 @@ function apply(ctx) {
|
|
|
535
553
|
}
|
|
536
554
|
},
|
|
537
555
|
required: ["path", "fields"]
|
|
538
|
-
}, async (args) => {
|
|
556
|
+
}, async (args, exec) => {
|
|
539
557
|
try {
|
|
540
|
-
const p = resolveSafe(vaultPath(), args.path);
|
|
558
|
+
const p = resolveSafe(vaultPath(sessionCwdOf(exec)), args.path);
|
|
541
559
|
const updated = updateFrontmatter(await readFile(p, "utf8"), args.fields);
|
|
542
560
|
await writeFile(p, updated, "utf8");
|
|
543
561
|
return {
|
|
@@ -611,7 +629,7 @@ function apply(ctx) {
|
|
|
611
629
|
};
|
|
612
630
|
}
|
|
613
631
|
try {
|
|
614
|
-
const p = resolveSafe(vaultPath(), `50-Tasks/${args.task_id}.md`);
|
|
632
|
+
const p = resolveSafe(vaultPath(sessionCwdOf(exec)), `50-Tasks/${args.task_id}.md`);
|
|
615
633
|
const raw = await readFile(p, "utf8");
|
|
616
634
|
await writeFile(p, updateFrontmatter(raw, { session: String(activation.childId ?? "") }), "utf8");
|
|
617
635
|
} catch {}
|
package/lib/worker-vault.mjs
CHANGED
|
@@ -32,29 +32,35 @@ function resolveSafe(vaultRoot, rel) {
|
|
|
32
32
|
/**
|
|
33
33
|
* vault 路径解析——唯一事实源,全插件共享(source/router-tools/worker-vault/worker-watchdog)。
|
|
34
34
|
*
|
|
35
|
-
*
|
|
35
|
+
* 解析优先级(vaultPath() 每次调用重读,运行期变更即时生效):
|
|
36
36
|
* ① 环境变量 SUN_VAULT(启动命令注入,随进程生效)
|
|
37
37
|
* ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写,持久化,不入库)
|
|
38
|
-
* ③
|
|
38
|
+
* ③ ~/.dsh/state/sun-space/state.json 的 vault 字段(Router 会话创建时插件自动登记 = 用户选定的工作区目录)
|
|
39
|
+
* ④ 会话工作目录(调用方从 exec 提取传入——用户在哪个工作区开会话,vault 就解析到哪)
|
|
40
|
+
* ⑤ 默认约定 ~/Workspace/sun-vault
|
|
39
41
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
42
|
+
* ③ 的设计动机:用户的 vault 就是他在 DSH 里选定的那个目录——Router 会话创建时
|
|
43
|
+
* 插件自动把该会话 cwd 登记为 vault 根(registerVaultRootFromSession),无需任何
|
|
44
|
+
* 手工配置。①② 供自定义/覆盖;⑤ 仅在全新裸环境兜底。
|
|
43
45
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
+
* 解析结果写回 state.json,作为浏览器端(孙学空间)的发现通道——浏览器经
|
|
47
|
+
* workspaceFiles/read 以绝对路径读取该文件(工作区外绝对路径读取被宿主允许)。
|
|
48
|
+
*
|
|
49
|
+
* 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(session-superagent 同款)。
|
|
50
|
+
* 本文件不携带任何个人路径;个人 vault 位置只存在于①②③(均不入库)。
|
|
46
51
|
*/
|
|
47
52
|
const DEFAULT_VAULT_DIRNAME = "sun-vault";
|
|
48
53
|
/** 插件自有状态目录(<DSH_HOME 或 ~/>/.dsh/state/sun-space/),沿用官方 state/<插件名> 约定。
|
|
49
54
|
* 跟随 DSH_HOME:多实例(隔离 HOME 的测试环境)之间状态互不串扰。 */
|
|
50
55
|
const stateDir = () => join(process.env.DSH_HOME ?? homedir(), ".dsh", "state", "sun-space");
|
|
51
|
-
/**
|
|
52
|
-
function computeVaultPath(envValue, configJson, home = homedir()) {
|
|
56
|
+
/** 纯函数:五级优先解析(测试与 vaultPath 共用)。 */
|
|
57
|
+
function computeVaultPath(envValue, configJson, stateJson, sessionCwd, home = homedir()) {
|
|
53
58
|
if (envValue && envValue.trim() !== "") return envValue;
|
|
54
|
-
try {
|
|
55
|
-
const
|
|
56
|
-
if (typeof
|
|
59
|
+
for (const raw of [configJson, stateJson]) try {
|
|
60
|
+
const parsed = JSON.parse(raw ?? "");
|
|
61
|
+
if (typeof parsed?.vault === "string" && parsed.vault.startsWith("/")) return parsed.vault;
|
|
57
62
|
} catch {}
|
|
63
|
+
if (sessionCwd && sessionCwd.startsWith("/")) return sessionCwd;
|
|
58
64
|
return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
|
|
59
65
|
}
|
|
60
66
|
let cached = {
|
|
@@ -62,18 +68,24 @@ let cached = {
|
|
|
62
68
|
key: "",
|
|
63
69
|
value: ""
|
|
64
70
|
};
|
|
65
|
-
/**
|
|
66
|
-
|
|
71
|
+
/** 运行期解析:每次调用重读状态目录(60s 微缓存防抖),登记/手改后即时生效。
|
|
72
|
+
* sessionCwd:调用方从 exec 提取的会话工作目录(可选)——用户在哪个工作区开会话,vault 就解析到哪。 */
|
|
73
|
+
function vaultPath(sessionCwd, now = Date.now()) {
|
|
67
74
|
const dir = stateDir();
|
|
68
75
|
let configJson;
|
|
76
|
+
let stateJson;
|
|
69
77
|
try {
|
|
70
78
|
configJson = readFileSync(join(dir, "config.json"), "utf8");
|
|
71
79
|
} catch {}
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
try {
|
|
81
|
+
stateJson = readFileSync(join(dir, "state.json"), "utf8");
|
|
82
|
+
} catch {}
|
|
83
|
+
const key = `${sessionCwd ?? ""}|${configJson ?? ""}|${stateJson ?? ""}`;
|
|
84
|
+
if (now - cached.at < 6e4 && cached.key === key) return cached.value;
|
|
85
|
+
const value = computeVaultPath(process.env.SUN_VAULT, configJson, stateJson, sessionCwd);
|
|
74
86
|
cached = {
|
|
75
87
|
at: now,
|
|
76
|
-
key
|
|
88
|
+
key,
|
|
77
89
|
value
|
|
78
90
|
};
|
|
79
91
|
return value;
|
|
@@ -81,6 +93,12 @@ function vaultPath(now = Date.now()) {
|
|
|
81
93
|
vaultPath();
|
|
82
94
|
// src/worker-vault.ts
|
|
83
95
|
const inject = ["tools"];
|
|
96
|
+
/** 从工具执行上下文提取会话工作目录(同 router-tools)。 */
|
|
97
|
+
function sessionCwdOf(exec) {
|
|
98
|
+
const e = exec;
|
|
99
|
+
const c = e?.agent?.session?.cwd ?? e?.agent?.session?.header?.cwd ?? e?.session?.cwd;
|
|
100
|
+
return typeof c === "string" && c.startsWith("/") ? c : void 0;
|
|
101
|
+
}
|
|
84
102
|
const TASK_ID = /^T-\d{8}-\d{2}$/;
|
|
85
103
|
/** 纯函数:拼装受控产物路径;非法输入抛 VaultPathError。可单测。 */
|
|
86
104
|
function artifactPath(taskId, filename) {
|
|
@@ -126,11 +144,11 @@ function apply(ctx) {
|
|
|
126
144
|
text: JSON.stringify(value)
|
|
127
145
|
}]
|
|
128
146
|
},
|
|
129
|
-
execute: async (args) => {
|
|
147
|
+
execute: async (args, exec) => {
|
|
130
148
|
const { task_id: taskId, filename, content } = args ?? {};
|
|
131
149
|
try {
|
|
132
150
|
const rel = artifactPath(String(taskId), String(filename));
|
|
133
|
-
const abs = resolveSafe(vaultPath(), rel);
|
|
151
|
+
const abs = resolveSafe(vaultPath(sessionCwdOf(exec)), rel);
|
|
134
152
|
await mkdir(join(abs, ".."), { recursive: true });
|
|
135
153
|
await writeFile(abs, String(content ?? ""), "utf8");
|
|
136
154
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sun-space",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sun Space:DeepSeek Harness 个人超智能体插件——Router/Worker 双预设工具白名单、钉钉待办轮询、Obsidian 知识库落库、会话工作台与定时任务管理。",
|
|
6
6
|
"exports": {
|
|
@@ -54,4 +54,4 @@
|
|
|
54
54
|
"esbuild"
|
|
55
55
|
]
|
|
56
56
|
}
|
|
57
|
-
}
|
|
57
|
+
}
|
package/src/client.js
CHANGED
|
@@ -22,7 +22,7 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
// 会话解析链:session/list(sun-router 会话)→ 90-Meta/system/state.json 裁决 → updatedAt 最新兜底。
|
|
23
23
|
// vault 解析链:会话 cwd 推 HOME → 约定布局探测 state.json → state.json 的 vault 字段裁决(跨机自愈)。
|
|
24
24
|
// 写回:① 创建会话后经 session/prompt 引导自登记(含 vault 字段);② 晨报自检发现缺失/不一致自动修正。
|
|
25
|
-
const WB_REV = '
|
|
25
|
+
const WB_REV = 'v18' // 根节点 data-wb-rev 版本标记:判别浏览器是否吃到旧 client.js(immutable 缓存坑,HANDOFF 踩坑 1)
|
|
26
26
|
|
|
27
27
|
const STATUS_META = {
|
|
28
28
|
proposed: { label: '待确认派发', color: '#9e9e9e' },
|
|
@@ -115,7 +115,16 @@ window.__ModuleLoader__.load({
|
|
|
115
115
|
} catch { /* 该文件不存在 → 下一个 */ }
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
// ②
|
|
118
|
+
// ② 会话 cwd 即 vault(用户"新建目录作为工作区并开会话"流:cwd 下有 90-Meta 即认定)
|
|
119
|
+
try {
|
|
120
|
+
const cwd = String((items.find(s => s.sessionId === scope) ?? {}).cwd ?? '')
|
|
121
|
+
if (cwd && cwd.startsWith('/')) {
|
|
122
|
+
await readVaultFile(scope, `${cwd}/90-Meta/runbook.md`, 2)
|
|
123
|
+
D.vaultFoundNoField = cwd
|
|
124
|
+
return cwd
|
|
125
|
+
}
|
|
126
|
+
} catch { /* 该 cwd 无 90-Meta → 约定兜底 */ }
|
|
127
|
+
// ③ 约定布局探测(默认 ~/Workspace/sun-vault;自定义 vault 路径由①覆盖)
|
|
119
128
|
const candidates = [...new Set(homes.map(h => `${h}/Workspace/${VAULT_DIRNAME}`))]
|
|
120
129
|
for (const cand of candidates) {
|
|
121
130
|
try {
|
|
@@ -130,9 +139,11 @@ window.__ModuleLoader__.load({
|
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
// ===== Router 会话解析(架构 §4.1 不变量:会话会轮换,呈现层只认 vault state.json + 动态发现)=====
|
|
133
|
-
async function resolveRouterSession(vault) {
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
async function resolveRouterSession(vault, items) {
|
|
143
|
+
if (!items) {
|
|
144
|
+
const v = await gw('session/list', {})
|
|
145
|
+
items = Array.isArray(v?.items) ? v.items : Object.values(v?.items ?? v ?? {})
|
|
146
|
+
}
|
|
136
147
|
// 注意:cwd 是"工作区分桶路径"(实例 boot cwd),不是 vault——过滤只看预设(踩坑 5 延伸)
|
|
137
148
|
const candidates = items.filter(s => s?.projections?.values?.agentPreset === 'sun-router')
|
|
138
149
|
if (!candidates.length) return null
|
|
@@ -176,12 +187,14 @@ window.__ModuleLoader__.load({
|
|
|
176
187
|
c.presets = false
|
|
177
188
|
c.notes.push('预设服务不可达 → 确认 Harness 实例在线、插件已启用')
|
|
178
189
|
}
|
|
179
|
-
// ③ Router
|
|
190
|
+
// ③ Router 会话:动态解析 + 首个 sun-router 会话创建时自动登记其 cwd 为 vault 根
|
|
191
|
+
// ("当前目录作为 vault"语义的插件层落点;env/config 更高优先级不受影响)
|
|
180
192
|
try {
|
|
181
193
|
const sl = await gw('session/list', {})
|
|
182
194
|
const items = Array.isArray(sl?.items) ? sl.items : Object.values(sl?.items ?? {})
|
|
183
195
|
c.currentCwd = (items.find(s => s.sessionId === currentSessionId) ?? {}).cwd ?? null
|
|
184
|
-
|
|
196
|
+
// vault 根登记由宿主插件在 sun-router 会话创建时自动完成(index.ts agent/created 钩子)
|
|
197
|
+
c.routerSession = vault ? await resolveRouterSession(vault, items) : null
|
|
185
198
|
if (!c.routerSession) c.notes.push('Router 会话不存在 → 点下方「创建 Router 会话」(sun-router 预设,随当前工作区),创建后自动登记 state.json')
|
|
186
199
|
} catch {
|
|
187
200
|
c.notes.push('会话列表不可达 → 重试或检查实例')
|
|
@@ -265,13 +278,12 @@ window.__ModuleLoader__.load({
|
|
|
265
278
|
sessionId: sid,
|
|
266
279
|
mode: 'queue',
|
|
267
280
|
content: [{ type: 'text', text: `【孙学空间初始化】你是新环境的 Sun Router(你的会话 id:${sid})。你的工作目录即知识库根(vault)。按顺序完成:
|
|
268
|
-
1)
|
|
269
|
-
2) 用 sun_vault_write 创建知识库骨架(此时插件已从 config.json 动态解析 vault = 你的工作目录):
|
|
281
|
+
1) 用 sun_vault_write 创建知识库骨架(vault 即你的工作目录,已在会话创建时自动登记):
|
|
270
282
|
- 90-Meta/schemas.md:frontmatter 契约最小集(通用字段 type/id/status/created/updated;task 的 status 枚举 proposed|running|need-confirm|done|blocked|dropped;写入纪律:Journal 由 Router 独占、日期一律本地时间 YYYY-MM-DDTHH:mm 不带时区后缀、结论必注出处)
|
|
271
283
|
- 90-Meta/runbook.md:初始版(# Sun 行为手册 + v0.1 版本行 + §1 角色:Router 只分派不执行 + §2 沟通规范:IM ≤100 字结论先行 + §3 打扰分级:info 攒批/ask 立即/urgent 加 DING)
|
|
272
284
|
- 90-Meta/memory.md:初始版(# 关于用户,待填充)
|
|
273
|
-
|
|
274
|
-
|
|
285
|
+
2) 用 sun_vault_write 写 90-Meta/system/state.json:{"routerSession": "${sid}", "vault": "<你的工作目录绝对路径>", "updatedAt": "<本地时间 YYYY-MM-DDTHH:mm>"}(自登记,供孙学空间发现与轮换机制使用)
|
|
286
|
+
3) 完成后列出生成的文件清单与知识库路径。` }],
|
|
275
287
|
clientTimeZone: 'Asia/Shanghai',
|
|
276
288
|
})
|
|
277
289
|
try { hostCtx.uiWorkspace.openSession(sid) } catch (e) { D.errors.push('openSession: ' + String(e)) }
|
|
@@ -523,6 +535,7 @@ window.__ModuleLoader__.load({
|
|
|
523
535
|
h('div', { key: 'tabs', style: { display: 'flex', gap: 3, marginBottom: 16, background: 'rgba(128,128,128,0.12)', borderRadius: 8, padding: 3 } }, [
|
|
524
536
|
['overview', '总览'], ['schedules', '定时任务'], ['setup', '设置'],
|
|
525
537
|
].map(([id, label]) => h('button', { key: id, onClick: () => setTab(id), style: { flex: 1, border: 'none', cursor: 'pointer', fontSize: 13, padding: '6px 0', borderRadius: 6, color: 'inherit', background: tab === id ? 'rgba(128,128,128,0.28)' : 'transparent', opacity: tab === id ? 1 : 0.6, transition: 'background 0.15s' } }, label))),
|
|
538
|
+
h('div', { key: 'vp', style: { fontSize: 12, opacity: 0.65, marginBottom: 12 } }, `📚 识别到的知识库:${vault ?? '(未定位——完成下方引导后自动登记)'}`),
|
|
526
539
|
h('div', { key: 'cnt', style: { display: 'flex', gap: 18, marginBottom: 16 } },
|
|
527
540
|
counters.map(([label, n, color]) => h('div', { key: label, style: { display: 'flex', alignItems: 'center', gap: 6 } }, [
|
|
528
541
|
h('span', { key: 'd', style: { width: 8, height: 8, borderRadius: 4, background: color, display: 'inline-block' } }),
|