@shgroup/dsh-serenity-hooks 1.57.0 → 1.57.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/lib/index.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
- import { a as hostSessions, c as hostWeb, i as hostService, l as hostWebServer, n as hostInjected, o as hostSettings, r as hostLlm, s as hostSubagents, t as hostAgents } from "./access-CNccNtgf.js";
2
+ import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-GIJ3A_tw.js";
3
3
  import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readTrajectorySkills, i as findGitRoot, l as pathInside, m as readSafeModeHiddenTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readExclusiveTools, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-B0ZGL9lx.js";
4
- import { i as cccRootForExec, n as agentCwdFor, o as listCccs, r as cccRootForCwd, t as NO_CCC_FROM_AGENT_CWD } from "./ccc-roots-BC8mn8iM.js";
5
- import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-BcCRsEOh.js";
4
+ import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-B4P3Uw6D.js";
6
5
  import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-B9gU1YXz.js";
7
- import { a as unregisterSkiffSession$1, i as skiffSessionSnapshot$1, n as skiffRoleFor$1, o as ACC_VERSION, r as skiffSessionInfo$1, t as registerSkiffSession$1 } from "./skiff-registry-BjIX_KD1.js";
6
+ import { a as GATEWAY_PORT, c as SKIFF_DEBUG_PORT, d as skiffRoleFor$1, f as skiffSessionInfo$1, i as FACE_PORTS, l as WEIXIN_SEND_PORT, m as ACC_VERSION, n as readSimpleSettings, o as MAIN_WEB_PORT, p as unregisterSkiffSession$1, r as registerSettingsSection, s as MECHANISM_PORTS, u as registerSkiffSession$1 } from "./settings-section-DQIVsTqE.js";
8
7
  import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-B615L9MV.js";
9
- import { a as ACP_HTTP_PORT, c as MAIN_WEB_PORT, d as WEIXIN_SEND_PORT, l as MECHANISM_PORTS, n as readSimpleSettings, o as FACE_PORTS, r as registerSettingsSection, s as GATEWAY_PORT, u as SKIFF_DEBUG_PORT } from "./settings-section-xHABl8nk.js";
10
8
  import { REGISTERED_TOOLS } from "./invariant.js";
11
9
  import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-BrRcPiQs.js";
12
10
  import { createRequire } from "node:module";
@@ -1086,7 +1084,8 @@ Directory of ACC capabilities — each area lists where to go for details (guide
1086
1084
  cheap-worker(杂工 agent 编排——guide 子命令:cheap-worker guide)
1087
1085
  专属工具(**默认对所有 CCC 隐藏**,只有在自己 exclusiveTools 里点名的 CCC 可见)→ acc-diag(运行态诊断)
1088
1086
  ④ 角色与对外面 → container_admin role(Skiff 认知子集角色:guide/validate/apply/list——skiff_admin 已并入)
1089
- ACP(程序化 JSON-RPC 3100)/ Skiff 问答页(公网 ask)——面板「外部能力」组
1087
+ 3099 Skiff 调试问答页(人工调试面)。🔴 2026-10-15:自造 ACP 面已退役
1088
+ ⇒ 外部程序化接入改走官方 dsh --profile acp + 现成 ACP 客户端
1090
1089
  ⑤ 自主与接入 → container_trajectory send-now(跨轨迹的**即时**递送,**不排队**:live 目标在跑就当场注入当前轮、空闲则立即起轮;非 live 冷载入——等效于直接 wake;**有同步回执**)
1091
1090
  container_trajectory send-later(跨轨迹的"未来时刻 + 一条 message";fire-and-forget,无回执无回收)
1092
1091
  weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:msm("weixin-doctor", ["guide"]))
@@ -2943,10 +2942,6 @@ function defaultAdvancedSettings() {
2943
2942
  mode: "",
2944
2943
  overrideText: ""
2945
2944
  },
2946
- publicAsk: {
2947
- key: "",
2948
- allowed: []
2949
- },
2950
2945
  weixinApi: {
2951
2946
  enabled: true,
2952
2947
  port: WEIXIN_SEND_PORT
@@ -2999,7 +2994,6 @@ function mergeWithDefaults(raw) {
2999
2994
  const o = raw;
3000
2995
  const gateway = o.gateway ?? {};
3001
2996
  const persona = o.persona ?? {};
3002
- const publicAsk = o.publicAsk ?? {};
3003
2997
  const weixinApi = o.weixinApi ?? {};
3004
2998
  return {
3005
2999
  gateway: {
@@ -3016,10 +3010,6 @@ function mergeWithDefaults(raw) {
3016
3010
  mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
3017
3011
  overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
3018
3012
  },
3019
- publicAsk: {
3020
- key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
3021
- allowed: Array.isArray(publicAsk.allowed) ? publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : def.publicAsk.allowed
3022
- },
3023
3013
  weixinApi: {
3024
3014
  enabled: typeof weixinApi.enabled === "boolean" ? weixinApi.enabled : def.weixinApi.enabled,
3025
3015
  port: typeof weixinApi.port === "number" && Number.isInteger(weixinApi.port) && weixinApi.port >= 0 ? weixinApi.port : def.weixinApi.port
@@ -3057,10 +3047,6 @@ function updateAdvancedSettings(patch) {
3057
3047
  mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
3058
3048
  overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
3059
3049
  } : current.persona,
3060
- publicAsk: patch.publicAsk !== void 0 ? {
3061
- key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
3062
- allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
3063
- } : current.publicAsk,
3064
3050
  weixinApi: patch.weixinApi !== void 0 ? {
3065
3051
  enabled: typeof patch.weixinApi.enabled === "boolean" ? patch.weixinApi.enabled : current.weixinApi.enabled,
3066
3052
  port: typeof patch.weixinApi.port === "number" && Number.isInteger(patch.weixinApi.port) && patch.weixinApi.port >= 0 ? patch.weixinApi.port : current.weixinApi.port
@@ -3070,87 +3056,6 @@ function updateAdvancedSettings(patch) {
3070
3056
  return next;
3071
3057
  }
3072
3058
  /**
3073
- * 确保问答页访问 key 存在(首次启用自动生成 + 写回固定,用户拍板"配置时生成"):
3074
- * 读取全局配置 publicAsk.key;为空 → 生成 32 字节随机 hex(64 字符)→ 写回持久化。
3075
- * @returns 当前固定 key(已生成则返回既有值——**幂等**,不会覆盖用户手改的 key)
3076
- */
3077
- function ensurePublicAskKey() {
3078
- const current = readAdvancedSettings();
3079
- if (current.publicAsk.key !== "") return current.publicAsk.key;
3080
- const key = randomBytes(32).toString("hex");
3081
- updateAdvancedSettings({ publicAsk: {
3082
- key,
3083
- allowed: current.publicAsk.allowed
3084
- } });
3085
- return key;
3086
- }
3087
- /** 校验问答页访问 key(timing-safe;key 未生成 → 恒 false——没有 key 不工作) */
3088
- function verifyPublicAskKey(provided) {
3089
- const expected = readAdvancedSettings().publicAsk.key;
3090
- if (!expected || typeof provided !== "string" || provided === "") return false;
3091
- const a = Buffer.from(provided);
3092
- const b = Buffer.from(expected);
3093
- return a.length === b.length && timingSafeEqual(a, b);
3094
- }
3095
- /**
3096
- * 轮换问答页访问 key(v1.26.5,S142 用户:公网开放 + 不好说要换 key 呢):
3097
- * 生成新 32 字节随机 hex(64 字符)→ 覆盖写回(**强制替换**——旧 key 立即失效)。
3098
- * @returns 新 key(面板展示给管理员,重新分享给使用者)
3099
- */
3100
- function rotatePublicAskKey() {
3101
- const current = readAdvancedSettings();
3102
- const key = randomBytes(32).toString("hex");
3103
- updateAdvancedSettings({ publicAsk: {
3104
- key,
3105
- allowed: current.publicAsk.allowed
3106
- } });
3107
- return key;
3108
- }
3109
- /** 失败锁定阈值(连续失败次数) */
3110
- const PUBLIC_ASK_FAIL_THRESHOLD = 5;
3111
- /** 首次锁定基础时长(指数退避底数) */
3112
- const PUBLIC_ASK_LOCK_BASE_MS = 9e5;
3113
- /** 锁定上限 */
3114
- const PUBLIC_ASK_LOCK_MAX_MS = 144e5;
3115
- const ipFailStates = /* @__PURE__ */ new Map();
3116
- /** 重置 IP 失败状态(测试/管理员解封) */
3117
- function resetPublicAskIpFail(ip) {
3118
- ipFailStates.delete(ip);
3119
- }
3120
- /** IP 当前是否锁定(到期自动解锁) */
3121
- function isPublicAskIpLocked(ip) {
3122
- const st = ipFailStates.get(ip);
3123
- if (!st || st.lockedUntil === 0) return false;
3124
- if (Date.now() >= st.lockedUntil) {
3125
- st.lockedUntil = 0;
3126
- st.count = 0;
3127
- return false;
3128
- }
3129
- return true;
3130
- }
3131
- /** 记录一次 key 校验失败;达到阈值 → 锁定(指数退避)。返回锁定剩余毫秒(0 = 未锁定) */
3132
- function recordPublicAskFail(ip) {
3133
- let st = ipFailStates.get(ip);
3134
- if (!st) {
3135
- st = {
3136
- count: 0,
3137
- lockedUntil: 0,
3138
- lockRound: 0
3139
- };
3140
- ipFailStates.set(ip, st);
3141
- }
3142
- if (st.lockedUntil > 0 && Date.now() < st.lockedUntil) return st.lockedUntil - Date.now();
3143
- st.count += 1;
3144
- if (st.count >= PUBLIC_ASK_FAIL_THRESHOLD) {
3145
- const base = PUBLIC_ASK_LOCK_BASE_MS * 2 ** st.lockRound;
3146
- st.lockedUntil = Date.now() + Math.min(base, PUBLIC_ASK_LOCK_MAX_MS);
3147
- st.lockRound += 1;
3148
- st.count = 0;
3149
- return st.lockedUntil - Date.now();
3150
- }
3151
- return 0;
3152
- }
3153
- /**
3154
3059
  * 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
3155
3060
  * 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
3156
3061
  * @param root - CCC 根(localstore.json 所在目录)
@@ -3183,8 +3088,7 @@ function toWire(settings) {
3183
3088
  allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
3184
3089
  totpEnabled: settings.gateway.totpEnabled
3185
3090
  },
3186
- persona: settings.persona,
3187
- publicAsk: { allowed: [...settings.publicAsk.allowed] }
3091
+ persona: settings.persona
3188
3092
  };
3189
3093
  }
3190
3094
  /**
@@ -3253,11 +3157,6 @@ function applyWirePatch(wire) {
3253
3157
  if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
3254
3158
  patch.persona = psPatch;
3255
3159
  }
3256
- if (wire.publicAsk !== void 0 && Array.isArray(wire.publicAsk.allowed)) {
3257
- const paPatch = { ...current.publicAsk };
3258
- paPatch.allowed = wire.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "");
3259
- patch.publicAsk = paPatch;
3260
- }
3261
3160
  return updateAdvancedSettings(patch);
3262
3161
  }
3263
3162
  //#endregion
@@ -3877,15 +3776,6 @@ function unregisterSkiffSession(sessionId) {
3877
3776
  skiffAgents.delete(sessionId);
3878
3777
  unregisterSkiffSession$1(sessionId);
3879
3778
  }
3880
- /** 测试/调试:注册表快照(sessionId → {role} 兼容展示) */
3881
- function skiffSessionSnapshot() {
3882
- const out = /* @__PURE__ */ new Map();
3883
- for (const [id, b] of skiffSessionSnapshot$1()) out.set(id, {
3884
- role: b.role,
3885
- ccc: b.ccc
3886
- });
3887
- return out;
3888
- }
3889
3779
  /** 自动创建的 bound 标记(readLastBound 恢复判定——只恢复本机制建的;手工 use 的旧 bound 不认) */
3890
3780
  const AUTO_BOUND_NOTE_PREFIX = "auto-created for skiff role";
3891
3781
  /**
@@ -3974,7 +3864,7 @@ function ensureWorkspacePromptSection(agent, scope) {
3974
3864
  *
3975
3865
  * 判定链:
3976
3866
  * ⓪ **临时身份不建工作台**(v1.30.9,S142 用户报"SESSION 爆炸"根治):`persistent=false`
3977
- * (ACP/调试页/问答页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
3867
+ * (调试页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
3978
3868
  * 调用都是新 id、无身份延续,为其建 SESSION = 每次问答留一个永久空目录(无界熵增);
3979
3869
  * 用户模型「skiff 本质是临时会话机制」——工作台属于**持久身份**,不属于单次调用。
3980
3870
  * ① 角色未启用 session 能力(trajectory.session !== true)→ null(现状零变化)
@@ -4043,10 +3933,10 @@ const SKIFF_PRESET = "standard";
4043
3933
  * 指定 sessionId 且磁盘已有持久化 log → `ctx.agents.resume`(DSH 持久化语义:
4044
3934
  * sessionId 即身份,已持久化会话必须 resume——历史 + turn 续号延续,同用户长期记忆保留);
4045
3935
  * 无 log(首次)/持久化未配置 → `ctx.agents.create`(新建)。不指定 sessionId →
4046
- * 随机 id 恒 create(ACP/调试页默认路径)。
3936
+ * 随机 id 恒 create(调试页默认路径)。
4047
3937
  *
4048
3938
  * @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现用户↔会话长期映射——
4049
- * 不传则随机生成 skiff-<role>-<uuid>,ACP/调试页默认路径)
3939
+ * 不传则随机生成 skiff-<role>-<uuid>,调试页默认路径)
4050
3940
  */
4051
3941
  async function createSkiffAgent(ctx, root, roleName, role, defaultModel, sessionId) {
4052
3942
  if (!ctx.agents) throw new Error("skiff: ctx.agents unavailable");
@@ -4257,8 +4147,9 @@ function truncate(s, n) {
4257
4147
  * 提问一轮:followup → 等 idle → 读答案 + 轨迹。
4258
4148
  * @param eventsStart 轨迹起点:显式传 0 = 全量轨迹(会话追问时页面重绘完整时间线,
4259
4149
  * v1.25.10 用户拍板);不传(undefined)= 本轮增量(followup 前 events 之后)
4260
- * @param options.includeTrajectory 是否计算轨迹(v1.26.10:**3100 对外只提供问答**——
4261
- * 公开问答页 / ACP JSON-RPC 不返回 trajectory,传 false 跳过计算;3099 调试页默认 true 保留)
4150
+ * @param options.includeTrajectory 是否计算轨迹(v1.26.10 起供**对外面**传 false 跳过计算——
4151
+ * 原消费方 = 3100 公开问答页 / ACP JSON-RPC,**该面已于 2026-10-15 退役** ⇒ 现无生产调用方传 false;
4152
+ * 3099 调试页默认 true 保留)
4262
4153
  */
4263
4154
  async function askSkiff(ctx, agent, question, eventsStart, options) {
4264
4155
  const before = eventsStart === void 0 ? sessionEvents(agent.session).length : eventsStart;
@@ -12275,11 +12166,10 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
12275
12166
 
12276
12167
  ── 运行 ──
12277
12168
  调试:设置面板「Serenity」页 Skiff 区块开启 → http://127.0.0.1:<debugPort> 问答页实测
12278
- ACP 程序化面(v1.26.0):设置面板「ACP」区块开启 → HTTP JSON-RPC 端点(默认 3100)——
12279
- POST / {jsonrpc, id, method, params}
12280
- session/new {ccc, role, sessionId?} → 创建/延续会话(sessionId 可选=进程内追问)
12281
- session/prompt {sessionId, question} → {answer, trajectory}
12282
- session/cancel / session/close / session/list / request_permission(恒 allow)
12169
+ 🔴 外部程序化接入(2026-10-15 起改走官方):本插件自造的 ACP 面(HTTP JSON-RPC 3100 /
12170
+ 建议问答页)**已整体退役**(owner 裁「砍掉外部问答」)。现在要程序化/远程接入,走宿主
12171
+ 自带的官方 ACP server:dsh --profile acp(纯 stdio、零认证)+ 任一 ACP 客户端
12172
+ (手机端如 Agmente / ACP UI)经隧道接入。详见 S142 的接入指南文档。
12283
12173
  启停 = 人工(设置面板开关,不随插件加载自动启动);未开启零资源占用
12284
12174
  生效机制:角色配置**实时读取**(改 .opencode/serenity.json → 刷新调试页即生效);
12285
12175
  改配置后用 container_admin role apply 做显式校验 + 应用确认(绑定 CCC + 角色清单)
@@ -12660,7 +12550,9 @@ function claimsUserNotification(text) {
12660
12550
  /**
12661
12551
  * **不许开**的会话(设计 §3 的"建议写死",本版**确实写死**):
12662
12552
  * `skiff-`(F4 问答 / **微信桥会话也是这个前缀** —— 桥那侧的"用户"是**家人**,
12663
- * 代理替家人说话 = **冒充**)| `acp-`(对外程序化面)| `cheap-worker-`(自主 worker,已有自己的完成码循环)。
12553
+ * 代理替家人说话 = **冒充**)| `acp-`(对外程序化面 —— ⚠️ **2026-10-15 起已成死判据**:
12554
+ * 该前缀从无生产者且面 E 已退役;理由与"为何保留"见 `output-guard-seam.ts` 文件头)|
12555
+ * `cheap-worker-`(自主 worker,已有自己的完成码循环)。
12664
12556
  *
12665
12557
  * ⚠️ **`rebuild-` 不在排除面**(与 `output-guard-seam.ts` 的取向**刻意不同**):重建会话是
12666
12558
  * **同一轨迹的载体**(内部维护面),不是对外面;把它排除会让"重建后就没人推了"。
@@ -14733,7 +14625,6 @@ const UPLOAD_PATH = "/serenity/image-upload";
14733
14625
  const FILE_UPLOAD_PATH = "/serenity/file-upload";
14734
14626
  const CONFIG_PATH = "/serenity/config";
14735
14627
  const CCCS_PATH = "/serenity/cccs";
14736
- const PUBLIC_ASK_PATH = "/serenity/public-ask";
14737
14628
  const WEIXIN_PATH = "/serenity/weixin";
14738
14629
  /** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
14739
14630
  const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
@@ -14817,7 +14708,7 @@ function saveFileToTmp(root, fileName, data) {
14817
14708
  writeFileSync(join(dir, filename), bytes);
14818
14709
  return `${FILE_UPLOAD_DIR}/${filename}`;
14819
14710
  }
14820
- function readBody$4(req, maxBytes) {
14711
+ function readBody$3(req, maxBytes) {
14821
14712
  return new Promise((resolve, reject) => {
14822
14713
  let data = "";
14823
14714
  req.on("data", (chunk) => {
@@ -14831,7 +14722,7 @@ function readBody$4(req, maxBytes) {
14831
14722
  req.on("error", reject);
14832
14723
  });
14833
14724
  }
14834
- function sendJson$4(res, code, body) {
14725
+ function sendJson$3(res, code, body) {
14835
14726
  const payload = JSON.stringify(body);
14836
14727
  res.writeHead(code, {
14837
14728
  "content-type": "application/json; charset=utf-8",
@@ -14856,7 +14747,7 @@ function senderIsWebUi(req) {
14856
14747
  */
14857
14748
  function requireWebUi(req, res, message) {
14858
14749
  if (senderIsWebUi(req)) return true;
14859
- sendJson$4(res, 403, { error: message });
14750
+ sendJson$3(res, 403, { error: message });
14860
14751
  return false;
14861
14752
  }
14862
14753
  /**
@@ -14889,11 +14780,11 @@ function registerStatusApi(ctx, opts = {}) {
14889
14780
  handler: async (req, res) => {
14890
14781
  try {
14891
14782
  if (req.method !== "POST") {
14892
- sendJson$4(res, 405, { error: "method not allowed" });
14783
+ sendJson$3(res, 405, { error: "method not allowed" });
14893
14784
  return;
14894
14785
  }
14895
14786
  if (!requireWebUi(req, res, "图片落盘仅限 WebUI(client 专用)")) return;
14896
- const raw = await readBody$4(req, 20971520);
14787
+ const raw = await readBody$3(req, 20971520);
14897
14788
  const body = JSON.parse(raw);
14898
14789
  const workspace = resolveWorkspace(ctx, {
14899
14790
  sessionId: body.sessionId,
@@ -14901,12 +14792,12 @@ function registerStatusApi(ctx, opts = {}) {
14901
14792
  });
14902
14793
  const root = cccRootForCwd(workspace);
14903
14794
  if (!root) {
14904
- sendJson$4(res, 404, { error: `no CCC found from workspace: ${workspace}` });
14795
+ sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
14905
14796
  return;
14906
14797
  }
14907
- sendJson$4(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
14798
+ sendJson$3(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
14908
14799
  } catch (err) {
14909
- sendJson$4(res, 400, { error: err.message ?? String(err) });
14800
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
14910
14801
  }
14911
14802
  }
14912
14803
  });
@@ -14916,11 +14807,11 @@ function registerStatusApi(ctx, opts = {}) {
14916
14807
  handler: async (req, res) => {
14917
14808
  try {
14918
14809
  if (req.method !== "POST") {
14919
- sendJson$4(res, 405, { error: "method not allowed" });
14810
+ sendJson$3(res, 405, { error: "method not allowed" });
14920
14811
  return;
14921
14812
  }
14922
14813
  if (!requireWebUi(req, res, "文件落盘仅限 WebUI(client 专用)")) return;
14923
- const raw = await readBody$4(req, 20971520);
14814
+ const raw = await readBody$3(req, 20971520);
14924
14815
  const body = JSON.parse(raw);
14925
14816
  const workspace = resolveWorkspace(ctx, {
14926
14817
  sessionId: body.sessionId,
@@ -14928,12 +14819,12 @@ function registerStatusApi(ctx, opts = {}) {
14928
14819
  });
14929
14820
  const root = cccRootForCwd(workspace);
14930
14821
  if (!root) {
14931
- sendJson$4(res, 404, { error: `no CCC found from workspace: ${workspace}` });
14822
+ sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
14932
14823
  return;
14933
14824
  }
14934
- sendJson$4(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
14825
+ sendJson$3(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
14935
14826
  } catch (err) {
14936
- sendJson$4(res, 400, { error: err.message ?? String(err) });
14827
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
14937
14828
  }
14938
14829
  }
14939
14830
  });
@@ -14949,7 +14840,7 @@ function registerStatusApi(ctx, opts = {}) {
14949
14840
  workspace: url.searchParams.get("workspace") ?? void 0
14950
14841
  }), configPaths);
14951
14842
  const runtime = ctx.get("codeRuntime");
14952
- sendJson$4(res, 200, {
14843
+ sendJson$3(res, 200, {
14953
14844
  ...status,
14954
14845
  ...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
14955
14846
  language: runtime.language,
@@ -14960,7 +14851,7 @@ function registerStatusApi(ctx, opts = {}) {
14960
14851
  }
14961
14852
  if (req.method === "POST") {
14962
14853
  if (!requireWebUi(req, res, "safe-mode 切换仅限 WebUI(agent 不可自行开关)")) return;
14963
- const raw = await readBody$4(req, 65536);
14854
+ const raw = await readBody$3(req, 65536);
14964
14855
  const body = JSON.parse(raw);
14965
14856
  const workspace = resolveWorkspace(ctx, {
14966
14857
  sessionId: body.sessionId,
@@ -14968,19 +14859,19 @@ function registerStatusApi(ctx, opts = {}) {
14968
14859
  });
14969
14860
  const root = cccRootForCwd(workspace);
14970
14861
  if (!root) {
14971
- sendJson$4(res, 404, { error: `no CCC found from workspace: ${workspace}` });
14862
+ sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
14972
14863
  return;
14973
14864
  }
14974
14865
  const result = setSafeMode(root, body.on === true);
14975
- sendJson$4(res, 200, {
14866
+ sendJson$3(res, 200, {
14976
14867
  ...getStatus(workspace, configPaths),
14977
14868
  ...result
14978
14869
  });
14979
14870
  return;
14980
14871
  }
14981
- sendJson$4(res, 405, { error: "method not allowed" });
14872
+ sendJson$3(res, 405, { error: "method not allowed" });
14982
14873
  } catch (err) {
14983
- sendJson$4(res, 400, { error: err.message ?? String(err) });
14874
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
14984
14875
  }
14985
14876
  }
14986
14877
  });
@@ -14996,24 +14887,24 @@ function registerStatusApi(ctx, opts = {}) {
14996
14887
  try {
14997
14888
  known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
14998
14889
  } catch {}
14999
- sendJson$4(res, 200, {
14890
+ sendJson$3(res, 200, {
15000
14891
  config: toWire(settings),
15001
14892
  knownWorkspaces: known
15002
14893
  });
15003
14894
  return;
15004
14895
  }
15005
14896
  if (req.method === "PUT") {
15006
- const raw = await readBody$4(req, 131072);
14897
+ const raw = await readBody$3(req, 131072);
15007
14898
  const saved = applyWirePatch(JSON.parse(raw).config ?? {});
15008
14899
  try {
15009
14900
  ctx.emit?.("serenity/config-updated");
15010
14901
  } catch {}
15011
- sendJson$4(res, 200, { config: toWire(saved) });
14902
+ sendJson$3(res, 200, { config: toWire(saved) });
15012
14903
  return;
15013
14904
  }
15014
- sendJson$4(res, 405, { error: "method not allowed" });
14905
+ sendJson$3(res, 405, { error: "method not allowed" });
15015
14906
  } catch (err) {
15016
- sendJson$4(res, 400, { error: err.message ?? String(err) });
14907
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15017
14908
  }
15018
14909
  }
15019
14910
  });
@@ -15023,7 +14914,7 @@ function registerStatusApi(ctx, opts = {}) {
15023
14914
  handler: async (req, res) => {
15024
14915
  try {
15025
14916
  if (req.method !== "GET") {
15026
- sendJson$4(res, 405, { error: "method not allowed" });
14917
+ sendJson$3(res, 405, { error: "method not allowed" });
15027
14918
  return;
15028
14919
  }
15029
14920
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -15032,54 +14923,12 @@ function registerStatusApi(ctx, opts = {}) {
15032
14923
  workspace: url.searchParams.get("workspace") ?? void 0
15033
14924
  });
15034
14925
  const root = cccRootForCwd(workspace) ?? "";
15035
- sendJson$4(res, 200, { cccs: await listCccs(ctx, {
14926
+ sendJson$3(res, 200, { cccs: await listCccs(ctx, {
15036
14927
  defaultRoot: root,
15037
14928
  withRoles: true
15038
14929
  }) });
15039
14930
  } catch (err) {
15040
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15041
- }
15042
- }
15043
- });
15044
- ctx.webServer.register({
15045
- kind: "exact",
15046
- path: PUBLIC_ASK_PATH,
15047
- handler: async (req, res) => {
15048
- try {
15049
- if (!requireWebUi(req, res, "仅限 WebUI(key 属敏感凭据)")) return;
15050
- if (req.method === "PUT") {
15051
- const raw = await readBody$4(req, 16384);
15052
- if (JSON.parse(raw).action !== "rotate") {
15053
- sendJson$4(res, 400, { error: "unsupported action (expected \"rotate\")" });
15054
- return;
15055
- }
15056
- sendJson$4(res, 200, { key: rotatePublicAskKey() });
15057
- return;
15058
- }
15059
- if (req.method !== "GET") {
15060
- sendJson$4(res, 405, { error: "method not allowed" });
15061
- return;
15062
- }
15063
- const settings = readAdvancedSettings();
15064
- const { readSimpleSettings } = await import("./settings-section-xHABl8nk.js").then((n) => n.i);
15065
- const simple = readSimpleSettings();
15066
- const allowed = settings.publicAsk.allowed;
15067
- const port = simple.acpHttpPort ?? ACP_HTTP_PORT;
15068
- const base = `http://127.0.0.1:${port}`;
15069
- const containerUrls = allowed.map((name) => ({
15070
- name,
15071
- url: `${base}/c/${encodeURIComponent(name)}`
15072
- }));
15073
- sendJson$4(res, 200, {
15074
- enabled: simple.publicAskEnabled ?? false,
15075
- port,
15076
- key: ensurePublicAskKey(),
15077
- allowed,
15078
- urls: containerUrls,
15079
- listUrl: `${base}/`
15080
- });
15081
- } catch (err) {
15082
- sendJson$4(res, 400, { error: err.message ?? String(err) });
14931
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15083
14932
  }
15084
14933
  }
15085
14934
  });
@@ -15093,7 +14942,7 @@ function registerStatusApi(ctx, opts = {}) {
15093
14942
  if (req.method === "GET") {
15094
14943
  const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
15095
14944
  if (!chk.ok) {
15096
- sendJson$4(res, 400, { error: chk.error });
14945
+ sendJson$3(res, 400, { error: chk.error });
15097
14946
  return;
15098
14947
  }
15099
14948
  const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
@@ -15107,7 +14956,7 @@ function registerStatusApi(ctx, opts = {}) {
15107
14956
  enabled: a.enabled !== false,
15108
14957
  bound: readWeixinCredential(chk.root, a.accountId) !== null
15109
14958
  }));
15110
- sendJson$4(res, 200, {
14959
+ sendJson$3(res, 200, {
15111
14960
  enabled: settings.enabled === true,
15112
14961
  botType: settings.botType ?? void 0,
15113
14962
  accounts,
@@ -15117,14 +14966,14 @@ function registerStatusApi(ctx, opts = {}) {
15117
14966
  return;
15118
14967
  }
15119
14968
  if (req.method !== "POST") {
15120
- sendJson$4(res, 405, { error: "method not allowed" });
14969
+ sendJson$3(res, 405, { error: "method not allowed" });
15121
14970
  return;
15122
14971
  }
15123
- const raw = await readBody$4(req, 131072);
14972
+ const raw = await readBody$3(req, 131072);
15124
14973
  const body = JSON.parse(raw);
15125
14974
  const chk = requireCcc(body.ccc);
15126
14975
  if (!chk.ok) {
15127
- sendJson$4(res, 400, { error: chk.error });
14976
+ sendJson$3(res, 400, { error: chk.error });
15128
14977
  return;
15129
14978
  }
15130
14979
  const root = chk.root;
@@ -15141,7 +14990,7 @@ function registerStatusApi(ctx, opts = {}) {
15141
14990
  startedAt: Date.now(),
15142
14991
  polling: false
15143
14992
  });
15144
- sendJson$4(res, 200, {
14993
+ sendJson$3(res, 200, {
15145
14994
  qrcode: qr.qrcode,
15146
14995
  qrcode_img_content: qr.qrcode_img_content,
15147
14996
  loginKey
@@ -15150,31 +14999,31 @@ function registerStatusApi(ctx, opts = {}) {
15150
14999
  }
15151
15000
  if (body.action === "remove-account") {
15152
15001
  if (!body.accountId) {
15153
- sendJson$4(res, 400, { error: "missing accountId" });
15002
+ sendJson$3(res, 400, { error: "missing accountId" });
15154
15003
  return;
15155
15004
  }
15156
15005
  const { removeWeixinAccount } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15157
15006
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15158
15007
  removeWeixinAccount(root, body.accountId);
15159
15008
  syncCccBridge(ctx, root);
15160
- sendJson$4(res, 200, { removed: body.accountId });
15009
+ sendJson$3(res, 200, { removed: body.accountId });
15161
15010
  return;
15162
15011
  }
15163
15012
  if (body.action === "save-routes") {
15164
15013
  const routes = body.routes ?? [];
15165
15014
  if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
15166
- sendJson$4(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
15015
+ sendJson$3(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
15167
15016
  return;
15168
15017
  }
15169
15018
  const { saveWeixinRoutes } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15170
15019
  const { readSkiffRoles } = await import("./skiff-role-B615L9MV.js").then((n) => n.u);
15171
15020
  const roles = readSkiffRoles(root);
15172
15021
  for (const r of routes) if (!roles.has(r.role)) {
15173
- sendJson$4(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
15022
+ sendJson$3(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
15174
15023
  return;
15175
15024
  }
15176
15025
  saveWeixinRoutes(root, routes);
15177
- sendJson$4(res, 200, { saved: routes.length });
15026
+ sendJson$3(res, 200, { saved: routes.length });
15178
15027
  return;
15179
15028
  }
15180
15029
  if (body.action === "set-enabled") {
@@ -15183,7 +15032,7 @@ function registerStatusApi(ctx, opts = {}) {
15183
15032
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15184
15033
  const settings = readWeixinSettings(root);
15185
15034
  if (enabled && (settings.accounts ?? []).length === 0) {
15186
- sendJson$4(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
15035
+ sendJson$3(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
15187
15036
  return;
15188
15037
  }
15189
15038
  setWeixinEnabled(root, enabled);
@@ -15192,12 +15041,12 @@ function registerStatusApi(ctx, opts = {}) {
15192
15041
  const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15193
15042
  stopCccBridge(root);
15194
15043
  }
15195
- sendJson$4(res, 200, { enabled });
15044
+ sendJson$3(res, 200, { enabled });
15196
15045
  return;
15197
15046
  }
15198
- sendJson$4(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
15047
+ sendJson$3(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
15199
15048
  } catch (err) {
15200
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15049
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15201
15050
  }
15202
15051
  }
15203
15052
  });
@@ -15208,18 +15057,18 @@ function registerStatusApi(ctx, opts = {}) {
15208
15057
  try {
15209
15058
  if (!requireWebUi(req, res, "微信桥配置仅限 WebUI(client 专用)")) return;
15210
15059
  if (req.method !== "GET") {
15211
- sendJson$4(res, 405, { error: "method not allowed" });
15060
+ sendJson$3(res, 405, { error: "method not allowed" });
15212
15061
  return;
15213
15062
  }
15214
15063
  const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
15215
15064
  const login = weixinLogins.get(key);
15216
15065
  if (!login) {
15217
- sendJson$4(res, 404, { error: "login not found or expired" });
15066
+ sendJson$3(res, 404, { error: "login not found or expired" });
15218
15067
  return;
15219
15068
  }
15220
15069
  if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
15221
15070
  weixinLogins.delete(key);
15222
- sendJson$4(res, 200, { status: "expired" });
15071
+ sendJson$3(res, 200, { status: "expired" });
15223
15072
  return;
15224
15073
  }
15225
15074
  const { pollQRStatus } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
@@ -15232,7 +15081,7 @@ function registerStatusApi(ctx, opts = {}) {
15232
15081
  switch (status.status) {
15233
15082
  case "confirmed": {
15234
15083
  if (!status.bot_token) {
15235
- sendJson$4(res, 200, {
15084
+ sendJson$3(res, 200, {
15236
15085
  status: "error",
15237
15086
  error: "confirmed but no bot_token"
15238
15087
  });
@@ -15253,7 +15102,7 @@ function registerStatusApi(ctx, opts = {}) {
15253
15102
  });
15254
15103
  syncCccBridge(ctx, login.root);
15255
15104
  weixinLogins.delete(key);
15256
- sendJson$4(res, 200, {
15105
+ sendJson$3(res, 200, {
15257
15106
  status: "confirmed",
15258
15107
  accountId,
15259
15108
  tokenSaved: true
@@ -15262,14 +15111,14 @@ function registerStatusApi(ctx, opts = {}) {
15262
15111
  }
15263
15112
  case "expired":
15264
15113
  weixinLogins.delete(key);
15265
- sendJson$4(res, 200, { status: "expired" });
15114
+ sendJson$3(res, 200, { status: "expired" });
15266
15115
  return;
15267
15116
  default:
15268
- sendJson$4(res, 200, { status: status.status ?? "wait" });
15117
+ sendJson$3(res, 200, { status: status.status ?? "wait" });
15269
15118
  return;
15270
15119
  }
15271
15120
  } catch (err) {
15272
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15121
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15273
15122
  }
15274
15123
  }
15275
15124
  });
@@ -15291,7 +15140,7 @@ function registerStatusApi(ctx, opts = {}) {
15291
15140
  const root = sessionsRootDir();
15292
15141
  if (req.method === "GET") {
15293
15142
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
15294
- sendJson$4(res, 200, {
15143
+ sendJson$3(res, 200, {
15295
15144
  root,
15296
15145
  olderThanDays: days,
15297
15146
  dryRun: true,
@@ -15305,12 +15154,12 @@ function registerStatusApi(ctx, opts = {}) {
15305
15154
  return;
15306
15155
  }
15307
15156
  if (req.method !== "POST") {
15308
- sendJson$4(res, 405, { error: "method not allowed" });
15157
+ sendJson$3(res, 405, { error: "method not allowed" });
15309
15158
  return;
15310
15159
  }
15311
15160
  if (!requireWebUi(req, res, "会话清理仅限 WebUI(client 专用)")) return;
15312
15161
  const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
15313
- sendJson$4(res, 200, {
15162
+ sendJson$3(res, 200, {
15314
15163
  root,
15315
15164
  olderThanDays: days,
15316
15165
  dryRun: false,
@@ -15318,7 +15167,7 @@ function registerStatusApi(ctx, opts = {}) {
15318
15167
  errors: result.errors
15319
15168
  });
15320
15169
  } catch (err) {
15321
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15170
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15322
15171
  }
15323
15172
  }
15324
15173
  });
@@ -15371,7 +15220,7 @@ function loadVoyageHtml(assetPath = voyageAssetPath()) {
15371
15220
  return readFileSync(assetPath, "utf-8");
15372
15221
  }
15373
15222
  /** 发 JSON(错误分支用;与 api.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
15374
- function sendJson$3(res, status, body) {
15223
+ function sendJson$2(res, status, body) {
15375
15224
  res.statusCode = status;
15376
15225
  res.setHeader("content-type", "application/json; charset=utf-8");
15377
15226
  res.end(JSON.stringify(body));
@@ -15386,14 +15235,14 @@ function registerVoyageApi(ctx, opts = {}) {
15386
15235
  path: VOYAGE_PATH,
15387
15236
  handler: (req, res) => {
15388
15237
  if (req.method !== "GET") {
15389
- sendJson$3(res, 405, { error: "method not allowed" });
15238
+ sendJson$2(res, 405, { error: "method not allowed" });
15390
15239
  return;
15391
15240
  }
15392
15241
  let html;
15393
15242
  try {
15394
15243
  html = loadVoyageHtml(opts.assetPath ?? voyageAssetPath());
15395
15244
  } catch (err) {
15396
- sendJson$3(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
15245
+ sendJson$2(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
15397
15246
  return;
15398
15247
  }
15399
15248
  res.statusCode = 200;
@@ -15988,14 +15837,16 @@ function summarizeHostContract(report) {
15988
15837
  *
15989
15838
  * ## 现状 → 本模块要收掉什么(取证:`…/acc-c4c5-current-state.md` §①/§③ 3.1)
15990
15839
  *
15991
- * 插件自己 spawn 了**四个** node:http 监听器(B 3081 网关 / C 3082 微信发送 / D 3099 Skiff 调试
15992
- * / E 3100 ACP+问答)。此前四处各写一份同款样板:`createServer` + `server.once('error')` +
15840
+ * 插件自己 spawn 了**三个** node:http 监听器(B 3081 网关 / C 3082 微信发送 / D 3099 Skiff 调试)。
15841
+ * ⚠️ **2026-10-15 变更**:原为四个(含 **E 3100 ACP+问答**),E 已随 owner「砍掉外部问答」令**整面退役**
15842
+ * ⇒ 仍保有面 B/C/D 三个;本段保留"当年是四个"的记述,因为下方"两套拆卸"的病史由它解释。
15843
+ * 此前四处各写一份同款样板:`createServer` + `server.once('error')` +
15993
15844
  * `listen(port, host)` + 模块级 `active` 变量 + `close()`;拆卸也分裂成**两套**——
15994
15845
  * `seams/lifecycle.ts` 有一个聚合 disposer,而 `gateway.ts` 与 `weixin-send-api.ts`
15995
15846
  * **各自另注册**一个(D/E 干脆没有自己的 disposer)。
15996
15847
  *
15997
15848
  * 本模块把"listener 生命周期"收成一处:**active 表 + 统一 listen/close + 统一错误与端口占用
15998
- * 处理**。四个面各自只提供 `{ name, port, host, enabled, handler }`。
15849
+ * 处理**。各面各自只提供 `{ name, port, host, enabled, handler }`。
15999
15850
  *
16000
15851
  * ## 🔴 共享的是「生命周期与样板」,**绝不是**「路由 / 监听器 / 鉴权」
16001
15852
  *
@@ -16003,12 +15854,12 @@ function summarizeHostContract(report) {
16003
15854
  * · **A ↔ C 解耦**(`weixin-send-api.ts:4-7`):3080 会被 3081 原样反代(连请求头),
16004
15855
  * 把 3082 的端点挂到 3080 上 = 已登录的外部家庭账号即可冒充 bot 发消息
16005
15856
  * ⇒ 本模块**不提供**"把面挂到别的面"的能力,也不共享路由表;
16006
- * · **E ↔ D** 共用会话核心(acp-core → skiff-core)但各自独立 listener + 独立路由/HTML;
15857
+ * · (原 **E ↔ D** 共用会话核心但各自独立 listener 一条,**随 E 退役删除**);
16007
15858
  * · **B 是 A 的代理宿主**(把请求打到 `127.0.0.1:<主端口>`),不是另一个路由面。
16008
15859
  *
16009
15860
  * ## 面 A(3080)不在本模块之列
16010
15861
  * 它是**宿主 DSH 主 WebUI**,插件只往上挂 `/serenity/*` 路由(`ctx.webServer.register`),
16011
- * **插件不起这个 listener** ⇒ 面宿主只持有 B/C/D/E 四个自起面。
15862
+ * **插件不起这个 listener** ⇒ 面宿主只持有 B/C/D 三个自起面。
16012
15863
  *
16013
15864
  * ## 两个入口的分工(这是本模块唯一的"抽象决策",别混)
16014
15865
  * · {@link startFace} —— **机械启动**:把一个 handler 绑到 `host:port` 上。同名面若已在监听,
@@ -16633,7 +16484,7 @@ function createDshCookieProvider(connection, authority) {
16633
16484
  /** 面名(C4 块 A:listener 生命周期归 `face-host`) */
16634
16485
  const FACE_GATEWAY = "gateway";
16635
16486
  /** 读取请求体(≤ maxBytes;超限 reject) */
16636
- function readBody$3(req, maxBytes) {
16487
+ function readBody$2(req, maxBytes) {
16637
16488
  return new Promise((resolve, reject) => {
16638
16489
  let data = "";
16639
16490
  req.on("data", (chunk) => {
@@ -16760,7 +16611,7 @@ async function startGateway(config, getAccounts, getDshCookie) {
16760
16611
  const session = authed(req);
16761
16612
  if (session !== void 0) {
16762
16613
  if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
16763
- readBody$3(req, 131072).then((body) => {
16614
+ readBody$2(req, 131072).then((body) => {
16764
16615
  const { rpcId, path } = parseWorkspaceCreateBody(body);
16765
16616
  if (!allowWorkspaceCreate) {
16766
16617
  res.writeHead(403, { "content-type": "application/json" });
@@ -19090,7 +18941,7 @@ const SKIFF_DEBUG_HOST = FACE_PORTS.skiffDebug.host ?? "127.0.0.1";
19090
18941
  function skiffDebugActive() {
19091
18942
  return faceActive(FACE_SKIFF_DEBUG);
19092
18943
  }
19093
- function readBody$2(req) {
18944
+ function readBody$1(req) {
19094
18945
  return new Promise((resolve, reject) => {
19095
18946
  const chunks = [];
19096
18947
  req.on("data", (c) => chunks.push(c));
@@ -19098,7 +18949,7 @@ function readBody$2(req) {
19098
18949
  req.on("error", reject);
19099
18950
  });
19100
18951
  }
19101
- function sendJson$2(res, status, payload) {
18952
+ function sendJson$1(res, status, payload) {
19102
18953
  const body = JSON.stringify(payload);
19103
18954
  res.writeHead(status, {
19104
18955
  "Content-Type": "application/json; charset=utf-8",
@@ -19106,7 +18957,7 @@ function sendJson$2(res, status, payload) {
19106
18957
  });
19107
18958
  res.end(body);
19108
18959
  }
19109
- function sendHtml$1(res, html) {
18960
+ function sendHtml(res, html) {
19110
18961
  res.writeHead(200, {
19111
18962
  "Content-Type": "text/html; charset=utf-8",
19112
18963
  "Cache-Control": "no-store"
@@ -19303,8 +19154,8 @@ function escapeHtml(s) {
19303
19154
  * 在 JSON **文本层**把它们替换为 `\uXXXX` 转义序列:JSON.parse 后语义完全一致(还原原字符),
19304
19155
  * 且 JSC 正则看到的是常规 ASCII 转义(与 JSON.stringify 对控制字符的输出同形态,安全)。
19305
19156
  *
19306
- * 3100 问答页客户端 `await res.json()`(acp-http.ts)与页面内嵌 JSON(skiff-debug/acp-http)
19307
- * 均需此兼容层——iOS Safari 用户实测"复杂回答"触发。
19157
+ * 3099 调试页客户端 `await res.json()` 与页面内嵌 JSON 均需此兼容层
19158
+ * ——iOS Safari 用户实测"复杂回答"触发。(原同批还列了 3100 问答页,该面已退役,2026-10-15)
19308
19159
  *
19309
19160
  * @param jsonText JSON.stringify 的输出文本;原地等价替换,返回 JSC 安全文本
19310
19161
  */
@@ -19432,7 +19283,7 @@ function skiffDebugSpec(ctx, root, port, webPort) {
19432
19283
  port,
19433
19284
  host: SKIFF_DEBUG_HOST,
19434
19285
  enabled: () => readSimpleSettings().skiffEnabled,
19435
- handler: (req, res) => handle$1(ctx, root, webPort, req, res)
19286
+ handler: (req, res) => handle(ctx, root, webPort, req, res)
19436
19287
  };
19437
19288
  }
19438
19289
  /**
@@ -19456,11 +19307,11 @@ async function startSkiffDebugServer(ctx, root, port, webPort) {
19456
19307
  function stopSkiffDebugServer() {
19457
19308
  stopFace(FACE_SKIFF_DEBUG);
19458
19309
  }
19459
- async function handle$1(ctx, defaultRoot, webPort, req, res) {
19310
+ async function handle(ctx, defaultRoot, webPort, req, res) {
19460
19311
  try {
19461
19312
  const url = (req.url ?? "/").split("?")[0] ?? "/";
19462
19313
  if (req.method === "GET" && url === "/") {
19463
- sendHtml$1(res, skiffDebugPage(await listCccs(ctx, {
19314
+ sendHtml(res, skiffDebugPage(await listCccs(ctx, {
19464
19315
  defaultRoot,
19465
19316
  withRoles: true
19466
19317
  }), defaultRoot, webPort));
@@ -19469,11 +19320,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19469
19320
  if (req.method === "POST" && url === "/ask") {
19470
19321
  let body;
19471
19322
  try {
19472
- const parsed = JSON.parse(await readBody$2(req));
19323
+ const parsed = JSON.parse(await readBody$1(req));
19473
19324
  if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
19474
19325
  body = parsed;
19475
19326
  } catch {
19476
- sendJson$2(res, 400, { error: "invalid JSON body" });
19327
+ sendJson$1(res, 400, { error: "invalid JSON body" });
19477
19328
  return;
19478
19329
  }
19479
19330
  const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot;
@@ -19482,11 +19333,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19482
19333
  const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
19483
19334
  const role = readSkiffRoles(ccc).get(roleName);
19484
19335
  if (!roleName || !role) {
19485
- sendJson$2(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
19336
+ sendJson$1(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
19486
19337
  return;
19487
19338
  }
19488
19339
  if (!question.trim()) {
19489
- sendJson$2(res, 400, { error: "empty question" });
19340
+ sendJson$1(res, 400, { error: "empty question" });
19490
19341
  return;
19491
19342
  }
19492
19343
  let agent;
@@ -19495,11 +19346,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19495
19346
  const info = skiffSessionInfo(sessionId);
19496
19347
  const live = getSkiffAgent(sessionId);
19497
19348
  if (!info || !live) {
19498
- sendJson$2(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
19349
+ sendJson$1(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
19499
19350
  return;
19500
19351
  }
19501
19352
  if (info.role !== roleName || info.ccc !== ccc) {
19502
- sendJson$2(res, 400, { error: `session "${sessionId}" belongs to role "${info.role}" in another CCC — start a new conversation` });
19353
+ sendJson$1(res, 400, { error: `session "${sessionId}" belongs to role "${info.role}" in another CCC — start a new conversation` });
19503
19354
  return;
19504
19355
  }
19505
19356
  agent = live;
@@ -19508,7 +19359,7 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19508
19359
  const hc = readCheapWorkerConfig(ccc);
19509
19360
  if (!agent) agent = (await createSkiffAgent(ctx, ccc, roleName, role, hc?.defaultModel)).agent;
19510
19361
  const result = await askSkiff(ctx, agent, question, 0);
19511
- sendJson$2(res, 200, {
19362
+ sendJson$1(res, 200, {
19512
19363
  answer: result.answer,
19513
19364
  answer_html: renderSkiffMarkdown(result.answer),
19514
19365
  sessionId: result.sessionId,
@@ -19517,9 +19368,9 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19517
19368
  });
19518
19369
  return;
19519
19370
  }
19520
- sendJson$2(res, 404, { error: "not found" });
19371
+ sendJson$1(res, 404, { error: "not found" });
19521
19372
  } catch (err) {
19522
- sendJson$2(res, 500, { error: err?.message ?? String(err) });
19373
+ sendJson$1(res, 500, { error: err?.message ?? String(err) });
19523
19374
  }
19524
19375
  }
19525
19376
  //#endregion
@@ -19663,7 +19514,9 @@ const rebukeStates = /* @__PURE__ */ new Map();
19663
19514
  //#endregion
19664
19515
  //#region src/output-guard-seam.ts
19665
19516
  /** 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免):
19666
- * skiff-(F4 问答)/ acp-(F4c 程序化)/ rebuild-(v1.22.4 重建会话)前缀 */
19517
+ * skiff-(F4 问答)/ rebuild-(v1.22.4 重建会话)前缀。
19518
+ * 🔴 `acp-` 分支保留但**已是死判据**(无生产者;面 E 已于 2026-10-15 退役)——
19519
+ * 理由与"为何不擅自删"见文件头注释。 */
19667
19520
  function isExternalFaceSession(sessionId) {
19668
19521
  if (!sessionId) return false;
19669
19522
  return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
@@ -19958,760 +19811,6 @@ function registerUnattendedSeam(ctx) {
19958
19811
  });
19959
19812
  }
19960
19813
  //#endregion
19961
- //#region src/acp-core.ts
19962
- /** JSON-RPC 错误码(对齐标准) */
19963
- const RPC_ERROR = {
19964
- PARSE: -32700,
19965
- INVALID_REQUEST: -32600,
19966
- METHOD_NOT_FOUND: -32601,
19967
- INVALID_PARAMS: -32602,
19968
- INTERNAL: -32603
19969
- };
19970
- /**
19971
- * ACP 会话管理器:持有 skiff agent 会话(与 skiff-core 注册表同源——
19972
- * 创建走 createSkiffAgent(自动注册),关闭走 unregisterSkiffSession)。
19973
- * 传输无关:HTTP 端点/企业微信桥/未来 stdio 都调 handle(method, params)。
19974
- */
19975
- var AcpServer = class {
19976
- ctx;
19977
- constructor(ctx) {
19978
- this.ctx = ctx;
19979
- }
19980
- /** 实现级方法调度(返回 result 或抛 JSON-RPC 语义错误) */
19981
- async handle(method, params) {
19982
- switch (method) {
19983
- case "initialize": return this.initialize();
19984
- case "authenticate": return {};
19985
- case "session/new": return this.newSession(params);
19986
- case "session/list": return this.listSessions();
19987
- case "session/prompt": return this.prompt(params);
19988
- case "session/cancel": return this.cancel(params);
19989
- case "session/close": return this.closeSession(params);
19990
- case "session/request_permission": return { allow: true };
19991
- default: throw new RpcMethodError(`method not found: ${method}`);
19992
- }
19993
- }
19994
- initialize() {
19995
- return {
19996
- protocolVersion: 1,
19997
- capabilities: {
19998
- sessionId: true,
19999
- ccc: true,
20000
- role: true
20001
- },
20002
- serverInfo: {
20003
- name: "dsh-serenity-hooks-acp",
20004
- version: "1.26.0"
20005
- }
20006
- };
20007
- }
20008
- /**
20009
- * session/new:{ ccc, role, sessionId? } → 创建或延续 skiff 会话。
20010
- * 无 sessionId → createSkiffAgent(新会话,continued:false);
20011
- * 有 → 进程内延续(getSkiffAgent 命中 + role/ccc 绑定校验;未命中 → 错误)。
20012
- */
20013
- async newSession(params) {
20014
- const ccc = typeof params?.ccc === "string" && params.ccc !== "" ? params.ccc : void 0;
20015
- const roleName = typeof params?.role === "string" ? params.role : void 0;
20016
- const sessionId = typeof params?.sessionId === "string" && params.sessionId !== "" ? params.sessionId : void 0;
20017
- if (!ccc) throw new RpcInvalidParams("session/new requires ccc (CCC root path)");
20018
- if (!roleName) throw new RpcInvalidParams("session/new requires role");
20019
- const role = readSkiffRoles(ccc).get(roleName);
20020
- if (!role) throw new RpcInvalidParams(`unknown role: ${roleName} (ccc: ${ccc})`);
20021
- if (sessionId) {
20022
- const info = skiffSessionInfo(sessionId);
20023
- const live = getSkiffAgent(sessionId);
20024
- if (!info || !live) throw new RpcInvalidParams("session is not recoverable (process restarted or session unknown) — start a new conversation");
20025
- if (info.role !== roleName || info.ccc !== ccc) throw new RpcInvalidParams(`session "${sessionId}" belongs to role "${info.role}" in another CCC`);
20026
- return {
20027
- sessionId,
20028
- role: roleName,
20029
- ccc,
20030
- continued: true
20031
- };
20032
- }
20033
- const hc = readCheapWorkerConfig(ccc);
20034
- return {
20035
- sessionId: (await createSkiffAgent(this.ctx, ccc, roleName, role, hc?.defaultModel)).sessionId,
20036
- role: roleName,
20037
- ccc,
20038
- continued: false
20039
- };
20040
- }
20041
- listSessions() {
20042
- const sessions = [];
20043
- for (const [id, b] of skiffSessionSnapshot()) sessions.push({
20044
- sessionId: id,
20045
- role: b.role,
20046
- ccc: b.ccc
20047
- });
20048
- return { sessions };
20049
- }
20050
- /**
20051
- * session/prompt:{ sessionId, question } → 答案(**v1.26.10:不返回 trajectory**——
20052
- * 3100 对外只提供问答,轨迹含工具结果等内部信息;3099 调试页另走 /ask 保留)
20053
- */
20054
- async prompt(params) {
20055
- const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
20056
- const question = typeof params?.question === "string" ? params.question : void 0;
20057
- if (!sessionId) throw new RpcInvalidParams("session/prompt requires sessionId");
20058
- if (!question?.trim()) throw new RpcInvalidParams("session/prompt requires question");
20059
- const agent = getSkiffAgent(sessionId);
20060
- if (!agent) throw new RpcInvalidParams(`unknown session: ${sessionId} (not recoverable — process restarted?)`);
20061
- const result = await askSkiff(this.ctx, agent, question, void 0, { includeTrajectory: false });
20062
- return {
20063
- answer: result.answer,
20064
- sessionId: result.sessionId
20065
- };
20066
- }
20067
- /** session/cancel:{ sessionId } → 取消该会话的当前活动(Agent.cancel,宿主 rc.1 契约) */
20068
- cancel(params) {
20069
- const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
20070
- if (!sessionId) return { cancelled: false };
20071
- const agent = getSkiffAgent(sessionId);
20072
- if (!agent) return { cancelled: false };
20073
- try {
20074
- agent.cancel({ kind: "user" });
20075
- } catch {}
20076
- return { cancelled: true };
20077
- }
20078
- /** session/close:{ sessionId } → 释放 agent + 注册表清理(进程内) */
20079
- closeSession(params) {
20080
- const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
20081
- if (!sessionId) return { closed: false };
20082
- if (!getSkiffAgent(sessionId)) return { closed: false };
20083
- unregisterSkiffSession(sessionId);
20084
- return { closed: true };
20085
- }
20086
- };
20087
- /** 方法不存在错误(JSON-RPC -32601) */
20088
- var RpcMethodError = class extends Error {
20089
- constructor(message) {
20090
- super(message);
20091
- this.name = "RpcMethodError";
20092
- }
20093
- };
20094
- /** 参数错误(JSON-RPC -32602) */
20095
- var RpcInvalidParams = class extends Error {
20096
- constructor(message) {
20097
- super(message);
20098
- this.name = "RpcInvalidParams";
20099
- }
20100
- };
20101
- /**
20102
- * JSON-RPC 2.0 批处理(单帧):解析请求/通知 → 调 AcpServer.handle → 响应。
20103
- * 传输无关:acp-http 端点 / 未来 stdio 共用。
20104
- * @returns 响应帧数组(请求有响应;通知无;解析失败 → 单错误响应)
20105
- */
20106
- async function dispatchRpc(server, frame) {
20107
- if (frame === null || typeof frame !== "object") return [rpcError(null, RPC_ERROR.INVALID_REQUEST, "invalid request")];
20108
- const req = frame;
20109
- if (req.jsonrpc !== "2.0" || typeof req.method !== "string") return [rpcError(req.id ?? null, RPC_ERROR.INVALID_REQUEST, "invalid request")];
20110
- if (req.id === void 0) {
20111
- try {
20112
- await server.handle(req.method, req.params);
20113
- } catch {}
20114
- return [];
20115
- }
20116
- try {
20117
- const result = await server.handle(req.method, req.params);
20118
- return [{
20119
- jsonrpc: "2.0",
20120
- id: req.id,
20121
- result
20122
- }];
20123
- } catch (error) {
20124
- return [rpcError(req.id, rpcCodeOf(error), error instanceof Error ? error.message : String(error))];
20125
- }
20126
- }
20127
- function rpcCodeOf(error) {
20128
- if (error instanceof RpcMethodError) return RPC_ERROR.METHOD_NOT_FOUND;
20129
- if (error instanceof RpcInvalidParams) return RPC_ERROR.INVALID_PARAMS;
20130
- return RPC_ERROR.INTERNAL;
20131
- }
20132
- function rpcError(id, code, message) {
20133
- return {
20134
- jsonrpc: "2.0",
20135
- id,
20136
- error: {
20137
- code,
20138
- message
20139
- }
20140
- };
20141
- }
20142
- //#endregion
20143
- //#region src/acp-http.ts
20144
- /** 面名(C4 块 A) */
20145
- const FACE_ACP_HTTP = "acp-http";
20146
- /** 监听地址:**只绑 loopback**(JSON-RPC 子面无认证——仅靠 loopback;问答页另有 key 认证) */
20147
- const ACP_HTTP_HOST = FACE_PORTS.acpHttp.host ?? "127.0.0.1";
20148
- function acpHttpActive() {
20149
- return faceActive(FACE_ACP_HTTP);
20150
- }
20151
- function readBody$1(req) {
20152
- return new Promise((resolve, reject) => {
20153
- const chunks = [];
20154
- req.on("data", (c) => chunks.push(c));
20155
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
20156
- req.on("error", reject);
20157
- });
20158
- }
20159
- function sendJson$1(res, status, payload) {
20160
- const body = jscSafeJsonText(JSON.stringify(payload));
20161
- res.writeHead(status, {
20162
- "Content-Type": "application/json; charset=utf-8",
20163
- "Cache-Control": "no-store"
20164
- });
20165
- res.end(body);
20166
- }
20167
- function sendHtml(res, html) {
20168
- res.writeHead(200, {
20169
- "Content-Type": "text/html; charset=utf-8",
20170
- "Cache-Control": "no-store"
20171
- });
20172
- res.end(html);
20173
- }
20174
- /**
20175
- * 本面的规格(C4 块 A/④:**只提供** name/port/host/enabled/handler)。
20176
- * 两个子面(ACP JSON-RPC / 对外问答页)的路由与鉴权仍全部归 {@link handle}——
20177
- * 面宿主不介入面语义,也**不合并** D/E 两面(两者共用会话核心但各有独立 listener/路由/HTML)。
20178
- */
20179
- function acpHttpSpec(ctx, port, defaultRoot) {
20180
- return {
20181
- name: FACE_ACP_HTTP,
20182
- port,
20183
- host: ACP_HTTP_HOST,
20184
- enabled: () => {
20185
- const s = readSimpleSettings();
20186
- return s.acpEnabled || s.publicAskEnabled;
20187
- },
20188
- handler: (req, res) => handle(ctx, defaultRoot ?? "", req, res)
20189
- };
20190
- }
20191
- /**
20192
- * 启动 ACP HTTP JSON-RPC + 建议问答页服务(单实例;重复启动幂等返回既有实例)。
20193
- * C4 块 A:listener 的创建/监听/错误/端口占用/关闭归 `face-host`。
20194
- * @param ctx 插件上下文(AcpServer 内部经 skiff-core 创建 agent)
20195
- * @param port 监听端口(仅 127.0.0.1)
20196
- * @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
20197
- */
20198
- async function startAcpHttpServer(ctx, port, defaultRoot) {
20199
- if (acpHttpActive()) return;
20200
- const handle = await startFace(acpHttpSpec(ctx, port, defaultRoot));
20201
- const s = readSimpleSettings();
20202
- const faces = [];
20203
- if (s.acpEnabled) faces.push("JSON-RPC");
20204
- if (s.publicAskEnabled) faces.push("问答页(key 认证)");
20205
- console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${handle.port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
20206
- }
20207
- function stopAcpHttpServer() {
20208
- stopFace(FACE_ACP_HTTP);
20209
- }
20210
- async function handle(ctx, defaultRoot, req, res) {
20211
- try {
20212
- const url = (req.url ?? "/").split("?")[0] ?? "/";
20213
- const s = readSimpleSettings();
20214
- const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
20215
- if (req.method === "POST" && url === "/") {
20216
- if (!s.acpEnabled) {
20217
- sendJson$1(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
20218
- return;
20219
- }
20220
- let body;
20221
- try {
20222
- body = JSON.parse(await readBody$1(req));
20223
- } catch {
20224
- sendJson$1(res, 400, [{
20225
- jsonrpc: "2.0",
20226
- id: null,
20227
- error: {
20228
- code: -32700,
20229
- message: "parse error"
20230
- }
20231
- }]);
20232
- return;
20233
- }
20234
- sendJson$1(res, 200, await dispatchRpc(new AcpServer(ctx), body));
20235
- return;
20236
- }
20237
- if (!s.publicAskEnabled) {
20238
- if (req.method === "GET" && (url === "/" || url.startsWith("/c/"))) {
20239
- sendHtml(res, publicAskDisabledHtml());
20240
- return;
20241
- }
20242
- if (req.method === "POST" && (url === "/ask" || url.startsWith("/c/"))) {
20243
- sendJson$1(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
20244
- return;
20245
- }
20246
- sendJson$1(res, 404, { error: "not found" });
20247
- return;
20248
- }
20249
- const containerAllowed = (name) => {
20250
- const allowed = readAdvancedSettings().publicAsk.allowed;
20251
- return allowed.length === 0 || allowed.includes(name);
20252
- };
20253
- let cccsCache = null;
20254
- const cccsOf = async () => {
20255
- if (cccsCache === null) cccsCache = await listCccs(ctx, {
20256
- defaultRoot: defaultRoot || (cccRootForCwd(process.cwd()) ?? ""),
20257
- withRoles: true
20258
- });
20259
- return cccsCache;
20260
- };
20261
- const cMatch = /^\/c\/([^/]+)$/.exec(url);
20262
- if (req.method === "GET" && cMatch) {
20263
- const name = decodeURIComponent(cMatch[1] ?? "");
20264
- const ccc = (await cccsOf()).find((c) => c.name === name);
20265
- if (!ccc) {
20266
- sendHtml(res, publicAskContainerUnknownHtml(name));
20267
- return;
20268
- }
20269
- if (!containerAllowed(name)) {
20270
- sendHtml(res, publicAskContainerClosedHtml(name));
20271
- return;
20272
- }
20273
- sendHtml(res, publicAskContainerPage(ccc));
20274
- return;
20275
- }
20276
- if (req.method === "GET" && url === "/") {
20277
- sendHtml(res, publicAskListPage((await cccsOf()).filter((c) => containerAllowed(c.name))));
20278
- return;
20279
- }
20280
- const aMatch = /^\/c\/([^/]+)\/ask$/.exec(url);
20281
- if (req.method === "POST" && aMatch) {
20282
- const name = decodeURIComponent(aMatch[1] ?? "");
20283
- const ccc = (await cccsOf()).find((c) => c.name === name);
20284
- if (!ccc || !containerAllowed(name)) {
20285
- sendJson$1(res, 403, { error: `container "${name}" is not open for public ask` });
20286
- return;
20287
- }
20288
- let body;
20289
- try {
20290
- const parsed = JSON.parse(await readBody$1(req));
20291
- if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
20292
- body = parsed;
20293
- } catch {
20294
- sendJson$1(res, 400, { error: "invalid JSON body" });
20295
- return;
20296
- }
20297
- await handleAskParsed(ctx, ccc, body, res, ip);
20298
- return;
20299
- }
20300
- if (req.method === "POST" && url === "/ask") {
20301
- let body;
20302
- try {
20303
- const parsed = JSON.parse(await readBody$1(req));
20304
- if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
20305
- body = parsed;
20306
- } catch {
20307
- sendJson$1(res, 400, { error: "invalid JSON body" });
20308
- return;
20309
- }
20310
- const cccValue = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : void 0;
20311
- const nameValue = typeof body.name === "string" && body.name !== "" ? body.name : void 0;
20312
- let ccc;
20313
- if (nameValue) ccc = (await cccsOf()).find((c) => c.name === nameValue);
20314
- else if (cccValue) {
20315
- ccc = (await cccsOf()).find((c) => c.root === cccValue);
20316
- if (!ccc && cccValue !== "") ccc = {
20317
- root: cccValue,
20318
- name: cccValue.split("/").filter(Boolean).pop() ?? cccValue,
20319
- roles: [...readSkiffRoles(cccValue).keys()]
20320
- };
20321
- }
20322
- if (!ccc || !containerAllowed(ccc.name)) {
20323
- sendJson$1(res, 403, { error: "container is not open for public ask" });
20324
- return;
20325
- }
20326
- await handleAskParsed(ctx, ccc, body, res, ip);
20327
- return;
20328
- }
20329
- sendJson$1(res, 404, { error: "not found" });
20330
- } catch (err) {
20331
- sendJson$1(res, 500, { error: err?.message ?? String(err) });
20332
- }
20333
- }
20334
- /**
20335
- * 单容器问答核心(/c/<name>/ask 与 /ask 共用):
20336
- * body { key, role?, question, sessionId? } → key 校验(401 + IP 失败锁定)→ 角色缺省取 CCC 第一个
20337
- * → 会话延续(v1.25.10 语义,静默新建)→ askSkiff(0 全量轨迹) → { answer, answer_html, sessionId, continued, trajectory }
20338
- * @param ip 请求来源 IP(v1.26.5 公网 key 失败锁定:X-Forwarded-For 优先——公网反代,回退 remoteAddress)
20339
- */
20340
- async function handleAskParsed(ctx, ccc, body, res, ip) {
20341
- const key = typeof body.key === "string" ? body.key : void 0;
20342
- if (isPublicAskIpLocked(ip)) {
20343
- sendJson$1(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
20344
- return;
20345
- }
20346
- if (!verifyPublicAskKey(key)) {
20347
- const remaining = recordPublicAskFail(ip);
20348
- sendJson$1(res, remaining > 0 ? 429 : 401, { error: remaining > 0 ? `too many failed attempts — locked for ${Math.ceil(remaining / 6e4)} min` : "invalid or missing key" });
20349
- return;
20350
- }
20351
- resetPublicAskIpFail(ip);
20352
- const root = ccc.root;
20353
- const roleName = typeof body.role === "string" && body.role !== "" ? body.role : void 0;
20354
- const question = typeof body.question === "string" ? body.question : "";
20355
- const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
20356
- if (!question.trim()) {
20357
- sendJson$1(res, 400, { error: "empty question" });
20358
- return;
20359
- }
20360
- const roles = readSkiffRoles(root);
20361
- const explicit = roleName ? roles.get(roleName) : void 0;
20362
- const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
20363
- const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
20364
- if (!effectiveRole || !effectiveCfg) {
20365
- sendJson$1(res, 400, { error: `no skiff role available in ccc: ${root}` });
20366
- return;
20367
- }
20368
- let agent;
20369
- let continued = false;
20370
- if (sessionId) {
20371
- const info = skiffSessionInfo(sessionId);
20372
- const live = getSkiffAgent(sessionId);
20373
- if (info && live && info.role === effectiveRole && info.ccc === root) {
20374
- agent = live;
20375
- continued = true;
20376
- }
20377
- }
20378
- if (!agent) agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, readCheapWorkerConfig(root)?.defaultModel)).agent;
20379
- const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
20380
- sendJson$1(res, 200, {
20381
- answer: result.answer,
20382
- answer_html: renderSkiffMarkdown(result.answer, true),
20383
- sessionId: result.sessionId,
20384
- continued
20385
- });
20386
- }
20387
- /** 问答页未启用提示页 */
20388
- function publicAskDisabledHtml() {
20389
- return `<!DOCTYPE html>
20390
- <html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
20391
- <body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
20392
- <h2>Serenity Public Ask</h2>
20393
- <p>问答页未启用(请在 dsh 设置面板「Serenity」页开启「建议问答」)。</p>
20394
- </body></html>`;
20395
- }
20396
- /** 容器不存在提示页(URL 容器名不在已发现 CCC 中) */
20397
- function publicAskContainerUnknownHtml(name) {
20398
- return `<!DOCTYPE html>
20399
- <html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
20400
- <body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
20401
- <h2>Serenity Public Ask</h2>
20402
- <p>认知容器「${name}」不存在。</p>
20403
- <p style="font-size:13px"><a href="/" style="color:#0ba875">返回容器列表</a></p>
20404
- </body></html>`;
20405
- }
20406
- /** 容器未开放提示页(白名单外) */
20407
- function publicAskContainerClosedHtml(name) {
20408
- return `<!DOCTYPE html>
20409
- <html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
20410
- <body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
20411
- <h2>Serenity Public Ask</h2>
20412
- <p>认知容器「${name}」当前未开放问答(管理员在设置面板「开放容器」中配置)。</p>
20413
- <p style="font-size:13px"><a href="/" style="color:#0ba875">返回容器列表</a></p>
20414
- </body></html>`;
20415
- }
20416
- /** 页面基础样式(列表页 + 单容器页共用;v1.26.4 聊天 UI) */
20417
- const ASK_CSS = `
20418
- :root { color-scheme: light dark; --sp-green: #0ba875; --sp-green-dim: #059669; }
20419
- * { box-sizing: border-box; }
20420
- html, body { height: 100%; }
20421
- body { font-family: system-ui, -apple-system, sans-serif; margin: 0; background: #f6f7f9; color: #1f2328; }
20422
- @media (prefers-color-scheme: dark) { body { background: #16181c; color: #e6e6e6; } }
20423
- main { max-width: 720px; margin: 0 auto; }
20424
- h1 { font-size: 18px; margin: 0 0 4px; }
20425
- .sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
20426
- .card { background: #fff; border: 1px solid #d0d7de; border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; display: block; text-decoration: none; color: inherit; }
20427
- @media (prefers-color-scheme: dark) { .card { background: #26282c; border-color: #3a3d42; } }
20428
- .card:hover { border-color: var(--sp-green); }
20429
- .card .cname { font-weight: 600; font-size: 15px; }
20430
- .card .cmeta { opacity: .6; font-size: 12px; margin-top: 2px; }
20431
- .empty { text-align: center; opacity: .7; padding: 24px 0; }
20432
- .ccname { font-weight: 600; color: var(--sp-green); }
20433
- /* key 门(v1.26.6:选容器前必填) */
20434
- .keyGate { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; background: rgba(127,127,127,.06); border: 1px solid rgba(127,127,127,.18); border-radius: 10px; padding: 10px 14px; }
20435
- .keyGate label { font-size: 12px; opacity: .7; white-space: nowrap; }
20436
- .keyGate input { flex: 1; border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 8px 12px; font-size: 14px; }
20437
- .keyGate input:focus { outline: none; border-color: var(--sp-green); }
20438
-
20439
- /* ── 聊天布局(v1.26.4 体验升级:参考 ChatGPT/Claude 会话形态)── */
20440
- .chat { display: flex; flex-direction: column; height: 100vh; max-width: 720px; margin: 0 auto; }
20441
- .chatHead { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-bottom: 1px solid rgba(127,127,127,.18); background: rgba(127,127,127,.04); position: sticky; top: 0; z-index: 5; }
20442
- .chatHead .back { color: var(--sp-green); text-decoration: none; font-size: 13px; white-space: nowrap; }
20443
- .chatHead .title { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
20444
- .chatHead .spacer { flex: 1; }
20445
- .chatHead select { border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 4px 8px; font-size: 12px; }
20446
- .chatHead .newBtn { border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 4px 10px; font-size: 12px; cursor: pointer; white-space: nowrap; }
20447
- .chatHead .newBtn:hover { border-color: var(--sp-green); color: var(--sp-green); }
20448
- /* 消息流 */
20449
- .msgList { flex: 1; overflow-y: auto; padding: 18px 16px; display: flex; flex-direction: column; gap: 16px; }
20450
- .msg { display: flex; flex-direction: column; max-width: 88%; }
20451
- .msg.user { align-self: flex-end; align-items: flex-end; }
20452
- .msg.assistant { align-self: flex-start; align-items: flex-start; }
20453
- .msg .who { font-size: 11px; opacity: .55; margin-bottom: 4px; padding: 0 4px; }
20454
- .msg .bubble { padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.65; word-break: break-word; white-space: normal; }
20455
- .msg.user .bubble { background: var(--sp-green); color: #fff; border-bottom-right-radius: 4px; }
20456
- .msg.assistant .bubble { background: #fff; border: 1px solid #d0d7de; border-bottom-left-radius: 4px; }
20457
- @media (prefers-color-scheme: dark) { .msg.assistant .bubble { background: #26282c; border-color: #3a3d42; } }
20458
- .bubble p { margin: 6px 0; }
20459
- .bubble p:first-child { margin-top: 0; }
20460
- .bubble p:last-child { margin-bottom: 0; }
20461
- .bubble h1, .bubble h2, .bubble h3, .bubble h4 { margin: 12px 0 6px; font-size: 15px; line-height: 1.4; }
20462
- .bubble ul, .bubble ol { margin: 6px 0; padding-left: 22px; }
20463
- .bubble code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, monospace; }
20464
- .bubble pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 8px 0; }
20465
- .bubble pre code { background: none; padding: 0; }
20466
- .bubble blockquote { margin: 6px 0; padding: 2px 12px; border-left: 3px solid rgba(127,127,127,.3); opacity: .85; }
20467
- .bubble a { color: var(--sp-green); }
20468
- .bubble table { border-collapse: collapse; margin: 8px 0; }
20469
- .bubble th, .bubble td { border: 1px solid rgba(127,127,127,.25); padding: 4px 10px; font-size: 13px; }
20470
- .bubble img { max-width: 100%; border-radius: 8px; }
20471
- .bubble hr { border: 0; border-top: 1px solid rgba(127,127,127,.2); margin: 10px 0; }
20472
- .typing { display: inline-flex; align-items: center; gap: 4px; padding: 4px 2px; }
20473
- .typing i { width: 6px; height: 6px; border-radius: 50%; background: rgba(127,127,127,.5); animation: blink 1.2s infinite; }
20474
- .typing i:nth-child(2) { animation-delay: .2s; }
20475
- .typing i:nth-child(3) { animation-delay: .4s; }
20476
- @keyframes blink { 0%, 80%, 100% { opacity: .3 } 40% { opacity: 1 } }
20477
- .err { color: #cf222e; white-space: pre-wrap; font-size: 13px; }
20478
- .msg .err { padding: 0 4px; }
20479
- /* 输入区 */
20480
- .chatInput { border-top: 1px solid rgba(127,127,127,.18); padding: 10px 16px calc(12px + env(safe-area-inset-bottom)); background: rgba(127,127,127,.04); }
20481
- .inputRow { display: flex; align-items: flex-end; gap: 8px; }
20482
- .inputRow textarea { flex: 1; border: 1px solid rgba(127,127,127,.25); border-radius: 12px; background: #fff; color: inherit; padding: 10px 12px; font-size: 14px; resize: none; min-height: 44px; max-height: 140px; line-height: 1.5; }
20483
- @media (prefers-color-scheme: dark) { .inputRow textarea { background: #26282c; border-color: #3a3d42; } }
20484
- .inputRow textarea:focus { outline: none; border-color: var(--sp-green); }
20485
- .inputRow .sendBtn { border: 0; border-radius: 12px; background: var(--sp-green); color: #fff; padding: 10px 18px; font-size: 14px; font-weight: 600; cursor: pointer; white-space: nowrap; }
20486
- .inputRow .sendBtn:disabled { opacity: .5; cursor: wait; }
20487
- .inputRow .sendBtn:hover:not(:disabled) { background: var(--sp-green-dim); }
20488
- .hint { text-align: center; font-size: 12px; opacity: .55; padding: 20px 0; }
20489
-
20490
- /* ── 移动端适配(v1.26.6:手机/平板——iOS 防放大 16px / 触控 ≥44px / 100dvh / safe-area)── */
20491
- @media (max-width: 640px) {
20492
- main { padding: 20px 16px calc(28px + env(safe-area-inset-bottom)); }
20493
- .sub { font-size: 14px; margin-bottom: 14px; }
20494
- .keyGate { flex-wrap: wrap; gap: 6px; }
20495
- .keyGate input { flex: 1 1 100%; font-size: 16px; padding: 12px 14px; min-height: 48px; } /* 16px 防 iOS 聚焦放大 + 触控 ≥44px */
20496
- .card { padding: 16px; border-radius: 12px; }
20497
- .card .cname { font-size: 16px; }
20498
- .card .cmeta { font-size: 13px; }
20499
- .chat { height: 100vh; height: 100dvh; } /* dvh:iOS 地址栏收起/展开不跳动 */
20500
- .chatHead { gap: 8px; padding: 8px 12px calc(8px + env(safe-area-inset-top)); }
20501
- .chatHead .back { font-size: 14px; padding: 10px 4px; } /* 触控目标 ≥44px */
20502
- .chatHead .title { font-size: 14px; }
20503
- .chatHead select { font-size: 16px; padding: 8px 6px; min-height: 44px; } /* 16px 防放大 + 触控 */
20504
- .chatHead .newBtn { font-size: 14px; padding: 10px 12px; min-height: 44px; }
20505
- .msgList { padding: 14px 12px calc(20px + env(safe-area-inset-bottom)); gap: 14px; }
20506
- .msg { max-width: 94%; }
20507
- .msg .bubble { font-size: 15px; padding: 12px 14px; }
20508
- .msg .who { font-size: 12px; }
20509
- .chatInput { padding: 8px 12px calc(10px + env(safe-area-inset-bottom)); }
20510
- .inputRow textarea { font-size: 16px; padding: 12px 14px; min-height: 48px; } /* 16px 防放大 + 触控 */
20511
- .inputRow .sendBtn { font-size: 16px; padding: 12px 20px; min-height: 48px; }
20512
- }
20513
- `;
20514
- /** 容器列表页(v1.26.2 → v1.26.6):key 在选容器前填写(v1.26.6 用户拍板:不填 key 不让选容器) */
20515
- function publicAskListPage(cccs) {
20516
- const cards = cccs.map((c) => `
20517
- <a class="card" data-name="${c.name}" href="/c/${encodeURIComponent(c.name)}">
20518
- <div class="cname">${c.name}</div>
20519
- <div class="cmeta">${c.roles.length} 个问答角色 · 点击进入</div>
20520
- </a>`).join("") || "<div class=\"empty\">暂无可问答的认知容器(管理员尚未开放)</div>";
20521
- return `<!DOCTYPE html>
20522
- <html lang="zh">
20523
- <head>
20524
- <meta charset="utf-8">
20525
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
20526
- <title>Serenity Public Ask</title>
20527
- <style>${ASK_CSS}</style>
20528
- </head>
20529
- <body>
20530
- <main>
20531
- <h1>Serenity Public Ask</h1>
20532
- <div class="sub">先填写访问 key,再选择认知容器开始提问</div>
20533
- <div class="keyGate">
20534
- <label for="gateKey">访问 Key</label>
20535
- <input id="gateKey" type="password" placeholder="请输入访问 key(仅本次浏览器记忆)" autocomplete="off" autocapitalize="none" enterkeyhint="done">
20536
- </div>
20537
- <div id="cards">${cards}</div>
20538
- </main>
20539
- <script>
20540
- const KEY_STORE = 'serenity-public-ask-key'
20541
- const gateKey = document.getElementById('gateKey')
20542
- const cardList = document.getElementById('cards')
20543
- // 容器卡片(key 未填时禁用——v1.26.6 用户拍板:key 在选容器前填写)
20544
- function applyGate() {
20545
- const ok = gateKey.value.trim().length > 0
20546
- for (const card of cardList.querySelectorAll('a.card')) {
20547
- card.classList.toggle('locked', !ok)
20548
- card.setAttribute('aria-disabled', ok ? 'false' : 'true')
20549
- if (!ok) { card.removeAttribute('href'); card.style.pointerEvents = 'none'; card.style.opacity = '.4' }
20550
- else { card.setAttribute('href', '/c/' + encodeURIComponent(card.dataset.name)); card.style.pointerEvents = ''; card.style.opacity = '' }
20551
- }
20552
- }
20553
- gateKey.addEventListener('input', () => {
20554
- // 即存 localStorage(v1.26.7 修复:v1.26.6 漏掉保存——填一次就记录,对话页读取)
20555
- try { localStorage.setItem(KEY_STORE, gateKey.value.trim()) } catch {}
20556
- applyGate()
20557
- })
20558
- // key 自动从 localStorage 恢复
20559
- let hasSavedKey = false
20560
- try { const saved = localStorage.getItem(KEY_STORE); if (saved) { gateKey.value = saved; hasSavedKey = true } } catch {}
20561
- if (hasSavedKey) gateKey.placeholder = '访问 Key(已记忆,可修改)'
20562
- applyGate()
20563
- gateKey.focus()
20564
- <\/script>
20565
- </body>
20566
- </html>`;
20567
- }
20568
- /** 单容器问答页(v1.26.2→v1.26.6):URL 锁定容器;聊天 UI——消息流 + 连续对话 + key 从列表页记忆(v1.26.6 用户拍板:对话页不再填 key) */
20569
- function publicAskContainerPage(ccc) {
20570
- const data = jscSafeJsonText(JSON.stringify({
20571
- name: ccc.name,
20572
- root: ccc.root,
20573
- roles: ccc.roles
20574
- }).replace(/</g, "\\u003c"));
20575
- return `<!DOCTYPE html>
20576
- <html lang="zh">
20577
- <head>
20578
- <meta charset="utf-8">
20579
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
20580
- <title>Serenity · ${ccc.name}</title>
20581
- <style>${ASK_CSS}</style>
20582
- </head>
20583
- <body>
20584
- <div class="chat">
20585
- <div class="chatHead">
20586
- <a class="back" href="/">← 容器</a>
20587
- <span class="title">Serenity · <span class="ccname">${ccc.name}</span></span>
20588
- <span class="spacer"></span>
20589
- <select id="role" title="问答角色"></select>
20590
- <button id="newChat" class="newBtn" title="清空当前对话(不存储,刷新即失)">新对话</button>
20591
- </div>
20592
-
20593
- <div id="msgs" class="msgList"></div>
20594
-
20595
- <div class="chatInput">
20596
- <div class="inputRow">
20597
- <textarea id="q" placeholder="向 ${ccc.name} 提问…(Enter 发送,Shift+Enter 换行)" rows="1" enterkeyhint="send" autocapitalize="sentences"></textarea>
20598
- <button id="ask" class="sendBtn">发送</button>
20599
- </div>
20600
- </div>
20601
- </div>
20602
- <script id="ask-data" type="application/json">${data}<\/script>
20603
- <script>
20604
- const CCC = JSON.parse(document.getElementById('ask-data').textContent)
20605
- const KEY_STORE = 'serenity-public-ask-key'
20606
- const roleSel = document.getElementById('role')
20607
- const btn = document.getElementById('ask')
20608
- const msgs = document.getElementById('msgs')
20609
- const qInput = document.getElementById('q')
20610
- let sessionId = ''
20611
- let busy = false
20612
-
20613
- // 角色下拉
20614
- roleSel.innerHTML = (CCC.roles || []).map((r) => '<option value="' + r + '">' + r + '</option>').join('') || '<option value="">(无角色)</option>'
20615
- // key 从 localStorage 读取(列表页填写时记忆;对话页不填——v1.26.6 用户拍板)
20616
- function getStoredKey() {
20617
- try { return (localStorage.getItem(KEY_STORE) || '').trim() } catch { return '' }
20618
- }
20619
-
20620
- /** 追加一条消息气泡(role: user|assistant) */
20621
- function addMsg(role, htmlOrText, isHtml) {
20622
- const m = document.createElement('div')
20623
- m.className = 'msg ' + role
20624
- const who = document.createElement('div')
20625
- who.className = 'who'
20626
- who.textContent = role === 'user' ? '你' : CCC.name
20627
- const bubble = document.createElement('div')
20628
- bubble.className = 'bubble'
20629
- if (isHtml) bubble.innerHTML = htmlOrText
20630
- else bubble.textContent = htmlOrText
20631
- m.appendChild(who)
20632
- m.appendChild(bubble)
20633
- msgs.appendChild(m)
20634
- msgs.scrollTop = msgs.scrollHeight
20635
- return bubble
20636
- }
20637
-
20638
- /** 打字指示器(等待回答时显示) */
20639
- function addTyping() {
20640
- const m = document.createElement('div')
20641
- m.className = 'msg assistant'
20642
- const who = document.createElement('div')
20643
- who.className = 'who'
20644
- who.textContent = CCC.name
20645
- const bubble = document.createElement('div')
20646
- bubble.className = 'bubble'
20647
- bubble.innerHTML = '<span class="typing"><i></i><i></i><i></i></span>'
20648
- m.appendChild(who)
20649
- m.appendChild(bubble)
20650
- msgs.appendChild(m)
20651
- msgs.scrollTop = msgs.scrollHeight
20652
- return bubble
20653
- }
20654
-
20655
- async function ask() {
20656
- const q = qInput.value.trim()
20657
- const key = getStoredKey()
20658
- const role = roleSel.value || undefined
20659
- if (!q) return
20660
- if (!key) { addMsg('assistant', '⚠️ 未找到访问 key,请先回到 <a href="/">容器列表页</a> 填写 key。', true); return }
20661
- if (busy) return
20662
- // 追加用户消息 + 打字指示
20663
- addMsg('user', q, false)
20664
- qInput.value = ''
20665
- autoGrow()
20666
- const typing = addTyping()
20667
- busy = true
20668
- btn.disabled = true
20669
- try {
20670
- const res = await fetch('/c/' + encodeURIComponent(CCC.name) + '/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, role, question: q, sessionId: sessionId || undefined }) })
20671
- const data = await res.json()
20672
- typing.remove() // 移除打字指示,替换为真实回答
20673
- if (!res.ok) {
20674
- sessionId = ''
20675
- addMsg('assistant', '⚠️ ' + (data.error || ('HTTP ' + res.status)), true)
20676
- return
20677
- }
20678
- addMsg('assistant', data.answer_html || data.answer || '(空回答)', true)
20679
- sessionId = data.sessionId || ''
20680
- } catch (err) {
20681
- typing.remove()
20682
- addMsg('assistant', '⚠️ ' + String(err.message || err), true)
20683
- } finally {
20684
- busy = false
20685
- btn.disabled = false
20686
- qInput.focus()
20687
- }
20688
- }
20689
-
20690
- function autoGrow() {
20691
- qInput.style.height = 'auto'
20692
- qInput.style.height = Math.min(qInput.scrollHeight, 140) + 'px'
20693
- }
20694
-
20695
- btn.addEventListener('click', () => void ask())
20696
- qInput.addEventListener('keydown', (e) => {
20697
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void ask() }
20698
- })
20699
- qInput.addEventListener('input', autoGrow)
20700
- // 新对话:重置 sessionId + 清空消息流(前端内存,不存储——刷新即失)
20701
- document.getElementById('newChat').addEventListener('click', () => {
20702
- sessionId = ''
20703
- msgs.innerHTML = ''
20704
- qInput.focus()
20705
- })
20706
-
20707
- // 初始欢迎消息(外层模板求值注入容器名;textContent 渲染防注入)
20708
- addMsg('assistant', '你好,我是「${ccc.name}」的认知助手。有什么可以帮你的?(本对话仅当前页面有效,刷新即失)', false)
20709
- qInput.focus()
20710
- <\/script>
20711
- </body>
20712
- </html>`;
20713
- }
20714
- //#endregion
20715
19814
  //#region src/human-channel.ts
20716
19815
  /**
20717
19816
  * human-channel.ts — 人机信道的**机制层**(机器级;2026-09-26 owner 定案 ⇒ 方案 §2.6)
@@ -21033,7 +20132,7 @@ function buildManualOutputRebuke(session) {
21033
20132
  }
21034
20133
  /**
21035
20134
  * 装配闸门(index.ts apply 调用)。
21036
- * 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff/ACP 临时会话)零干预。
20135
+ * 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff 会话)零干预。
21037
20136
  */
21038
20137
  function registerWeixinOutputGuard(ctx) {
21039
20138
  try {
@@ -21394,7 +20493,7 @@ function manualOutputFallbackNeeded(input) {
21394
20493
  */
21395
20494
  async function resolveCccRootByAlias(ctx, alias) {
21396
20495
  try {
21397
- const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
20496
+ const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
21398
20497
  const entries = await listCccs(ctx);
21399
20498
  const byName = entries.find((e) => e.name === alias);
21400
20499
  if (byName) return byName.root;
@@ -21893,7 +20992,7 @@ function syncHumanChannel(ctx) {
21893
20992
  (async () => {
21894
20993
  try {
21895
20994
  stopMachineLoops();
21896
- const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
20995
+ const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
21897
20996
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
21898
20997
  } catch {}
21899
20998
  })();
@@ -22129,7 +21228,7 @@ function matchCcc(input, candidates) {
22129
21228
  }
22130
21229
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
22131
21230
  async function collectCandidates(ctx) {
22132
- const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
21231
+ const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
22133
21232
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
22134
21233
  const seen = /* @__PURE__ */ new Set();
22135
21234
  const out = [];
@@ -22395,7 +21494,7 @@ function registerLifecycle(ctx) {
22395
21494
  stopAllBridges();
22396
21495
  } catch {}
22397
21496
  };
22398
- registerDisposer(ctx, "self-started resources (gateway/weixin/skiff-debug/acp faces + weixin bridges)", disposeAll);
21497
+ registerDisposer(ctx, "self-started resources (gateway/weixin/skiff-debug faces + weixin bridges)", disposeAll);
22399
21498
  }
22400
21499
  //#endregion
22401
21500
  //#region src/web-fetch-provider.ts
@@ -24597,7 +23696,7 @@ function renderDsl(model) {
24597
23696
  * 🔴 **为什么 PNG 是必需的**:宿主的对话内图片通路**白名单只有光栅格式**
24598
23697
  * (`image/png|jpeg|webp|gif`,见 `dsh-attachment/types.d.ts`)⇒ **SVG 进不去**。
24599
23698
  *
24600
- * 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 skiff/acp 口径。
23699
+ * 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 skiff 口径。
24601
23700
  *
24602
23701
  * ## 🔴 两条**宿主契约**(2026-09-29 现场取证;第 ① 条是本文件第一版写错的地方)
24603
23702
  *
@@ -24971,10 +24070,6 @@ const Config = z.object({
24971
24070
  enabled: z.boolean().default(false),
24972
24071
  debugPort: z.number().min(1024).max(65535).default(SKIFF_DEBUG_PORT)
24973
24072
  }),
24974
- acp: z.object({
24975
- enabled: z.boolean().default(false),
24976
- httpPort: z.number().min(1024).max(65535).default(ACP_HTTP_PORT)
24977
- }),
24978
24073
  webFetch: z.object({ enabled: z.boolean().default(true) }),
24979
24074
  opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
24980
24075
  visionPatch: z.object({ enabled: z.boolean().default(true) }),
@@ -24989,9 +24084,6 @@ const Config = z.object({
24989
24084
  rebuildThresholdK: z.number().min(50).max(4e3).volatile(),
24990
24085
  skiffEnabled: z.boolean().volatile(),
24991
24086
  skiffDebugPort: z.number().min(1024).max(65535).volatile(),
24992
- acpEnabled: z.boolean().volatile(),
24993
- acpHttpPort: z.number().min(1024).max(65535).volatile(),
24994
- publicAskEnabled: z.boolean().volatile(),
24995
24087
  croEnabled: z.boolean().volatile(),
24996
24088
  unattendedEnabled: z.boolean().volatile(),
24997
24089
  diagramEnabled: z.boolean().volatile()
@@ -25054,7 +24146,6 @@ function apply(ctx, config) {
25054
24146
  if (config.opencodeSkills) registerOpencodeSkills(ctx);
25055
24147
  registerBootstrap(ctx);
25056
24148
  registerSkiff(ctx);
25057
- registerAcp(ctx);
25058
24149
  registerCroTurnTracking(ctx);
25059
24150
  registerWakeScheduler(ctx);
25060
24151
  registerWeixinBridge(ctx);
@@ -25078,7 +24169,7 @@ function apply(ctx, config) {
25078
24169
  * ① 听宿主的**真事件** `settings/document-updated`(ns = 我们那张设置页)——顺带说明为什么不用
25079
24170
  * 我们自己的 `serenity/settings-changed`:那个事件**全仓没有任何 `emit` 点**(只有 `ctx.on` 侧
25080
24171
  * 在用)=**死通道**,照着它接线等于接了个永远不响的铃;
25081
- * ② 兜底再挂 `agent/created` / `session/created`(与 skiff/acp 同款做法,那两条在实测里确实会 fire)
24172
+ * ② 兜底再挂 `agent/created` / `session/created`(与 skiff 同款做法,那两条在实测里确实会 fire)
25082
24173
  * —— 宿主事件面若换名,至少还有这一条能自愈;
25083
24174
  * ③ 启动时同步一次(apply 时按当前配置定初始态)。
25084
24175
  * 🔴 **关的时候要注销**(不是"留着但不响应"):与"关时不注册"同一口径 —— 工具不出现在模型清单里。
@@ -25125,7 +24216,8 @@ function registerDiagramTool(ctx) {
25125
24216
  * 同步一次——此刻通常还没有 live 会话,进程 cwd(服务启动目录)也不在 CCC 内 →
25126
24217
  * `resolveSkiffRoot` 返回 null → 打印一行警告后**永不重试**(实证:重启日志
25127
24218
  * `✗ Skiff 调试服务未启动:无法定位 CCC root`,`ss -ltn` 无 3099;同批 ACP 因容忍
25128
- * `root ?? undefined` 而正常启动)。
24219
+ * `root ?? undefined` 而正常启动 —— ⚠️ **该对照已随面 E 退役而失效**(2026-10-15),
24220
+ * 但本条记录的**病因与修法不受影响**(Skiff 侧仍是"首次未定位 → 曾永不重试")。
25129
24221
  *
25130
24222
  * v1.30.14(重启日志实证后的收敛):**并发启动守卫** + 首次未定位降级为信息级日志。
25131
24223
  * - 实证(v1.30.13 重启日志):定时触发与"会话就绪"事件几乎同时到达 → 两次 sync 都
@@ -25249,29 +24341,5 @@ function readWebPort(ctx) {
25249
24341
  return MAIN_WEB_PORT;
25250
24342
  }
25251
24343
  }
25252
- /**
25253
- * F4c ACP HTTP + F4d 建议问答页装配(v1.26.x 实验性):启停 = 人工(设置面板
25254
- * 「Serenity」页 ACP / 问答页开关,settings 持久化)。任一面开启即启动服务
25255
- * (同一端口:POST / = JSON-RPC 需 acpEnabled;GET / + /ask = 问答页需 publicAskEnabled)。
25256
- * 会话创建/延续走 acp-core → skiff-core;仅监听 127.0.0.1;未开启零资源占用。
25257
- */
25258
- function registerAcp(ctx) {
25259
- const sync = async () => {
25260
- const s = readSimpleSettings();
25261
- const anyFace = s.acpEnabled || s.publicAskEnabled;
25262
- if (anyFace && !acpHttpActive()) {
25263
- const root = await resolveSkiffRoot(ctx);
25264
- return startAcpHttpServer(ctx, s.acpHttpPort, root ?? void 0).catch((err) => {
25265
- console.error(`[serenity-hooks] ✗ ACP HTTP 服务启动失败: ${String(err?.message ?? err)}`);
25266
- });
25267
- } else if (!anyFace && acpHttpActive()) stopAcpHttpServer();
25268
- };
25269
- try {
25270
- ctx.on("serenity/settings-changed", () => {
25271
- sync();
25272
- });
25273
- } catch {}
25274
- sync();
25275
- }
25276
24344
  //#endregion
25277
24345
  export { Config, apply, humanChannelAccountSchema, humanChannelSubscriptionSchema, inject, name };