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 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
- * ② 实例启动目录下的 sun-space.config.json 的 vault 字段(持久化配置,重启不丢)
23
+ * ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写,持久化,不入库)
23
24
  * ③ 默认约定 ~/Workspace/sun-vault
24
25
  *
25
- * 解析结果同时写入 <实例cwd>/sun-space.json(仅 vault 字段),作为浏览器端(孙学空间)
26
- * 的发现通道——浏览器读不到进程环境,靠这个文件拿到真实 vault 路径(workspaceFiles
27
- * 的相对路径读取以会话工作区根为基准,即实例 cwd,两者同目录)。
26
+ * 解析结果写回 ~/.dsh/state/sun-space/state.json,作为浏览器端(孙学空间)的发现
27
+ * 通道——浏览器读不到进程环境与插件 config,经 workspaceFiles/read 以绝对路径读取
28
+ * 该文件(工作区外绝对路径读取被宿主允许,与 vault 本体读取同机制)。
28
29
  *
29
- * 注意:本文件不携带任何个人路径;个人 vault 位置只存在于①/②(均不入库)。
30
+ * 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(社区插件 session-superagent 同款)。
31
+ * 本文件不携带任何个人路径;个人 vault 位置只存在于①/②(均不入库)。
30
32
  */
31
33
  const DEFAULT_VAULT_DIRNAME = "sun-vault";
32
- /** 解析 vault 绝对路径(① env ② 实例目录配置文件 ③ 默认约定)。 */
33
- function resolveVaultPath(cwd = process.cwd(), home = process.env.HOME ?? "") {
34
- if (process.env.SUN_VAULT && process.env.SUN_VAULT.trim() !== "") return process.env.SUN_VAULT;
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(readFileSync(join(cwd, "sun-space.config.json"), "utf8"));
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
- const VAULT = resolveVaultPath();
42
- /** 把解析结果落盘为客户端发现文件(apply 时调用,尽力而为)。 */
43
- function publishVaultForClient(cwd = process.cwd()) {
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
- writeFileSync(join(cwd, "sun-space.json"), JSON.stringify({ vault: VAULT }, null, 2), "utf8");
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 TRIGGER_DIR$1 = `${VAULT}/90-Meta/triggers`;
168
- const SNAPSHOT_FILE = `${VAULT}/90-Meta/mock-data/last-snapshot.json`;
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(SNAPSHOT_FILE, JSON.stringify(snap), "utf8");
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(SNAPSHOT_FILE, JSON.stringify(snap), "utf8");
256
+ await writeFile(snapshotFile(), JSON.stringify(snap), "utf8");
227
257
  health.fails = 0;
228
- await pruneTriggers(TRIGGER_DIR$1).catch(() => {});
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(SNAPSHOT_FILE, "utf8"));
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(TRIGGER_DIR$1, { recursive: true });
295
+ await mkdir(triggerDir$1(), { recursive: true });
266
296
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
267
- await writeFile(join(TRIGGER_DIR$1, `${ts}-${label}.json`), JSON.stringify({
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 TRIGGER_DIR = `${VAULT}/90-Meta/triggers`;
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(TRIGGER_DIR, { recursive: true });
407
+ await mkdir(triggerDir(), { recursive: true });
379
408
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
380
- await writeFile(join(TRIGGER_DIR, `${ts}-watchdog-stall.json`), JSON.stringify({
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
- publishVaultForClient();
493
+ publishVaultState();
465
494
  ctx.plugin(source_exports);
466
495
  ctx.plugin(worker_watchdog_exports);
467
496
  }
@@ -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
- * ② 实例启动目录下的 sun-space.config.json 的 vault 字段(持久化配置,重启不丢)
194
+ * ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写,持久化,不入库)
194
195
  * ③ 默认约定 ~/Workspace/sun-vault
195
196
  *
196
- * 解析结果同时写入 <实例cwd>/sun-space.json(仅 vault 字段),作为浏览器端(孙学空间)
197
- * 的发现通道——浏览器读不到进程环境,靠这个文件拿到真实 vault 路径(workspaceFiles
198
- * 的相对路径读取以会话工作区根为基准,即实例 cwd,两者同目录)。
197
+ * 解析结果写回 ~/.dsh/state/sun-space/state.json,作为浏览器端(孙学空间)的发现
198
+ * 通道——浏览器读不到进程环境与插件 config,经 workspaceFiles/read 以绝对路径读取
199
+ * 该文件(工作区外绝对路径读取被宿主允许,与 vault 本体读取同机制)。
199
200
  *
200
- * 注意:本文件不携带任何个人路径;个人 vault 位置只存在于①/②(均不入库)。
201
+ * 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(社区插件 session-superagent 同款)。
202
+ * 本文件不携带任何个人路径;个人 vault 位置只存在于①/②(均不入库)。
201
203
  */
202
204
  const DEFAULT_VAULT_DIRNAME = "sun-vault";
203
- /** 解析 vault 绝对路径(① env ② 实例目录配置文件 ③ 默认约定)。 */
204
- function resolveVaultPath(cwd = process.cwd(), home = process.env.HOME ?? "") {
205
- if (process.env.SUN_VAULT && process.env.SUN_VAULT.trim() !== "") return process.env.SUN_VAULT;
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(readFileSync(join(cwd, "sun-space.config.json"), "utf8"));
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
- const VAULT = resolveVaultPath();
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(VAULT, "90-Meta/triggers");
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(VAULT, `40-Journal/${d}.md`);
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(VAULT, "50-Tasks");
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(VAULT, `90-Meta/${args.what}.md`);
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(VAULT, args.path), "utf8");
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(VAULT, args.path);
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(VAULT, args.path);
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(VAULT, `50-Tasks/${args.task_id}.md`);
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 { VAULT, apply, inject, lossless };
626
+ export { apply, inject, lossless };
@@ -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
- * ② 实例启动目录下的 sun-space.config.json 的 vault 字段(持久化配置,重启不丢)
37
+ * ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写,持久化,不入库)
37
38
  * ③ 默认约定 ~/Workspace/sun-vault
38
39
  *
39
- * 解析结果同时写入 <实例cwd>/sun-space.json(仅 vault 字段),作为浏览器端(孙学空间)
40
- * 的发现通道——浏览器读不到进程环境,靠这个文件拿到真实 vault 路径(workspaceFiles
41
- * 的相对路径读取以会话工作区根为基准,即实例 cwd,两者同目录)。
40
+ * 解析结果写回 ~/.dsh/state/sun-space/state.json,作为浏览器端(孙学空间)的发现
41
+ * 通道——浏览器读不到进程环境与插件 config,经 workspaceFiles/read 以绝对路径读取
42
+ * 该文件(工作区外绝对路径读取被宿主允许,与 vault 本体读取同机制)。
42
43
  *
43
- * 注意:本文件不携带任何个人路径;个人 vault 位置只存在于①/②(均不入库)。
44
+ * 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(社区插件 session-superagent 同款)。
45
+ * 本文件不携带任何个人路径;个人 vault 位置只存在于①/②(均不入库)。
44
46
  */
45
47
  const DEFAULT_VAULT_DIRNAME = "sun-vault";
46
- /** 解析 vault 绝对路径(① env ② 实例目录配置文件 ③ 默认约定)。 */
47
- function resolveVaultPath(cwd = process.cwd(), home = process.env.HOME ?? "") {
48
- if (process.env.SUN_VAULT && process.env.SUN_VAULT.trim() !== "") return process.env.SUN_VAULT;
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(readFileSync(join(cwd, "sun-space.config.json"), "utf8"));
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
- const VAULT = resolveVaultPath();
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(VAULT, rel);
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 { VAULT, apply, artifactPath, inject };
154
+ export { apply, artifactPath, inject };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sun-space",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "description": "Sun Space:DeepSeek Harness 个人超智能体插件——Router/Worker 双预设工具白名单、钉钉待办轮询、Obsidian 知识库落库、会话工作台与定时任务管理。",
6
6
  "exports": {
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 = 'v15' // 根节点 data-wb-rev 版本标记:判别浏览器是否吃到旧 client.js(immutable 缓存坑,HANDOFF 踩坑 1)
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
- // ① 插件落盘的运行配置(实例 cwd 的 sun-space.json;相对路径以会话工作区根解析,即实例 cwd)
107
- try {
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
- // 创建 Router 会话 → 自登记 state.json → 跳转 → 重检(InitPanel 与 OnboardPanel 共用)
266
- async function createAndBootstrap(checks, onRetry) {
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
- await bootstrapRouterSession(sid, checks.vaultPath) // 自登记 state.json(含 vault 字段;失败不阻断——下次检查由最近活跃兜底)
272
- try { hostCtx.uiWorkspace.openSession(sid) } catch (e) { D.errors.push('openSession: ' + String(e)) }
273
- onRetry() // 新会话的 tab 即完整工作台(state.json 已指向它)
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: () => createAndBootstrap(checks, onRetry), style: btnStyle }, '创建 Router 会话') : null,
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: '创建 Router 会话', desc: checks.routerSession ? `已就绪:${checks.routerSession.slice(0, 28)}…` : '一键创建 sun-router 预设会话并自动登记(孙学空间与定时任务都挂在这上面)', action: checks.presets && !checks.routerSession ? 'create-router' : null },
390
- { n: 3, ok: checks.vault, title: '知识库就绪', desc: checks.vault ? `已就绪:${checks.vaultPath}` : `用 Obsidian 打开(或创建)~/Workspace/sun-vault 并放好 90-Meta 骨架;自定义位置让 Router 把绝对路径写进 state.json 的 vault 字段`, action: null },
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: () => createAndBootstrap(checks, onRetry), style: btnStyle }, '创建 Router 会话') : null,
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 } }, '↻ 重新检查'),