dsh-sun-space 0.2.1 → 0.2.3

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.
Files changed (3) hide show
  1. package/lib/index.mjs +205 -11
  2. package/package.json +3 -3
  3. package/src/client.js +81 -28
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$1,
122
- inject: () => inject$1,
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$1 = ["timer"];
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$1(ctx) {
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.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
- "description": "Sun Space — personal super-agent on DeepSeek Harness (router/worker presets, dws source, vault, panel)",
5
+ "description": "Sun Space:DeepSeek Harness 个人超智能体插件——Router/Worker 双预设工具白名单、钉钉待办轮询、Obsidian 知识库落库、会话工作台与定时任务管理。",
6
6
  "exports": {
7
7
  ".": "./lib/index.mjs",
8
8
  "./client": "./src/client.js",
@@ -54,4 +54,4 @@
54
54
  "esbuild"
55
55
  ]
56
56
  }
57
- }
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 = 'v12' // 根节点 data-wb-rev 版本标记:判别浏览器是否吃到旧 client.js(immutable 缓存坑,HANDOFF 踩坑 1)
25
+ const WB_REV = 'v15' // 根节点 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: async () => {
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 })
@@ -418,7 +461,7 @@ window.__ModuleLoader__.load({
418
461
  h(SunIcon, { key: 'i', size: 20 }),
419
462
  h('div', { key: 'b', style: { flex: 1, fontSize: 13, lineHeight: 1.7 } }, [
420
463
  h('div', { key: 't', style: { fontWeight: 600 } }, '本会话不是当前 Router 会话'),
421
- h('div', { key: 'd', style: { opacity: 0.7, fontSize: 12 } }, `Sun 工作台在 Router 会话上(当前:${routerSession.slice(0, 28)}…)。派活树、定时任务与初始化检查都在那边。`),
464
+ h('div', { key: 'd', style: { opacity: 0.7, fontSize: 12 } }, `孙学空间在 Router 会话上(当前:${routerSession.slice(0, 28)}…)。派活树、定时任务与初始化检查都在那边。`),
422
465
  ]),
423
466
  h('button', { key: 'go', onClick: () => { try { hostCtx.uiWorkspace.openSession(routerSession) } catch (e) { D.errors.push(String(e)) } }, style: btnStyle }, '打开 Router 会话'),
424
467
  ]),
@@ -426,20 +469,29 @@ 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 } }, [
432
486
  h(SunIcon, { key: 'i', size: 22 }),
433
- h('h1', { key: 't', style: { margin: 0, fontSize: 20 } }, 'Sun 工作台'),
487
+ h('h1', { key: 't', style: { margin: 0, fontSize: 20 } }, '孙学空间'),
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
- init.checking ? h('p', { key: 'w', style: { opacity: 0.6, fontSize: 13 } }, '初始化检查中…') : null,
438
- !init.checking && init.checks && !(init.checks.vault && init.checks.presets && routerSession)
439
- ? h('div', { key: 'init', style: { marginBottom: 16 } }, h(InitPanel, { key: 'ip', checks: init.checks, currentSessionId: sessionId, onRetry: runChecks }))
440
- : null,
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: 3, marginBottom: 16, background: 'rgba(128,128,128,0.12)', borderRadius: 8, padding: 3 } }, [
492
+ ['overview', '总览'], ['schedules', '定时任务'], ['setup', '设置'],
493
+ ].map(([id, label]) => h('button', { key: id, onClick: () => setTab(id), style: { flex: 1, border: 'none', cursor: 'pointer', fontSize: 13, padding: '6px 0', borderRadius: 6, color: 'inherit', background: tab === id ? 'rgba(128,128,128,0.28)' : 'transparent', opacity: tab === id ? 1 : 0.6, transition: 'background 0.15s' } }, 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
- isRouterView && h('div', { key: 'root', title: '点击打开 Router 会话', onClick: () => { try { hostCtx.uiWorkspace.openSession(routerSession) } catch (e) { D.errors.push(String(e)) } }, style: {
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
- isRouterView && tasks.length > 0 && h('div', { key: 'trunk', style: { width: 2, height: 14, background: 'rgba(128,128,128,0.35)', marginLeft: 14 } }),
467
- isRouterView && tasks.map(t => h('div', { key: t.id, style: { position: 'relative', paddingLeft: 20, paddingBottom: 8 } }, [
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
- isRouterView && tasks.length === 0 && !state.loading && h('p', { key: 'empty', style: { opacity: 0.5, fontSize: 13, paddingLeft: 20 } }, '暂无任务——晨报生成后这里会长出派活树'),
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
- isRouterView && routerSession ? h(ScheduleSection, { key: 'schedsec', routerSession }) : null,
538
+ tab === 'schedules' && routerSession ? h(ScheduleSection, { key: 'schedsec', routerSession }) : null,
487
539
 
488
540
  // ===== Journal 入口卡(仅 Router 视图)=====
489
- isRouterView && h('div', { key: 'journal', title: 'Journal 在 Obsidian 中查看与编辑', style: {
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
  }
@@ -511,7 +564,7 @@ window.__ModuleLoader__.load({
511
564
  name: 'conversation.view',
512
565
  id: PANEL_ID,
513
566
  order: 40,
514
- label: () => '工作台',
567
+ label: () => '孙学空间',
515
568
  inject: (sessionId) => { D.currentSessionId = sessionId; return {} },
516
569
  }, Workbench)
517
570
  })