dsh-sun-space 0.2.0 → 0.2.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 +205 -11
- package/package.json +9 -4
- package/src/client.js +78 -25
package/lib/index.mjs
CHANGED
|
@@ -17,6 +17,10 @@ var __exportAll = (all, no_symbols) => {
|
|
|
17
17
|
* dws 执行器:单次命令调用(无长连接)。供 sun_notify / Source 轮询使用。
|
|
18
18
|
* 输出一律 --format json;失败抛错并带 stderr 摘要。
|
|
19
19
|
*/
|
|
20
|
+
var dws_exports = /* @__PURE__ */ __exportAll({
|
|
21
|
+
currentUserId: () => currentUserId,
|
|
22
|
+
notifySelf: () => notifySelf
|
|
23
|
+
});
|
|
20
24
|
function run(args, timeoutMs = 3e4) {
|
|
21
25
|
return new Promise((resolve, reject) => {
|
|
22
26
|
execFile("dws", args, {
|
|
@@ -117,23 +121,23 @@ var source_exports = /* @__PURE__ */ __exportAll({
|
|
|
117
121
|
CREDENTIAL_ALERT_COOLDOWN_MS: () => CREDENTIAL_ALERT_COOLDOWN_MS,
|
|
118
122
|
CREDENTIAL_ALERT_STREAK: () => 2,
|
|
119
123
|
TRIGGER_TTL_MS: () => TRIGGER_TTL_MS,
|
|
120
|
-
VAULT: () => VAULT,
|
|
121
|
-
apply: () => apply$
|
|
122
|
-
inject: () => inject$
|
|
124
|
+
VAULT: () => VAULT$1,
|
|
125
|
+
apply: () => apply$2,
|
|
126
|
+
inject: () => inject$2,
|
|
123
127
|
parseTriggerTs: () => parseTriggerTs,
|
|
124
128
|
pruneTriggers: () => pruneTriggers,
|
|
125
129
|
shouldAlertCredential: () => shouldAlertCredential
|
|
126
130
|
});
|
|
127
|
-
const inject$
|
|
128
|
-
const VAULT = process.env.SUN_VAULT ?? `${process.env.HOME}/Workspace/云顶天宫`;
|
|
131
|
+
const inject$2 = ["timer"];
|
|
132
|
+
const VAULT$1 = process.env.SUN_VAULT ?? `${process.env.HOME}/Workspace/云顶天宫`;
|
|
129
133
|
/** 轮询周期(毫秒):纯程序层,可调密。 */
|
|
130
134
|
const POLL_MS = 18e5;
|
|
131
|
-
const TRIGGER_DIR = `${VAULT}/90-Meta/triggers`;
|
|
132
|
-
const SNAPSHOT_FILE = `${VAULT}/90-Meta/mock-data/last-snapshot.json`;
|
|
135
|
+
const TRIGGER_DIR$1 = `${VAULT$1}/90-Meta/triggers`;
|
|
136
|
+
const SNAPSHOT_FILE = `${VAULT$1}/90-Meta/mock-data/last-snapshot.json`;
|
|
133
137
|
/** trigger 文件 TTL:超过即视为已消费/过期,轮询成功路径顺带清理。 */
|
|
134
138
|
const TRIGGER_TTL_MS = 6048e5;
|
|
135
139
|
const CREDENTIAL_ALERT_COOLDOWN_MS = 36e5;
|
|
136
|
-
function apply$
|
|
140
|
+
function apply$2(ctx) {
|
|
137
141
|
schedulePolling(ctx);
|
|
138
142
|
}
|
|
139
143
|
/** 从 trigger 文件名(ISO mangled:冒号/点被替换为 '-')解析创建时刻;不可解析返回 null(保守不删)。 */
|
|
@@ -189,7 +193,7 @@ function schedulePolling(ctx) {
|
|
|
189
193
|
}
|
|
190
194
|
await writeFile(SNAPSHOT_FILE, JSON.stringify(snap), "utf8");
|
|
191
195
|
health.fails = 0;
|
|
192
|
-
await pruneTriggers(TRIGGER_DIR).catch(() => {});
|
|
196
|
+
await pruneTriggers(TRIGGER_DIR$1).catch(() => {});
|
|
193
197
|
} catch (e) {
|
|
194
198
|
ctx.logger?.warn?.(`[sun-space] poll failed: ${String(e.message).slice(0, 150)}`);
|
|
195
199
|
health.fails++;
|
|
@@ -226,17 +230,207 @@ async function readPrev() {
|
|
|
226
230
|
}
|
|
227
231
|
}
|
|
228
232
|
async function writeTrigger(label, payload) {
|
|
229
|
-
await mkdir(TRIGGER_DIR, { recursive: true });
|
|
233
|
+
await mkdir(TRIGGER_DIR$1, { recursive: true });
|
|
230
234
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
231
|
-
await writeFile(join(TRIGGER_DIR, `${ts}-${label}.json`), JSON.stringify({
|
|
235
|
+
await writeFile(join(TRIGGER_DIR$1, `${ts}-${label}.json`), JSON.stringify({
|
|
232
236
|
label,
|
|
233
237
|
payload
|
|
234
238
|
}, null, 2), "utf8");
|
|
235
239
|
}
|
|
240
|
+
// src/worker-watchdog.ts
|
|
241
|
+
/**
|
|
242
|
+
* worker-watchdog —— 父侧盯着 sun-worker 子代理,长期无活动就停掉它,让卡死"有声音"。
|
|
243
|
+
*
|
|
244
|
+
* ── 解决什么问题 ─────────────────────────────────────────────────────────────
|
|
245
|
+
*
|
|
246
|
+
* Router 的纪律是"派发后等 Worker 结论再推进"。这条纪律成立的前提是 Router 会被
|
|
247
|
+
* 唤醒:Worker 干完活,宿主发 settled 通知,Router 才有一轮可处理。可 Worker
|
|
248
|
+
* **静默卡死**(陷在同一个错误构造里反复推演、不调工具、不退回、不报错)时该前提
|
|
249
|
+
* 不成立——宿主没有任何子代理空闲超时,只有真正 idle 才发结算通知。于是 Router
|
|
250
|
+
* 再也不会被叫醒,任务悬在半空,界面无异常。
|
|
251
|
+
*
|
|
252
|
+
* ── 为什么是这个形态 ────────────────────────────────────────────────────────
|
|
253
|
+
*
|
|
254
|
+
* 判据只能是"多久没有活动",活动 = 该 Worker 的任何一次工具调用结束
|
|
255
|
+
* (tools/post-execute)。它挂在**全局 scope**(index.ts 顶层插件)而不是 Router
|
|
256
|
+
* preset scope:我们的 Worker 是独立 preset(非 composeFrom),preset scope 收不到
|
|
257
|
+
* 跨 preset 的子会话事件;cordis 事件冒泡保证全局 scope 全收,归属用会话头判。
|
|
258
|
+
* 停止动作选 **interrupt**(可观测的最小动作):它触发 settled 通知——Router 被
|
|
259
|
+
* 唤醒,卡死从静默变成可见;重派是 Router 的决定(它看得见报告),看门狗不越权。
|
|
260
|
+
* 另加本仓库自己的观测通道:写 trigger 队列 + urgent 通知(Interrupt 通道万一失效
|
|
261
|
+
* 也不至于全瞎,且钉进钉钉推送)。
|
|
262
|
+
*
|
|
263
|
+
* ── 判据与失败取向 ──────────────────────────────────────────────────────────
|
|
264
|
+
*
|
|
265
|
+
* 归属(顺序即取舍):会话头 origin/parentSession/delegationDepth 任一成立 =
|
|
266
|
+
* 委派子代理;且 agentPreset === 'sun-worker' 才在册。手工创建的 sun-worker 会话、
|
|
267
|
+
* Router 会话、其他插件会话一律不管——"判错方向不对称:多盯一个是日志,误停一个
|
|
268
|
+
* 别的会话是替别人做决定"。所有异常**绝不抛**:服务缺席、interrupt 拒绝、定时器
|
|
269
|
+
* 起不来,一律记日志退场——最坏是卡死仍静默,抛错的代价是把正常会话弄坏。
|
|
270
|
+
* 阈值默认 15 分钟(任何一次完成的工具调用都刷新计时;长命令完成后即重置),
|
|
271
|
+
* 可用 SUN_WATCHDOG_STALL_MS 覆盖;SUN_WATCHDOG_DISABLED=1 整体关闭。
|
|
272
|
+
*
|
|
273
|
+
* @module sun-space/worker-watchdog
|
|
274
|
+
*/
|
|
275
|
+
var worker_watchdog_exports = /* @__PURE__ */ __exportAll({
|
|
276
|
+
DEFAULT_STALL_MS: () => DEFAULT_STALL_MS,
|
|
277
|
+
DEFAULT_TICK_MS: () => DEFAULT_TICK_MS,
|
|
278
|
+
VAULT: () => VAULT,
|
|
279
|
+
apply: () => apply$1,
|
|
280
|
+
inject: () => inject$1,
|
|
281
|
+
isDelegatedChildHeader: () => isDelegatedChildHeader,
|
|
282
|
+
name: () => name$1,
|
|
283
|
+
parsePositiveMs: () => parsePositiveMs,
|
|
284
|
+
shouldWatchHeader: () => shouldWatchHeader,
|
|
285
|
+
stalledKeys: () => stalledKeys
|
|
286
|
+
});
|
|
287
|
+
const name$1 = "worker-watchdog";
|
|
288
|
+
const inject$1 = ["timer"];
|
|
289
|
+
const VAULT = process.env.SUN_VAULT ?? `${process.env.HOME}/Workspace/云顶天宫`;
|
|
290
|
+
const TRIGGER_DIR = `${VAULT}/90-Meta/triggers`;
|
|
291
|
+
/** 默认停滞阈值 15 分钟:长思考/长命令完成后即重置计时,真卡死撑不到这么久。 */
|
|
292
|
+
const DEFAULT_STALL_MS = 9e5;
|
|
293
|
+
/** 巡检间隔 30 秒。 */
|
|
294
|
+
const DEFAULT_TICK_MS = 3e4;
|
|
295
|
+
/** 解析正数毫秒配置;非法退回默认。 */
|
|
296
|
+
function parsePositiveMs(value, fallback) {
|
|
297
|
+
const n = Number(value);
|
|
298
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
299
|
+
}
|
|
300
|
+
/** 会话头是否表明这是被委派出来的子代理(三信号任一成立)。 */
|
|
301
|
+
function isDelegatedChildHeader(header) {
|
|
302
|
+
if (header === void 0 || header === null || typeof header !== "object") return false;
|
|
303
|
+
if (header.origin === "subagent") return true;
|
|
304
|
+
if (typeof header.parentSession === "string" && header.parentSession !== "") return true;
|
|
305
|
+
if (typeof header.delegationDepth === "number" && header.delegationDepth > 0) return true;
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
/** 归属判定:委派子代理 且 sun-worker 预设(手工创建的 sun-worker 会话被 origin 判据排除)。 */
|
|
309
|
+
function shouldWatchHeader(header) {
|
|
310
|
+
return isDelegatedChildHeader(header) && header.agentPreset === "sun-worker";
|
|
311
|
+
}
|
|
312
|
+
/** 纯扫描:返回停滞条目的 key(idle > timeout)。调用方负责出册与善后。 */
|
|
313
|
+
function stalledKeys(entries, now, stallMs) {
|
|
314
|
+
const out = [];
|
|
315
|
+
for (const [key, entry] of entries) if (now - entry.at > stallMs) out.push(key);
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
/** 从会话头取父会话 id(可能 undefined)。 */
|
|
319
|
+
function parentSessionOf(agent) {
|
|
320
|
+
const p = (agent?.session?.header)?.parentSession;
|
|
321
|
+
return typeof p === "string" && p !== "" ? p : void 0;
|
|
322
|
+
}
|
|
323
|
+
function sessionIdOf(agent) {
|
|
324
|
+
const id = (agent?.session)?.id ?? agent?.id;
|
|
325
|
+
return typeof id === "string" ? id : void 0;
|
|
326
|
+
}
|
|
327
|
+
function apply$1(ctx) {
|
|
328
|
+
if (process.env.SUN_WATCHDOG_DISABLED === "1") {
|
|
329
|
+
ctx.logger?.info?.("[sun-space] worker-watchdog disabled by env");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const stallMs = parsePositiveMs(process.env.SUN_WATCHDOG_STALL_MS, DEFAULT_STALL_MS);
|
|
333
|
+
const tickMs = parsePositiveMs(process.env.SUN_WATCHDOG_TICK_MS, DEFAULT_TICK_MS);
|
|
334
|
+
/** 在册表:agent 对象 → { key(id), parentId, at(最后活动时刻) }。Map 以便遍历;disposed 即删,不累积。 */
|
|
335
|
+
const watched = /* @__PURE__ */ new Map();
|
|
336
|
+
let engagedNoted = false;
|
|
337
|
+
const log = (level, msg) => {
|
|
338
|
+
try {
|
|
339
|
+
(ctx.logger?.[level] ?? ctx.logger?.info)?.call(ctx.logger, `[sun-space/watchdog] ${msg}`);
|
|
340
|
+
} catch {}
|
|
341
|
+
};
|
|
342
|
+
/** 停滞善后:先 trigger 落盘(本仓库观测通道),再 urgent 通知,再 interrupt 唤醒 Router。 */
|
|
343
|
+
const handleStall = async (entry) => {
|
|
344
|
+
const idleMin = Math.round((Date.now() - entry.at) / 6e4);
|
|
345
|
+
log("warn", `stalled: child=${entry.id} idle=${idleMin}min — interrupting & notifying`);
|
|
346
|
+
try {
|
|
347
|
+
await mkdir(TRIGGER_DIR, { recursive: true });
|
|
348
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
349
|
+
await writeFile(join(TRIGGER_DIR, `${ts}-watchdog-stall.json`), JSON.stringify({
|
|
350
|
+
label: "watchdog-stall",
|
|
351
|
+
payload: {
|
|
352
|
+
childId: entry.id,
|
|
353
|
+
idleMinutes: idleMin
|
|
354
|
+
}
|
|
355
|
+
}, null, 2), "utf8");
|
|
356
|
+
} catch {}
|
|
357
|
+
try {
|
|
358
|
+
const { currentUserId, notifySelf } = await Promise.resolve().then(() => dws_exports);
|
|
359
|
+
await notifySelf(await currentUserId(), `[Sun·urgent] Watchdog 停止了卡死 Worker:${entry.id}(无活动 ${idleMin} 分钟),请复查或重派`);
|
|
360
|
+
} catch {}
|
|
361
|
+
try {
|
|
362
|
+
const subagents = ctx.get("subagents");
|
|
363
|
+
if (!subagents || typeof subagents.interrupt !== "function") {
|
|
364
|
+
log("info", "subagents.interrupt unavailable — stall stays silent. [reason=no-interrupt-api]");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const parentAgent = entry.parentId ? ctx.get("agents")?.get?.(entry.parentId) : void 0;
|
|
368
|
+
if (!parentAgent) {
|
|
369
|
+
log("warn", `parent agent unresolvable for ${entry.id} — not interrupting (no guess). [reason=no-parent]`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
subagents.interrupt(entry.id, {
|
|
373
|
+
kind: "ancestor",
|
|
374
|
+
agent: parentAgent
|
|
375
|
+
});
|
|
376
|
+
} catch (e) {
|
|
377
|
+
log("info", `interrupt declined: ${String(e.message).slice(0, 120)} [reason=interrupt-threw]`);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
ctx.interval(() => {
|
|
381
|
+
for (const key of stalledKeys(watched, Date.now(), stallMs)) {
|
|
382
|
+
const entry = watched.get(key);
|
|
383
|
+
if (!entry) continue;
|
|
384
|
+
watched.delete(key);
|
|
385
|
+
handleStall(entry);
|
|
386
|
+
}
|
|
387
|
+
}, tickMs);
|
|
388
|
+
ctx.effect(() => () => {});
|
|
389
|
+
ctx.on("agent/created", (...args) => {
|
|
390
|
+
try {
|
|
391
|
+
const agent = args[0]?.agent;
|
|
392
|
+
if (agent === void 0 || agent === null) return;
|
|
393
|
+
const header = agent?.session?.header;
|
|
394
|
+
if (!shouldWatchHeader(header)) return;
|
|
395
|
+
const id = sessionIdOf(agent);
|
|
396
|
+
if (watched.has(agent)) {
|
|
397
|
+
const e = watched.get(agent);
|
|
398
|
+
e.at = Date.now();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
watched.set(agent, {
|
|
402
|
+
id: id ?? "?",
|
|
403
|
+
parentId: parentSessionOf(agent),
|
|
404
|
+
at: Date.now()
|
|
405
|
+
});
|
|
406
|
+
if (!engagedNoted) {
|
|
407
|
+
engagedNoted = true;
|
|
408
|
+
log("info", `engaged: watching sun-worker children (stall=${stallMs / 1e3}s, tick=${tickMs / 1e3}s)`);
|
|
409
|
+
}
|
|
410
|
+
} catch (e) {
|
|
411
|
+
log("info", `agent/created handling failed: ${String(e.message).slice(0, 120)}`);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
ctx.on("tools/post-execute", (...args) => {
|
|
415
|
+
const [exec, , next] = args;
|
|
416
|
+
try {
|
|
417
|
+
const entry = exec !== void 0 && exec !== null ? watched.get(exec) : void 0;
|
|
418
|
+
if (entry) entry.at = Date.now();
|
|
419
|
+
} catch {}
|
|
420
|
+
return next();
|
|
421
|
+
});
|
|
422
|
+
ctx.on("agent/disposed", (...args) => {
|
|
423
|
+
try {
|
|
424
|
+
const payload = args[0];
|
|
425
|
+
if (payload?.agent !== void 0 && payload.agent !== null) watched.delete(payload.agent);
|
|
426
|
+
} catch {}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
236
429
|
// src/index.ts
|
|
237
430
|
const name = "sun-space";
|
|
238
431
|
const inject = ["timer", "tools"];
|
|
239
432
|
function apply(ctx) {
|
|
240
433
|
ctx.plugin(source_exports);
|
|
434
|
+
ctx.plugin(worker_watchdog_exports);
|
|
241
435
|
}
|
|
242
436
|
export { apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sun-space",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Sun Space
|
|
5
|
+
"description": "Sun Space:DeepSeek Harness 个人超智能体插件——Router/Worker 双预设工具白名单、钉钉待办轮询、Obsidian 知识库落库、会话工作台与定时任务管理。",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./lib/index.mjs",
|
|
8
8
|
"./client": "./src/client.js",
|
|
@@ -48,5 +48,10 @@
|
|
|
48
48
|
"type": "git",
|
|
49
49
|
"url": "git+https://github.com/40huo/dsh-sun-space.git"
|
|
50
50
|
},
|
|
51
|
-
"license": "GPL-3.0-or-later"
|
|
52
|
-
|
|
51
|
+
"license": "GPL-3.0-or-later",
|
|
52
|
+
"pnpm": {
|
|
53
|
+
"onlyBuiltDependencies": [
|
|
54
|
+
"esbuild"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/client.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* target 自动绑当前会话);非 Router 会话 → 跳转引导卡;无 Router 会话 → 初始化面板。
|
|
7
7
|
*/
|
|
8
8
|
window.__ModuleLoader__.load({
|
|
9
|
-
id: 'sun-space/client',
|
|
9
|
+
id: 'dsh-sun-space/client',
|
|
10
10
|
factory(require) {
|
|
11
11
|
const D = { phase: 'factory-entered', errors: [], currentSessionId: null }
|
|
12
12
|
try {
|
|
@@ -22,7 +22,7 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
// 会话解析链:session/list(sun-router 会话)→ 90-Meta/system/state.json 裁决 → updatedAt 最新兜底。
|
|
23
23
|
// vault 解析链:会话 cwd 推 HOME → 约定布局探测 state.json → state.json 的 vault 字段裁决(跨机自愈)。
|
|
24
24
|
// 写回:① 创建会话后经 session/prompt 引导自登记(含 vault 字段);② 晨报自检发现缺失/不一致自动修正。
|
|
25
|
-
const WB_REV = '
|
|
25
|
+
const WB_REV = 'v13' // 根节点 data-wb-rev 版本标记:判别浏览器是否吃到旧 client.js(immutable 缓存坑,HANDOFF 踩坑 1)
|
|
26
26
|
|
|
27
27
|
const STATUS_META = {
|
|
28
28
|
proposed: { label: '待确认派发', color: '#9e9e9e' },
|
|
@@ -255,6 +255,19 @@ window.__ModuleLoader__.load({
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
// 创建 Router 会话 → 自登记 state.json → 跳转 → 重检(InitPanel 与 OnboardPanel 共用)
|
|
259
|
+
async function createAndBootstrap(checks, onRetry) {
|
|
260
|
+
try {
|
|
261
|
+
const v = await createRouterSession()
|
|
262
|
+
const sid = typeof v === 'string' ? v : (v?.sessionId ?? v)
|
|
263
|
+
if (sid) {
|
|
264
|
+
await bootstrapRouterSession(sid, checks.vaultPath) // 自登记 state.json(含 vault 字段;失败不阻断——下次检查由最近活跃兜底)
|
|
265
|
+
try { hostCtx.uiWorkspace.openSession(sid) } catch (e) { D.errors.push('openSession: ' + String(e)) }
|
|
266
|
+
onRetry() // 新会话的 tab 即完整工作台(state.json 已指向它)
|
|
267
|
+
}
|
|
268
|
+
} catch (e) { D.errors.push(String(e)) }
|
|
269
|
+
}
|
|
270
|
+
|
|
258
271
|
function InitPanel({ checks, currentSessionId, onRetry }) {
|
|
259
272
|
const rows = [
|
|
260
273
|
{ ok: checks.vault, title: '① 知识库(vault)', desc: checks.notes.find(n => n.includes('知识库')) || `已就绪:${checks.vaultPath}` },
|
|
@@ -270,22 +283,20 @@ window.__ModuleLoader__.load({
|
|
|
270
283
|
h('div', { key: 'd', style: { fontSize: 12, opacity: 0.7, lineHeight: 1.6 } }, r.desc),
|
|
271
284
|
]),
|
|
272
285
|
r.action === 'plugins' ? h('button', { key: 'a', onClick: () => { try { hostCtx.layout.selectPanel('plugins') } catch (e) { D.errors.push(String(e)) } }, style: btnStyle }, '打开插件面板') : null,
|
|
273
|
-
r.action === 'create-router' ? h('button', { key: 'a', onClick:
|
|
274
|
-
try {
|
|
275
|
-
const v = await createRouterSession()
|
|
276
|
-
const sid = typeof v === 'string' ? v : (v?.sessionId ?? v)
|
|
277
|
-
if (sid) {
|
|
278
|
-
await bootstrapRouterSession(sid, checks.vaultPath) // 自登记 state.json(含 vault 字段;失败不阻断——下次检查由最近活跃兜底)
|
|
279
|
-
try { hostCtx.uiWorkspace.openSession(sid) } catch (e) { D.errors.push('openSession: ' + String(e)) }
|
|
280
|
-
onRetry() // 新会话的 tab 即完整工作台(state.json 已指向它)
|
|
281
|
-
}
|
|
282
|
-
} catch (e) { D.errors.push(String(e)) }
|
|
283
|
-
}, style: btnStyle }, '创建 Router 会话') : null,
|
|
286
|
+
r.action === 'create-router' ? h('button', { key: 'a', onClick: () => createAndBootstrap(checks, onRetry), style: btnStyle }, '创建 Router 会话') : null,
|
|
284
287
|
])),
|
|
285
288
|
h('button', { key: 'retry', onClick: onRetry, style: { ...btnStyle, marginTop: 10 } }, '↻ 重新检查'),
|
|
286
289
|
])
|
|
287
290
|
}
|
|
288
291
|
|
|
292
|
+
// 四条生产预设:一键预填表单(新机器启用的最后一公里)
|
|
293
|
+
const SCHEDULE_TEMPLATES = [
|
|
294
|
+
{ label: '晨报 08:45', title: 'Sun 晨间简报', cron: '45 8 * * 1-5', prompt: '【定时 · 晨间简报】按 runbook §5 执行晨报:sun_read(triggers/tasks/runbook/memory) → 生成今日计划追加到 40-Journal/当日.md 晨报小节 → 预建/更新任务笔记(sun_vault_meta 流转状态)→ sun_notify(ask) 推送摘要请我确认。触发队列里 sys-onboarding-guide 类待办按 runbook §6 过滤。自检兜底:若 90-Meta/system/state.json 的 routerSession/vault 字段与本会话环境不符 → sun_vault_write(overwrite) 修正。' },
|
|
295
|
+
{ label: '日报 18:30', title: 'Sun 日报', cron: '30 18 * * 1-5', prompt: '【定时 · 日报】按 runbook §7 执行日报:sun_read(tasks/journal/note full) 汇总当日任务 → 日报追加到 40-Journal/当日.md(今日完成/未完成与原因/明日建议,保持 status=draft)→ sun_notify(ask) 请求审核。' },
|
|
296
|
+
{ label: 'Watchdog 14/21 点', title: 'Sun Watchdog 巡检', cron: '0 14,21 * * 1-5', prompt: '【定时 · Watchdog 巡检】按 runbook §3 分级巡检,quiet by default——一切正常则不发任何通知。检查:①running 超 2h → urgent+重派建议 ②need-confirm 超 2h → urgent 重推 ③工作日缺晨报/日报小节 → 按 §5/§7 补跑 ④核对 90-Meta/system/state.json 的 routerSession/vault 字段,不符则修正。' },
|
|
297
|
+
{ label: '周复盘 周五 17:00', title: 'Sun 周复盘', cron: '0 17 * * 5', prompt: '【定时 · 周复盘】生成 90-Meta/reflections/当周.md(frontmatter: type reflection/status draft/week/本地时间戳):§本周概览含派发预算执行 §做得好/不好各≤3 §runbook 修订建议 diff §下周关注含轮换触发线检视 → 周报以「## 周复盘」追加 Journal(status=draft)→ sun_notify(ask) 请求审核,人审后 diff 生效。' },
|
|
298
|
+
]
|
|
299
|
+
|
|
289
300
|
// ===== 定时任务区:列表 + 新建表单(target 自动绑当前 Router 会话,补 UI 无 session 选项的缺口)=====
|
|
290
301
|
function ScheduleSection({ routerSession }) {
|
|
291
302
|
const [sched, setSched] = React.useState({ loading: true, items: [], error: null })
|
|
@@ -348,6 +359,11 @@ window.__ModuleLoader__.load({
|
|
|
348
359
|
// 新建表单
|
|
349
360
|
h('div', { key: 'form', style: { border: '1px dashed rgba(128,128,128,0.4)', borderRadius: 8, padding: '10px 12px', marginTop: 8 } }, [
|
|
350
361
|
h('div', { key: 'ft', style: { fontSize: 12, fontWeight: 600, marginBottom: 6 } }, '+ 新增定时任务(cron,工作日建议 1-5)'),
|
|
362
|
+
h('div', { key: 'tpl', style: { display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center', marginBottom: 6 } }, [
|
|
363
|
+
h('span', { key: 'lbl', style: { fontSize: 11, opacity: 0.6 } }, '快速填入:'),
|
|
364
|
+
...SCHEDULE_TEMPLATES.map(t => h('button', { key: t.label, onClick: () => setForm(f => ({ ...f, title: t.title, cron: t.cron, prompt: t.prompt, msg: null })), style: btnStyle }, t.label)),
|
|
365
|
+
]),
|
|
366
|
+
active.length === 0 ? h('p', { key: 'emptyhint', style: { fontSize: 11, opacity: 0.55, margin: '0 0 6px' } }, '还没有生效的定时任务——选一个模板填入,点「创建」即绑定本会话。') : null,
|
|
351
367
|
h('input', { key: 'title', placeholder: '标题,如:Sun 生活提醒', value: form.title, onChange: e => setForm(f => ({ ...f, title: e.target.value })), style: { ...inputStyle, marginBottom: 6 } }),
|
|
352
368
|
h('input', { key: 'cron', placeholder: 'cron:分 时 日 月 周(五段),时区固定 Asia/Shanghai', value: form.cron, onChange: e => setForm(f => ({ ...f, cron: e.target.value })), style: { ...inputStyle, marginBottom: 6, fontFamily: 'monospace' } }),
|
|
353
369
|
h('textarea', { key: 'prompt', placeholder: '任务指令(薄引用写法:按 runbook §N 执行……)', value: form.prompt, onChange: e => setForm(f => ({ ...f, prompt: e.target.value })), rows: 3, style: { ...inputStyle, marginBottom: 6, resize: 'vertical' } }),
|
|
@@ -359,10 +375,36 @@ window.__ModuleLoader__.load({
|
|
|
359
375
|
])
|
|
360
376
|
}
|
|
361
377
|
|
|
378
|
+
// 新用户三步启用引导(主理鱼同款编号步骤卡;替代旧的一锅炖检查面板)
|
|
379
|
+
function OnboardPanel({ checks, currentSessionId, onRetry }) {
|
|
380
|
+
const steps = [
|
|
381
|
+
{ n: 1, ok: checks.presets, title: '确认依赖插件', desc: checks.presets ? '智能体协作 / 子智能体插件已启用' : '打开插件面板,启用 sun-space / 智能体协作相关插件后重启实例', action: checks.presets ? null : 'plugins' },
|
|
382
|
+
{ 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 },
|
|
383
|
+
{ n: 3, ok: checks.vault, title: '知识库就绪', desc: checks.vault ? `已就绪:${checks.vaultPath}` : `用 Obsidian 打开(或创建)~/Workspace/云顶天宫 并放好 90-Meta 骨架;自定义位置让 Router 把绝对路径写进 state.json 的 vault 字段`, action: null },
|
|
384
|
+
]
|
|
385
|
+
const allOk = steps.every(x => x.ok)
|
|
386
|
+
return h('div', null, [
|
|
387
|
+
h('h2', { key: 'h', style: { margin: '0 0 4px', fontSize: 18 } }, '三步启用 Sun'),
|
|
388
|
+
h('p', { key: 'sub', style: { margin: '0 0 14px', fontSize: 12, opacity: 0.6 } }, '工作台与定时任务都挂在 Router 会话上——三步完成后,本页即变为日常工作台。'),
|
|
389
|
+
...steps.map(st => h('div', { key: st.n, style: { ...cardStyle(st.ok ? '#57b26a' : 'rgba(128,128,128,0.35)'), alignItems: 'flex-start', marginBottom: 10, padding: '10px 14px' } }, [
|
|
390
|
+
h('span', { key: 'n', style: { width: 22, height: 22, borderRadius: 11, flexShrink: 0, marginTop: 2, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 600, background: st.ok ? '#57b26a' : 'rgba(128,128,128,0.25)', color: st.ok ? '#fff' : 'inherit' } }, st.ok ? '✓' : String(st.n)),
|
|
391
|
+
h('div', { key: 'b', style: { flex: 1 } }, [
|
|
392
|
+
h('div', { key: 't', style: { fontWeight: 600, fontSize: 13 } }, st.title),
|
|
393
|
+
h('div', { key: 'd', style: { fontSize: 12, opacity: 0.7, lineHeight: 1.6, marginTop: 2 } }, st.desc),
|
|
394
|
+
]),
|
|
395
|
+
st.action === 'plugins' ? h('button', { key: 'a', onClick: () => { try { hostCtx.layout.selectPanel('plugins') } catch (e) { D.errors.push(String(e)) } }, style: btnStyle }, '打开插件面板') : null,
|
|
396
|
+
st.action === 'create-router' ? h('button', { key: 'a', onClick: () => createAndBootstrap(checks, onRetry), style: btnStyle }, '创建 Router 会话') : null,
|
|
397
|
+
])),
|
|
398
|
+
allOk ? h('p', { key: 'done', style: { color: '#57b26a', fontSize: 13 } }, '✓ 全部就绪——到「定时任务」用模板创建晨报等四条生产任务,即可开始使用。') : null,
|
|
399
|
+
h('button', { key: 'retry', onClick: onRetry, style: { ...btnStyle, marginTop: 10 } }, '↻ 重新检查'),
|
|
400
|
+
])
|
|
401
|
+
}
|
|
402
|
+
|
|
362
403
|
function Workbench(props) {
|
|
363
404
|
// conversation.view 的 inject(sessionId) 捕获当前会话;props 兜底
|
|
364
405
|
const sessionId = props?.sessionId ?? D.currentSessionId ?? null
|
|
365
406
|
const [init, setInit] = React.useState({ checking: true, checks: null })
|
|
407
|
+
const [tab, setTab] = React.useState('overview')
|
|
366
408
|
const runChecks = React.useCallback(() => {
|
|
367
409
|
setInit({ checking: true, checks: null })
|
|
368
410
|
runInitChecks(sessionId).then(checks => setInit({ checking: false, checks }))
|
|
@@ -371,6 +413,7 @@ window.__ModuleLoader__.load({
|
|
|
371
413
|
const routerSession = init.checks?.routerSession ?? null
|
|
372
414
|
const vault = init.checks?.vaultPath ?? null
|
|
373
415
|
const isRouterView = !!(routerSession && sessionId && routerSession === sessionId)
|
|
416
|
+
const allReady = !!(init.checks && init.checks.vault && init.checks.presets && routerSession)
|
|
374
417
|
const aliveRef = React.useRef(true)
|
|
375
418
|
React.useEffect(() => () => { aliveRef.current = false }, [])
|
|
376
419
|
const [state, setState] = React.useState({ loading: true, tasks: [], error: null })
|
|
@@ -426,6 +469,17 @@ window.__ModuleLoader__.load({
|
|
|
426
469
|
])
|
|
427
470
|
}
|
|
428
471
|
|
|
472
|
+
// 未就绪 → 三步启用引导;检查中 → 占位
|
|
473
|
+
if (init.checking) {
|
|
474
|
+
return h('div', { style: { padding: '20px 30px', maxWidth: 960 }, 'data-wb-rev': WB_REV }, [
|
|
475
|
+
h('p', { key: 'w', style: { opacity: 0.6, fontSize: 13 } }, '初始化检查中…'),
|
|
476
|
+
])
|
|
477
|
+
}
|
|
478
|
+
if (init.checks && !allReady) {
|
|
479
|
+
return h('div', { style: { padding: '20px 30px', maxWidth: 960 }, 'data-wb-rev': WB_REV }, [
|
|
480
|
+
h(SunErrorBoundary, { key: 'eb' }, h(OnboardPanel, { checks: init.checks, currentSessionId: sessionId, onRetry: runChecks })),
|
|
481
|
+
])
|
|
482
|
+
}
|
|
429
483
|
return h('div', { style: { padding: '20px 30px', maxWidth: 960 }, 'data-wb-rev': WB_REV }, [
|
|
430
484
|
h(SunErrorBoundary, { key: 'eb' }, [
|
|
431
485
|
h('div', { key: 'head', style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 } }, [
|
|
@@ -434,12 +488,10 @@ window.__ModuleLoader__.load({
|
|
|
434
488
|
h('button', { key: 'r', onClick: () => { reload(); runChecks() }, style: { marginLeft: 'auto', fontSize: 12, padding: '4px 10px', borderRadius: 6, border: '1px solid rgba(128,128,128,0.35)', background: 'transparent', color: 'inherit', cursor: 'pointer' } }, state.loading ? '刷新中…' : '刷新'),
|
|
435
489
|
]),
|
|
436
490
|
// 初始化面板:三项检查任一未就绪时出现(含无 Router 会话的场景)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
!isRouterView && h('p', { key: 'wait', style: { opacity: 0.6, fontSize: 13 } }, '初始化完成后,这里会展示派活树与定时任务。'),
|
|
442
|
-
isRouterView && h('div', { key: 'cnt', style: { display: 'flex', gap: 18, marginBottom: 16 } },
|
|
491
|
+
h('div', { key: 'tabs', style: { display: 'flex', gap: 18, marginBottom: 14, borderBottom: '1px solid rgba(128,128,128,0.2)' } }, [
|
|
492
|
+
['overview', '总览'], ['schedules', '定时任务'], ['setup', '设置'],
|
|
493
|
+
].map(([id, label]) => h('button', { key: id, onClick: () => setTab(id), style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 14, padding: '6px 2px', color: 'inherit', opacity: tab === id ? 1 : 0.55, borderBottom: tab === id ? '2px solid currentColor' : '2px solid transparent' } }, label))),
|
|
494
|
+
h('div', { key: 'cnt', style: { display: 'flex', gap: 18, marginBottom: 16 } },
|
|
443
495
|
counters.map(([label, n, color]) => h('div', { key: label, style: { display: 'flex', alignItems: 'center', gap: 6 } }, [
|
|
444
496
|
h('span', { key: 'd', style: { width: 8, height: 8, borderRadius: 4, background: color, display: 'inline-block' } }),
|
|
445
497
|
h('span', { key: 'l', style: { fontSize: 13 } }, `${label} ${n}`),
|
|
@@ -447,7 +499,7 @@ window.__ModuleLoader__.load({
|
|
|
447
499
|
state.error && h('p', { key: 'err', style: { color: '#e8634d', fontSize: 13 } }, '加载失败:' + state.error),
|
|
448
500
|
|
|
449
501
|
// ===== 派活树(仅 Router 视图)=====
|
|
450
|
-
|
|
502
|
+
tab === 'overview' && h('div', { key: 'root', title: '点击打开 Router 会话', onClick: () => { try { hostCtx.uiWorkspace.openSession(routerSession) } catch (e) { D.errors.push(String(e)) } }, style: {
|
|
451
503
|
display: 'inline-flex', alignItems: 'center', gap: 10, cursor: 'pointer',
|
|
452
504
|
border: anyRunning ? '1px solid rgba(87,178,106,0.55)' : '1px solid rgba(128,128,128,0.3)',
|
|
453
505
|
background: anyRunning ? 'rgba(87,178,106,0.10)' : 'transparent',
|
|
@@ -463,8 +515,8 @@ window.__ModuleLoader__.load({
|
|
|
463
515
|
anyRunning ? '干活中' : '待命',
|
|
464
516
|
]),
|
|
465
517
|
]),
|
|
466
|
-
|
|
467
|
-
|
|
518
|
+
tab === 'overview' && tasks.length > 0 && h('div', { key: 'trunk', style: { width: 2, height: 14, background: 'rgba(128,128,128,0.35)', marginLeft: 14 } }),
|
|
519
|
+
tab === 'overview' && tasks.map(t => h('div', { key: t.id, style: { position: 'relative', paddingLeft: 20, paddingBottom: 8 } }, [
|
|
468
520
|
h('div', { key: 'elbow', style: { position: 'absolute', left: 13, top: -8, width: 8, height: 30, borderLeft: '2px solid rgba(128,128,128,0.35)', borderBottom: '2px solid rgba(128,128,128,0.35)', borderTop: '2px solid rgba(128,128,128,0.35)', borderTopLeftRadius: 6 } }),
|
|
469
521
|
h('div', { key: 'card', title: '点击打开对应会话', onClick: () => openTaskSession(t), style: {
|
|
470
522
|
cursor: 'pointer', ...cardStyle(t.status === 'need-confirm' ? STATUS_META['need-confirm'].color : undefined),
|
|
@@ -480,18 +532,19 @@ window.__ModuleLoader__.load({
|
|
|
480
532
|
t.status === 'need-confirm' ? h('span', { key: 'wait', style: { fontSize: 12, color: STATUS_META['need-confirm'].color, marginLeft: 'auto' } }, '等你决策 →') : null,
|
|
481
533
|
]),
|
|
482
534
|
])),
|
|
483
|
-
|
|
535
|
+
tab === 'overview' && tasks.length === 0 && !state.loading && h('p', { key: 'empty', style: { opacity: 0.5, fontSize: 13, paddingLeft: 20 } }, '暂无任务——晨报生成后这里会长出派活树'),
|
|
484
536
|
|
|
485
537
|
// ===== 定时任务(列表 + 新建表单,仅 Router 视图)=====
|
|
486
|
-
|
|
538
|
+
tab === 'schedules' && routerSession ? h(ScheduleSection, { key: 'schedsec', routerSession }) : null,
|
|
487
539
|
|
|
488
540
|
// ===== Journal 入口卡(仅 Router 视图)=====
|
|
489
|
-
|
|
541
|
+
tab === 'overview' && h('div', { key: 'journal', title: 'Journal 在 Obsidian 中查看与编辑', style: {
|
|
490
542
|
border: '1px solid rgba(128,128,128,0.25)', borderRadius: 10, padding: '12px 16px', marginTop: 18,
|
|
491
543
|
} }, [
|
|
492
544
|
h('div', { key: 't', style: { fontWeight: 600, fontSize: 13, marginBottom: 4 } }, '📓 今日 Journal'),
|
|
493
545
|
h('div', { key: 'd', style: { fontSize: 12, opacity: 0.7, lineHeight: 1.6 } }, `40-Journal/${new Date().toLocaleDateString('sv-SE', { timeZone: 'Asia/Shanghai' })}.md —— Obsidian 打开 vault 即见`),
|
|
494
546
|
]),
|
|
547
|
+
tab === 'setup' && h('div', { key: 'setup' }, h(InitPanel, { checks: init.checks, currentSessionId: sessionId, onRetry: runChecks })),
|
|
495
548
|
]),
|
|
496
549
|
])
|
|
497
550
|
}
|