dsh-sun-space 0.2.5 → 0.2.7
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 +57 -28
- package/lib/router-tools.mjs +45 -19
- package/lib/worker-vault.mjs +38 -12
- package/package.json +1 -1
- package/src/client.js +58 -39
package/lib/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
5
|
import { execFile } from "node:child_process";
|
|
@@ -19,30 +20,60 @@ var __exportAll = (all, no_symbols) => {
|
|
|
19
20
|
*
|
|
20
21
|
* 解析优先级:
|
|
21
22
|
* ① 环境变量 SUN_VAULT(启动命令注入,随进程生效)
|
|
22
|
-
* ②
|
|
23
|
+
* ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写,持久化,不入库)
|
|
23
24
|
* ③ 默认约定 ~/Workspace/sun-vault
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* 解析结果写回 ~/.dsh/state/sun-space/state.json,作为浏览器端(孙学空间)的发现
|
|
27
|
+
* 通道——浏览器读不到进程环境与插件 config,经 workspaceFiles/read 以绝对路径读取
|
|
28
|
+
* 该文件(工作区外绝对路径读取被宿主允许,与 vault 本体读取同机制)。
|
|
28
29
|
*
|
|
29
|
-
*
|
|
30
|
+
* 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(社区插件 session-superagent 同款)。
|
|
31
|
+
* 本文件不携带任何个人路径;个人 vault 位置只存在于①/②(均不入库)。
|
|
30
32
|
*/
|
|
31
33
|
const DEFAULT_VAULT_DIRNAME = "sun-vault";
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
/** 插件自有状态目录(<DSH_HOME 或 ~/>/.dsh/state/sun-space/),沿用官方 state/<插件名> 约定。
|
|
35
|
+
* 跟随 DSH_HOME:多实例(隔离 HOME 的测试环境)之间状态互不串扰。 */
|
|
36
|
+
const stateDir = () => join(process.env.DSH_HOME ?? homedir(), ".dsh", "state", "sun-space");
|
|
37
|
+
/** 纯函数:给定 env 与 config 内容时的解析结果(测试与 vaultPath 共用)。 */
|
|
38
|
+
function computeVaultPath(envValue, configJson, home = homedir()) {
|
|
39
|
+
if (envValue && envValue.trim() !== "") return envValue;
|
|
35
40
|
try {
|
|
36
|
-
const cfg = JSON.parse(
|
|
41
|
+
const cfg = JSON.parse(configJson ?? "");
|
|
37
42
|
if (typeof cfg?.vault === "string" && cfg.vault.startsWith("/")) return cfg.vault;
|
|
38
43
|
} catch {}
|
|
39
44
|
return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
|
|
40
45
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
let cached = {
|
|
47
|
+
at: 0,
|
|
48
|
+
key: "",
|
|
49
|
+
value: ""
|
|
50
|
+
};
|
|
51
|
+
/** 运行期解析:每次调用重读 config.json(60s 微缓存防抖),"以当前目录初始化"写入后即时生效。 */
|
|
52
|
+
function vaultPath(now = Date.now()) {
|
|
53
|
+
const dir = stateDir();
|
|
54
|
+
let configJson;
|
|
55
|
+
try {
|
|
56
|
+
configJson = readFileSync(join(dir, "config.json"), "utf8");
|
|
57
|
+
} catch {}
|
|
58
|
+
if (now - cached.at < 6e4 && cached.key === (configJson ?? "")) return cached.value;
|
|
59
|
+
const value = computeVaultPath(process.env.SUN_VAULT, configJson);
|
|
60
|
+
cached = {
|
|
61
|
+
at: now,
|
|
62
|
+
key: configJson ?? "",
|
|
63
|
+
value
|
|
64
|
+
};
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
vaultPath();
|
|
68
|
+
/** 把解析结果写回状态目录的 state.json(apply 时调用,尽力而为),供客户端发现。 */
|
|
69
|
+
function publishVaultState(now = /* @__PURE__ */ new Date()) {
|
|
44
70
|
try {
|
|
45
|
-
|
|
71
|
+
const dir = stateDir();
|
|
72
|
+
mkdirSync(dir, { recursive: true });
|
|
73
|
+
writeFileSync(join(dir, "state.json"), JSON.stringify({
|
|
74
|
+
vault: vaultPath(now.getTime()),
|
|
75
|
+
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")}`
|
|
76
|
+
}, null, 2), "utf8");
|
|
46
77
|
} catch {}
|
|
47
78
|
}
|
|
48
79
|
// src/dws.ts
|
|
@@ -154,7 +185,6 @@ var source_exports = /* @__PURE__ */ __exportAll({
|
|
|
154
185
|
CREDENTIAL_ALERT_COOLDOWN_MS: () => CREDENTIAL_ALERT_COOLDOWN_MS,
|
|
155
186
|
CREDENTIAL_ALERT_STREAK: () => 2,
|
|
156
187
|
TRIGGER_TTL_MS: () => TRIGGER_TTL_MS,
|
|
157
|
-
VAULT: () => VAULT,
|
|
158
188
|
apply: () => apply$2,
|
|
159
189
|
inject: () => inject$2,
|
|
160
190
|
parseTriggerTs: () => parseTriggerTs,
|
|
@@ -164,8 +194,8 @@ var source_exports = /* @__PURE__ */ __exportAll({
|
|
|
164
194
|
const inject$2 = ["timer"];
|
|
165
195
|
/** 轮询周期(毫秒):纯程序层,可调密。 */
|
|
166
196
|
const POLL_MS = 18e5;
|
|
167
|
-
const
|
|
168
|
-
const
|
|
197
|
+
const triggerDir$1 = () => `${vaultPath()}/90-Meta/triggers`;
|
|
198
|
+
const snapshotFile = () => `${vaultPath()}/90-Meta/mock-data/last-snapshot.json`;
|
|
169
199
|
/** trigger 文件 TTL:超过即视为已消费/过期,轮询成功路径顺带清理。 */
|
|
170
200
|
const TRIGGER_TTL_MS = 6048e5;
|
|
171
201
|
const CREDENTIAL_ALERT_COOLDOWN_MS = 36e5;
|
|
@@ -211,7 +241,7 @@ function schedulePolling(ctx) {
|
|
|
211
241
|
const snap = await snapshot();
|
|
212
242
|
const prev = await readPrev();
|
|
213
243
|
if (!prev) {
|
|
214
|
-
await writeFile(
|
|
244
|
+
await writeFile(snapshotFile(), JSON.stringify(snap), "utf8");
|
|
215
245
|
return;
|
|
216
246
|
}
|
|
217
247
|
const fresh = diff(prev, snap);
|
|
@@ -223,9 +253,9 @@ function schedulePolling(ctx) {
|
|
|
223
253
|
if (fresh.newApprovals.length) parts.push(`新审批 ${fresh.newApprovals.length} 件`);
|
|
224
254
|
await notifySelf(uid, `[Sun·info] ${parts.join("、")},已入触发队列(${(/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN")})`);
|
|
225
255
|
}
|
|
226
|
-
await writeFile(
|
|
256
|
+
await writeFile(snapshotFile(), JSON.stringify(snap), "utf8");
|
|
227
257
|
health.fails = 0;
|
|
228
|
-
await pruneTriggers(
|
|
258
|
+
await pruneTriggers(triggerDir$1()).catch(() => {});
|
|
229
259
|
} catch (e) {
|
|
230
260
|
ctx.logger?.warn?.(`[sun-space] poll failed: ${String(e.message).slice(0, 150)}`);
|
|
231
261
|
health.fails++;
|
|
@@ -256,15 +286,15 @@ function schedulePolling(ctx) {
|
|
|
256
286
|
}
|
|
257
287
|
async function readPrev() {
|
|
258
288
|
try {
|
|
259
|
-
return JSON.parse(await readFile(
|
|
289
|
+
return JSON.parse(await readFile(snapshotFile(), "utf8"));
|
|
260
290
|
} catch {
|
|
261
291
|
return null;
|
|
262
292
|
}
|
|
263
293
|
}
|
|
264
294
|
async function writeTrigger(label, payload) {
|
|
265
|
-
await mkdir(
|
|
295
|
+
await mkdir(triggerDir$1(), { recursive: true });
|
|
266
296
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
267
|
-
await writeFile(join(
|
|
297
|
+
await writeFile(join(triggerDir$1(), `${ts}-${label}.json`), JSON.stringify({
|
|
268
298
|
label,
|
|
269
299
|
payload
|
|
270
300
|
}, null, 2), "utf8");
|
|
@@ -307,7 +337,6 @@ async function writeTrigger(label, payload) {
|
|
|
307
337
|
var worker_watchdog_exports = /* @__PURE__ */ __exportAll({
|
|
308
338
|
DEFAULT_STALL_MS: () => DEFAULT_STALL_MS,
|
|
309
339
|
DEFAULT_TICK_MS: () => DEFAULT_TICK_MS,
|
|
310
|
-
VAULT: () => VAULT,
|
|
311
340
|
apply: () => apply$1,
|
|
312
341
|
inject: () => inject$1,
|
|
313
342
|
isDelegatedChildHeader: () => isDelegatedChildHeader,
|
|
@@ -318,7 +347,7 @@ var worker_watchdog_exports = /* @__PURE__ */ __exportAll({
|
|
|
318
347
|
});
|
|
319
348
|
const name$1 = "worker-watchdog";
|
|
320
349
|
const inject$1 = ["timer"];
|
|
321
|
-
const
|
|
350
|
+
const triggerDir = () => `${vaultPath()}/90-Meta/triggers`;
|
|
322
351
|
/** 默认停滞阈值 15 分钟:长思考/长命令完成后即重置计时,真卡死撑不到这么久。 */
|
|
323
352
|
const DEFAULT_STALL_MS = 9e5;
|
|
324
353
|
/** 巡检间隔 30 秒。 */
|
|
@@ -375,9 +404,9 @@ function apply$1(ctx) {
|
|
|
375
404
|
const idleMin = Math.round((Date.now() - entry.at) / 6e4);
|
|
376
405
|
log("warn", `stalled: child=${entry.id} idle=${idleMin}min — interrupting & notifying`);
|
|
377
406
|
try {
|
|
378
|
-
await mkdir(
|
|
407
|
+
await mkdir(triggerDir(), { recursive: true });
|
|
379
408
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
380
|
-
await writeFile(join(
|
|
409
|
+
await writeFile(join(triggerDir(), `${ts}-watchdog-stall.json`), JSON.stringify({
|
|
381
410
|
label: "watchdog-stall",
|
|
382
411
|
payload: {
|
|
383
412
|
childId: entry.id,
|
|
@@ -461,7 +490,7 @@ function apply$1(ctx) {
|
|
|
461
490
|
const name = "sun-space";
|
|
462
491
|
const inject = ["timer", "tools"];
|
|
463
492
|
function apply(ctx) {
|
|
464
|
-
|
|
493
|
+
publishVaultState();
|
|
465
494
|
ctx.plugin(source_exports);
|
|
466
495
|
ctx.plugin(worker_watchdog_exports);
|
|
467
496
|
}
|
package/lib/router-tools.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promise
|
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
5
6
|
// src/vault.ts
|
|
6
7
|
/**
|
|
7
8
|
* vault 访问:路径守卫 + frontmatter 解析/序列化。纯函数为主,可单测。
|
|
@@ -190,26 +191,51 @@ async function dingSelf(userId, content, type = "app") {
|
|
|
190
191
|
*
|
|
191
192
|
* 解析优先级:
|
|
192
193
|
* ① 环境变量 SUN_VAULT(启动命令注入,随进程生效)
|
|
193
|
-
* ②
|
|
194
|
+
* ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写,持久化,不入库)
|
|
194
195
|
* ③ 默认约定 ~/Workspace/sun-vault
|
|
195
196
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
197
|
+
* 解析结果写回 ~/.dsh/state/sun-space/state.json,作为浏览器端(孙学空间)的发现
|
|
198
|
+
* 通道——浏览器读不到进程环境与插件 config,经 workspaceFiles/read 以绝对路径读取
|
|
199
|
+
* 该文件(工作区外绝对路径读取被宿主允许,与 vault 本体读取同机制)。
|
|
199
200
|
*
|
|
200
|
-
*
|
|
201
|
+
* 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(社区插件 session-superagent 同款)。
|
|
202
|
+
* 本文件不携带任何个人路径;个人 vault 位置只存在于①/②(均不入库)。
|
|
201
203
|
*/
|
|
202
204
|
const DEFAULT_VAULT_DIRNAME = "sun-vault";
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
-
|
|
205
|
+
/** 插件自有状态目录(<DSH_HOME 或 ~/>/.dsh/state/sun-space/),沿用官方 state/<插件名> 约定。
|
|
206
|
+
* 跟随 DSH_HOME:多实例(隔离 HOME 的测试环境)之间状态互不串扰。 */
|
|
207
|
+
const stateDir = () => join(process.env.DSH_HOME ?? homedir(), ".dsh", "state", "sun-space");
|
|
208
|
+
/** 纯函数:给定 env 与 config 内容时的解析结果(测试与 vaultPath 共用)。 */
|
|
209
|
+
function computeVaultPath(envValue, configJson, home = homedir()) {
|
|
210
|
+
if (envValue && envValue.trim() !== "") return envValue;
|
|
206
211
|
try {
|
|
207
|
-
const cfg = JSON.parse(
|
|
212
|
+
const cfg = JSON.parse(configJson ?? "");
|
|
208
213
|
if (typeof cfg?.vault === "string" && cfg.vault.startsWith("/")) return cfg.vault;
|
|
209
214
|
} catch {}
|
|
210
215
|
return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
|
|
211
216
|
}
|
|
212
|
-
|
|
217
|
+
let cached = {
|
|
218
|
+
at: 0,
|
|
219
|
+
key: "",
|
|
220
|
+
value: ""
|
|
221
|
+
};
|
|
222
|
+
/** 运行期解析:每次调用重读 config.json(60s 微缓存防抖),"以当前目录初始化"写入后即时生效。 */
|
|
223
|
+
function vaultPath(now = Date.now()) {
|
|
224
|
+
const dir = stateDir();
|
|
225
|
+
let configJson;
|
|
226
|
+
try {
|
|
227
|
+
configJson = readFileSync(join(dir, "config.json"), "utf8");
|
|
228
|
+
} catch {}
|
|
229
|
+
if (now - cached.at < 6e4 && cached.key === (configJson ?? "")) return cached.value;
|
|
230
|
+
const value = computeVaultPath(process.env.SUN_VAULT, configJson);
|
|
231
|
+
cached = {
|
|
232
|
+
at: now,
|
|
233
|
+
key: configJson ?? "",
|
|
234
|
+
value
|
|
235
|
+
};
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
vaultPath();
|
|
213
239
|
// src/router-tools.ts
|
|
214
240
|
const inject = ["tools", "subagents"];
|
|
215
241
|
const text = (s) => [{
|
|
@@ -290,7 +316,7 @@ function apply(ctx) {
|
|
|
290
316
|
}, async (args) => {
|
|
291
317
|
try {
|
|
292
318
|
if (args.what === "triggers") {
|
|
293
|
-
const dir = resolveSafe(
|
|
319
|
+
const dir = resolveSafe(vaultPath(), "90-Meta/triggers");
|
|
294
320
|
try {
|
|
295
321
|
const files = (await readdir(dir)).filter((f) => f.endsWith(".json")).sort().reverse().slice(0, 10);
|
|
296
322
|
const triggers = [];
|
|
@@ -322,7 +348,7 @@ function apply(ctx) {
|
|
|
322
348
|
}
|
|
323
349
|
if (args.what === "journal") {
|
|
324
350
|
const d = args.date ?? localDate();
|
|
325
|
-
const p = resolveRead(
|
|
351
|
+
const p = resolveRead(vaultPath(), `40-Journal/${d}.md`);
|
|
326
352
|
const raw = await readFile(p, "utf8");
|
|
327
353
|
if (args.full) return {
|
|
328
354
|
ok: true,
|
|
@@ -337,7 +363,7 @@ function apply(ctx) {
|
|
|
337
363
|
};
|
|
338
364
|
}
|
|
339
365
|
if (args.what === "tasks") {
|
|
340
|
-
const dir = resolveSafe(
|
|
366
|
+
const dir = resolveSafe(vaultPath(), "50-Tasks");
|
|
341
367
|
const files = (await readdir(dir)).filter((f) => f.endsWith(".md"));
|
|
342
368
|
const tasks = [];
|
|
343
369
|
for (const f of files) {
|
|
@@ -360,7 +386,7 @@ function apply(ctx) {
|
|
|
360
386
|
};
|
|
361
387
|
}
|
|
362
388
|
if (args.what === "runbook" || args.what === "memory") {
|
|
363
|
-
const p = resolveSafe(
|
|
389
|
+
const p = resolveSafe(vaultPath(), `90-Meta/${args.what}.md`);
|
|
364
390
|
return {
|
|
365
391
|
ok: true,
|
|
366
392
|
data: { text: (await readFile(p, "utf8")).slice(0, 8e3) }
|
|
@@ -371,7 +397,7 @@ function apply(ctx) {
|
|
|
371
397
|
ok: false,
|
|
372
398
|
note: "note 需要 path"
|
|
373
399
|
};
|
|
374
|
-
const raw = await readFile(resolveRead(
|
|
400
|
+
const raw = await readFile(resolveRead(vaultPath(), args.path), "utf8");
|
|
375
401
|
if (args.full) return {
|
|
376
402
|
ok: true,
|
|
377
403
|
data: { text: raw.slice(0, 8e3) }
|
|
@@ -415,7 +441,7 @@ function apply(ctx) {
|
|
|
415
441
|
required: ["path", "content"]
|
|
416
442
|
}, async (args) => {
|
|
417
443
|
try {
|
|
418
|
-
const p = resolveSafe(
|
|
444
|
+
const p = resolveSafe(vaultPath(), args.path);
|
|
419
445
|
await mkdir(join(p, ".."), { recursive: true });
|
|
420
446
|
if (args.mode === "append") await appendFile(p, args.content, "utf8");
|
|
421
447
|
else await writeFile(p, args.content, "utf8");
|
|
@@ -511,7 +537,7 @@ function apply(ctx) {
|
|
|
511
537
|
required: ["path", "fields"]
|
|
512
538
|
}, async (args) => {
|
|
513
539
|
try {
|
|
514
|
-
const p = resolveSafe(
|
|
540
|
+
const p = resolveSafe(vaultPath(), args.path);
|
|
515
541
|
const updated = updateFrontmatter(await readFile(p, "utf8"), args.fields);
|
|
516
542
|
await writeFile(p, updated, "utf8");
|
|
517
543
|
return {
|
|
@@ -585,7 +611,7 @@ function apply(ctx) {
|
|
|
585
611
|
};
|
|
586
612
|
}
|
|
587
613
|
try {
|
|
588
|
-
const p = resolveSafe(
|
|
614
|
+
const p = resolveSafe(vaultPath(), `50-Tasks/${args.task_id}.md`);
|
|
589
615
|
const raw = await readFile(p, "utf8");
|
|
590
616
|
await writeFile(p, updateFrontmatter(raw, { session: String(activation.childId ?? "") }), "utf8");
|
|
591
617
|
} catch {}
|
|
@@ -597,4 +623,4 @@ function apply(ctx) {
|
|
|
597
623
|
}));
|
|
598
624
|
ctx.logger?.info?.("[sun-space] router tools registered: sun_read sun_vault_write sun_vault_meta sun_notify sun_route(subagents)");
|
|
599
625
|
}
|
|
600
|
-
export {
|
|
626
|
+
export { apply, inject, lossless };
|
package/lib/worker-vault.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
// src/vault.ts
|
|
5
6
|
/**
|
|
6
7
|
* vault 访问:路径守卫 + frontmatter 解析/序列化。纯函数为主,可单测。
|
|
@@ -33,26 +34,51 @@ function resolveSafe(vaultRoot, rel) {
|
|
|
33
34
|
*
|
|
34
35
|
* 解析优先级:
|
|
35
36
|
* ① 环境变量 SUN_VAULT(启动命令注入,随进程生效)
|
|
36
|
-
* ②
|
|
37
|
+
* ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写,持久化,不入库)
|
|
37
38
|
* ③ 默认约定 ~/Workspace/sun-vault
|
|
38
39
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* 解析结果写回 ~/.dsh/state/sun-space/state.json,作为浏览器端(孙学空间)的发现
|
|
41
|
+
* 通道——浏览器读不到进程环境与插件 config,经 workspaceFiles/read 以绝对路径读取
|
|
42
|
+
* 该文件(工作区外绝对路径读取被宿主允许,与 vault 本体读取同机制)。
|
|
42
43
|
*
|
|
43
|
-
*
|
|
44
|
+
* 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(社区插件 session-superagent 同款)。
|
|
45
|
+
* 本文件不携带任何个人路径;个人 vault 位置只存在于①/②(均不入库)。
|
|
44
46
|
*/
|
|
45
47
|
const DEFAULT_VAULT_DIRNAME = "sun-vault";
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
/** 插件自有状态目录(<DSH_HOME 或 ~/>/.dsh/state/sun-space/),沿用官方 state/<插件名> 约定。
|
|
49
|
+
* 跟随 DSH_HOME:多实例(隔离 HOME 的测试环境)之间状态互不串扰。 */
|
|
50
|
+
const stateDir = () => join(process.env.DSH_HOME ?? homedir(), ".dsh", "state", "sun-space");
|
|
51
|
+
/** 纯函数:给定 env 与 config 内容时的解析结果(测试与 vaultPath 共用)。 */
|
|
52
|
+
function computeVaultPath(envValue, configJson, home = homedir()) {
|
|
53
|
+
if (envValue && envValue.trim() !== "") return envValue;
|
|
49
54
|
try {
|
|
50
|
-
const cfg = JSON.parse(
|
|
55
|
+
const cfg = JSON.parse(configJson ?? "");
|
|
51
56
|
if (typeof cfg?.vault === "string" && cfg.vault.startsWith("/")) return cfg.vault;
|
|
52
57
|
} catch {}
|
|
53
58
|
return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
|
|
54
59
|
}
|
|
55
|
-
|
|
60
|
+
let cached = {
|
|
61
|
+
at: 0,
|
|
62
|
+
key: "",
|
|
63
|
+
value: ""
|
|
64
|
+
};
|
|
65
|
+
/** 运行期解析:每次调用重读 config.json(60s 微缓存防抖),"以当前目录初始化"写入后即时生效。 */
|
|
66
|
+
function vaultPath(now = Date.now()) {
|
|
67
|
+
const dir = stateDir();
|
|
68
|
+
let configJson;
|
|
69
|
+
try {
|
|
70
|
+
configJson = readFileSync(join(dir, "config.json"), "utf8");
|
|
71
|
+
} catch {}
|
|
72
|
+
if (now - cached.at < 6e4 && cached.key === (configJson ?? "")) return cached.value;
|
|
73
|
+
const value = computeVaultPath(process.env.SUN_VAULT, configJson);
|
|
74
|
+
cached = {
|
|
75
|
+
at: now,
|
|
76
|
+
key: configJson ?? "",
|
|
77
|
+
value
|
|
78
|
+
};
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
vaultPath();
|
|
56
82
|
// src/worker-vault.ts
|
|
57
83
|
const inject = ["tools"];
|
|
58
84
|
const TASK_ID = /^T-\d{8}-\d{2}$/;
|
|
@@ -104,7 +130,7 @@ function apply(ctx) {
|
|
|
104
130
|
const { task_id: taskId, filename, content } = args ?? {};
|
|
105
131
|
try {
|
|
106
132
|
const rel = artifactPath(String(taskId), String(filename));
|
|
107
|
-
const abs = resolveSafe(
|
|
133
|
+
const abs = resolveSafe(vaultPath(), rel);
|
|
108
134
|
await mkdir(join(abs, ".."), { recursive: true });
|
|
109
135
|
await writeFile(abs, String(content ?? ""), "utf8");
|
|
110
136
|
return {
|
|
@@ -125,4 +151,4 @@ function apply(ctx) {
|
|
|
125
151
|
});
|
|
126
152
|
ctx.logger?.info?.("[sun-space] worker artifact tool registered: sun_artifact_write");
|
|
127
153
|
}
|
|
128
|
-
export {
|
|
154
|
+
export { apply, artifactPath, inject };
|
package/package.json
CHANGED
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 = 'v17' // 根节点 data-wb-rev 版本标记:判别浏览器是否吃到旧 client.js(immutable 缓存坑,HANDOFF 踩坑 1)
|
|
26
26
|
|
|
27
27
|
const STATUS_META = {
|
|
28
28
|
proposed: { label: '待确认派发', color: '#9e9e9e' },
|
|
@@ -103,14 +103,19 @@ window.__ModuleLoader__.load({
|
|
|
103
103
|
const items = Array.isArray(v?.items) ? v.items : Object.values(v?.items ?? v ?? {})
|
|
104
104
|
const scope = items.find(s => s?.projections?.values?.agentPreset === 'sun-router')?.sessionId ?? items[0]?.sessionId
|
|
105
105
|
if (!scope) return null
|
|
106
|
-
// ①
|
|
107
|
-
|
|
108
|
-
const raw = await readVaultFile(scope, 'sun-space.json', 4)
|
|
109
|
-
const cfg = JSON.parse(raw)
|
|
110
|
-
if (cfg?.vault && cfg.vault.startsWith('/')) return cfg.vault
|
|
111
|
-
} catch { /* 无发现文件 → 走约定探测 */ }
|
|
112
|
-
// ② 约定布局探测(默认 ~/Workspace/sun-vault;自定义 vault 路径由①覆盖)
|
|
106
|
+
// ① 插件状态目录(HOME 从会话 cwd 推导,绝对路径读取):
|
|
107
|
+
// config.json 优先(运行期事实源——引导初始化/用户手写即时生效),state.json 兜底(启动期快照)
|
|
113
108
|
const homes = [...new Set(items.map(s => String(s.cwd || '')).filter(c => c.startsWith('/')).map(c => c.split('/').slice(0, 3).join('/')))]
|
|
109
|
+
for (const home of homes) {
|
|
110
|
+
for (const file of ['config.json', 'state.json']) {
|
|
111
|
+
try {
|
|
112
|
+
const raw = await readVaultFile(scope, `${home}/.dsh/state/sun-space/${file}`, 8)
|
|
113
|
+
const st = JSON.parse(raw)
|
|
114
|
+
if (st?.vault && st.vault.startsWith('/')) return st.vault
|
|
115
|
+
} catch { /* 该文件不存在 → 下一个 */ }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ② 约定布局探测(默认 ~/Workspace/sun-vault;自定义 vault 路径由①覆盖)
|
|
114
119
|
const candidates = [...new Set(homes.map(h => `${h}/Workspace/${VAULT_DIRNAME}`))]
|
|
115
120
|
for (const cand of candidates) {
|
|
116
121
|
try {
|
|
@@ -186,26 +191,6 @@ window.__ModuleLoader__.load({
|
|
|
186
191
|
return gw('session/create', { agentPreset: 'sun-router' })
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
// 创建后引导新会话自登记 state.json(workspaceFiles 无写通道,sun_vault_write 是唯一写路径)
|
|
190
|
-
async function bootstrapRouterSession(sid, vault) {
|
|
191
|
-
const text = '初始化自登记:用 sun_vault_write(mode=overwrite)写 90-Meta/system/state.json,内容 ' +
|
|
192
|
-
JSON.stringify({
|
|
193
|
-
routerSession: sid,
|
|
194
|
-
vault: vault ?? '(vault 绝对路径,用你环境里 sun 工具实际使用的知识库路径)',
|
|
195
|
-
updatedAt: new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Shanghai' }),
|
|
196
|
-
}) +
|
|
197
|
-
';完成后一句话报告。'
|
|
198
|
-
try {
|
|
199
|
-
await gw('session/prompt', {
|
|
200
|
-
requestId: 'sun-boot-' + Date.now(),
|
|
201
|
-
sessionId: sid,
|
|
202
|
-
mode: 'queue',
|
|
203
|
-
content: [{ type: 'text', text }],
|
|
204
|
-
clientTimeZone: 'Asia/Shanghai',
|
|
205
|
-
})
|
|
206
|
-
return true
|
|
207
|
-
} catch (e) { D.errors.push('bootstrapRouterSession: ' + String(e)); return false }
|
|
208
|
-
}
|
|
209
194
|
|
|
210
195
|
async function loadTasks(routerSession, vault) {
|
|
211
196
|
if (!routerSession || !vault) return []
|
|
@@ -262,16 +247,48 @@ window.__ModuleLoader__.load({
|
|
|
262
247
|
}
|
|
263
248
|
}
|
|
264
249
|
|
|
265
|
-
//
|
|
266
|
-
|
|
250
|
+
// 一键初始化:创建 Router 会话 → 下发合并指令(90-Meta 骨架 + 自登记 state.json)。
|
|
251
|
+
// 全程走 sun_vault_write(vault 白名单内,零审批弹窗)——Router 是 sun-router 预设,
|
|
252
|
+
// 插件运行期动态解析(vaultPath())保证它写入"当前生效"的知识库路径。
|
|
253
|
+
async function createRouterAndInit(checks, onRetry) {
|
|
267
254
|
try {
|
|
268
255
|
const v = await createRouterSession()
|
|
269
256
|
const sid = typeof v === 'string' ? v : (v?.sessionId ?? v)
|
|
270
|
-
if (sid)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
257
|
+
if (!sid) return
|
|
258
|
+
await gw('session/prompt', {
|
|
259
|
+
requestId: 'sun-init-' + Date.now(),
|
|
260
|
+
sessionId: sid,
|
|
261
|
+
mode: 'queue',
|
|
262
|
+
content: [{ type: 'text', text: `【孙学空间初始化】你是新环境的 Sun Router(你的会话 id:${sid})。完成知识库初始化:
|
|
263
|
+
1) 用 sun_vault_write 创建知识库骨架(写入插件解析给你的 vault 路径,通常是 ~/Workspace/sun-vault 或用户自定义路径):
|
|
264
|
+
- 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 不带时区后缀、结论必注出处)
|
|
265
|
+
- 90-Meta/runbook.md:初始版(# Sun 行为手册 + v0.1 版本行 + §1 角色:Router 只分派不执行 + §2 沟通规范:IM ≤100 字结论先行 + §3 打扰分级:info 攒批/ask 立即/urgent 加 DING)
|
|
266
|
+
- 90-Meta/memory.md:初始版(# 关于用户,待填充)
|
|
267
|
+
2) 用 sun_vault_write 写 90-Meta/system/state.json:{"routerSession": "${sid}", "vault": "<你解析到的知识库绝对路径>", "updatedAt": "<本地时间 YYYY-MM-DDTHH:mm>"}(自登记,供孙学空间发现与轮换机制使用)
|
|
268
|
+
3) 完成后列出生成的文件清单与知识库路径。` }],
|
|
269
|
+
clientTimeZone: 'Asia/Shanghai',
|
|
270
|
+
})
|
|
271
|
+
try { hostCtx.uiWorkspace.openSession(sid) } catch (e) { D.errors.push('openSession: ' + String(e)) }
|
|
272
|
+
onRetry()
|
|
273
|
+
} catch (e) { D.errors.push(String(e)) }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Router 已在、知识库缺失时的骨架补建指令(发给现有 Router 会话)
|
|
277
|
+
async function initSkeletonOnRouter(routerSession, onRetry) {
|
|
278
|
+
try {
|
|
279
|
+
await gw('session/prompt', {
|
|
280
|
+
requestId: 'sun-skel-' + Date.now(),
|
|
281
|
+
sessionId: routerSession,
|
|
282
|
+
mode: 'queue',
|
|
283
|
+
content: [{ type: 'text', text: `【知识库骨架补建】用 sun_vault_write 在你的 vault 路径下创建:
|
|
284
|
+
- 90-Meta/schemas.md:frontmatter 契约最小集(通用字段 type/id/status/created/updated;写入纪律同 runbook)
|
|
285
|
+
- 90-Meta/runbook.md:若缺失则写初始版(# Sun 行为手册 + v0.1)
|
|
286
|
+
- 90-Meta/memory.md:若缺失则写初始版(# 关于用户)
|
|
287
|
+
- 90-Meta/system/state.json:{"routerSession": "${routerSession}", "vault": "<你的 vault 绝对路径>", "updatedAt": "<本地时间>"}(已有的话合并更新 routerSession/vault 字段)
|
|
288
|
+
完成后报告清单。` }],
|
|
289
|
+
clientTimeZone: 'Asia/Shanghai',
|
|
290
|
+
})
|
|
291
|
+
onRetry()
|
|
275
292
|
} catch (e) { D.errors.push(String(e)) }
|
|
276
293
|
}
|
|
277
294
|
|
|
@@ -290,7 +307,7 @@ window.__ModuleLoader__.load({
|
|
|
290
307
|
h('div', { key: 'd', style: { fontSize: 12, opacity: 0.7, lineHeight: 1.6 } }, r.desc),
|
|
291
308
|
]),
|
|
292
309
|
r.action === 'plugins' ? h('button', { key: 'a', onClick: () => { try { hostCtx.layout.selectPanel('plugins') } catch (e) { D.errors.push(String(e)) } }, style: btnStyle }, '打开插件面板') : null,
|
|
293
|
-
r.action === 'create-router' ? h('button', { key: 'a', onClick: () =>
|
|
310
|
+
r.action === 'create-router' ? h('button', { key: 'a', onClick: () => createRouterAndInit(checks, onRetry), style: btnStyle }, '创建 Router 会话') : null,
|
|
294
311
|
])),
|
|
295
312
|
h('button', { key: 'retry', onClick: onRetry, style: { ...btnStyle, marginTop: 10 } }, '↻ 重新检查'),
|
|
296
313
|
])
|
|
@@ -386,8 +403,8 @@ window.__ModuleLoader__.load({
|
|
|
386
403
|
function OnboardPanel({ checks, currentSessionId, onRetry }) {
|
|
387
404
|
const steps = [
|
|
388
405
|
{ n: 1, ok: checks.presets, title: '确认依赖插件', desc: checks.presets ? '智能体协作 / 子智能体插件已启用' : '打开插件面板,启用 sun-space / 智能体协作相关插件后重启实例', action: checks.presets ? null : 'plugins' },
|
|
389
|
-
{ n: 2, ok: !!checks.routerSession, title: '
|
|
390
|
-
{ n: 3, ok: checks.vault, title: '知识库就绪', desc: checks.vault ? `已就绪:${checks.vaultPath}` :
|
|
406
|
+
{ n: 2, ok: !!checks.routerSession, title: '一键初始化(Router 会话 + 知识库)', desc: checks.routerSession ? `已就绪:${checks.routerSession.slice(0, 28)}…` : '一键完成:创建 sun-router 预设 Router 会话 → 在知识库写入 90-Meta 骨架 → 自登记状态(全程走 sun_vault_write,免审批)', action: checks.presets && !checks.routerSession ? 'create-router' : null },
|
|
407
|
+
{ n: 3, ok: checks.vault, title: '知识库就绪', desc: checks.vault ? `已就绪:${checks.vaultPath}` : (checks.routerSession ? '知识库骨架缺失 → 点下方按钮让 Router 补建' : '完成第②步后自动创建'), action: checks.presets && checks.routerSession && !checks.vault ? 'init-skeleton' : null },
|
|
391
408
|
]
|
|
392
409
|
const allOk = steps.every(x => x.ok)
|
|
393
410
|
return h('div', null, [
|
|
@@ -400,7 +417,9 @@ window.__ModuleLoader__.load({
|
|
|
400
417
|
h('div', { key: 'd', style: { fontSize: 12, opacity: 0.7, lineHeight: 1.6, marginTop: 2 } }, st.desc),
|
|
401
418
|
]),
|
|
402
419
|
st.action === 'plugins' ? h('button', { key: 'a', onClick: () => { try { hostCtx.layout.selectPanel('plugins') } catch (e) { D.errors.push(String(e)) } }, style: btnStyle }, '打开插件面板') : null,
|
|
403
|
-
st.action === 'create-router' ? h('button', { key: 'a', onClick: () =>
|
|
420
|
+
st.action === 'create-router' ? h('button', { key: 'a', onClick: () => createRouterAndInit(checks, onRetry), style: btnStyle }, '一键初始化') : null,
|
|
421
|
+
st.action === 'init-skeleton' ? h('button', { key: 'a', onClick: () => initSkeletonOnRouter(checks.routerSession, onRetry), style: btnStyle }, '补建知识库骨架') : null,
|
|
422
|
+
|
|
404
423
|
])),
|
|
405
424
|
allOk ? h('p', { key: 'done', style: { color: '#57b26a', fontSize: 13 } }, '✓ 全部就绪——到「定时任务」用模板创建晨报等四条生产任务,即可开始使用。') : null,
|
|
406
425
|
h('button', { key: 'retry', onClick: onRetry, style: { ...btnStyle, marginTop: 10 } }, '↻ 重新检查'),
|