@shgroup/dsh-serenity-hooks 1.26.0 → 1.26.1

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.26.0",
3
+ "version": "1.26.1",
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/client.js CHANGED
@@ -3913,6 +3913,7 @@ Instruction-following style:
3913
3913
  const skiffPort = value?.skiffDebugPort ?? 3099;
3914
3914
  const acpOn = value?.acpEnabled ?? false;
3915
3915
  const acpPort = value?.acpHttpPort ?? 3100;
3916
+ const publicAskOn = value?.publicAskEnabled ?? false;
3916
3917
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3917
3918
  className: "ss-section",
3918
3919
  children: [
@@ -4002,30 +4003,41 @@ Instruction-following style:
4002
4003
  }) })]
4003
4004
  }),
4004
4005
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Group, {
4005
- title: "ACP",
4006
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4007
- title: "ACP JSON-RPC 端点",
4008
- desc: "程序化调用 Skiff 角色(指定 CCC+角色+会话;实验性,仅监听 127.0.0.1)",
4009
- control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
4010
- checked: acpOn,
4011
- onChange: (on) => toggle("acpEnabled", on)
4012
- })
4013
- }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4014
- title: "HTTP 端口",
4015
- desc: "ACP JSON-RPC 端口(1024 ~ 65535,默认 3100)",
4016
- control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4017
- className: "ss-threshold",
4018
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4019
- className: "ss-portInput",
4020
- type: "number",
4021
- min: 1024,
4022
- max: 65535,
4023
- value: acpPort,
4024
- disabled: !acpOn,
4025
- onChange: (e) => setAcpPort(Number(e.target.value))
4006
+ title: "ACP / 建议问答",
4007
+ children: [
4008
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4009
+ title: "ACP JSON-RPC 端点",
4010
+ desc: "程序化调用 Skiff 角色(指定 CCC+角色+会话;实验性,仅监听 127.0.0.1)",
4011
+ control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
4012
+ checked: acpOn,
4013
+ onChange: (on) => toggle("acpEnabled", on)
4026
4014
  })
4027
- })
4028
- }) })]
4015
+ }) }),
4016
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4017
+ title: "建议问答页",
4018
+ desc: "按认知容器暴露问答页供他人验证(需 key;首次开启自动生成固定 key,见 ~/.dsh/serenity-hooks.json)",
4019
+ control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
4020
+ checked: publicAskOn,
4021
+ onChange: (on) => toggle("publicAskEnabled", on)
4022
+ })
4023
+ }) }),
4024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4025
+ title: "HTTP 端口",
4026
+ desc: "ACP JSON-RPC + 问答页共用端口(1024 ~ 65535,默认 3100)",
4027
+ control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4028
+ className: "ss-threshold",
4029
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4030
+ className: "ss-portInput",
4031
+ type: "number",
4032
+ min: 1024,
4033
+ max: 65535,
4034
+ value: acpPort,
4035
+ disabled: !acpOn && !publicAskOn,
4036
+ onChange: (e) => setAcpPort(Number(e.target.value))
4037
+ })
4038
+ })
4039
+ }) })
4040
+ ]
4029
4041
  }),
4030
4042
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4031
4043
  className: "ss-group",
package/lib/index.js CHANGED
@@ -5259,7 +5259,8 @@ const simpleSettingsSchema = z.object({
5259
5259
  skiffEnabled: z.boolean().default(false),
5260
5260
  skiffDebugPort: z.number().min(1024).max(65535).default(3099),
5261
5261
  acpEnabled: z.boolean().default(false),
5262
- acpHttpPort: z.number().min(1024).max(65535).default(3100)
5262
+ acpHttpPort: z.number().min(1024).max(65535).default(3100),
5263
+ publicAskEnabled: z.boolean().default(false)
5263
5264
  });
5264
5265
  /** 从插件 Config 提取 entry 默认(settings base 层) */
5265
5266
  function entryDefaults(config) {
@@ -5271,7 +5272,8 @@ function entryDefaults(config) {
5271
5272
  skiffEnabled: config.skiff?.enabled ?? false,
5272
5273
  skiffDebugPort: config.skiff?.debugPort ?? 3099,
5273
5274
  acpEnabled: config.acp?.enabled ?? false,
5274
- acpHttpPort: config.acp?.httpPort ?? 3100
5275
+ acpHttpPort: config.acp?.httpPort ?? 3100,
5276
+ publicAskEnabled: config.publicAsk?.enabled ?? false
5275
5277
  };
5276
5278
  }
5277
5279
  /** 运行时源(installSettingsSection 注入:settings scope 或 entry fallback) */
@@ -5286,7 +5288,8 @@ function defaultSimpleSettings() {
5286
5288
  skiffEnabled: false,
5287
5289
  skiffDebugPort: 3099,
5288
5290
  acpEnabled: false,
5289
- acpHttpPort: 3100
5291
+ acpHttpPort: 3100,
5292
+ publicAskEnabled: false
5290
5293
  };
5291
5294
  }
5292
5295
  /**
@@ -7369,7 +7372,8 @@ function defaultAdvancedSettings() {
7369
7372
  persona: {
7370
7373
  mode: "",
7371
7374
  overrideText: ""
7372
- }
7375
+ },
7376
+ publicAsk: { key: "" }
7373
7377
  };
7374
7378
  }
7375
7379
  const SCRYPT_KEYLEN = 32;
@@ -7420,6 +7424,7 @@ function mergeWithDefaults(raw) {
7420
7424
  const rebuild = o.rebuild ?? {};
7421
7425
  const naming = o.naming ?? {};
7422
7426
  const persona = o.persona ?? {};
7427
+ const publicAsk = o.publicAsk ?? {};
7423
7428
  return {
7424
7429
  gateway: {
7425
7430
  enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
@@ -7439,7 +7444,8 @@ function mergeWithDefaults(raw) {
7439
7444
  persona: {
7440
7445
  mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
7441
7446
  overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
7442
- }
7447
+ },
7448
+ publicAsk: { key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key }
7443
7449
  };
7444
7450
  }
7445
7451
  /** 读取全局配置(文件缺失/坏 JSON → 默认值) */
@@ -7479,12 +7485,33 @@ function updateAdvancedSettings(patch) {
7479
7485
  persona: ps !== void 0 ? {
7480
7486
  mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
7481
7487
  overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
7482
- } : current.persona
7488
+ } : current.persona,
7489
+ publicAsk: patch.publicAsk !== void 0 ? { key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key } : current.publicAsk
7483
7490
  };
7484
7491
  writeAdvancedSettings(next);
7485
7492
  return next;
7486
7493
  }
7487
7494
  /**
7495
+ * 确保问答页访问 key 存在(首次启用自动生成 + 写回固定,用户拍板"配置时生成"):
7496
+ * 读取全局配置 publicAsk.key;为空 → 生成 32 字节随机 hex(64 字符)→ 写回持久化。
7497
+ * @returns 当前固定 key(已生成则返回既有值——**幂等**,不会覆盖用户手改的 key)
7498
+ */
7499
+ function ensurePublicAskKey() {
7500
+ const current = readAdvancedSettings();
7501
+ if (current.publicAsk.key !== "") return current.publicAsk.key;
7502
+ const key = randomBytes(32).toString("hex");
7503
+ updateAdvancedSettings({ publicAsk: { key } });
7504
+ return key;
7505
+ }
7506
+ /** 校验问答页访问 key(timing-safe;key 未生成 → 恒 false——没有 key 不工作) */
7507
+ function verifyPublicAskKey(provided) {
7508
+ const expected = readAdvancedSettings().publicAsk.key;
7509
+ if (!expected || typeof provided !== "string" || provided === "") return false;
7510
+ const a = Buffer.from(provided);
7511
+ const b = Buffer.from(expected);
7512
+ return a.length === b.length && timingSafeEqual(a, b);
7513
+ }
7514
+ /**
7488
7515
  * 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
7489
7516
  * 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
7490
7517
  * @param root - CCC 根(localstore.json 所在目录)
@@ -11126,7 +11153,7 @@ function sendJson$1(res, status, payload) {
11126
11153
  });
11127
11154
  res.end(body);
11128
11155
  }
11129
- function sendHtml(res, html) {
11156
+ function sendHtml$1(res, html) {
11130
11157
  res.writeHead(200, {
11131
11158
  "Content-Type": "text/html; charset=utf-8",
11132
11159
  "Cache-Control": "no-store"
@@ -11411,7 +11438,7 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
11411
11438
  try {
11412
11439
  const url = (req.url ?? "/").split("?")[0] ?? "/";
11413
11440
  if (req.method === "GET" && url === "/") {
11414
- sendHtml(res, skiffDebugPage(await discoverCccs(ctx, defaultRoot), defaultRoot, webPort));
11441
+ sendHtml$1(res, skiffDebugPage(await discoverCccs(ctx, defaultRoot), defaultRoot, webPort));
11415
11442
  return;
11416
11443
  }
11417
11444
  if (req.method === "POST" && url === "/ask") {
@@ -11653,12 +11680,16 @@ function rpcError(id, code, message) {
11653
11680
  //#endregion
11654
11681
  //#region src/acp-http.ts
11655
11682
  /**
11656
- * acp-http.ts — ACP HTTP JSON-RPC 端点(F4c,v1.26.0 实验性)
11683
+ * acp-http.ts — ACP HTTP JSON-RPC + 建议问答页(F4c/F4d,v1.26.x 实验性)
11657
11684
  *
11658
11685
  * node:http 服务(默认关,仅监听 127.0.0.1;启停 = 人工——设置面板「Serenity」
11659
- * 页 ACP 区块开关,不随插件加载自动启动;仿 skiff 调试服务模式)。
11686
+ * 页 ACP 区块开关 / 问答页开关,不随插件加载自动启动;仿 skiff 调试服务模式)。
11660
11687
  *
11661
- * - POST / → JSON-RPC 2.0 单帧/批处理(body JSON)→ JSON-RPC 响应数组
11688
+ * - POST / → JSON-RPC 2.0 单帧/批处理(acpEnabled 门控;ACP 程序化面)
11689
+ * - GET / → 建议问答页 HTML(publicAskEnabled 门控;F4d,S142 用户:
11690
+ * 按认知容器暴露问答页供他人验证——key 认证,无 key 不工作)
11691
+ * - POST /ask → { key, ccc, role?, question, sessionId? } → key 校验(timing-safe)
11692
+ * → acp-core 会话 → { answer, answer_html, sessionId, trajectory }
11662
11693
  *
11663
11694
  * 与 skiff 调试页(人工测试面)共享同一会话核心(acp-core → skiff-core);
11664
11695
  * 企业微信桥(F4c-2)同进程直调 acp-core 处理器函数,不经本端点。
@@ -11684,15 +11715,23 @@ function sendJson(res, status, payload) {
11684
11715
  });
11685
11716
  res.end(body);
11686
11717
  }
11718
+ function sendHtml(res, html) {
11719
+ res.writeHead(200, {
11720
+ "Content-Type": "text/html; charset=utf-8",
11721
+ "Cache-Control": "no-store"
11722
+ });
11723
+ res.end(html);
11724
+ }
11687
11725
  /**
11688
- * 启动 ACP HTTP JSON-RPC 端点(单实例;重复启动幂等返回既有实例)。
11726
+ * 启动 ACP HTTP JSON-RPC + 建议问答页服务(单实例;重复启动幂等返回既有实例)。
11689
11727
  * @param ctx 插件上下文(AcpServer 内部经 skiff-core 创建 agent)
11690
11728
  * @param port 监听端口(仅 127.0.0.1)
11729
+ * @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
11691
11730
  */
11692
- async function startAcpHttpServer(ctx, port) {
11731
+ async function startAcpHttpServer(ctx, port, defaultRoot) {
11693
11732
  if (active) return;
11694
11733
  const server = createServer((req, res) => {
11695
- handle(ctx, req, res);
11734
+ handle(ctx, defaultRoot ?? "", req, res);
11696
11735
  });
11697
11736
  await new Promise((resolve, reject) => {
11698
11737
  server.once("error", reject);
@@ -11702,7 +11741,11 @@ async function startAcpHttpServer(ctx, port) {
11702
11741
  server,
11703
11742
  port
11704
11743
  };
11705
- console.log(`[serenity-hooks] ACP HTTP JSON-RPC: http://127.0.0.1:${port}(session/new 支持 {ccc, role, sessionId?})`);
11744
+ const s = readSimpleSettings();
11745
+ const faces = [];
11746
+ if (s.acpEnabled) faces.push("JSON-RPC");
11747
+ if (s.publicAskEnabled) faces.push("问答页(key 认证)");
11748
+ console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
11706
11749
  }
11707
11750
  function stopAcpHttpServer() {
11708
11751
  if (!active) return;
@@ -11711,10 +11754,15 @@ function stopAcpHttpServer() {
11711
11754
  } catch {}
11712
11755
  active = null;
11713
11756
  }
11714
- async function handle(ctx, req, res) {
11757
+ async function handle(ctx, defaultRoot, req, res) {
11715
11758
  try {
11716
11759
  const url = (req.url ?? "/").split("?")[0] ?? "/";
11760
+ const s = readSimpleSettings();
11717
11761
  if (req.method === "POST" && url === "/") {
11762
+ if (!s.acpEnabled) {
11763
+ sendJson(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
11764
+ return;
11765
+ }
11718
11766
  let body;
11719
11767
  try {
11720
11768
  body = JSON.parse(await readBody(req));
@@ -11732,11 +11780,177 @@ async function handle(ctx, req, res) {
11732
11780
  sendJson(res, 200, await dispatchRpc(new AcpServer(ctx), body));
11733
11781
  return;
11734
11782
  }
11783
+ if (req.method === "GET" && url === "/") {
11784
+ if (!s.publicAskEnabled) {
11785
+ sendHtml(res, publicAskDisabledHtml());
11786
+ return;
11787
+ }
11788
+ sendHtml(res, publicAskPage(await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? "")), readSimpleSettings() && hasPublicAskKey()));
11789
+ return;
11790
+ }
11791
+ if (req.method === "POST" && url === "/ask") {
11792
+ if (!s.publicAskEnabled) {
11793
+ sendJson(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
11794
+ return;
11795
+ }
11796
+ let body;
11797
+ try {
11798
+ const parsed = JSON.parse(await readBody(req));
11799
+ if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
11800
+ body = parsed;
11801
+ } catch {
11802
+ sendJson(res, 400, { error: "invalid JSON body" });
11803
+ return;
11804
+ }
11805
+ if (!verifyPublicAskKey(typeof body.key === "string" ? body.key : void 0)) {
11806
+ sendJson(res, 401, { error: "invalid or missing key" });
11807
+ return;
11808
+ }
11809
+ const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot || "";
11810
+ const roleName = typeof body.role === "string" && body.role !== "" ? body.role : void 0;
11811
+ const question = typeof body.question === "string" ? body.question : "";
11812
+ const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
11813
+ if (!ccc) {
11814
+ sendJson(res, 400, { error: "ccc required" });
11815
+ return;
11816
+ }
11817
+ if (!question.trim()) {
11818
+ sendJson(res, 400, { error: "empty question" });
11819
+ return;
11820
+ }
11821
+ const roles = readSkiffRoles(ccc);
11822
+ const explicit = roleName ? roles.get(roleName) : void 0;
11823
+ const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
11824
+ const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
11825
+ if (!effectiveRole || !effectiveCfg) {
11826
+ sendJson(res, 400, { error: `no skiff role available in ccc: ${ccc}` });
11827
+ return;
11828
+ }
11829
+ let agent;
11830
+ let continued = false;
11831
+ if (sessionId) {
11832
+ const info = skiffSessionInfo(sessionId);
11833
+ const live = getSkiffAgent(sessionId);
11834
+ if (info && live && info.role === effectiveRole && info.ccc === ccc) {
11835
+ agent = live;
11836
+ continued = true;
11837
+ }
11838
+ }
11839
+ if (!agent) agent = (await createSkiffAgent(ctx, ccc, effectiveRole, effectiveCfg, readHandymanConfig(ccc)?.defaultModel)).agent;
11840
+ const result = await askSkiff(ctx, agent, question, 0);
11841
+ sendJson(res, 200, {
11842
+ answer: result.answer,
11843
+ answer_html: renderSkiffMarkdown(result.answer),
11844
+ sessionId: result.sessionId,
11845
+ continued,
11846
+ trajectory: result.trajectory
11847
+ });
11848
+ return;
11849
+ }
11735
11850
  sendJson(res, 404, { error: "not found" });
11736
11851
  } catch (err) {
11737
11852
  sendJson(res, 500, { error: err?.message ?? String(err) });
11738
11853
  }
11739
11854
  }
11855
+ function hasPublicAskKey() {
11856
+ try {
11857
+ return ensurePublicAskKey() !== "";
11858
+ } catch {
11859
+ return false;
11860
+ }
11861
+ }
11862
+ /** 问答页未启用提示页 */
11863
+ function publicAskDisabledHtml() {
11864
+ return `<!DOCTYPE html>
11865
+ <html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
11866
+ <body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
11867
+ <h2>Serenity Public Ask</h2>
11868
+ <p>问答页未启用(请在 dsh 设置面板「Serenity」页开启「建议问答」)。</p>
11869
+ </body></html>`;
11870
+ }
11871
+ /** 建议问答页(F4d):key 输入 + CCC 选择 + 问题 + 答案(marked 服务端渲染) */
11872
+ function publicAskPage(cccs, _hasKey) {
11873
+ return `<!DOCTYPE html>
11874
+ <html lang="zh">
11875
+ <head>
11876
+ <meta charset="utf-8">
11877
+ <meta name="viewport" content="width=device-width, initial-scale=1">
11878
+ <title>Serenity Public Ask</title>
11879
+ <style>
11880
+ :root { color-scheme: light dark; }
11881
+ body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 24px; background: #f6f7f9; color: #1f2328; }
11882
+ @media (prefers-color-scheme: dark) { body { background: #1a1b1e; color: #e6e6e6; } }
11883
+ main { max-width: 720px; margin: 0 auto; }
11884
+ h1 { font-size: 18px; margin: 0 0 4px; }
11885
+ .sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
11886
+ label { font-size: 13px; font-weight: 600; display: block; margin: 12px 0 4px; }
11887
+ input, 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; }
11888
+ @media (prefers-color-scheme: dark) { input, select, textarea { background: #26282c; border-color: #3a3d42; } }
11889
+ textarea { min-height: 72px; resize: vertical; }
11890
+ button { margin-top: 12px; padding: 9px 18px; border-radius: 8px; border: 0; background: #0ba875; color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; }
11891
+ button:disabled { opacity: .55; cursor: wait; }
11892
+ #answer { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; padding: 12px; margin-top: 16px; font-size: 14px; line-height: 1.6; min-height: 48px; word-break: break-word; }
11893
+ @media (prefers-color-scheme: dark) { #answer { background: #26282c; border-color: #3a3d42; } }
11894
+ #answer p { margin: 6px 0; }
11895
+ #answer h1, #answer h2, #answer h3 { margin: 12px 0 6px; font-size: 15px; }
11896
+ #answer code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, monospace; }
11897
+ #answer pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; }
11898
+ #answer ul, #answer ol { margin: 6px 0; padding-left: 22px; }
11899
+ .err { color: #cf222e; white-space: pre-wrap; }
11900
+ .muted { opacity: .6; font-size: 13px; }
11901
+ details.think { margin: 8px 0; border: 1px solid rgba(210,153,34,.4); border-radius: 8px; background: rgba(210,153,34,.06); }
11902
+ details.think summary { cursor: pointer; padding: 6px 10px; font-size: 12px; color: #d29922; font-weight: 600; }
11903
+ </style>
11904
+ </head>
11905
+ <body>
11906
+ <main>
11907
+ <h1>Serenity Public Ask</h1>
11908
+ <div class="sub">认知容器问答验证页 — 选择认知容器后提问(访问需 key)</div>
11909
+ <label for="key">访问 Key</label>
11910
+ <input id="key" type="password" placeholder="请输入访问 key" autocomplete="off">
11911
+ <label for="ccc">认知容器</label>
11912
+ <select id="ccc"></select>
11913
+ <label for="q">问题</label>
11914
+ <textarea id="q" placeholder="向该认知容器提问…"></textarea>
11915
+ <button id="ask">提问</button>
11916
+ <div id="answer" class="muted">等待提问…</div>
11917
+ </main>
11918
+ <script id="ask-data" type="application/json">${JSON.stringify(cccs).replace(/</g, "\\u003c")}<\/script>
11919
+ <script>
11920
+ const CCCS = JSON.parse(document.getElementById('ask-data').textContent)
11921
+ const keyInput = document.getElementById('key')
11922
+ const cccSel = document.getElementById('ccc')
11923
+ const btn = document.getElementById('ask')
11924
+ const answer = document.getElementById('answer')
11925
+ let sessionId = ''
11926
+ cccSel.innerHTML = CCCS.map((c) => '<option value="' + c.root + '">' + c.name + (c.roles.length ? ' (' + c.roles.length + ' 角色)' : '') + '</option>').join('') || '<option value="">(未发现认知容器)</option>'
11927
+ btn.addEventListener('click', async () => {
11928
+ const ccc = cccSel.value
11929
+ const q = document.getElementById('q').value.trim()
11930
+ const key = keyInput.value.trim()
11931
+ if (!ccc || !q) { answer.className = 'err'; answer.textContent = '请选择认知容器并输入问题'; return }
11932
+ if (!key) { answer.className = 'err'; answer.textContent = '请输入访问 key(没有 key 无法使用)'; return }
11933
+ btn.disabled = true
11934
+ answer.className = 'muted'
11935
+ answer.textContent = '运行中…'
11936
+ try {
11937
+ const res = await fetch('/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, ccc, question: q, sessionId: sessionId || undefined }) })
11938
+ const data = await res.json()
11939
+ if (!res.ok) { sessionId = ''; throw new Error(data.error || ('HTTP ' + res.status)) }
11940
+ answer.className = ''
11941
+ answer.innerHTML = data.answer_html || data.answer || '(空回答)'
11942
+ sessionId = data.sessionId || ''
11943
+ } catch (err) {
11944
+ answer.className = 'err'
11945
+ answer.textContent = String(err.message || err)
11946
+ } finally {
11947
+ btn.disabled = false
11948
+ }
11949
+ })
11950
+ <\/script>
11951
+ </body>
11952
+ </html>`;
11953
+ }
11740
11954
  //#endregion
11741
11955
  //#region src/index.ts
11742
11956
  const name = "dsh-serenity-hooks";
@@ -11882,20 +12096,24 @@ function readWebPort(ctx) {
11882
12096
  }
11883
12097
  }
11884
12098
  /**
11885
- * F4c ACP HTTP JSON-RPC 端点装配(v1.26.0 实验性):启停 = 人工(设置面板
11886
- * 「Serenity」页 ACP 区块开关,settings 持久化)。会话创建/延续走 acp-core →
11887
- * skiff-core(与调试页同核心);仅监听 127.0.0.1;未开启零资源占用。
12099
+ * F4c ACP HTTP + F4d 建议问答页装配(v1.26.x 实验性):启停 = 人工(设置面板
12100
+ * 「Serenity」页 ACP / 问答页开关,settings 持久化)。任一面开启即启动服务
12101
+ * (同一端口:POST / = JSON-RPC 需 acpEnabled;GET / + /ask = 问答页需 publicAskEnabled)。
12102
+ * 会话创建/延续走 acp-core → skiff-core;仅监听 127.0.0.1;未开启零资源占用。
11888
12103
  */
11889
12104
  function registerAcp(ctx) {
11890
12105
  let started = false;
11891
12106
  const sync = () => {
11892
12107
  const s = readSimpleSettings();
11893
- if (s.acpEnabled && !started) startAcpHttpServer(ctx, s.acpHttpPort).then(() => {
11894
- started = true;
11895
- }).catch((err) => {
11896
- console.error(`[serenity-hooks] ACP HTTP 端点启动失败: ${String(err?.message ?? err)}`);
11897
- });
11898
- else if (!s.acpEnabled && started) {
12108
+ const anyFace = s.acpEnabled || s.publicAskEnabled;
12109
+ if (anyFace && !started) {
12110
+ const root = resolveSkiffRoot(ctx);
12111
+ startAcpHttpServer(ctx, s.acpHttpPort, root ?? void 0).then(() => {
12112
+ started = true;
12113
+ }).catch((err) => {
12114
+ console.error(`[serenity-hooks] ✗ ACP HTTP 服务启动失败: ${String(err?.message ?? err)}`);
12115
+ });
12116
+ } else if (!anyFace && started) {
11899
12117
  stopAcpHttpServer();
11900
12118
  started = false;
11901
12119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.26.0",
3
+ "version": "1.26.1",
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": {