dsh-sun-space 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/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,57 @@ 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/state/sun-space/),沿用官方 state/<插件名> 约定。 */
35
+ const STATE_DIR = join(homedir(), ".dsh", "state", "sun-space");
36
+ /** 纯函数:给定 env 与 config 内容时的解析结果(测试与 vaultPath 共用)。 */
37
+ function computeVaultPath(envValue, configJson, home = homedir()) {
38
+ if (envValue && envValue.trim() !== "") return envValue;
35
39
  try {
36
- const cfg = JSON.parse(readFileSync(join(cwd, "sun-space.config.json"), "utf8"));
40
+ const cfg = JSON.parse(configJson ?? "");
37
41
  if (typeof cfg?.vault === "string" && cfg.vault.startsWith("/")) return cfg.vault;
38
42
  } catch {}
39
43
  return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
40
44
  }
41
- const VAULT = resolveVaultPath();
42
- /** 把解析结果落盘为客户端发现文件(apply 时调用,尽力而为)。 */
43
- function publishVaultForClient(cwd = process.cwd()) {
45
+ let cached = {
46
+ at: 0,
47
+ key: "",
48
+ value: ""
49
+ };
50
+ /** 运行期解析:每次调用重读 config.json(60s 微缓存防抖),"以当前目录初始化"写入后即时生效。 */
51
+ function vaultPath(now = Date.now()) {
52
+ let configJson;
53
+ try {
54
+ configJson = readFileSync(join(STATE_DIR, "config.json"), "utf8");
55
+ } catch {}
56
+ if (now - cached.at < 6e4 && cached.key === (configJson ?? "")) return cached.value;
57
+ const value = computeVaultPath(process.env.SUN_VAULT, configJson);
58
+ cached = {
59
+ at: now,
60
+ key: configJson ?? "",
61
+ value
62
+ };
63
+ return value;
64
+ }
65
+ vaultPath();
66
+ /** 把解析结果写回状态目录的 state.json(apply 时调用,尽力而为),供客户端发现。 */
67
+ function publishVaultState(now = /* @__PURE__ */ new Date()) {
44
68
  try {
45
- writeFileSync(join(cwd, "sun-space.json"), JSON.stringify({ vault: VAULT }, null, 2), "utf8");
69
+ mkdirSync(STATE_DIR, { recursive: true });
70
+ writeFileSync(join(STATE_DIR, "state.json"), JSON.stringify({
71
+ vault: vaultPath(now.getTime()),
72
+ 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")}`
73
+ }, null, 2), "utf8");
46
74
  } catch {}
47
75
  }
48
76
  // src/dws.ts
@@ -154,7 +182,6 @@ var source_exports = /* @__PURE__ */ __exportAll({
154
182
  CREDENTIAL_ALERT_COOLDOWN_MS: () => CREDENTIAL_ALERT_COOLDOWN_MS,
155
183
  CREDENTIAL_ALERT_STREAK: () => 2,
156
184
  TRIGGER_TTL_MS: () => TRIGGER_TTL_MS,
157
- VAULT: () => VAULT,
158
185
  apply: () => apply$2,
159
186
  inject: () => inject$2,
160
187
  parseTriggerTs: () => parseTriggerTs,
@@ -164,8 +191,8 @@ var source_exports = /* @__PURE__ */ __exportAll({
164
191
  const inject$2 = ["timer"];
165
192
  /** 轮询周期(毫秒):纯程序层,可调密。 */
166
193
  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`;
194
+ const triggerDir$1 = () => `${vaultPath()}/90-Meta/triggers`;
195
+ const snapshotFile = () => `${vaultPath()}/90-Meta/mock-data/last-snapshot.json`;
169
196
  /** trigger 文件 TTL:超过即视为已消费/过期,轮询成功路径顺带清理。 */
170
197
  const TRIGGER_TTL_MS = 6048e5;
171
198
  const CREDENTIAL_ALERT_COOLDOWN_MS = 36e5;
@@ -211,7 +238,7 @@ function schedulePolling(ctx) {
211
238
  const snap = await snapshot();
212
239
  const prev = await readPrev();
213
240
  if (!prev) {
214
- await writeFile(SNAPSHOT_FILE, JSON.stringify(snap), "utf8");
241
+ await writeFile(snapshotFile(), JSON.stringify(snap), "utf8");
215
242
  return;
216
243
  }
217
244
  const fresh = diff(prev, snap);
@@ -223,9 +250,9 @@ function schedulePolling(ctx) {
223
250
  if (fresh.newApprovals.length) parts.push(`新审批 ${fresh.newApprovals.length} 件`);
224
251
  await notifySelf(uid, `[Sun·info] ${parts.join("、")},已入触发队列(${(/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN")})`);
225
252
  }
226
- await writeFile(SNAPSHOT_FILE, JSON.stringify(snap), "utf8");
253
+ await writeFile(snapshotFile(), JSON.stringify(snap), "utf8");
227
254
  health.fails = 0;
228
- await pruneTriggers(TRIGGER_DIR$1).catch(() => {});
255
+ await pruneTriggers(triggerDir$1()).catch(() => {});
229
256
  } catch (e) {
230
257
  ctx.logger?.warn?.(`[sun-space] poll failed: ${String(e.message).slice(0, 150)}`);
231
258
  health.fails++;
@@ -256,15 +283,15 @@ function schedulePolling(ctx) {
256
283
  }
257
284
  async function readPrev() {
258
285
  try {
259
- return JSON.parse(await readFile(SNAPSHOT_FILE, "utf8"));
286
+ return JSON.parse(await readFile(snapshotFile(), "utf8"));
260
287
  } catch {
261
288
  return null;
262
289
  }
263
290
  }
264
291
  async function writeTrigger(label, payload) {
265
- await mkdir(TRIGGER_DIR$1, { recursive: true });
292
+ await mkdir(triggerDir$1(), { recursive: true });
266
293
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
267
- await writeFile(join(TRIGGER_DIR$1, `${ts}-${label}.json`), JSON.stringify({
294
+ await writeFile(join(triggerDir$1(), `${ts}-${label}.json`), JSON.stringify({
268
295
  label,
269
296
  payload
270
297
  }, null, 2), "utf8");
@@ -307,7 +334,6 @@ async function writeTrigger(label, payload) {
307
334
  var worker_watchdog_exports = /* @__PURE__ */ __exportAll({
308
335
  DEFAULT_STALL_MS: () => DEFAULT_STALL_MS,
309
336
  DEFAULT_TICK_MS: () => DEFAULT_TICK_MS,
310
- VAULT: () => VAULT,
311
337
  apply: () => apply$1,
312
338
  inject: () => inject$1,
313
339
  isDelegatedChildHeader: () => isDelegatedChildHeader,
@@ -318,7 +344,7 @@ var worker_watchdog_exports = /* @__PURE__ */ __exportAll({
318
344
  });
319
345
  const name$1 = "worker-watchdog";
320
346
  const inject$1 = ["timer"];
321
- const TRIGGER_DIR = `${VAULT}/90-Meta/triggers`;
347
+ const triggerDir = () => `${vaultPath()}/90-Meta/triggers`;
322
348
  /** 默认停滞阈值 15 分钟:长思考/长命令完成后即重置计时,真卡死撑不到这么久。 */
323
349
  const DEFAULT_STALL_MS = 9e5;
324
350
  /** 巡检间隔 30 秒。 */
@@ -375,9 +401,9 @@ function apply$1(ctx) {
375
401
  const idleMin = Math.round((Date.now() - entry.at) / 6e4);
376
402
  log("warn", `stalled: child=${entry.id} idle=${idleMin}min — interrupting & notifying`);
377
403
  try {
378
- await mkdir(TRIGGER_DIR, { recursive: true });
404
+ await mkdir(triggerDir(), { recursive: true });
379
405
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
380
- await writeFile(join(TRIGGER_DIR, `${ts}-watchdog-stall.json`), JSON.stringify({
406
+ await writeFile(join(triggerDir(), `${ts}-watchdog-stall.json`), JSON.stringify({
381
407
  label: "watchdog-stall",
382
408
  payload: {
383
409
  childId: entry.id,
@@ -461,7 +487,7 @@ function apply$1(ctx) {
461
487
  const name = "sun-space";
462
488
  const inject = ["timer", "tools"];
463
489
  function apply(ctx) {
464
- publishVaultForClient();
490
+ publishVaultState();
465
491
  ctx.plugin(source_exports);
466
492
  ctx.plugin(worker_watchdog_exports);
467
493
  }
@@ -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,49 @@ 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/state/sun-space/),沿用官方 state/<插件名> 约定。 */
206
+ const STATE_DIR = join(homedir(), ".dsh", "state", "sun-space");
207
+ /** 纯函数:给定 env 与 config 内容时的解析结果(测试与 vaultPath 共用)。 */
208
+ function computeVaultPath(envValue, configJson, home = homedir()) {
209
+ if (envValue && envValue.trim() !== "") return envValue;
206
210
  try {
207
- const cfg = JSON.parse(readFileSync(join(cwd, "sun-space.config.json"), "utf8"));
211
+ const cfg = JSON.parse(configJson ?? "");
208
212
  if (typeof cfg?.vault === "string" && cfg.vault.startsWith("/")) return cfg.vault;
209
213
  } catch {}
210
214
  return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
211
215
  }
212
- const VAULT = resolveVaultPath();
216
+ let cached = {
217
+ at: 0,
218
+ key: "",
219
+ value: ""
220
+ };
221
+ /** 运行期解析:每次调用重读 config.json(60s 微缓存防抖),"以当前目录初始化"写入后即时生效。 */
222
+ function vaultPath(now = Date.now()) {
223
+ let configJson;
224
+ try {
225
+ configJson = readFileSync(join(STATE_DIR, "config.json"), "utf8");
226
+ } catch {}
227
+ if (now - cached.at < 6e4 && cached.key === (configJson ?? "")) return cached.value;
228
+ const value = computeVaultPath(process.env.SUN_VAULT, configJson);
229
+ cached = {
230
+ at: now,
231
+ key: configJson ?? "",
232
+ value
233
+ };
234
+ return value;
235
+ }
236
+ vaultPath();
213
237
  // src/router-tools.ts
214
238
  const inject = ["tools", "subagents"];
215
239
  const text = (s) => [{
@@ -290,7 +314,7 @@ function apply(ctx) {
290
314
  }, async (args) => {
291
315
  try {
292
316
  if (args.what === "triggers") {
293
- const dir = resolveSafe(VAULT, "90-Meta/triggers");
317
+ const dir = resolveSafe(vaultPath(), "90-Meta/triggers");
294
318
  try {
295
319
  const files = (await readdir(dir)).filter((f) => f.endsWith(".json")).sort().reverse().slice(0, 10);
296
320
  const triggers = [];
@@ -322,7 +346,7 @@ function apply(ctx) {
322
346
  }
323
347
  if (args.what === "journal") {
324
348
  const d = args.date ?? localDate();
325
- const p = resolveRead(VAULT, `40-Journal/${d}.md`);
349
+ const p = resolveRead(vaultPath(), `40-Journal/${d}.md`);
326
350
  const raw = await readFile(p, "utf8");
327
351
  if (args.full) return {
328
352
  ok: true,
@@ -337,7 +361,7 @@ function apply(ctx) {
337
361
  };
338
362
  }
339
363
  if (args.what === "tasks") {
340
- const dir = resolveSafe(VAULT, "50-Tasks");
364
+ const dir = resolveSafe(vaultPath(), "50-Tasks");
341
365
  const files = (await readdir(dir)).filter((f) => f.endsWith(".md"));
342
366
  const tasks = [];
343
367
  for (const f of files) {
@@ -360,7 +384,7 @@ function apply(ctx) {
360
384
  };
361
385
  }
362
386
  if (args.what === "runbook" || args.what === "memory") {
363
- const p = resolveSafe(VAULT, `90-Meta/${args.what}.md`);
387
+ const p = resolveSafe(vaultPath(), `90-Meta/${args.what}.md`);
364
388
  return {
365
389
  ok: true,
366
390
  data: { text: (await readFile(p, "utf8")).slice(0, 8e3) }
@@ -371,7 +395,7 @@ function apply(ctx) {
371
395
  ok: false,
372
396
  note: "note 需要 path"
373
397
  };
374
- const raw = await readFile(resolveRead(VAULT, args.path), "utf8");
398
+ const raw = await readFile(resolveRead(vaultPath(), args.path), "utf8");
375
399
  if (args.full) return {
376
400
  ok: true,
377
401
  data: { text: raw.slice(0, 8e3) }
@@ -415,7 +439,7 @@ function apply(ctx) {
415
439
  required: ["path", "content"]
416
440
  }, async (args) => {
417
441
  try {
418
- const p = resolveSafe(VAULT, args.path);
442
+ const p = resolveSafe(vaultPath(), args.path);
419
443
  await mkdir(join(p, ".."), { recursive: true });
420
444
  if (args.mode === "append") await appendFile(p, args.content, "utf8");
421
445
  else await writeFile(p, args.content, "utf8");
@@ -511,7 +535,7 @@ function apply(ctx) {
511
535
  required: ["path", "fields"]
512
536
  }, async (args) => {
513
537
  try {
514
- const p = resolveSafe(VAULT, args.path);
538
+ const p = resolveSafe(vaultPath(), args.path);
515
539
  const updated = updateFrontmatter(await readFile(p, "utf8"), args.fields);
516
540
  await writeFile(p, updated, "utf8");
517
541
  return {
@@ -585,7 +609,7 @@ function apply(ctx) {
585
609
  };
586
610
  }
587
611
  try {
588
- const p = resolveSafe(VAULT, `50-Tasks/${args.task_id}.md`);
612
+ const p = resolveSafe(vaultPath(), `50-Tasks/${args.task_id}.md`);
589
613
  const raw = await readFile(p, "utf8");
590
614
  await writeFile(p, updateFrontmatter(raw, { session: String(activation.childId ?? "") }), "utf8");
591
615
  } catch {}
@@ -597,4 +621,4 @@ function apply(ctx) {
597
621
  }));
598
622
  ctx.logger?.info?.("[sun-space] router tools registered: sun_read sun_vault_write sun_vault_meta sun_notify sun_route(subagents)");
599
623
  }
600
- export { VAULT, apply, inject, lossless };
624
+ 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,49 @@ 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/state/sun-space/),沿用官方 state/<插件名> 约定。 */
49
+ const STATE_DIR = join(homedir(), ".dsh", "state", "sun-space");
50
+ /** 纯函数:给定 env 与 config 内容时的解析结果(测试与 vaultPath 共用)。 */
51
+ function computeVaultPath(envValue, configJson, home = homedir()) {
52
+ if (envValue && envValue.trim() !== "") return envValue;
49
53
  try {
50
- const cfg = JSON.parse(readFileSync(join(cwd, "sun-space.config.json"), "utf8"));
54
+ const cfg = JSON.parse(configJson ?? "");
51
55
  if (typeof cfg?.vault === "string" && cfg.vault.startsWith("/")) return cfg.vault;
52
56
  } catch {}
53
57
  return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
54
58
  }
55
- const VAULT = resolveVaultPath();
59
+ let cached = {
60
+ at: 0,
61
+ key: "",
62
+ value: ""
63
+ };
64
+ /** 运行期解析:每次调用重读 config.json(60s 微缓存防抖),"以当前目录初始化"写入后即时生效。 */
65
+ function vaultPath(now = Date.now()) {
66
+ let configJson;
67
+ try {
68
+ configJson = readFileSync(join(STATE_DIR, "config.json"), "utf8");
69
+ } catch {}
70
+ if (now - cached.at < 6e4 && cached.key === (configJson ?? "")) return cached.value;
71
+ const value = computeVaultPath(process.env.SUN_VAULT, configJson);
72
+ cached = {
73
+ at: now,
74
+ key: configJson ?? "",
75
+ value
76
+ };
77
+ return value;
78
+ }
79
+ vaultPath();
56
80
  // src/worker-vault.ts
57
81
  const inject = ["tools"];
58
82
  const TASK_ID = /^T-\d{8}-\d{2}$/;
@@ -104,7 +128,7 @@ function apply(ctx) {
104
128
  const { task_id: taskId, filename, content } = args ?? {};
105
129
  try {
106
130
  const rel = artifactPath(String(taskId), String(filename));
107
- const abs = resolveSafe(VAULT, rel);
131
+ const abs = resolveSafe(vaultPath(), rel);
108
132
  await mkdir(join(abs, ".."), { recursive: true });
109
133
  await writeFile(abs, String(content ?? ""), "utf8");
110
134
  return {
@@ -125,4 +149,4 @@ function apply(ctx) {
125
149
  });
126
150
  ctx.logger?.info?.("[sun-space] worker artifact tool registered: sun_artifact_write");
127
151
  }
128
- export { VAULT, apply, artifactPath, inject };
152
+ 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.6",
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 = 'v16' // 根节点 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 {
@@ -262,6 +267,17 @@ window.__ModuleLoader__.load({
262
267
  }
263
268
  }
264
269
 
270
+ // 知识库初始化指令(下发到当前会话执行——标准预设有完整文件工具;
271
+ // 第 6 步写 ~/.dsh 超出工作区,会在对话中弹一次审批卡,由用户批准)
272
+ const VAULT_INIT_PROMPT = `【孙学空间知识库初始化】以当前工作目录为知识库根(vault):
273
+ 1) 创建目录:90-Meta/system、40-Journal、50-Tasks/artifacts、00-Inbox、60-Archive
274
+ 2) 写 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 不带时区后缀;结论必注出处)
275
+ 3) 写 90-Meta/runbook.md:初始版(# Sun 行为手册 + v0.1 版本行 + §1 角色:Router 只分派不执行 + §2 沟通规范:IM ≤100 字结论先行 + §3 打扰分级:info 攒批/ask 立即/urgent 加 DING)
276
+ 4) 写 90-Meta/memory.md:初始版(# 关于用户,待填充)
277
+ 5) 创建目录 90-Meta/system
278
+ 6) 把知识库绝对路径登记到 ~/.dsh/state/sun-space/config.json:内容 {"vault": "<当前目录绝对路径>"}(目录不存在则创建)
279
+ 7) 完成后列出生成的文件清单。`
280
+
265
281
  // 创建 Router 会话 → 自登记 state.json → 跳转 → 重检(InitPanel 与 OnboardPanel 共用)
266
282
  async function createAndBootstrap(checks, onRetry) {
267
283
  try {
@@ -384,10 +400,11 @@ window.__ModuleLoader__.load({
384
400
 
385
401
  // 新用户三步启用引导(主理鱼同款编号步骤卡;替代旧的一锅炖检查面板)
386
402
  function OnboardPanel({ checks, currentSessionId, onRetry }) {
403
+ const [initBusy, setInitBusy] = React.useState(false)
387
404
  const steps = [
388
405
  { n: 1, ok: checks.presets, title: '确认依赖插件', desc: checks.presets ? '智能体协作 / 子智能体插件已启用' : '打开插件面板,启用 sun-space / 智能体协作相关插件后重启实例', action: checks.presets ? null : 'plugins' },
389
406
  { 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 },
407
+ { n: 3, ok: checks.vault, title: '知识库(以当前目录初始化)', desc: checks.vault ? `已就绪:${checks.vaultPath}` : (currentSessionId ? '以当前目录为 vault 根创建 90-Meta 骨架,并把路径登记到 ~/.dsh/state/sun-space/config.json——会在对话中请求一次该写入的批准' : '先打开一个会话再来执行此步'), action: checks.vault || !currentSessionId || initBusy ? null : 'init-vault' },
391
408
  ]
392
409
  const allOk = steps.every(x => x.ok)
393
410
  return h('div', null, [
@@ -401,6 +418,20 @@ window.__ModuleLoader__.load({
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
420
  st.action === 'create-router' ? h('button', { key: 'a', onClick: () => createAndBootstrap(checks, onRetry), style: btnStyle }, '创建 Router 会话') : null,
421
+ st.action === 'init-vault' ? h('button', { key: 'a', onClick: async () => {
422
+ setInitBusy(true)
423
+ try {
424
+ await gw('session/prompt', {
425
+ requestId: 'sun-init-' + Date.now(),
426
+ sessionId: currentSessionId,
427
+ mode: 'queue',
428
+ content: [{ type: 'text', text: VAULT_INIT_PROMPT }],
429
+ clientTimeZone: 'Asia/Shanghai',
430
+ })
431
+ checks.notes.push('初始化任务已下发到当前会话——请在对话中批准 ~/.dsh 写入并等待完成,然后点「↻ 重新检查」')
432
+ } catch (e) { D.errors.push('init-vault: ' + String(e)) }
433
+ setInitBusy(false)
434
+ }, style: btnStyle }, '以当前目录初始化知识库') : null,
404
435
  ])),
405
436
  allOk ? h('p', { key: 'done', style: { color: '#57b26a', fontSize: 13 } }, '✓ 全部就绪——到「定时任务」用模板创建晨报等四条生产任务,即可开始使用。') : null,
406
437
  h('button', { key: 'retry', onClick: onRetry, style: { ...btnStyle, marginTop: 10 } }, '↻ 重新检查'),