dsh-sun-space 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.mjs +115 -37
- package/lib/router-tools.mjs +184 -23
- package/lib/worker-vault.mjs +61 -21
- package/package.json +2 -2
- package/src/client.js +52 -23
package/lib/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -20,43 +20,95 @@ var __exportAll = (all, no_symbols) => {
|
|
|
20
20
|
*
|
|
21
21
|
* 解析优先级(vaultPath() 每次调用重读,运行期变更即时生效):
|
|
22
22
|
* ① 环境变量 SUN_VAULT(启动命令注入,随进程生效)
|
|
23
|
-
* ② ~/.dsh/state/sun-space/config.json 的 vault
|
|
24
|
-
* ③
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写/补建指令写入,持久化,不入库)
|
|
24
|
+
* ③ 会话工作目录(调用方从 exec 提取传入,须过 cwdHasSkeleton 门控——目录已含 90-Meta
|
|
25
|
+
* 骨架才算;"空目录"不算:DSH 默认 boot 桶 default-workspace 全新时也是空的,无法与
|
|
26
|
+
* "用户新建目录当知识库"区分,空目录意图在会话创建时由 registerVaultRootFromSession 落到 ④)
|
|
27
|
+
* ④ state.json 的 workspaces 映射(分桶登记:workspaces[<会话/实例 cwd>] = vault。
|
|
28
|
+
* DSH 会话按实例 boot cwd 分桶,一桶一 vault——多实例并存互不污染:
|
|
29
|
+
* homevault 实例登记 homevault,主实例解析不受影响)
|
|
30
|
+
* ⑤ state.json 的 vault 字段(全局兜底;只承载 ①② 显式值的镜像,见 publishVaultState)
|
|
31
|
+
* ⑥ 默认约定 ~/Workspace/sun-vault(仅全新裸环境兜底)
|
|
27
32
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* 手工配置。①② 供自定义/覆盖;⑤ 仅在全新裸环境兜底。
|
|
33
|
+
* 无会话上下文的调用方(source/worker-watchdog):用 vaultPathForHost()——以实例进程
|
|
34
|
+
* cwd(= 本桶 boot 目录)查 ④ 映射,与本实例会话解析一致。
|
|
31
35
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
36
|
+
* publishVaultState 纪律(v0.3.2 根因修复):落盘值仅限显式来源(①②⑤),⑥ 兜底值不写——
|
|
37
|
+
* 落了会把默认路径固化成登记值,堵死 ③④ 级(v0.3.1 及之前每次插件启动即写入,新开工作区
|
|
38
|
+
* 时"识别到的知识库"永远是 ~/Workspace/sun-vault 的幽灵)。
|
|
39
|
+
*
|
|
40
|
+
* registerVaultRootFromSession(首个 sun-router 会话创建时调用):仅对 vault 候选目录
|
|
41
|
+
* (空目录或已含 90-Meta)登记 workspaces[cwd]=cwd,不写全局 vault 字段——全局槽一旦
|
|
42
|
+
* 写入首个工作区,会遮蔽其他实例/桶的解析(多 vault 场景交叉污染)。
|
|
34
43
|
*
|
|
35
44
|
* 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(session-superagent 同款)。
|
|
36
|
-
* 本文件不携带任何个人路径;个人 vault
|
|
45
|
+
* 本文件不携带任何个人路径;个人 vault 位置只存在于①②⑤(均不入库)。
|
|
37
46
|
*/
|
|
38
47
|
const DEFAULT_VAULT_DIRNAME = "sun-vault";
|
|
39
48
|
/** 插件自有状态目录(<DSH_HOME 或 ~/>/.dsh/state/sun-space/),沿用官方 state/<插件名> 约定。
|
|
40
49
|
* 跟随 DSH_HOME:多实例(隔离 HOME 的测试环境)之间状态互不串扰。 */
|
|
41
50
|
const stateDir = () => join(process.env.DSH_HOME ?? homedir(), ".dsh", "state", "sun-space");
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
function parseState(stateJson) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(stateJson ?? "");
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 目录是否已含 vault 骨架(③ 级门控:已初始化的工作区目录就是 vault)。 */
|
|
59
|
+
function cwdHasSkeleton(cwd) {
|
|
60
|
+
if (!cwd || !cwd.startsWith("/")) return false;
|
|
61
|
+
try {
|
|
62
|
+
return existsSync(join(cwd, "90-Meta"));
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** 目录是否为 vault 候选(登记门控):空目录(非隐藏项为 0,git init 过的也算)
|
|
68
|
+
* 或已含 90-Meta 骨架。有内容而无骨架的目录(代码仓库根等)不是候选。 */
|
|
69
|
+
function cwdIsVaultCandidate(cwd) {
|
|
70
|
+
if (!cwd || !cwd.startsWith("/")) return false;
|
|
71
|
+
try {
|
|
72
|
+
if (existsSync(join(cwd, "90-Meta"))) return true;
|
|
73
|
+
return readdirSync(cwd).every((name) => name.startsWith("."));
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** 纯函数:六级优先解析(测试与 vaultPath 共用)。
|
|
79
|
+
* sessionCwd 须由调用方先过 cwdHasSkeleton 门控;bucketCwd 为分桶目录(④ 映射查询用,
|
|
80
|
+
* 缺省同 sessionCwd)。 */
|
|
81
|
+
function computeVaultPath(envValue, configJson, stateJson, sessionCwd, home = homedir(), bucketCwd) {
|
|
44
82
|
if (envValue && envValue.trim() !== "") return envValue;
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
if (typeof
|
|
83
|
+
try {
|
|
84
|
+
const cfg = JSON.parse(configJson ?? "");
|
|
85
|
+
if (typeof cfg?.vault === "string" && cfg.vault.startsWith("/")) return cfg.vault;
|
|
48
86
|
} catch {}
|
|
87
|
+
const state = parseState(stateJson);
|
|
49
88
|
if (sessionCwd && sessionCwd.startsWith("/")) return sessionCwd;
|
|
89
|
+
const bucket = bucketCwd ?? sessionCwd;
|
|
90
|
+
if (bucket && bucket.startsWith("/")) {
|
|
91
|
+
const mapped = state?.workspaces?.[bucket];
|
|
92
|
+
if (typeof mapped === "string" && mapped.startsWith("/")) return mapped;
|
|
93
|
+
}
|
|
94
|
+
if (typeof state?.vault === "string" && state.vault.startsWith("/")) return state.vault;
|
|
50
95
|
return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
|
|
51
96
|
}
|
|
97
|
+
/** ①②⑤ 显式来源(env/config/全局镜像)的取值;全空返回 undefined——
|
|
98
|
+
* ⑥ 兜底、③ 会话目录、④ 分桶映射都不算"显式"(publishVaultState 据此决定是否落盘)。 */
|
|
99
|
+
function explicitVaultValue(envValue, configJson, stateJson) {
|
|
100
|
+
if (envValue && envValue.trim() !== "") return envValue;
|
|
101
|
+
for (const raw of [configJson, stateJson]) {
|
|
102
|
+
const parsed = parseState(raw);
|
|
103
|
+
if (parsed && typeof parsed.vault === "string" && parsed.vault.startsWith("/")) return parsed.vault;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
52
106
|
let cached = {
|
|
53
107
|
at: 0,
|
|
54
108
|
key: "",
|
|
55
109
|
value: ""
|
|
56
110
|
};
|
|
57
|
-
|
|
58
|
-
* sessionCwd:调用方从 exec 提取的会话工作目录(可选)——用户在哪个工作区开会话,vault 就解析到哪。 */
|
|
59
|
-
function vaultPath(sessionCwd, now = Date.now()) {
|
|
111
|
+
function resolve(sessionCwd, now) {
|
|
60
112
|
const dir = stateDir();
|
|
61
113
|
let configJson;
|
|
62
114
|
let stateJson;
|
|
@@ -66,9 +118,11 @@ function vaultPath(sessionCwd, now = Date.now()) {
|
|
|
66
118
|
try {
|
|
67
119
|
stateJson = readFileSync(join(dir, "state.json"), "utf8");
|
|
68
120
|
} catch {}
|
|
69
|
-
const
|
|
121
|
+
const cwdSkeleton = cwdHasSkeleton(sessionCwd) ? sessionCwd : void 0;
|
|
122
|
+
const bucket = sessionCwd && sessionCwd.startsWith("/") ? sessionCwd : void 0;
|
|
123
|
+
const key = `${cwdSkeleton ?? ""}|${bucket ?? ""}|${configJson ?? ""}|${stateJson ?? ""}`;
|
|
70
124
|
if (now - cached.at < 6e4 && cached.key === key) return cached.value;
|
|
71
|
-
const value = computeVaultPath(process.env.SUN_VAULT, configJson, stateJson,
|
|
125
|
+
const value = computeVaultPath(process.env.SUN_VAULT, configJson, stateJson, cwdSkeleton, homedir(), bucket);
|
|
72
126
|
cached = {
|
|
73
127
|
at: now,
|
|
74
128
|
key,
|
|
@@ -76,26 +130,41 @@ function vaultPath(sessionCwd, now = Date.now()) {
|
|
|
76
130
|
};
|
|
77
131
|
return value;
|
|
78
132
|
}
|
|
133
|
+
/** 运行期解析:每次调用重读状态目录(60s 微缓存防抖),登记/手改后即时生效。
|
|
134
|
+
* sessionCwd:调用方从 exec 提取的会话工作目录(可选)——本桶已初始化(含 90-Meta)
|
|
135
|
+
* 或已在 workspaces 映射登记时,vault 就解析到哪。 */
|
|
136
|
+
function vaultPath(sessionCwd, now = Date.now()) {
|
|
137
|
+
return resolve(sessionCwd, now);
|
|
138
|
+
}
|
|
139
|
+
/** 无会话上下文的调用方(source/watchdog 等):以实例进程 cwd(= 本桶 boot 目录)解析,
|
|
140
|
+
* 与本实例会话一致(④ 映射按桶命中)。 */
|
|
141
|
+
function vaultPathForHost(now = Date.now()) {
|
|
142
|
+
return resolve(process.cwd(), now);
|
|
143
|
+
}
|
|
79
144
|
vaultPath();
|
|
80
145
|
/**
|
|
81
146
|
* Router 会话创建时登记 vault 根("当前目录作为 vault"语义的落点)。
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
147
|
+
* 仅对候选目录(空目录或已含 90-Meta 骨架)登记 workspaces[cwd]=cwd——分桶登记,
|
|
148
|
+
* 不写全局 vault 字段(防多实例交叉污染);env 钉死时不登记(尊重显式覆盖)。
|
|
149
|
+
* 同桶幂等;尽力而为:任何失败静默退场(最坏是回退到约定路径)。
|
|
85
150
|
*/
|
|
86
151
|
function registerVaultRootFromSession(cwd, now = /* @__PURE__ */ new Date()) {
|
|
87
|
-
if (!cwd
|
|
152
|
+
if (!cwdIsVaultCandidate(cwd)) return false;
|
|
88
153
|
if (process.env.SUN_VAULT?.trim()) return false;
|
|
89
154
|
try {
|
|
90
155
|
const dir = stateDir();
|
|
91
156
|
mkdirSync(dir, { recursive: true });
|
|
92
157
|
const p = join(dir, "state.json");
|
|
93
|
-
let
|
|
158
|
+
let raw;
|
|
94
159
|
try {
|
|
95
|
-
|
|
160
|
+
raw = readFileSync(p, "utf8");
|
|
96
161
|
} catch {}
|
|
97
|
-
|
|
98
|
-
state.
|
|
162
|
+
const state = parseState(raw) ?? {};
|
|
163
|
+
if (state.workspaces?.[cwd]) return false;
|
|
164
|
+
state.workspaces = {
|
|
165
|
+
...state.workspaces ?? {},
|
|
166
|
+
[cwd]: cwd
|
|
167
|
+
};
|
|
99
168
|
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
169
|
writeFileSync(p, JSON.stringify(state, null, 2), "utf8");
|
|
101
170
|
return true;
|
|
@@ -103,16 +172,25 @@ function registerVaultRootFromSession(cwd, now = /* @__PURE__ */ new Date()) {
|
|
|
103
172
|
return false;
|
|
104
173
|
}
|
|
105
174
|
}
|
|
106
|
-
/**
|
|
175
|
+
/** 把显式解析结果写回状态目录的 state.json(apply 时调用,尽力而为),供客户端发现。
|
|
176
|
+
* 纪律:仅显式来源(①env/②config/⑤全局镜像)才落盘;⑥ 兜底值不写——
|
|
177
|
+
* 否则默认路径被固化成登记值,堵死 ③④ 级(v0.3.2 修复的根因)。
|
|
178
|
+
* workspaces 分桶映射由 registerVaultRootFromSession 维护,此处原样保留。 */
|
|
107
179
|
function publishVaultState(now = /* @__PURE__ */ new Date()) {
|
|
108
180
|
try {
|
|
109
181
|
const dir = stateDir();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let state = {};
|
|
182
|
+
let configJson;
|
|
183
|
+
let stateJson;
|
|
113
184
|
try {
|
|
114
|
-
|
|
185
|
+
configJson = readFileSync(join(dir, "config.json"), "utf8");
|
|
115
186
|
} catch {}
|
|
187
|
+
try {
|
|
188
|
+
stateJson = readFileSync(join(dir, "state.json"), "utf8");
|
|
189
|
+
} catch {}
|
|
190
|
+
if (!explicitVaultValue(process.env.SUN_VAULT, configJson, stateJson)) return;
|
|
191
|
+
mkdirSync(dir, { recursive: true });
|
|
192
|
+
const p = join(dir, "state.json");
|
|
193
|
+
const state = parseState(stateJson) ?? {};
|
|
116
194
|
state.vault = vaultPath(void 0, now.getTime());
|
|
117
195
|
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
196
|
writeFileSync(p, JSON.stringify(state, null, 2), "utf8");
|
|
@@ -236,8 +314,8 @@ var source_exports = /* @__PURE__ */ __exportAll({
|
|
|
236
314
|
const inject$2 = ["timer"];
|
|
237
315
|
/** 轮询周期(毫秒):纯程序层,可调密。 */
|
|
238
316
|
const POLL_MS = 18e5;
|
|
239
|
-
const triggerDir$1 = () => `${
|
|
240
|
-
const snapshotFile = () => `${
|
|
317
|
+
const triggerDir$1 = () => `${vaultPathForHost()}/90-Meta/triggers`;
|
|
318
|
+
const snapshotFile = () => `${vaultPathForHost()}/90-Meta/mock-data/last-snapshot.json`;
|
|
241
319
|
/** trigger 文件 TTL:超过即视为已消费/过期,轮询成功路径顺带清理。 */
|
|
242
320
|
const TRIGGER_TTL_MS = 6048e5;
|
|
243
321
|
const CREDENTIAL_ALERT_COOLDOWN_MS = 36e5;
|
|
@@ -389,7 +467,7 @@ var worker_watchdog_exports = /* @__PURE__ */ __exportAll({
|
|
|
389
467
|
});
|
|
390
468
|
const name$1 = "worker-watchdog";
|
|
391
469
|
const inject$1 = ["timer"];
|
|
392
|
-
const triggerDir = () => `${
|
|
470
|
+
const triggerDir = () => `${vaultPathForHost()}/90-Meta/triggers`;
|
|
393
471
|
/** 默认停滞阈值 15 分钟:长思考/长命令完成后即重置计时,真卡死撑不到这么久。 */
|
|
394
472
|
const DEFAULT_STALL_MS = 9e5;
|
|
395
473
|
/** 巡检间隔 30 秒。 */
|
package/lib/router-tools.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
// src/vault.ts
|
|
7
7
|
/**
|
|
@@ -191,33 +191,78 @@ async function dingSelf(userId, content, type = "app") {
|
|
|
191
191
|
*
|
|
192
192
|
* 解析优先级(vaultPath() 每次调用重读,运行期变更即时生效):
|
|
193
193
|
* ① 环境变量 SUN_VAULT(启动命令注入,随进程生效)
|
|
194
|
-
* ② ~/.dsh/state/sun-space/config.json 的 vault
|
|
195
|
-
* ③
|
|
196
|
-
*
|
|
197
|
-
*
|
|
194
|
+
* ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写/补建指令写入,持久化,不入库)
|
|
195
|
+
* ③ 会话工作目录(调用方从 exec 提取传入,须过 cwdHasSkeleton 门控——目录已含 90-Meta
|
|
196
|
+
* 骨架才算;"空目录"不算:DSH 默认 boot 桶 default-workspace 全新时也是空的,无法与
|
|
197
|
+
* "用户新建目录当知识库"区分,空目录意图在会话创建时由 registerVaultRootFromSession 落到 ④)
|
|
198
|
+
* ④ state.json 的 workspaces 映射(分桶登记:workspaces[<会话/实例 cwd>] = vault。
|
|
199
|
+
* DSH 会话按实例 boot cwd 分桶,一桶一 vault——多实例并存互不污染:
|
|
200
|
+
* homevault 实例登记 homevault,主实例解析不受影响)
|
|
201
|
+
* ⑤ state.json 的 vault 字段(全局兜底;只承载 ①② 显式值的镜像,见 publishVaultState)
|
|
202
|
+
* ⑥ 默认约定 ~/Workspace/sun-vault(仅全新裸环境兜底)
|
|
198
203
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* 手工配置。①② 供自定义/覆盖;⑤ 仅在全新裸环境兜底。
|
|
204
|
+
* 无会话上下文的调用方(source/worker-watchdog):用 vaultPathForHost()——以实例进程
|
|
205
|
+
* cwd(= 本桶 boot 目录)查 ④ 映射,与本实例会话解析一致。
|
|
202
206
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
207
|
+
* publishVaultState 纪律(v0.3.2 根因修复):落盘值仅限显式来源(①②⑤),⑥ 兜底值不写——
|
|
208
|
+
* 落了会把默认路径固化成登记值,堵死 ③④ 级(v0.3.1 及之前每次插件启动即写入,新开工作区
|
|
209
|
+
* 时"识别到的知识库"永远是 ~/Workspace/sun-vault 的幽灵)。
|
|
210
|
+
*
|
|
211
|
+
* registerVaultRootFromSession(首个 sun-router 会话创建时调用):仅对 vault 候选目录
|
|
212
|
+
* (空目录或已含 90-Meta)登记 workspaces[cwd]=cwd,不写全局 vault 字段——全局槽一旦
|
|
213
|
+
* 写入首个工作区,会遮蔽其他实例/桶的解析(多 vault 场景交叉污染)。
|
|
205
214
|
*
|
|
206
215
|
* 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(session-superagent 同款)。
|
|
207
|
-
* 本文件不携带任何个人路径;个人 vault
|
|
216
|
+
* 本文件不携带任何个人路径;个人 vault 位置只存在于①②⑤(均不入库)。
|
|
208
217
|
*/
|
|
209
218
|
const DEFAULT_VAULT_DIRNAME = "sun-vault";
|
|
210
219
|
/** 插件自有状态目录(<DSH_HOME 或 ~/>/.dsh/state/sun-space/),沿用官方 state/<插件名> 约定。
|
|
211
220
|
* 跟随 DSH_HOME:多实例(隔离 HOME 的测试环境)之间状态互不串扰。 */
|
|
212
221
|
const stateDir = () => join(process.env.DSH_HOME ?? homedir(), ".dsh", "state", "sun-space");
|
|
213
|
-
|
|
214
|
-
|
|
222
|
+
function parseState(stateJson) {
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(stateJson ?? "");
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** 目录是否已含 vault 骨架(③ 级门控:已初始化的工作区目录就是 vault)。 */
|
|
230
|
+
function cwdHasSkeleton(cwd) {
|
|
231
|
+
if (!cwd || !cwd.startsWith("/")) return false;
|
|
232
|
+
try {
|
|
233
|
+
return existsSync(join(cwd, "90-Meta"));
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** 目录是否为 vault 候选(登记门控):空目录(非隐藏项为 0,git init 过的也算)
|
|
239
|
+
* 或已含 90-Meta 骨架。有内容而无骨架的目录(代码仓库根等)不是候选。 */
|
|
240
|
+
function cwdIsVaultCandidate(cwd) {
|
|
241
|
+
if (!cwd || !cwd.startsWith("/")) return false;
|
|
242
|
+
try {
|
|
243
|
+
if (existsSync(join(cwd, "90-Meta"))) return true;
|
|
244
|
+
return readdirSync(cwd).every((name) => name.startsWith("."));
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** 纯函数:六级优先解析(测试与 vaultPath 共用)。
|
|
250
|
+
* sessionCwd 须由调用方先过 cwdHasSkeleton 门控;bucketCwd 为分桶目录(④ 映射查询用,
|
|
251
|
+
* 缺省同 sessionCwd)。 */
|
|
252
|
+
function computeVaultPath(envValue, configJson, stateJson, sessionCwd, home = homedir(), bucketCwd) {
|
|
215
253
|
if (envValue && envValue.trim() !== "") return envValue;
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
if (typeof
|
|
254
|
+
try {
|
|
255
|
+
const cfg = JSON.parse(configJson ?? "");
|
|
256
|
+
if (typeof cfg?.vault === "string" && cfg.vault.startsWith("/")) return cfg.vault;
|
|
219
257
|
} catch {}
|
|
258
|
+
const state = parseState(stateJson);
|
|
220
259
|
if (sessionCwd && sessionCwd.startsWith("/")) return sessionCwd;
|
|
260
|
+
const bucket = bucketCwd ?? sessionCwd;
|
|
261
|
+
if (bucket && bucket.startsWith("/")) {
|
|
262
|
+
const mapped = state?.workspaces?.[bucket];
|
|
263
|
+
if (typeof mapped === "string" && mapped.startsWith("/")) return mapped;
|
|
264
|
+
}
|
|
265
|
+
if (typeof state?.vault === "string" && state.vault.startsWith("/")) return state.vault;
|
|
221
266
|
return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
|
|
222
267
|
}
|
|
223
268
|
let cached = {
|
|
@@ -225,9 +270,7 @@ let cached = {
|
|
|
225
270
|
key: "",
|
|
226
271
|
value: ""
|
|
227
272
|
};
|
|
228
|
-
|
|
229
|
-
* sessionCwd:调用方从 exec 提取的会话工作目录(可选)——用户在哪个工作区开会话,vault 就解析到哪。 */
|
|
230
|
-
function vaultPath(sessionCwd, now = Date.now()) {
|
|
273
|
+
function resolve$1(sessionCwd, now) {
|
|
231
274
|
const dir = stateDir();
|
|
232
275
|
let configJson;
|
|
233
276
|
let stateJson;
|
|
@@ -237,9 +280,11 @@ function vaultPath(sessionCwd, now = Date.now()) {
|
|
|
237
280
|
try {
|
|
238
281
|
stateJson = readFileSync(join(dir, "state.json"), "utf8");
|
|
239
282
|
} catch {}
|
|
240
|
-
const
|
|
283
|
+
const cwdSkeleton = cwdHasSkeleton(sessionCwd) ? sessionCwd : void 0;
|
|
284
|
+
const bucket = sessionCwd && sessionCwd.startsWith("/") ? sessionCwd : void 0;
|
|
285
|
+
const key = `${cwdSkeleton ?? ""}|${bucket ?? ""}|${configJson ?? ""}|${stateJson ?? ""}`;
|
|
241
286
|
if (now - cached.at < 6e4 && cached.key === key) return cached.value;
|
|
242
|
-
const value = computeVaultPath(process.env.SUN_VAULT, configJson, stateJson,
|
|
287
|
+
const value = computeVaultPath(process.env.SUN_VAULT, configJson, stateJson, cwdSkeleton, homedir(), bucket);
|
|
243
288
|
cached = {
|
|
244
289
|
at: now,
|
|
245
290
|
key,
|
|
@@ -247,7 +292,61 @@ function vaultPath(sessionCwd, now = Date.now()) {
|
|
|
247
292
|
};
|
|
248
293
|
return value;
|
|
249
294
|
}
|
|
295
|
+
/** 运行期解析:每次调用重读状态目录(60s 微缓存防抖),登记/手改后即时生效。
|
|
296
|
+
* sessionCwd:调用方从 exec 提取的会话工作目录(可选)——本桶已初始化(含 90-Meta)
|
|
297
|
+
* 或已在 workspaces 映射登记时,vault 就解析到哪。 */
|
|
298
|
+
function vaultPath(sessionCwd, now = Date.now()) {
|
|
299
|
+
return resolve$1(sessionCwd, now);
|
|
300
|
+
}
|
|
250
301
|
vaultPath();
|
|
302
|
+
/** 状态目录诊断快照(sun_state_read 用):config/state 解析值 + 调用方视角的实际解析结果。
|
|
303
|
+
* "登记了什么 vs 实际生效什么"一眼可比(晨报自检/vault 解析诊断)。只读,不触发写回。 */
|
|
304
|
+
function readStateSnapshot(sessionCwd, now = Date.now()) {
|
|
305
|
+
const dir = stateDir();
|
|
306
|
+
let config = null;
|
|
307
|
+
let state = null;
|
|
308
|
+
try {
|
|
309
|
+
config = JSON.parse(readFileSync(join(dir, "config.json"), "utf8"));
|
|
310
|
+
} catch {}
|
|
311
|
+
try {
|
|
312
|
+
state = JSON.parse(readFileSync(join(dir, "state.json"), "utf8"));
|
|
313
|
+
} catch {}
|
|
314
|
+
return {
|
|
315
|
+
config,
|
|
316
|
+
state,
|
|
317
|
+
resolvedVault: vaultPath(sessionCwd, now)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Router 会话创建时登记 vault 根("当前目录作为 vault"语义的落点)。
|
|
322
|
+
* 仅对候选目录(空目录或已含 90-Meta 骨架)登记 workspaces[cwd]=cwd——分桶登记,
|
|
323
|
+
* 不写全局 vault 字段(防多实例交叉污染);env 钉死时不登记(尊重显式覆盖)。
|
|
324
|
+
* 同桶幂等;尽力而为:任何失败静默退场(最坏是回退到约定路径)。
|
|
325
|
+
*/
|
|
326
|
+
function registerVaultRootFromSession(cwd, now = /* @__PURE__ */ new Date()) {
|
|
327
|
+
if (!cwdIsVaultCandidate(cwd)) return false;
|
|
328
|
+
if (process.env.SUN_VAULT?.trim()) return false;
|
|
329
|
+
try {
|
|
330
|
+
const dir = stateDir();
|
|
331
|
+
mkdirSync(dir, { recursive: true });
|
|
332
|
+
const p = join(dir, "state.json");
|
|
333
|
+
let raw;
|
|
334
|
+
try {
|
|
335
|
+
raw = readFileSync(p, "utf8");
|
|
336
|
+
} catch {}
|
|
337
|
+
const state = parseState(raw) ?? {};
|
|
338
|
+
if (state.workspaces?.[cwd]) return false;
|
|
339
|
+
state.workspaces = {
|
|
340
|
+
...state.workspaces ?? {},
|
|
341
|
+
[cwd]: cwd
|
|
342
|
+
};
|
|
343
|
+
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")}`;
|
|
344
|
+
writeFileSync(p, JSON.stringify(state, null, 2), "utf8");
|
|
345
|
+
return true;
|
|
346
|
+
} catch {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
251
350
|
// src/router-tools.ts
|
|
252
351
|
const inject = ["tools", "subagents"];
|
|
253
352
|
/** 从工具执行上下文提取会话工作目录(用户在哪个工作区开会话,vault 就解析到哪)。防御式:形状未知时返回 undefined。 */
|
|
@@ -459,7 +558,8 @@ function apply(ctx) {
|
|
|
459
558
|
required: ["path", "content"]
|
|
460
559
|
}, async (args, exec) => {
|
|
461
560
|
try {
|
|
462
|
-
const
|
|
561
|
+
const root = vaultPath(sessionCwdOf(exec));
|
|
562
|
+
const p = resolveSafe(root, args.path);
|
|
463
563
|
await mkdir(join(p, ".."), { recursive: true });
|
|
464
564
|
if (args.mode === "append") await appendFile(p, args.content, "utf8");
|
|
465
565
|
else await writeFile(p, args.content, "utf8");
|
|
@@ -467,6 +567,7 @@ function apply(ctx) {
|
|
|
467
567
|
ok: true,
|
|
468
568
|
data: {
|
|
469
569
|
path: args.path,
|
|
570
|
+
vault: root,
|
|
470
571
|
bytes: Buffer.byteLength(args.content)
|
|
471
572
|
}
|
|
472
573
|
};
|
|
@@ -576,6 +677,66 @@ function apply(ctx) {
|
|
|
576
677
|
};
|
|
577
678
|
}
|
|
578
679
|
}));
|
|
680
|
+
ctx.tools.register(def("sun_state_read", "读取插件状态目录(~/.dsh/state/sun-space/)的登记与解析视图:config(用户手写 vault 覆盖)、state(workspaces 分桶登记 + 全局 vault 镜像)、resolvedVault(本会话实际生效的知识库根)。vault 解析诊断 / 晨报自检\"登记 vs 生效\"比对用。", {
|
|
681
|
+
type: "object",
|
|
682
|
+
properties: {}
|
|
683
|
+
}, async (_args, exec) => {
|
|
684
|
+
try {
|
|
685
|
+
return {
|
|
686
|
+
ok: true,
|
|
687
|
+
data: readStateSnapshot(sessionCwdOf(exec))
|
|
688
|
+
};
|
|
689
|
+
} catch (e) {
|
|
690
|
+
return {
|
|
691
|
+
ok: false,
|
|
692
|
+
note: String(e.message).slice(0, 200)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}));
|
|
696
|
+
ctx.tools.register(def("sun_state_write", "把当前会话工作目录登记为本工作区的 vault 根(state.json 的 workspaces 分桶映射——一桶一 vault,不影响其他工作区/实例;仅空目录或已含 90-Meta 骨架的目录可登记)。零审批。用于:插件升级前创建的旧会话迁移、登记缺失自愈。值由系统从会话上下文派生,只接受 vault:true,不接受路径参数。", {
|
|
697
|
+
type: "object",
|
|
698
|
+
properties: { vault: {
|
|
699
|
+
type: "boolean",
|
|
700
|
+
description: "登记本会话工作目录为 vault 根(唯一合法值 true)"
|
|
701
|
+
} },
|
|
702
|
+
required: ["vault"]
|
|
703
|
+
}, async (args, exec) => {
|
|
704
|
+
if (args.vault !== true) return {
|
|
705
|
+
ok: false,
|
|
706
|
+
note: "仅接受 vault:true——登记值由系统从会话上下文派生,不接受手工传路径"
|
|
707
|
+
};
|
|
708
|
+
const cwd = sessionCwdOf(exec);
|
|
709
|
+
if (!cwd) return {
|
|
710
|
+
ok: false,
|
|
711
|
+
note: "无法从会话上下文提取工作目录(绝对路径)"
|
|
712
|
+
};
|
|
713
|
+
if (process.env.SUN_VAULT?.trim()) return {
|
|
714
|
+
ok: false,
|
|
715
|
+
note: `SUN_VAULT 已显式钉死(${process.env.SUN_VAULT}),不登记——尊重显式覆盖`
|
|
716
|
+
};
|
|
717
|
+
if (!cwdIsVaultCandidate(cwd)) return {
|
|
718
|
+
ok: false,
|
|
719
|
+
note: `${cwd} 不是 vault 候选(须为空目录或已含 90-Meta 骨架)——有内容而无骨架的工作区目录不登记`
|
|
720
|
+
};
|
|
721
|
+
try {
|
|
722
|
+
const changed = registerVaultRootFromSession(cwd);
|
|
723
|
+
const snap = readStateSnapshot(cwd);
|
|
724
|
+
return {
|
|
725
|
+
ok: true,
|
|
726
|
+
data: {
|
|
727
|
+
bucket: cwd,
|
|
728
|
+
registered: snap.state?.workspaces?.[cwd] ?? null,
|
|
729
|
+
changed,
|
|
730
|
+
resolvedVault: snap.resolvedVault
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
} catch (e) {
|
|
734
|
+
return {
|
|
735
|
+
ok: false,
|
|
736
|
+
note: String(e.message).slice(0, 200)
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
}));
|
|
579
740
|
ctx.tools.register(def("sun_route", "创建子 Worker 会话并分派任务(task_id + prompt,可选 cwd)。子会话以 task_id 命名、其会话 id 自动回写任务笔记 session 字段(工作台卡片可直达)。cwd 用于跨目录任务(如代码开发,传仓库根的绝对路径)——沙箱可写区自动跟随会话 cwd;产物落库仍走 sun_artifact_write(vault 绝对路径,不受 cwd 影响)。name 冲突时自动改用系统命名。", {
|
|
580
741
|
type: "object",
|
|
581
742
|
properties: {
|
|
@@ -639,6 +800,6 @@ function apply(ctx) {
|
|
|
639
800
|
child_id: activation.childId ?? null
|
|
640
801
|
};
|
|
641
802
|
}));
|
|
642
|
-
ctx.logger?.info?.("[sun-space] router tools registered: sun_read sun_vault_write sun_vault_meta sun_notify sun_route(subagents)");
|
|
803
|
+
ctx.logger?.info?.("[sun-space] router tools registered: sun_read sun_vault_write sun_vault_meta sun_notify sun_state_read sun_state_write sun_route(subagents)");
|
|
643
804
|
}
|
|
644
805
|
export { apply, inject, lossless };
|
package/lib/worker-vault.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
// src/vault.ts
|
|
6
6
|
/**
|
|
@@ -34,33 +34,67 @@ function resolveSafe(vaultRoot, rel) {
|
|
|
34
34
|
*
|
|
35
35
|
* 解析优先级(vaultPath() 每次调用重读,运行期变更即时生效):
|
|
36
36
|
* ① 环境变量 SUN_VAULT(启动命令注入,随进程生效)
|
|
37
|
-
* ② ~/.dsh/state/sun-space/config.json 的 vault
|
|
38
|
-
* ③
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* ② ~/.dsh/state/sun-space/config.json 的 vault 字段(用户手写/补建指令写入,持久化,不入库)
|
|
38
|
+
* ③ 会话工作目录(调用方从 exec 提取传入,须过 cwdHasSkeleton 门控——目录已含 90-Meta
|
|
39
|
+
* 骨架才算;"空目录"不算:DSH 默认 boot 桶 default-workspace 全新时也是空的,无法与
|
|
40
|
+
* "用户新建目录当知识库"区分,空目录意图在会话创建时由 registerVaultRootFromSession 落到 ④)
|
|
41
|
+
* ④ state.json 的 workspaces 映射(分桶登记:workspaces[<会话/实例 cwd>] = vault。
|
|
42
|
+
* DSH 会话按实例 boot cwd 分桶,一桶一 vault——多实例并存互不污染:
|
|
43
|
+
* homevault 实例登记 homevault,主实例解析不受影响)
|
|
44
|
+
* ⑤ state.json 的 vault 字段(全局兜底;只承载 ①② 显式值的镜像,见 publishVaultState)
|
|
45
|
+
* ⑥ 默认约定 ~/Workspace/sun-vault(仅全新裸环境兜底)
|
|
41
46
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* 手工配置。①② 供自定义/覆盖;⑤ 仅在全新裸环境兜底。
|
|
47
|
+
* 无会话上下文的调用方(source/worker-watchdog):用 vaultPathForHost()——以实例进程
|
|
48
|
+
* cwd(= 本桶 boot 目录)查 ④ 映射,与本实例会话解析一致。
|
|
45
49
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
50
|
+
* publishVaultState 纪律(v0.3.2 根因修复):落盘值仅限显式来源(①②⑤),⑥ 兜底值不写——
|
|
51
|
+
* 落了会把默认路径固化成登记值,堵死 ③④ 级(v0.3.1 及之前每次插件启动即写入,新开工作区
|
|
52
|
+
* 时"识别到的知识库"永远是 ~/Workspace/sun-vault 的幽灵)。
|
|
53
|
+
*
|
|
54
|
+
* registerVaultRootFromSession(首个 sun-router 会话创建时调用):仅对 vault 候选目录
|
|
55
|
+
* (空目录或已含 90-Meta)登记 workspaces[cwd]=cwd,不写全局 vault 字段——全局槽一旦
|
|
56
|
+
* 写入首个工作区,会遮蔽其他实例/桶的解析(多 vault 场景交叉污染)。
|
|
48
57
|
*
|
|
49
58
|
* 目录约定沿用 DSH 官方先例:~/.dsh/state/<插件名>/(session-superagent 同款)。
|
|
50
|
-
* 本文件不携带任何个人路径;个人 vault
|
|
59
|
+
* 本文件不携带任何个人路径;个人 vault 位置只存在于①②⑤(均不入库)。
|
|
51
60
|
*/
|
|
52
61
|
const DEFAULT_VAULT_DIRNAME = "sun-vault";
|
|
53
62
|
/** 插件自有状态目录(<DSH_HOME 或 ~/>/.dsh/state/sun-space/),沿用官方 state/<插件名> 约定。
|
|
54
63
|
* 跟随 DSH_HOME:多实例(隔离 HOME 的测试环境)之间状态互不串扰。 */
|
|
55
64
|
const stateDir = () => join(process.env.DSH_HOME ?? homedir(), ".dsh", "state", "sun-space");
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
function parseState(stateJson) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(stateJson ?? "");
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** 目录是否已含 vault 骨架(③ 级门控:已初始化的工作区目录就是 vault)。 */
|
|
73
|
+
function cwdHasSkeleton(cwd) {
|
|
74
|
+
if (!cwd || !cwd.startsWith("/")) return false;
|
|
75
|
+
try {
|
|
76
|
+
return existsSync(join(cwd, "90-Meta"));
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** 纯函数:六级优先解析(测试与 vaultPath 共用)。
|
|
82
|
+
* sessionCwd 须由调用方先过 cwdHasSkeleton 门控;bucketCwd 为分桶目录(④ 映射查询用,
|
|
83
|
+
* 缺省同 sessionCwd)。 */
|
|
84
|
+
function computeVaultPath(envValue, configJson, stateJson, sessionCwd, home = homedir(), bucketCwd) {
|
|
58
85
|
if (envValue && envValue.trim() !== "") return envValue;
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
if (typeof
|
|
86
|
+
try {
|
|
87
|
+
const cfg = JSON.parse(configJson ?? "");
|
|
88
|
+
if (typeof cfg?.vault === "string" && cfg.vault.startsWith("/")) return cfg.vault;
|
|
62
89
|
} catch {}
|
|
90
|
+
const state = parseState(stateJson);
|
|
63
91
|
if (sessionCwd && sessionCwd.startsWith("/")) return sessionCwd;
|
|
92
|
+
const bucket = bucketCwd ?? sessionCwd;
|
|
93
|
+
if (bucket && bucket.startsWith("/")) {
|
|
94
|
+
const mapped = state?.workspaces?.[bucket];
|
|
95
|
+
if (typeof mapped === "string" && mapped.startsWith("/")) return mapped;
|
|
96
|
+
}
|
|
97
|
+
if (typeof state?.vault === "string" && state.vault.startsWith("/")) return state.vault;
|
|
64
98
|
return join(home, "Workspace", DEFAULT_VAULT_DIRNAME);
|
|
65
99
|
}
|
|
66
100
|
let cached = {
|
|
@@ -68,9 +102,7 @@ let cached = {
|
|
|
68
102
|
key: "",
|
|
69
103
|
value: ""
|
|
70
104
|
};
|
|
71
|
-
|
|
72
|
-
* sessionCwd:调用方从 exec 提取的会话工作目录(可选)——用户在哪个工作区开会话,vault 就解析到哪。 */
|
|
73
|
-
function vaultPath(sessionCwd, now = Date.now()) {
|
|
105
|
+
function resolve$1(sessionCwd, now) {
|
|
74
106
|
const dir = stateDir();
|
|
75
107
|
let configJson;
|
|
76
108
|
let stateJson;
|
|
@@ -80,9 +112,11 @@ function vaultPath(sessionCwd, now = Date.now()) {
|
|
|
80
112
|
try {
|
|
81
113
|
stateJson = readFileSync(join(dir, "state.json"), "utf8");
|
|
82
114
|
} catch {}
|
|
83
|
-
const
|
|
115
|
+
const cwdSkeleton = cwdHasSkeleton(sessionCwd) ? sessionCwd : void 0;
|
|
116
|
+
const bucket = sessionCwd && sessionCwd.startsWith("/") ? sessionCwd : void 0;
|
|
117
|
+
const key = `${cwdSkeleton ?? ""}|${bucket ?? ""}|${configJson ?? ""}|${stateJson ?? ""}`;
|
|
84
118
|
if (now - cached.at < 6e4 && cached.key === key) return cached.value;
|
|
85
|
-
const value = computeVaultPath(process.env.SUN_VAULT, configJson, stateJson,
|
|
119
|
+
const value = computeVaultPath(process.env.SUN_VAULT, configJson, stateJson, cwdSkeleton, homedir(), bucket);
|
|
86
120
|
cached = {
|
|
87
121
|
at: now,
|
|
88
122
|
key,
|
|
@@ -90,6 +124,12 @@ function vaultPath(sessionCwd, now = Date.now()) {
|
|
|
90
124
|
};
|
|
91
125
|
return value;
|
|
92
126
|
}
|
|
127
|
+
/** 运行期解析:每次调用重读状态目录(60s 微缓存防抖),登记/手改后即时生效。
|
|
128
|
+
* sessionCwd:调用方从 exec 提取的会话工作目录(可选)——本桶已初始化(含 90-Meta)
|
|
129
|
+
* 或已在 workspaces 映射登记时,vault 就解析到哪。 */
|
|
130
|
+
function vaultPath(sessionCwd, now = Date.now()) {
|
|
131
|
+
return resolve$1(sessionCwd, now);
|
|
132
|
+
}
|
|
93
133
|
vaultPath();
|
|
94
134
|
// src/worker-vault.ts
|
|
95
135
|
const inject = ["tools"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sun-space",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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
|
@@ -20,9 +20,10 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
const VAULT_DIRNAME = 'sun-vault' // vault 目录名约定;绝对路径动态发现,见 resolveVault
|
|
21
21
|
// 架构 §4.1 不变量:呈现层不得持有会话 id / vault 绝对路径常量(Router 会话会轮换,机器会换)。
|
|
22
22
|
// 会话解析链:session/list(sun-router 会话)→ 90-Meta/system/state.json 裁决 → updatedAt 最新兜底。
|
|
23
|
-
// vault
|
|
23
|
+
// vault 解析链(server 端 vault-path.ts 为唯一事实源):config/分桶映射(workspaces[本工作区])→
|
|
24
|
+
// 约定布局发现;当前工作区无骨架时引导以工作区为根初始化(服务端登记仅放行空目录/已含骨架)。
|
|
24
25
|
// 写回:① 创建会话后经 session/prompt 引导自登记(含 vault 字段);② 晨报自检发现缺失/不一致自动修正。
|
|
25
|
-
const WB_REV = '
|
|
26
|
+
const WB_REV = 'v20' // 根节点 data-wb-rev 版本标记:判别浏览器是否吃到旧 client.js(immutable 缓存坑,HANDOFF 踩坑 1)
|
|
26
27
|
|
|
27
28
|
const STATUS_META = {
|
|
28
29
|
proposed: { label: '待确认派发', color: '#9e9e9e' },
|
|
@@ -97,12 +98,18 @@ window.__ModuleLoader__.load({
|
|
|
97
98
|
|
|
98
99
|
// ===== vault 绝对路径发现(跨机器;呈现层不持常量,架构 §4.1 同款不变量)=====
|
|
99
100
|
// 链:会话 cwd 推 HOME → 约定布局 <HOME>/Workspace/sun-vault 探测 state.json → state.json.vault 字段裁决。
|
|
100
|
-
// state.json
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
// state.json 由 Router 自登记/晨报自检维护(server 端解析链是唯一事实源);
|
|
102
|
+
// workspaces 分桶映射优先(一桶一 vault,多实例互不污染,与 server ④ 级一致)。
|
|
103
|
+
// items 可选:调用方已取过 session/list 时复用(免重复 RPC)。
|
|
104
|
+
async function resolveVault(itemsIn, currentSessionId) {
|
|
105
|
+
let items = itemsIn
|
|
106
|
+
if (!items) {
|
|
107
|
+
const v = await gw('session/list', {})
|
|
108
|
+
items = Array.isArray(v?.items) ? v.items : Object.values(v?.items ?? v ?? {})
|
|
109
|
+
}
|
|
104
110
|
const scope = items.find(s => s?.projections?.values?.agentPreset === 'sun-router')?.sessionId ?? items[0]?.sessionId
|
|
105
111
|
if (!scope) return null
|
|
112
|
+
const myCwd = currentSessionId ? String((items.find(s => s.sessionId === currentSessionId) ?? {}).cwd ?? '') : ''
|
|
106
113
|
// ① 插件状态目录(HOME 从会话 cwd 推导,绝对路径读取):
|
|
107
114
|
// config.json 优先(运行期事实源——引导初始化/用户手写即时生效),state.json 兜底(启动期快照)
|
|
108
115
|
const homes = [...new Set(items.map(s => String(s.cwd || '')).filter(c => c.startsWith('/')).map(c => c.split('/').slice(0, 3).join('/')))]
|
|
@@ -111,6 +118,8 @@ window.__ModuleLoader__.load({
|
|
|
111
118
|
try {
|
|
112
119
|
const raw = await readVaultFile(scope, `${home}/.dsh/state/sun-space/${file}`, 8)
|
|
113
120
|
const st = JSON.parse(raw)
|
|
121
|
+
const mapped = myCwd.startsWith('/') ? st?.workspaces?.[myCwd] : undefined // 分桶登记(本工作区)
|
|
122
|
+
if (typeof mapped === 'string' && mapped.startsWith('/')) return mapped
|
|
114
123
|
if (st?.vault && st.vault.startsWith('/')) return st.vault
|
|
115
124
|
} catch { /* 该文件不存在 → 下一个 */ }
|
|
116
125
|
}
|
|
@@ -163,15 +172,31 @@ window.__ModuleLoader__.load({
|
|
|
163
172
|
// ===== 初始化检查(在任何会话的 tab 上都能跑;① 用当前会话作 workspaceFiles scope)=====
|
|
164
173
|
async function runInitChecks(currentSessionId) {
|
|
165
174
|
const c = { vault: false, presets: false, routerSession: null, vaultPath: null, currentCwd: null, notes: [] }
|
|
175
|
+
// 会话列表只取一次:currentCwd 供 ① 跨工作区提示与一键初始化,router 解析复用
|
|
176
|
+
let items = null
|
|
177
|
+
try {
|
|
178
|
+
const sl = await gw('session/list', {})
|
|
179
|
+
items = Array.isArray(sl?.items) ? sl.items : Object.values(sl?.items ?? {})
|
|
180
|
+
c.currentCwd = (items.find(s => s.sessionId === currentSessionId) ?? {}).cwd ?? null
|
|
181
|
+
} catch { c.notes.push('会话列表不可达 → 重试或检查实例') }
|
|
166
182
|
// ① 知识库:vault 路径动态发现 + runbook 探测
|
|
167
|
-
const vault = await resolveVault()
|
|
183
|
+
const vault = await resolveVault(items, currentSessionId)
|
|
168
184
|
c.vaultPath = vault
|
|
169
185
|
if (vault && currentSessionId) {
|
|
170
186
|
try { await readVaultFile(currentSessionId, `${vault}/90-Meta/runbook.md`, 10); c.vault = true } catch {
|
|
171
187
|
c.notes.push(`知识库未就绪(已定位 ${vault},缺 90-Meta 骨架):① 用 Obsidian 打开该目录 ② 可让任意 Router 会话执行"初始化知识库"指令 ③ 重新检查`)
|
|
172
188
|
}
|
|
189
|
+
// 跨工作区提示:识别到的 vault 在别处、当前工作区无骨架——一键初始化将以当前工作区为根
|
|
190
|
+
//(服务端门控:仅空目录或已含 90-Meta 的工作区目录会被当 vault 登记/解析,见 vault-path.ts)
|
|
191
|
+
if (c.currentCwd && c.currentCwd !== vault) {
|
|
192
|
+
try { await readVaultFile(currentSessionId, `${c.currentCwd}/90-Meta/runbook.md`, 2) } catch {
|
|
193
|
+
c.notes.push(`知识库识别在 ${vault};当前工作区 ${c.currentCwd} 尚无骨架——【创建 Router 会话】将以当前工作区为知识库根初始化;若要沿用 ${vault},请到它所在的工作区开会话`)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
173
196
|
} else if (!vault) {
|
|
174
|
-
c.notes.push(
|
|
197
|
+
c.notes.push(c.currentCwd
|
|
198
|
+
? `未找到既有知识库——当前工作区 ${c.currentCwd} 将作为知识库根(待初始化):完成下方初始化引导即可`
|
|
199
|
+
: `未找到知识库目录:按约定应在 <用户目录>/Workspace/${VAULT_DIRNAME}(含 90-Meta/system/state.json);自定义位置需由 Router 把绝对路径写进 state.json 的 vault 字段`)
|
|
175
200
|
} else {
|
|
176
201
|
c.notes.push('知识库检查需要会话作读取通道 → 先创建会话')
|
|
177
202
|
}
|
|
@@ -188,11 +213,12 @@ window.__ModuleLoader__.load({
|
|
|
188
213
|
c.notes.push('预设服务不可达 → 确认 Harness 实例在线、插件已启用')
|
|
189
214
|
}
|
|
190
215
|
// ③ Router 会话:动态解析 + 首个 sun-router 会话创建时自动登记其 cwd 为 vault 根
|
|
191
|
-
// ("当前目录作为 vault"
|
|
216
|
+
// ("当前目录作为 vault"语义的插件层落点;门控在服务端——空目录/已含骨架才登记)
|
|
192
217
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
218
|
+
if (!items) { // 顶部取失败时兜底重取
|
|
219
|
+
const sl = await gw('session/list', {})
|
|
220
|
+
items = Array.isArray(sl?.items) ? sl.items : Object.values(sl?.items ?? {})
|
|
221
|
+
}
|
|
196
222
|
// vault 根登记由宿主插件在 sun-router 会话创建时自动完成(index.ts agent/created 钩子)
|
|
197
223
|
c.routerSession = vault ? await resolveRouterSession(vault, items) : null
|
|
198
224
|
if (!c.routerSession) c.notes.push('Router 会话不存在 → 点下方「创建 Router 会话」(sun-router 预设,随当前工作区),创建后自动登记 state.json')
|
|
@@ -277,13 +303,14 @@ window.__ModuleLoader__.load({
|
|
|
277
303
|
requestId: 'sun-init-' + Date.now(),
|
|
278
304
|
sessionId: sid,
|
|
279
305
|
mode: 'queue',
|
|
280
|
-
content: [{ type: 'text', text: `【孙学空间初始化】你是新环境的 Sun Router(你的会话 id:${sid}
|
|
281
|
-
1) 用
|
|
306
|
+
content: [{ type: 'text', text: `【孙学空间初始化】你是新环境的 Sun Router(你的会话 id:${sid})。按顺序完成:
|
|
307
|
+
1) 用 sun_state_write 确认/补全本工作区的 vault 根登记(参数 {"vault": true}——会话创建时已自动登记,此步幂等自愈;若 ok:false 按注记处理)
|
|
308
|
+
2) 用 sun_vault_write 创建知识库骨架(实际根以工具返回的 vault 字段为准,勿凭工作目录猜测):
|
|
282
309
|
- 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 不带时区后缀、结论必注出处)
|
|
283
310
|
- 90-Meta/runbook.md:初始版(# Sun 行为手册 + v0.1 版本行 + §1 角色:Router 只分派不执行 + §2 沟通规范:IM ≤100 字结论先行 + §3 打扰分级:info 攒批/ask 立即/urgent 加 DING)
|
|
284
311
|
- 90-Meta/memory.md:初始版(# 关于用户,待填充)
|
|
285
|
-
|
|
286
|
-
|
|
312
|
+
3) 用 sun_vault_write 写 90-Meta/system/state.json:{"routerSession": "${sid}", "vault": "<知识库根绝对路径——填第 2 步 sun_vault_write 返回的 vault 值>", "updatedAt": "<本地时间 YYYY-MM-DDTHH:mm>"}(自登记,供孙学空间发现与轮换机制使用)
|
|
313
|
+
4) 完成后列出生成的文件清单与知识库路径。` }],
|
|
287
314
|
clientTimeZone: 'Asia/Shanghai',
|
|
288
315
|
})
|
|
289
316
|
try { hostCtx.uiWorkspace.openSession(sid) } catch (e) { D.errors.push('openSession: ' + String(e)) }
|
|
@@ -298,12 +325,14 @@ window.__ModuleLoader__.load({
|
|
|
298
325
|
requestId: 'sun-skel-' + Date.now(),
|
|
299
326
|
sessionId: routerSession,
|
|
300
327
|
mode: 'queue',
|
|
301
|
-
content: [{ type: 'text', text:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
- 90-Meta/
|
|
305
|
-
- 90-Meta/
|
|
306
|
-
|
|
328
|
+
content: [{ type: 'text', text: `【知识库骨架补建 + vault 根登记】按顺序完成:
|
|
329
|
+
1) 用 sun_state_write 登记本工作区为 vault 根(参数 {"vault": true}——分桶登记,零审批,不影响其他工作区;值由系统从你的会话上下文派生。若 ok:false 按注记处理,禁止用 shell 手写状态目录)
|
|
330
|
+
2) 用 sun_vault_write 创建/补全知识库骨架(实际根以工具返回的 vault 字段为准,勿凭工作目录猜测):
|
|
331
|
+
- 90-Meta/schemas.md:frontmatter 契约最小集(通用字段 type/id/status/created/updated;task 的 status 枚举 proposed|running|need-confirm|done|blocked|dropped;写入纪律同 runbook)
|
|
332
|
+
- 90-Meta/runbook.md:若缺失则写初始版(# Sun 行为手册 + v0.1)
|
|
333
|
+
- 90-Meta/memory.md:若缺失则写初始版(# 关于用户)
|
|
334
|
+
- 90-Meta/system/state.json:{"routerSession": "${routerSession}", "vault": "<第 2 步 sun_vault_write 返回的 vault 值>", "updatedAt": "<本地时间 YYYY-MM-DDTHH:mm 无时区后缀>"}(已有的话合并更新 routerSession/vault 字段)
|
|
335
|
+
完成后报告清单与知识库路径。` }],
|
|
307
336
|
clientTimeZone: 'Asia/Shanghai',
|
|
308
337
|
})
|
|
309
338
|
onRetry()
|
|
@@ -421,7 +450,7 @@ window.__ModuleLoader__.load({
|
|
|
421
450
|
function OnboardPanel({ checks, currentSessionId, onRetry }) {
|
|
422
451
|
const steps = [
|
|
423
452
|
{ n: 1, ok: checks.presets, title: '确认依赖插件', desc: checks.presets ? '智能体协作 / 子智能体插件已启用' : '打开插件面板,启用 sun-space / 智能体协作相关插件后重启实例', action: checks.presets ? null : 'plugins' },
|
|
424
|
-
{ n: 2, ok: !!checks.routerSession, title: '一键初始化(Router 会话 + 知识库)', desc: checks.routerSession ? `已就绪:${checks.routerSession.slice(0, 28)}…` : `一键完成:以当前工作区目录${checks.currentCwd ? `(${checks.currentCwd})` : ''}为知识库根 → 创建 Router 会话 → 写入 90-Meta 骨架 →
|
|
453
|
+
{ n: 2, ok: !!checks.routerSession, title: '一键初始化(Router 会话 + 知识库)', desc: checks.routerSession ? `已就绪:${checks.routerSession.slice(0, 28)}…` : `一键完成:以当前工作区目录${checks.currentCwd ? `(${checks.currentCwd})` : ''}为知识库根 → 创建 Router 会话 → 写入 90-Meta 骨架 → 分桶自登记(全程零审批)`, action: checks.presets && !checks.routerSession ? 'create-router' : null },
|
|
425
454
|
{ n: 3, ok: checks.vault, title: '知识库就绪', desc: checks.vault ? `已就绪:${checks.vaultPath}` : (checks.routerSession ? '知识库骨架缺失 → 点下方按钮让 Router 补建' : '完成第②步后自动创建'), action: checks.presets && checks.routerSession && !checks.vault ? 'init-skeleton' : null },
|
|
426
455
|
]
|
|
427
456
|
const allOk = steps.every(x => x.ok)
|