@shgroup/dsh-serenity-hooks 1.25.9 → 1.25.11

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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.25.9",
3
+ "version": "1.25.11",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/handyman/session_rebuild/skiff_admin + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)+ Skiff 认知子集角色(实验性)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/index.js CHANGED
@@ -647,7 +647,8 @@ function readSkiffRoles(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
647
647
  keeper: role.trajectory.keeper === true,
648
648
  rebuild: role.trajectory.rebuild === true
649
649
  } : void 0,
650
- systemPrompt: typeof role.systemPrompt === "string" ? role.systemPrompt : void 0
650
+ systemPrompt: typeof role.systemPrompt === "string" ? role.systemPrompt : void 0,
651
+ systemPromptFile: typeof role.systemPromptFile === "string" ? role.systemPromptFile : void 0
651
652
  });
652
653
  }
653
654
  } catch {}
@@ -672,6 +673,32 @@ function roleMsmWhitelist(role) {
672
673
  return new Set(role?.msms ?? []);
673
674
  }
674
675
  /**
676
+ * 解析角色的系统提示词全文(v1.25.10,S142 用户:超长提示词 JSON 内嵌不可读):
677
+ * ① `systemPromptFile` 存在 → 读取文件内容(**推荐配置方法**;相对 CCC 根,
678
+ * 路径逃逸拒绝(resolveInside)+ BOM 剥除(readUtf8)+ 存在性校验)
679
+ * ② 否则 → 内嵌 `systemPrompt`(兼容旧配置)
680
+ * ③ 都无 → 空字符串
681
+ * 文件缺失/逃逸 → 抛错(调用方 catch 降级 + validate 报 issue)。
682
+ * 懒读取:readSkiffRoles 不读文件(guards/seams 每次工具调用查询的热路径零 IO),
683
+ * 仅在本函数(创建 agent / validate / list 时)读取。
684
+ */
685
+ function resolveRoleSystemPrompt(root, role) {
686
+ if (!role) return "";
687
+ const file = role.systemPromptFile?.trim();
688
+ if (file) {
689
+ const abs = resolveInside(root, file);
690
+ if (!existsSync(abs)) throw new Error(`skiff role "${role.systemPrompt ?? "(unnamed)"}": systemPromptFile "${file}" not found (resolved: ${abs})`);
691
+ return readUtf8(abs).trim();
692
+ }
693
+ return role.systemPrompt ?? "";
694
+ }
695
+ /** 角色系统提示词来源(validate/list 展示用) */
696
+ function systemPromptSource(role) {
697
+ if (role?.systemPromptFile?.trim()) return "file";
698
+ if (role?.systemPrompt?.trim()) return "inline";
699
+ return "none";
700
+ }
701
+ /**
675
702
  * Skiff 基础提示词(dsp 只给这部分;CCC 的 systemPrompt 段由调用方拼接):
676
703
  * 身份 + 可用 MSM/工具清单 + 调用协议 + 边界声明。动态生成(清单来自角色白名单)。
677
704
  */
@@ -694,22 +721,20 @@ function buildSkiffBasePrompt(roleName, role) {
694
721
  }
695
722
  //#endregion
696
723
  //#region src/skiff-registry.ts
697
- /**
698
- * skiff-registry.ts — Skiff 会话注册表(sessionId → role;零 DSH 依赖,可被任何 seams 安全 import)
699
- *
700
- * F4b 会话映射:Skiff agent 创建时注册(skiff-core),guards/seams 按 sessionId 查角色。
701
- * 独立成模块的原因:guards.ts 等拦截缝需要查角色而不引入 skiff-core 的运行时依赖
702
- * (skiff-core 依赖 @deepseek-ai/dsh-llm,测试/装配级联成本高)——注册表本身纯内存 Map。
703
- *
704
- * 生命周期:进程内存态;连接/页面关闭时 unregister;进程重启自然清空(skiff 会话不复存在)。
705
- */
706
724
  const skiffSessions = /* @__PURE__ */ new Map();
707
- /** 查 sessionId 的 Skiff 角色名(无 → null */
725
+ /** 查 sessionId 的 Skiff 角色名(无 → null)——向后兼容(guards/seams 依赖) */
708
726
  function skiffRoleFor$1(sessionId) {
727
+ return skiffSessions.get(sessionId)?.role ?? null;
728
+ }
729
+ /** 查 sessionId 的完整绑定(role + ccc;无 → null)——v1.25.10 会话追问校验 */
730
+ function skiffSessionInfo$1(sessionId) {
709
731
  return skiffSessions.get(sessionId) ?? null;
710
732
  }
711
- function registerSkiffSession$1(sessionId, role) {
712
- skiffSessions.set(sessionId, role);
733
+ function registerSkiffSession$1(sessionId, role, ccc) {
734
+ skiffSessions.set(sessionId, {
735
+ role,
736
+ ccc
737
+ });
713
738
  }
714
739
  //#endregion
715
740
  //#region src/seams/guards.ts
@@ -2357,9 +2382,17 @@ const skiffAgents = /* @__PURE__ */ new Map();
2357
2382
  function skiffRoleFor(sessionId) {
2358
2383
  return skiffRoleFor$1(sessionId);
2359
2384
  }
2360
- function registerSkiffSession(sessionId, role, agent) {
2385
+ /** sessionId 的会话绑定(role + ccc;无 → null)——v1.25.10 追问校验 */
2386
+ function skiffSessionInfo(sessionId) {
2387
+ return skiffSessionInfo$1(sessionId);
2388
+ }
2389
+ /** 查 sessionId 的活体 agent(进程内会话延续用;未注册/已清理 → undefined) */
2390
+ function getSkiffAgent(sessionId) {
2391
+ return skiffAgents.get(sessionId);
2392
+ }
2393
+ function registerSkiffSession(sessionId, role, ccc, agent) {
2361
2394
  skiffAgents.set(sessionId, agent);
2362
- registerSkiffSession$1(sessionId, role);
2395
+ registerSkiffSession$1(sessionId, role, ccc);
2363
2396
  }
2364
2397
  /** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
2365
2398
  * handyman 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
@@ -2387,16 +2420,22 @@ async function createSkiffAgent(ctx, root, roleName, role, defaultModel) {
2387
2420
  ...model ? { agentOptions: splitModel(model) } : {}
2388
2421
  });
2389
2422
  const agent = handle.agent;
2423
+ let cccPrompt = "";
2424
+ try {
2425
+ cccPrompt = resolveRoleSystemPrompt(root, role);
2426
+ } catch (err) {
2427
+ console.warn(`[serenity-hooks] skiff 角色 "${roleName}" 系统提示词解析失败(回退仅基础段): ${String(err?.message ?? err)}`);
2428
+ }
2390
2429
  try {
2391
2430
  agent.ctx.systemPrompt.section({
2392
2431
  name: "serenity-skiff",
2393
2432
  order: -60,
2394
- text: () => [buildSkiffBasePrompt(roleName, role), role.systemPrompt ?? ""].filter(Boolean).join("\n")
2433
+ text: () => [buildSkiffBasePrompt(roleName, role), cccPrompt].filter(Boolean).join("\n")
2395
2434
  });
2396
2435
  } catch (err) {
2397
2436
  console.warn(`[serenity-hooks] skiff 系统提示词注册失败: ${String(err?.message ?? err)}`);
2398
2437
  }
2399
- registerSkiffSession(sessionId, roleName, agent);
2438
+ registerSkiffSession(sessionId, roleName, root, agent);
2400
2439
  return {
2401
2440
  handle,
2402
2441
  agent,
@@ -2478,11 +2517,12 @@ function truncate(s, n) {
2478
2517
  return s.length > n ? `${s.slice(0, n)}…` : s;
2479
2518
  }
2480
2519
  /**
2481
- * 提问一轮:followup → 等 idle → 读答案 + 本 turn 轨迹(增量 = followup 前 events 之后)。
2482
- * @param eventsStart 本轮开始前的 events 长度(0 = 全量轨迹)
2520
+ * 提问一轮:followup → 等 idle → 读答案 + 轨迹。
2521
+ * @param eventsStart 轨迹起点:显式传 0 = 全量轨迹(会话追问时页面重绘完整时间线,
2522
+ * v1.25.10 用户拍板);不传(undefined)= 本轮增量(followup 前 events 之后)
2483
2523
  */
2484
- async function askSkiff(ctx, agent, question, eventsStart = 0) {
2485
- const before = eventsStart > 0 ? eventsStart : agent.session.events.length;
2524
+ async function askSkiff(ctx, agent, question, eventsStart) {
2525
+ const before = eventsStart === void 0 ? agent.session.events.length : eventsStart;
2486
2526
  agent.followup(createUserMessage({
2487
2527
  content: [{
2488
2528
  type: "text",
@@ -5944,6 +5984,43 @@ function renameDshSessionOnUse(deps, session, titles, active) {
5944
5984
  };
5945
5985
  }
5946
5986
  }
5987
+ /**
5988
+ * 从 create 结果构造命名用 ActiveSessionInfo(v1.25.11,S142 用户:create 也要命名):
5989
+ * createSession 返回的 result 已含 sessionId(S###/issue)与 dirName——无需等待 use 激活,
5990
+ * 直接构造(mdPath = sessionPath/SESSION.md,与 use 时一致)即可驱动 renameDshSessionOnUse。
5991
+ */
5992
+ function activeInfoFromCreate(result) {
5993
+ return {
5994
+ sessionId: result.sessionId,
5995
+ dirName: result.dirName,
5996
+ mdPath: join(result.sessionPath, "SESSION.md")
5997
+ };
5998
+ }
5999
+ /**
6000
+ * 把当前 dsh 会话重命名为指定 SESSION 的命名标题(use/create 共用;v1.25.11)。
6001
+ * 门控/失败可见性与 renameDshSessionOnUse 一致(不静默:成功 log / 失败 warn)。
6002
+ * 调用点(use 分支原内联逻辑提取,create 分支复用):
6003
+ * - use:激活后从 activeStore 取 info
6004
+ * - create:createSession 结果经 activeInfoFromCreate 构造 info
6005
+ */
6006
+ function renameDshSessionForActive(ctx, exec, info) {
6007
+ try {
6008
+ const titles = ctx.get?.("sessionTitle");
6009
+ const dshSession = exec.agent?.session;
6010
+ if (!dshSession) {
6011
+ console.warn(`[serenity-hooks] dsh 会话重命名未执行: 缺少 agent session(info: ${info.sessionId})`);
6012
+ return;
6013
+ }
6014
+ const result = renameDshSessionOnUse({
6015
+ namingEnabled: readSimpleSettings().namingEnabled,
6016
+ sessionTitleAvailable: true
6017
+ }, dshSession, titles, info);
6018
+ if (result.ok) console.log(`[serenity-hooks] dsh 会话已重命名: ${String(dshSession.id ?? "?")} → ${result.title}`);
6019
+ else console.warn(`[serenity-hooks] dsh 会话重命名未执行: ${result.reason}`);
6020
+ } catch (err) {
6021
+ console.warn(`[serenity-hooks] dsh 会话重命名异常: ${String(err?.message ?? err)}`);
6022
+ }
6023
+ }
5947
6024
  function renderText$3(value) {
5948
6025
  return [{
5949
6026
  type: "text",
@@ -6102,6 +6179,7 @@ function createSessionTool(ctx) {
6102
6179
  dryRun: args.dryRun ?? false
6103
6180
  });
6104
6181
  let message = result.message;
6182
+ if (!(args.dryRun ?? false)) renameDshSessionForActive(ctx, exec, activeInfoFromCreate(result));
6105
6183
  if (!(args.dryRun ?? false) && cccHooks.includes("create-transform")) try {
6106
6184
  const hookResult = await runMsmAsync(root, {
6107
6185
  action: "exec",
@@ -6119,23 +6197,8 @@ function createSessionTool(ctx) {
6119
6197
  if (!args.name) throw new Error("use requires name (S### or directory name)");
6120
6198
  const scope = agentScope$2(exec);
6121
6199
  const active = useSession(root, args.name, scope);
6122
- try {
6123
- const info = getActiveSessionInfo(scope);
6124
- if (info) {
6125
- const titles = ctx.get?.("sessionTitle");
6126
- const dshSession = exec.agent?.session;
6127
- if (dshSession) {
6128
- const result = renameDshSessionOnUse({
6129
- namingEnabled: readSimpleSettings().namingEnabled,
6130
- sessionTitleAvailable: true
6131
- }, dshSession, titles, info);
6132
- if (result.ok) console.log(`[serenity-hooks] dsh 会话已重命名: ${String(dshSession.id ?? "?")} → ${result.title}`);
6133
- else console.warn(`[serenity-hooks] dsh 会话重命名未执行: ${result.reason}`);
6134
- } else console.warn(`[serenity-hooks] dsh 会话重命名未执行: 缺少 agent session`);
6135
- }
6136
- } catch (err) {
6137
- console.warn(`[serenity-hooks] dsh 会话重命名异常: ${String(err?.message ?? err)}`);
6138
- }
6200
+ const info = getActiveSessionInfo(scope);
6201
+ if (info) renameDshSessionForActive(ctx, exec, info);
6139
6202
  return active;
6140
6203
  }
6141
6204
  case "close":
@@ -6806,7 +6869,9 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
6806
6869
  "msms": ["msm-a", "msm-b"], // MSM 白名单(独立):acc_msm exec 只能跑这些;register/deregister 必拒;list 只显示这些
6807
6870
  "tools": ["read","grep","glob",...], // 非 MSM 工具白名单(独立):白名单外工具一律不可用(guard 强制)
6808
6871
  "trajectory": { "session": false, "keeper": false, "rebuild": false }, // 轨迹纪律子集(缺省全关 = 完全独立)
6809
- "systemPrompt": "..." // 角色人格/认知边界/风格(CCC 完整定义;dsp 只给基础提示词)
6872
+ // 角色人格/认知边界/风格(CCC 完整定义;dsp 只给基础提示词)
6873
+ "systemPromptFile": ".opencode/skiff/<role>.md" // ★ 推荐:引用 md 文件(相对 CCC 根;超长提示词在 JSON 内嵌不可读)
6874
+ // "systemPrompt": "..." // 兼容:内嵌字符串仍可用;两者都设时 systemPromptFile 优先
6810
6875
  }
6811
6876
  } } }
6812
6877
 
@@ -6837,6 +6902,8 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
6837
6902
  启停 = 人工(设置面板开关,不随插件加载自动启动);未开启零资源占用
6838
6903
  生效机制:角色配置**实时读取**(改 .opencode/serenity.json → 刷新调试页即生效);
6839
6904
  改配置后用 skiff_admin apply 做显式校验 + 应用确认(绑定 CCC + 角色清单)
6905
+ 会话追问(v1.25.10):调试页持有当前会话,追问自动续接(同会话上下文延续);
6906
+ 「新对话」按钮开新会话;进程重启后旧会话不可续(WebUI 仍可见历史)
6840
6907
  注意:skill 加载对 skiff **恒可用**(不设白名单——读知识面,无写能力)`;
6841
6908
  /** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ handyman.models / systemPrompt 非空 */
6842
6909
  function validateSkiffConfig(root) {
@@ -6852,7 +6919,11 @@ function validateSkiffConfig(root) {
6852
6919
  const hc = readHandymanConfig(root);
6853
6920
  const allowedModels = hc ? new Set(hc.models) : null;
6854
6921
  for (const [name, role] of roles) {
6855
- if (!role.systemPrompt?.trim()) issues.push(`role "${name}": systemPrompt is empty (CCC should fully define the role persona)`);
6922
+ try {
6923
+ if (!resolveRoleSystemPrompt(root, role).trim()) issues.push(`role "${name}": system prompt is empty (define systemPromptFile (recommended) or systemPrompt)`);
6924
+ } catch (err) {
6925
+ issues.push(`role "${name}": ${String(err?.message ?? err)}`);
6926
+ }
6856
6927
  for (const m of role.msms ?? []) if (!registered.has(m)) issues.push(`role "${name}": msms "${m}" is not registered in mech-registry.json`);
6857
6928
  if (role.model && allowedModels && !allowedModels.has(role.model)) issues.push(`role "${name}": model "${role.model}" not in handyman.models (${[...allowedModels].join(", ") || "(none)"})`);
6858
6929
  }
@@ -6879,7 +6950,8 @@ function listSkiffRoles(root) {
6879
6950
  keeper: role.trajectory?.keeper === true,
6880
6951
  rebuild: role.trajectory?.rebuild === true
6881
6952
  },
6882
- hasSystemPrompt: Boolean(role.systemPrompt?.trim())
6953
+ promptSource: systemPromptSource(role),
6954
+ ...role.systemPromptFile ? { systemPromptFile: role.systemPromptFile } : {}
6883
6955
  }));
6884
6956
  return {
6885
6957
  roles: items,
@@ -6914,7 +6986,7 @@ function applySkiffConfig(root) {
6914
6986
  }
6915
6987
  const skiffAdminTool = defineTool({
6916
6988
  name: "skiff_admin",
6917
- description: "Skiff (F4, experimental): CCC cognitive-subset roles (subsets of the full serenity trajectory). guide: definition tutorial (concept/role schema/cognitive MSM writing/dual whitelist/trajectory subset/examples); validate: check the CCC skiff config (roles schema legal / msms registered / model in handyman.models / systemPrompt non-empty); apply: validate then confirm the config is live (bound CCC + role list; roles are read live on every request); list: role summary (name/model/msms/tools/trajectory). Roles are defined by the CCC in .opencode/serenity.json skiff.roles; skill loading is always available to skiff sessions (not whitelisted).",
6989
+ description: "Skiff (F4, experimental): CCC cognitive-subset roles (subsets of the full serenity trajectory). guide: definition tutorial (concept/role schema/cognitive MSM writing/dual whitelist/trajectory subset/examples); validate: check the CCC skiff config (roles schema legal / msms registered / model in handyman.models / system prompt resolvable — systemPromptFile (recommended) or systemPrompt non-empty); apply: validate then confirm the config is live (bound CCC + role list; roles are read live on every request); list: role summary (name/model/msms/tools/trajectory/prompt source). Roles are defined by the CCC in .opencode/serenity.json skiff.roles; skill loading is always available to skiff sessions (not whitelisted).",
6918
6990
  parameters: { action: {
6919
6991
  type: "string",
6920
6992
  enum: [
@@ -11084,6 +11156,13 @@ function skiffDebugPage(cccs, defaultRoot, webPort) {
11084
11156
  .sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
11085
11157
  .ccc { display: inline-block; padding: 3px 10px; border-radius: 999px; background: rgba(11,168,117,.12); color: #0ba875; font-size: 12px; font-weight: 600; margin-bottom: 12px; word-break: break-all; }
11086
11158
  @media (prefers-color-scheme: dark) { .ccc { color: #3ddc9a; } }
11159
+ .sessionbar { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
11160
+ .session-badge { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 12px; padding: 3px 10px; border-radius: 999px; background: rgba(127,127,127,.12); word-break: break-all; }
11161
+ .session-badge.continued { background: rgba(11,168,117,.12); color: #0ba875; }
11162
+ @media (prefers-color-scheme: dark) { .session-badge.continued { color: #3ddc9a; } }
11163
+ button.ghost { background: transparent; color: #57606a; border: 1px solid #d0d7de; padding: 4px 12px; font-size: 12px; border-radius: 8px; cursor: pointer; margin-top: 0; }
11164
+ @media (prefers-color-scheme: dark) { button.ghost { color: #8b949e; border-color: #3a3d42; } }
11165
+ button.ghost:hover { background: rgba(127,127,127,.1); }
11087
11166
  label { font-size: 13px; font-weight: 600; display: block; margin: 12px 0 4px; }
11088
11167
  select, textarea { width: 100%; box-sizing: border-box; padding: 8px 10px; border-radius: 8px; border: 1px solid #d0d7de; background: #fff; color: inherit; font-size: 14px; }
11089
11168
  @media (prefers-color-scheme: dark) { select, textarea { background: #26282c; border-color: #3a3d42; } }
@@ -11120,14 +11199,18 @@ function skiffDebugPage(cccs, defaultRoot, webPort) {
11120
11199
  <body>
11121
11200
  <main>
11122
11201
  <h1>Skiff Debug</h1>
11123
- <div class="sub">宁静号 trajectory 子集角色问答页(v1.25.9 实验性)— 多 CCC 可切换,走 DSH agent-loop 会话核心,回答 marked 服务端渲染 + think 折叠</div>
11202
+ <div class="sub">宁静号 trajectory 子集角色问答页(v1.25.10 实验性)— 多 CCC 可切换,同会话可追问(新对话按钮开新会话),回答 marked 服务端渲染 + think 折叠</div>
11124
11203
  <div class="ccc" id="cccBadge"></div>
11204
+ <div class="sessionbar">
11205
+ <span id="sessionBadge" class="session-badge muted">(新会话)</span>
11206
+ <button id="newChat" type="button" class="ghost">新对话</button>
11207
+ </div>
11125
11208
  <label for="ccc">认知容器</label>
11126
11209
  <select id="ccc"></select>
11127
11210
  <label for="role">角色</label>
11128
11211
  <select id="role"></select>
11129
11212
  <label for="q">问题</label>
11130
- <textarea id="q" placeholder="向该角色提问…"></textarea>
11213
+ <textarea id="q" placeholder="向该角色提问…(追问直接继续输入)"></textarea>
11131
11214
  <button id="ask">提问</button>
11132
11215
  <div id="answer" class="muted">等待提问…</div>
11133
11216
  <div id="trajectory"></div>
@@ -11140,10 +11223,26 @@ const defaultRoot = document.getElementById('skiff-data').dataset.default
11140
11223
  const cccSel = document.getElementById('ccc')
11141
11224
  const roleSel = document.getElementById('role')
11142
11225
  const badge = document.getElementById('cccBadge')
11226
+ const sessionBadge = document.getElementById('sessionBadge')
11227
+ const newChatBtn = document.getElementById('newChat')
11143
11228
  const btn = document.getElementById('ask')
11144
11229
  const answer = document.getElementById('answer')
11145
11230
  const traj = document.getElementById('trajectory')
11146
11231
  const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))
11232
+ let sessionId = ''
11233
+ function shortId(id) { return id.length > 24 ? id.slice(0, 18) + '…' + id.slice(-6) : id }
11234
+ function renderSessionBadge() {
11235
+ sessionBadge.textContent = sessionId ? '会话 ' + shortId(sessionId) + '(追问续接)' : '(新会话)'
11236
+ sessionBadge.className = 'session-badge' + (sessionId ? ' continued' : '')
11237
+ }
11238
+ function newConversation() {
11239
+ sessionId = ''
11240
+ renderSessionBadge()
11241
+ answer.className = 'muted'
11242
+ answer.textContent = '已开启新对话'
11243
+ traj.innerHTML = ''
11244
+ document.getElementById('q').value = ''
11245
+ }
11147
11246
  function currentCcc() {
11148
11247
  return CCCS.find((c) => c.root === cccSel.value) || (CCCS[0] || { root: '', name: '', roles: [] })
11149
11248
  }
@@ -11162,8 +11261,11 @@ function fillRoles() {
11162
11261
  ? c.roles.map((r) => '<option value="' + esc(r) + '">' + esc(r) + '</option>').join('')
11163
11262
  : '<option value="">(未配置角色)</option>'
11164
11263
  }
11165
- cccSel.addEventListener('change', fillRoles)
11264
+ cccSel.addEventListener('change', () => { fillRoles(); newConversation() })
11265
+ roleSel.addEventListener('change', () => newConversation())
11266
+ newChatBtn.addEventListener('click', newConversation)
11166
11267
  fillCccs()
11268
+ renderSessionBadge()
11167
11269
  btn.addEventListener('click', async () => {
11168
11270
  const c = currentCcc()
11169
11271
  const role = roleSel.value
@@ -11174,11 +11276,18 @@ btn.addEventListener('click', async () => {
11174
11276
  answer.textContent = '运行中…'
11175
11277
  traj.innerHTML = ''
11176
11278
  try {
11177
- const res = await fetch('/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ccc: c.root, role, question: q }) })
11279
+ const res = await fetch('/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ccc: c.root, role, question: q, sessionId: sessionId || undefined }) })
11178
11280
  const data = await res.json()
11179
- if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status))
11281
+ if (!res.ok) {
11282
+ // 会话不可恢复/绑定不匹配 → 前端清空 sessionId 重建(用户拍板:切换即新对话)
11283
+ sessionId = ''
11284
+ renderSessionBadge()
11285
+ throw new Error(data.error || ('HTTP ' + res.status))
11286
+ }
11180
11287
  answer.className = ''
11181
11288
  answer.innerHTML = data.answer_html || esc(data.answer || '') || '(空回答)'
11289
+ sessionId = data.sessionId || sessionId
11290
+ renderSessionBadge()
11182
11291
  renderTrajectory(data.trajectory || [], data.sessionId || '')
11183
11292
  } catch (err) {
11184
11293
  answer.className = 'err'
@@ -11288,6 +11397,7 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
11288
11397
  const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot;
11289
11398
  const roleName = typeof body.role === "string" ? body.role : "";
11290
11399
  const question = typeof body.question === "string" ? body.question : "";
11400
+ const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
11291
11401
  const role = readSkiffRoles(ccc).get(roleName);
11292
11402
  if (!roleName || !role) {
11293
11403
  sendJson(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
@@ -11297,11 +11407,30 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
11297
11407
  sendJson(res, 400, { error: "empty question" });
11298
11408
  return;
11299
11409
  }
11300
- const result = await askSkiff(ctx, (await createSkiffAgent(ctx, ccc, roleName, role, readHandymanConfig(ccc)?.defaultModel)).agent, question);
11410
+ let agent;
11411
+ let continued = false;
11412
+ if (sessionId) {
11413
+ const info = skiffSessionInfo(sessionId);
11414
+ const live = getSkiffAgent(sessionId);
11415
+ if (!info || !live) {
11416
+ sendJson(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
11417
+ return;
11418
+ }
11419
+ if (info.role !== roleName || info.ccc !== ccc) {
11420
+ sendJson(res, 400, { error: `session "${sessionId}" belongs to role "${info.role}" in another CCC — start a new conversation` });
11421
+ return;
11422
+ }
11423
+ agent = live;
11424
+ continued = true;
11425
+ }
11426
+ const hc = readHandymanConfig(ccc);
11427
+ if (!agent) agent = (await createSkiffAgent(ctx, ccc, roleName, role, hc?.defaultModel)).agent;
11428
+ const result = await askSkiff(ctx, agent, question, 0);
11301
11429
  sendJson(res, 200, {
11302
11430
  answer: result.answer,
11303
11431
  answer_html: renderSkiffMarkdown(result.answer),
11304
11432
  sessionId: result.sessionId,
11433
+ continued,
11305
11434
  trajectory: result.trajectory
11306
11435
  });
11307
11436
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.25.9",
3
+ "version": "1.25.11",
4
4
  "description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
5
5
  "license": "MIT",
6
6
  "repository": {