@shgroup/dsh-serenity-hooks 1.56.10 → 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.
Files changed (46) hide show
  1. package/README.md +25 -11
  2. package/dsh.plugin.json +3 -3
  3. package/lib/agent-idle.d.ts +2 -2
  4. package/lib/api.d.ts +0 -1
  5. package/lib/{ccc-DXky9UZu.js → ccc-B0ZGL9lx.js} +30 -13
  6. package/lib/{ccc-roots-CDW2dO0n.js → ccc-roots-GIJ3A_tw.js} +82 -5
  7. package/lib/ccc-roots.d.ts +3 -2
  8. package/lib/ccc.d.ts +23 -10
  9. package/lib/cheap-worker-ops.d.ts +49 -0
  10. package/lib/cheap-worker-preset-inherit.d.ts +35 -0
  11. package/lib/client.js +47 -471
  12. package/lib/config-ops.d.ts +3 -34
  13. package/lib/cro-guide.d.ts +1 -1
  14. package/lib/face-host.d.ts +7 -5
  15. package/lib/host/access.d.ts +2 -2
  16. package/lib/index.d.ts +0 -31
  17. package/lib/index.js +280 -1599
  18. package/lib/invariant.d.ts +1 -1
  19. package/lib/invariant.js +1 -1
  20. package/lib/localstore-ops.d.ts +1 -1
  21. package/lib/{message-source-CQNfQaTx.js → message-source-B4P3Uw6D.js} +1 -1
  22. package/lib/output-guard-seam.d.ts +9 -2
  23. package/lib/ports.d.ts +6 -6
  24. package/lib/seams/context.d.ts +2 -2
  25. package/lib/seams/lifecycle.d.ts +3 -3
  26. package/lib/{settings-section-xHABl8nk.js → settings-section-DQIVsTqE.js} +45 -30
  27. package/lib/settings-section.d.ts +3 -16
  28. package/lib/skiff-core.d.ts +6 -5
  29. package/lib/skiff-debug.d.ts +5 -4
  30. package/lib/{skiff-role-Det66X16.js → skiff-role-B615L9MV.js} +2 -2
  31. package/lib/skiff-role.d.ts +2 -2
  32. package/lib/status.d.ts +2 -2
  33. package/lib/tools/cheap-worker.d.ts +21 -0
  34. package/lib/tools/skiff-admin.d.ts +2 -2
  35. package/lib/unattended-ops.d.ts +6 -4
  36. package/lib/weixin-bridge.d.ts +9 -2
  37. package/lib/weixin-output-guard.d.ts +1 -1
  38. package/lib/{weixin-route-CxCciqQl.js → weixin-route-B9gU1YXz.js} +5 -5
  39. package/package.json +2 -2
  40. package/lib/access-CNccNtgf.js +0 -79
  41. package/lib/acp-core.d.ts +0 -80
  42. package/lib/acp-http.d.ts +0 -37
  43. package/lib/handyman-ops.d.ts +0 -97
  44. package/lib/handyman-preset-inherit.d.ts +0 -35
  45. package/lib/skiff-registry-BjIX_KD1.js +0 -43
  46. package/lib/tools/handyman.d.ts +0 -24
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";
3
- import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readExclusiveTools, h as readTrajectorySkills, i as findGitRoot, l as pathInside, m as readSafeModeHiddenTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readHandymanConfig, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-DXky9UZu.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-CDW2dO0n.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-CQNfQaTx.js";
6
- 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-CxCciqQl.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";
8
- 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-Det66X16.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";
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
+ 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 { 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";
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";
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";
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";
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";
@@ -1083,14 +1081,15 @@ Directory of ACC capabilities — each area lists where to go for details (guide
1083
1081
  与 praxis ontology 的分工 = 那个是**方法文本**、这个是**能跑的那个**)
1084
1082
  ③ 工具与执行 → container_fs(容器文件系统 15 子命令)/ container_git(git)/ dashboard(health 含注册表检查/time/wait)
1085
1083
  acc_msm 已并入 msm(执行+发现)与 container_admin(管理)——msm("<name>", ["<args>"]);inspect=true 查用法
1086
- handyman(杂工 agent 编排——guide 子命令:handyman guide)
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"]))
1093
- ⑥ CCC 配置总览 → container_admin msm ccc-config(9 段:handyman/sessionKeeper/localstore/hooks/safeMode/skiff/trajectory/weixin/**exclusiveTools**)
1092
+ ⑥ CCC 配置总览 → container_admin msm ccc-config(9 段:cheap-worker/sessionKeeper/localstore/hooks/safeMode/skiff/trajectory/weixin/**exclusiveTools**)
1094
1093
  ⑦ 注册表与安全 → mech-registry.json 由 container_admin msm register/deregister 管理(写保护——不可直接编辑)
1095
1094
  dashboard health 输出 registry 完整性检查;损坏恢复指引见 health registry.issues
1096
1095
  CCC 级开关:exclusiveTools(声明专属工具)/ safeMode.blacklist(写路径黑名单)
@@ -1158,24 +1157,27 @@ const CCC_CONFIG_REFERENCE = `═══ CCC Configuration Reference ═══
1158
1157
  CCC-level features are configured in .opencode/serenity.json.
1159
1158
  Below are all available configuration sections.
1160
1159
 
1161
- ── 1. handyman.models ──
1162
- handyman(杂工)工具可用模型白名单(provider/model 列表);未配置时 handyman 报错要求配置。
1163
- 缺省模型 = models[0](可用 handyman.defaultModel 指定,必须 ∈ models)。
1164
- **CCC 应把该白名单指向低成本模型**——handyman 是批量执行通道(v1.31.3 双模式共用同一白名单)。
1160
+ ── 1. cheap-worker.models ──
1161
+ cheap-worker(杂工)工具可用模型白名单(provider/model 列表);未配置时 cheap-worker 报错要求配置。
1162
+ 缺省模型 = models[0](可用 cheap-worker.defaultModel 指定,必须 ∈ models)。
1163
+ **CCC 应把该白名单指向低成本模型**——cheap-worker 是批量执行通道。
1164
+ 🔴 模型**不可由调用方指定**(工具无 model 参数)⇒ 本白名单是模型选择的唯一来源。
1165
1165
 
1166
- Modes(v1.31.3,缺省 foreground):
1167
- mode="foreground"(缺省):一次前台串行委派(宿主委派服务 ctx.subagents.start("spawn") +
1166
+ Behaviour(2026-10-14:background 模式已删除,只剩前台委派):
1167
+ cheap-worker(task, [label]):一次前台委派(宿主委派服务 ctx.subagents.start("spawn") +
1168
1168
  agentOptions 注入模型)——不循环、不校验完成码、不写进度文件;返回子 agent 最终文本。
1169
- 适合"一次调用一次结果"(逐用例回归、单次转换)。
1170
- mode="background":既有循环校验实现(内部 while 硬循环 + 随机完成码唯一判据 + 轮次上限
1171
- 默认 100 可续跑 + 异常自动重启 ≤100 + 进度文件 AGENT_SESSIONS/handyman-<label>.md/.json +
1172
- jobs 并行 maxParallel 默认 10)。适合长任务/无人值守/需抗提前收工。
1169
+ cheap-worker(jobs=[{task,label?},...]):并行扇出,上限 10(硬编码),结果保持传入顺序。
1170
+ ⇒ **无 background 模式、无进度文件、无续跑**(owner 2026-10-14 令「删除后台运行能力」)。
1171
+
1172
+ Old key(兼容):
1173
+ 读不到 "cheap-worker" 时**回落到旧键 "handyman"**(同形状:models / defaultModel)。
1174
+ 旧键里的 "maxRounds"/"maxParallel" 已随 background 退场,留着无害但不再生效。
1173
1175
 
1174
1176
  Config:
1175
- { "handyman": { "models": ["provider/model-name"] } }
1177
+ { "cheap-worker": { "models": ["provider/model-name"] } }
1176
1178
 
1177
1179
  Example:
1178
- { "handyman": { "models": ["minimax-cn-coding-plan/MiniMax-M3"], "defaultModel": "minimax-cn-coding-plan/MiniMax-M3", "maxRounds": 100, "maxParallel": 10 } }
1180
+ { "cheap-worker": { "models": ["minimax-cn-coding-plan/MiniMax-M3"], "defaultModel": "minimax-cn-coding-plan/MiniMax-M3" } }
1179
1181
 
1180
1182
  ── 2. sessionKeeper.threshold / sessionKeeper.sessionMdMaxKB ──
1181
1183
  trajectory-assistant(TRAJECTORY-ASSISTANT · CHECKPOINT)提醒机制的积分阈值(非 headless 主 agent)。
@@ -1821,77 +1823,14 @@ async function runMsmAsync(root, args) {
1821
1823
  }
1822
1824
  }
1823
1825
  //#endregion
1824
- //#region src/handyman-ops.ts
1826
+ //#region src/cheap-worker-ops.ts
1825
1827
  /**
1826
- * handyman-ops.ts — handyman(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
1828
+ * cheap-worker-ops.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
1827
1829
  *
1828
- * v1.24.0:loop(牛马)→ handyman(杂工)重命名。语义对齐 osp loop:
1829
- * 进度文件(handyman-<label>.md/.json)、续跑、轮次 prompt 结构、stop token。
1830
- * 不兼容旧 loop- 进度文件(用户拍板:仅新 handyman- 前缀)。
1830
+ * 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
1831
+ * 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
1832
+ * 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
1831
1833
  */
1832
- /** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长(按码点截断,修复代理对切散 U+FFFD) */
1833
- function sanitizeLabel(label) {
1834
- return [...label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "")].slice(0, 50).join("");
1835
- }
1836
- function handymanProgressPaths(root, label) {
1837
- const dir = join(root, "AGENT_SESSIONS");
1838
- const safe = sanitizeLabel(label);
1839
- return {
1840
- md: join(dir, `handyman-${safe}.md`),
1841
- json: join(dir, `handyman-${safe}.json`)
1842
- };
1843
- }
1844
- /** 读取进度(续跑);无文件返回 round 0 */
1845
- function readProgress(root, label) {
1846
- const { json } = handymanProgressPaths(root, label);
1847
- if (!existsSync(json)) return null;
1848
- try {
1849
- return JSON.parse(readFileSync(json, "utf-8"));
1850
- } catch {
1851
- return null;
1852
- }
1853
- }
1854
- function writeProgress(root, label, p) {
1855
- const { md, json } = handymanProgressPaths(root, label);
1856
- mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
1857
- writeFileSync(json, JSON.stringify({
1858
- ...p,
1859
- status: p.status ?? "running",
1860
- updated: isoLocal()
1861
- }, null, 2) + "\n", "utf-8");
1862
- const lines = [
1863
- `# handyman: ${label}`,
1864
- `- Model: ${p.model}`,
1865
- `- Round: ${p.round}`,
1866
- `- Done: ${p.done}`,
1867
- "",
1868
- `## Latest response`,
1869
- "",
1870
- p.lastResponse,
1871
- ""
1872
- ];
1873
- writeFileSync(md, lines.join("\n"), "utf-8");
1874
- }
1875
- /** 失败状态落盘(对齐 osp writeFailedStatus:done=true / status=failed / errorCode) */
1876
- function writeFailedStatus(root, label, info) {
1877
- const { json } = handymanProgressPaths(root, label);
1878
- mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
1879
- const prev = readProgress(root, label);
1880
- writeFileSync(json, JSON.stringify({
1881
- round: prev?.round ?? 0,
1882
- done: true,
1883
- label,
1884
- model: prev?.model ?? "",
1885
- status: "failed",
1886
- errorCode: info.errorCode,
1887
- errorMessage: info.errorMessage,
1888
- updated: isoLocal(),
1889
- lastResponse: prev?.lastResponse ?? ""
1890
- }, null, 2) + "\n", "utf-8");
1891
- }
1892
- function newStopToken() {
1893
- return `SERENITY_HANDYMAN_DONE_${randomBytes(8).toString("hex")}`;
1894
- }
1895
1834
  /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
1896
1835
  function splitModel(model) {
1897
1836
  const idx = model.indexOf("/");
@@ -1906,13 +1845,13 @@ function splitModel(model) {
1906
1845
  }
1907
1846
  /** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
1908
1847
  function requireWhitelistedModel(model, models) {
1909
- if (!models.includes(model)) throw new Error(`handyman: model "${model}" is not in the CCC whitelist. Configure .opencode/serenity.json "handyman.models" with one of: ${models.join(", ")}`);
1848
+ if (!models.includes(model)) throw new Error(`cheap-worker: model "${model}" is not in the CCC whitelist. Configure .opencode/serenity.json "cheap-worker.models" with one of: ${models.join(", ")}`);
1910
1849
  }
1911
1850
  /**
1912
1851
  * providerAvailabilityError — 判定「**本机有没有该 provider 的 adapter**」,并产出**可执行**的错误文本。
1913
1852
  * 返回 `null` = 全部可用(调用方照常继续)。
1914
1853
  *
1915
- * 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`handyman` 的模型白名单写在
1854
+ * 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`cheap-worker` 的模型白名单写在
1916
1855
  * **CCC**(`.opencode/serenity.json`,**随 git 走**),而 provider adapter 由**每台机器的宿主**
1917
1856
  * 注册(profile 配置 / settings 命名空间)⇒ 同一份白名单在一台机器可用、在另一台必失败。
1918
1857
  * 实测症状:宿主抛 `no adapter registered for provider "<p>"`(`dsh-llm`,code `NO_ADAPTER`),
@@ -1938,11 +1877,11 @@ function providerAvailabilityError(models, facts) {
1938
1877
  const configPath = facts.configPath ?? "<ccc>/.opencode/serenity.json";
1939
1878
  const missingList = missing.map((p) => `"${p}"`).join(", ");
1940
1879
  return [
1941
- `handyman: provider ${missingList} has no adapter registered on this machine.`,
1880
+ `cheap-worker: provider ${missingList} has no adapter registered on this machine.`,
1942
1881
  "",
1943
1882
  " Requested model(s) :",
1944
1883
  ...requested.map((e) => ` ${e.raw}`),
1945
- ` Configured in : ${configPath} → "handyman.models" / "handyman.defaultModel"`,
1884
+ ` Configured in : ${configPath} → "cheap-worker.models" / "cheap-worker.defaultModel"`,
1946
1885
  "",
1947
1886
  " Why this happens : the whitelist lives in the CCC (it travels with the repository), while",
1948
1887
  " provider adapters are registered per machine by the host. An entry that",
@@ -1956,135 +1895,75 @@ function providerAvailabilityError(models, facts) {
1956
1895
  "",
1957
1896
  " Fix (pick one, then retry):",
1958
1897
  ` 1. Point the whitelist at a provider that IS registered here — edit ${configPath}`,
1959
- " (\"handyman.models\", and \"handyman.defaultModel\" if that is what you relied on).",
1898
+ " (\"cheap-worker.models\", and \"cheap-worker.defaultModel\" if that is what you relied on).",
1960
1899
  ` 2. Register/activate provider ${missingList} on this machine (host profile config,`,
1961
1900
  " or the settings namespace listed above).",
1962
1901
  "",
1963
1902
  " Do not retry unchanged — the same call fails identically."
1964
1903
  ].join("\n");
1965
1904
  }
1966
- /** 轮次 prompt(对齐老 loop 结构:回顾进度 → 自由工作 → 汇报;S134 EAP 化:固定详尽) */
1967
- function buildRoundPrompt(opts) {
1968
- const { root, session, label, round, stopToken, progress, task } = opts;
1969
- const resumeNote = progress && progress.round > 0 ? `Previous round (round ${progress.round}) completed: ${progress.lastResponse.slice(0, 300)}\nAlways continue from where you left off; never redo completed work.` : "This is the first round.";
1970
- return `# ${label} — handyman round ${round}
1971
-
1972
- CCC root: ${root}
1973
- ${session ? `Work session: ${session} (progress recorded in AGENT_SESSIONS/${session}/SESSION.md)` : ""}
1974
- ${task ? `Task: ${task}` : `Task: follow the work corresponding to label "${label}" (if a work session exists, read SESSION.md first to clarify the goal)`}
1975
- ${resumeNote}
1976
-
1977
- ## Work rules (fixed every round, must follow)
1978
- 1. Work freely within this round: read files, modify code, execute commands — use every means to advance the task.
1979
- 2. If this task is **reading/curating or text-writing work** (extracting from files, summarizing, writing docs, generating text, etc.),
1980
- first load eap (acc-eap skill) and organize output per the EAP standard:
1981
- - E↑ Explicit: entities/variables clearly defined, relationships with direction and cardinality, boundaries drawn, no ambiguous words
1982
- - R↓ Reconstructable: key conclusions record sources and reasoning, rebuildable by later agents
1983
- - S↑ Stable: output structure regenerates repeatably, no reliance on implicit context
1984
- 3. Reports must be concrete and verifiable — no filler.
1985
-
1986
- ## Per-round report (fixed format, answer each item)
1987
- 1. What was done this round (concrete)
1988
- 2. Next-step plan
1989
- 3. Whether the task is complete (if complete, output only ${stopToken})
1990
-
1991
- If the task is complete, output only ${stopToken}.`;
1992
- }
1993
1905
  /**
1994
- * handyman 规模化使用指引(guide 子命令输出;S134 继承 + v1.24.0 更新):
1995
- * 使用 handyman 前必须先加载 eap 设计方案;并行策略(jobs 编排);提示词规范(详尽固定 EAP);
1996
- * 阅读/文字编写类 handyman 内部也加载 eap。
1906
+ * cheap-worker 规模化使用指引(guide 子命令输出)。
1907
+ *
1908
+ * 🆕 2026-10-14:**双模式段已整段退场**(owner 令「删除后台运行能力」)。本指引只描述现行机制:
1909
+ * 前台委派(单任务)/前台并行扇出(jobs)/白名单与 provider 预检。
1997
1910
  */
1998
- const HANDYMAN_GUIDE = `# handyman — Scale-Up Usage Guide (guide)
1911
+ const CHEAP_WORKER_GUIDE = `# cheap-worker — Scale-Up Usage Guide (guide)
1999
1912
 
2000
- ## Two modes (v1.31.3; default = foreground)
2001
- | | foreground (default) | background |
1913
+ ## What it is
1914
+ One delegation primitive: **foreground spawn**. The host starts child agent(s) through its delegation
1915
+ service, awaits them, and returns their final text. There is no loop, no completion code, no progress
1916
+ file, and no unattended/background run.
1917
+
1918
+ | | single | parallel (jobs) |
2002
1919
  |---|---|---|
2003
- | What | child agent(s) run the task ONCE and return their final text (jobs=[...] ⇒ parallel, cap 10) | loop-validated worker: hard while-loop + random completion code |
2004
- | Use when | you need result(s) back now (per-case testers, single conversions, parallel fan-out) | long/unattended work needing anti-early-finish, resumability, or parallel jobs |
2005
- | Loop / validation | none | stop-token is the ONLY completion condition; round cap (default 100); auto-restart (≤100) |
2006
- | Progress file | none | AGENT_SESSIONS/handyman-<label>.md/.json (same label resumes) |
2007
- | Parallel jobs | **yes** — jobs=[{task,label?,model?},...], **cap 10 (fixed)** | yes (jobs=[...], handyman.maxParallel default 10) |
2008
- | Mechanism | host delegation service ctx.subagents.start("spawn") + agentOptions | ctx.agents.create + internal while-loop |
2009
- | Model source | CCC handyman.models whitelist (same for both) | same |
1920
+ | Call | cheap-worker(task, [label]) | cheap-worker(jobs=[{task, label?}, ...]) |
1921
+ | Behaviour | one child runs the task once | all children start at once, results keep your order |
1922
+ | Cap | — | **10 (fixed, hardcoded)** |
1923
+ | Model | CCC config (single source) | same |
2010
1924
 
2011
1925
  ## ⚠️ Before using: load eap and design the plan
2012
- Before calling handyman, load eap (acc-eap skill) and design the "scale-up handyman plan" based on the EAP framework.
1926
+ Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up cheap-worker plan" based on the EAP framework.
2013
1927
 
2014
1928
  ### 1. Task decomposition (E↑ Explicit)
2015
1929
  - Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
2016
- - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel (both modes)
1930
+ - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel via \`jobs\`
2017
1931
 
2018
- ### 2. Prompt design (handyman's task parameter)
1932
+ ### 2. Prompt design (cheap-worker's task parameter)
2019
1933
  - task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
2020
- - foreground mode: the child does NOT share your conversation — restate everything it needs in task
1934
+ - the child does NOT share your conversation — restate everything it needs in task
2021
1935
  - Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
2022
1936
  output a JSON array (fields id/conclusion/evidence), do not modify the original file"
2023
1937
  - Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
2024
- the handyman-internal agent is also required to load eap and organize output per the EAP standard
1938
+ the cheap-worker-internal agent is also required to load eap and organize output per the EAP standard
2025
1939
 
2026
- ### 3. Model whitelist (CCC-configured, mandatory)
2027
- - handyman only uses models listed in .opencode/serenity.json "handyman.models" — never arbitrary models
2028
- - The CCC is expected to point this whitelist at LOW-COST models: handyman is the bulk-execution channel
2029
- - background mode: recursive subagents inside a worker inherit the worker's model automatically (DSH native)
2030
- - foreground mode: the child is started through the host delegation service with the resolved model
1940
+ ### 3. Model (CCC-configured, not selectable per call)
1941
+ - The model is read from .opencode/serenity.json "cheap-worker.models" — **the caller cannot pass one**
1942
+ (there is no "model" parameter). "cheap-worker.defaultModel" names which entry is used; it falls back to models[0].
1943
+ - The CCC is expected to point this whitelist at LOW-COST models: cheap-worker is the bulk-execution channel
1944
+ - A child's own recursive subagents inherit the child's model automatically (DSH native)
2031
1945
 
2032
1946
  ### 3b. 🔴 Provider availability (per-machine — the whitelist is NOT enough)
2033
1947
  - The whitelist lives in the **CCC** ("serenity.json", it **travels with the repository**), but provider
2034
1948
  **adapters are registered per machine by the host** (profile config / settings namespace). An entry that
2035
1949
  works on one machine can fail on another with: "no adapter registered for provider <p>".
2036
- - Before spawning, handyman verifies the provider of the resolved model against the adapters registered
1950
+ - Before spawning, cheap-worker verifies the provider of the resolved model against the adapters registered
2037
1951
  **on this machine**; if it is missing it reports the requested model, where it is configured, **the
2038
1952
  providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the
2039
- exact fix — foreground puts this in "diagnostic", background throws it.
2040
- - ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "handyman.models"
2041
- (and "handyman.defaultModel") to a provider listed as available, or activate/register the provider on this
1953
+ exact fix — delivered in the result's "diagnostic" field (never an opaque failure, never a created child).
1954
+ - ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "cheap-worker.models"
1955
+ (and "cheap-worker.defaultModel") to a provider listed as available, or activate/register the provider on this
2042
1956
  machine. **Do not retry the same call unchanged** — it fails identically.
2043
1957
 
2044
- ### 4. Parallel strategy (both modes)
2045
- - Independent subtasks can run in parallel:
2046
- · **foreground** — handyman(jobs=[{task,label?,model?},...]) ⇒ fan-out of custom-model subagents, **cap 10**,
2047
- results come back **in the order you passed them** (label is only a display name here; per-job "model" allowed)
2048
- · **background** — handyman(mode="background", jobs=[...]): each job gets its own label + task + stop token + progress file
2049
- - Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label
2050
- (AGENT_SESSIONS/handyman-<label>.json) — same label resumes, different labels never interfere
2051
- - Parallel cap: **foreground 10 (fixed)** / background handyman.maxParallel (default 10 — cheap models are cheap)
2052
- - Aggregation: foreground returns all outputs in one result; background: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)
2053
- - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead
1958
+ ### 4. Parallel strategy
1959
+ - Independent subtasks: cheap-worker(jobs=[{task,label?},...]) ⇒ fan-out, **cap 10**, results come back
1960
+ **in the order you passed them** (label is only a display name).
1961
+ - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead.
2054
1962
 
2055
1963
  ## Completion criteria
2056
- - foreground: the child's run settles; done=true only when stopReason="completed" (otherwise read diagnostic)
2057
- - background (osp loop standard): the only completion condition = the worker echoes this round's random verification
2058
- code (stop token); dialogue round cap (default 100, osp fail-safe, forced stop beyond the cap, resumable);
2059
- automatic restart on abnormal agent stop (≤100 restarts, anti-infinite-loop)
2060
-
2061
- ## Waiting UI
2062
- - The WebUI session-header Serenity detail card shows running background handymen's progress
2063
- (label / round / last response), one line per parallel job, ~3s refresh (foreground mode writes no progress)
1964
+ - The child's run settles; done=true only when stopReason="completed" — otherwise read "diagnostic"
1965
+ (and "output" for whatever partial text was produced).
2064
1966
  `;
2065
- /** 列出 AGENT_SESSIONS/handyman-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
2066
- function listActiveHandymen(root) {
2067
- const dir = join(root, "AGENT_SESSIONS");
2068
- if (!existsSync(dir)) return [];
2069
- const out = [];
2070
- for (const entry of readdirSync(dir)) {
2071
- if (!entry.startsWith("handyman-") || !entry.endsWith(".json")) continue;
2072
- try {
2073
- const data = JSON.parse(readFileSync(join(dir, entry), "utf-8"));
2074
- if (typeof data.label !== "string" || typeof data.round !== "number") continue;
2075
- out.push({
2076
- label: data.label,
2077
- round: data.round,
2078
- done: data.done === true,
2079
- model: typeof data.model === "string" ? data.model : "",
2080
- updated: typeof data.updated === "string" ? data.updated : "",
2081
- lastResponse: typeof data.lastResponse === "string" ? data.lastResponse : ""
2082
- });
2083
- } catch {}
2084
- }
2085
- out.sort((a, b) => Date.parse(b.updated) - Date.parse(a.updated));
2086
- return out;
2087
- }
2088
1967
  //#endregion
2089
1968
  //#region src/agent-idle.ts
2090
1969
  /**
@@ -3063,10 +2942,6 @@ function defaultAdvancedSettings() {
3063
2942
  mode: "",
3064
2943
  overrideText: ""
3065
2944
  },
3066
- publicAsk: {
3067
- key: "",
3068
- allowed: []
3069
- },
3070
2945
  weixinApi: {
3071
2946
  enabled: true,
3072
2947
  port: WEIXIN_SEND_PORT
@@ -3119,7 +2994,6 @@ function mergeWithDefaults(raw) {
3119
2994
  const o = raw;
3120
2995
  const gateway = o.gateway ?? {};
3121
2996
  const persona = o.persona ?? {};
3122
- const publicAsk = o.publicAsk ?? {};
3123
2997
  const weixinApi = o.weixinApi ?? {};
3124
2998
  return {
3125
2999
  gateway: {
@@ -3136,10 +3010,6 @@ function mergeWithDefaults(raw) {
3136
3010
  mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
3137
3011
  overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
3138
3012
  },
3139
- publicAsk: {
3140
- key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
3141
- allowed: Array.isArray(publicAsk.allowed) ? publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : def.publicAsk.allowed
3142
- },
3143
3013
  weixinApi: {
3144
3014
  enabled: typeof weixinApi.enabled === "boolean" ? weixinApi.enabled : def.weixinApi.enabled,
3145
3015
  port: typeof weixinApi.port === "number" && Number.isInteger(weixinApi.port) && weixinApi.port >= 0 ? weixinApi.port : def.weixinApi.port
@@ -3177,10 +3047,6 @@ function updateAdvancedSettings(patch) {
3177
3047
  mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
3178
3048
  overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
3179
3049
  } : current.persona,
3180
- publicAsk: patch.publicAsk !== void 0 ? {
3181
- key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
3182
- allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
3183
- } : current.publicAsk,
3184
3050
  weixinApi: patch.weixinApi !== void 0 ? {
3185
3051
  enabled: typeof patch.weixinApi.enabled === "boolean" ? patch.weixinApi.enabled : current.weixinApi.enabled,
3186
3052
  port: typeof patch.weixinApi.port === "number" && Number.isInteger(patch.weixinApi.port) && patch.weixinApi.port >= 0 ? patch.weixinApi.port : current.weixinApi.port
@@ -3190,87 +3056,6 @@ function updateAdvancedSettings(patch) {
3190
3056
  return next;
3191
3057
  }
3192
3058
  /**
3193
- * 确保问答页访问 key 存在(首次启用自动生成 + 写回固定,用户拍板"配置时生成"):
3194
- * 读取全局配置 publicAsk.key;为空 → 生成 32 字节随机 hex(64 字符)→ 写回持久化。
3195
- * @returns 当前固定 key(已生成则返回既有值——**幂等**,不会覆盖用户手改的 key)
3196
- */
3197
- function ensurePublicAskKey() {
3198
- const current = readAdvancedSettings();
3199
- if (current.publicAsk.key !== "") return current.publicAsk.key;
3200
- const key = randomBytes(32).toString("hex");
3201
- updateAdvancedSettings({ publicAsk: {
3202
- key,
3203
- allowed: current.publicAsk.allowed
3204
- } });
3205
- return key;
3206
- }
3207
- /** 校验问答页访问 key(timing-safe;key 未生成 → 恒 false——没有 key 不工作) */
3208
- function verifyPublicAskKey(provided) {
3209
- const expected = readAdvancedSettings().publicAsk.key;
3210
- if (!expected || typeof provided !== "string" || provided === "") return false;
3211
- const a = Buffer.from(provided);
3212
- const b = Buffer.from(expected);
3213
- return a.length === b.length && timingSafeEqual(a, b);
3214
- }
3215
- /**
3216
- * 轮换问答页访问 key(v1.26.5,S142 用户:公网开放 + 不好说要换 key 呢):
3217
- * 生成新 32 字节随机 hex(64 字符)→ 覆盖写回(**强制替换**——旧 key 立即失效)。
3218
- * @returns 新 key(面板展示给管理员,重新分享给使用者)
3219
- */
3220
- function rotatePublicAskKey() {
3221
- const current = readAdvancedSettings();
3222
- const key = randomBytes(32).toString("hex");
3223
- updateAdvancedSettings({ publicAsk: {
3224
- key,
3225
- allowed: current.publicAsk.allowed
3226
- } });
3227
- return key;
3228
- }
3229
- /** 失败锁定阈值(连续失败次数) */
3230
- const PUBLIC_ASK_FAIL_THRESHOLD = 5;
3231
- /** 首次锁定基础时长(指数退避底数) */
3232
- const PUBLIC_ASK_LOCK_BASE_MS = 9e5;
3233
- /** 锁定上限 */
3234
- const PUBLIC_ASK_LOCK_MAX_MS = 144e5;
3235
- const ipFailStates = /* @__PURE__ */ new Map();
3236
- /** 重置 IP 失败状态(测试/管理员解封) */
3237
- function resetPublicAskIpFail(ip) {
3238
- ipFailStates.delete(ip);
3239
- }
3240
- /** IP 当前是否锁定(到期自动解锁) */
3241
- function isPublicAskIpLocked(ip) {
3242
- const st = ipFailStates.get(ip);
3243
- if (!st || st.lockedUntil === 0) return false;
3244
- if (Date.now() >= st.lockedUntil) {
3245
- st.lockedUntil = 0;
3246
- st.count = 0;
3247
- return false;
3248
- }
3249
- return true;
3250
- }
3251
- /** 记录一次 key 校验失败;达到阈值 → 锁定(指数退避)。返回锁定剩余毫秒(0 = 未锁定) */
3252
- function recordPublicAskFail(ip) {
3253
- let st = ipFailStates.get(ip);
3254
- if (!st) {
3255
- st = {
3256
- count: 0,
3257
- lockedUntil: 0,
3258
- lockRound: 0
3259
- };
3260
- ipFailStates.set(ip, st);
3261
- }
3262
- if (st.lockedUntil > 0 && Date.now() < st.lockedUntil) return st.lockedUntil - Date.now();
3263
- st.count += 1;
3264
- if (st.count >= PUBLIC_ASK_FAIL_THRESHOLD) {
3265
- const base = PUBLIC_ASK_LOCK_BASE_MS * 2 ** st.lockRound;
3266
- st.lockedUntil = Date.now() + Math.min(base, PUBLIC_ASK_LOCK_MAX_MS);
3267
- st.lockRound += 1;
3268
- st.count = 0;
3269
- return st.lockedUntil - Date.now();
3270
- }
3271
- return 0;
3272
- }
3273
- /**
3274
3059
  * 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
3275
3060
  * 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
3276
3061
  * @param root - CCC 根(localstore.json 所在目录)
@@ -3303,8 +3088,7 @@ function toWire(settings) {
3303
3088
  allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
3304
3089
  totpEnabled: settings.gateway.totpEnabled
3305
3090
  },
3306
- persona: settings.persona,
3307
- publicAsk: { allowed: [...settings.publicAsk.allowed] }
3091
+ persona: settings.persona
3308
3092
  };
3309
3093
  }
3310
3094
  /**
@@ -3373,11 +3157,6 @@ function applyWirePatch(wire) {
3373
3157
  if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
3374
3158
  patch.persona = psPatch;
3375
3159
  }
3376
- if (wire.publicAsk !== void 0 && Array.isArray(wire.publicAsk.allowed)) {
3377
- const paPatch = { ...current.publicAsk };
3378
- paPatch.allowed = wire.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "");
3379
- patch.publicAsk = paPatch;
3380
- }
3381
3160
  return updateAdvancedSettings(patch);
3382
3161
  }
3383
3162
  //#endregion
@@ -3446,7 +3225,7 @@ function toolsBlock() {
3446
3225
  " msm — execute a registered CCC MSM: msm(name, args); partial name returns candidates; inspect=true shows usage",
3447
3226
  " praxis — actionable theory injection: praxis (index) / praxis eap / praxis neat / praxis cce / praxis ontology",
3448
3227
  " ontology — ontology engineering ENGINE (executable): init (write template) / check (M1-M10 structural; level=strict adds L1-L5 hints) / explain (summary) / verify (V1-V4 runtime compliance) / review (semantic-review prompt) / compliance (axiom-vs-skill checklist). Sibling of `praxis ontology` — that one is the METHOD TEXT, this one RUNS it. path must stay inside the container; the receipt carries the engine exit code (0/1/2/4) and the engine text verbatim, so read it rather than assuming success",
3449
- " handyman — delegate work to a worker agent on a CCC-whitelisted model; mode=\"foreground\" (default) = child agent(s) run once and return their final text (jobs=[...] fans out in parallel, cap 10, per-job model); mode=\"background\" = loop-validated worker (completion code + round cap + restart + progress file), jobs=[] orchestrates parallel work",
3228
+ " cheap-worker — delegate work to a worker agent on the CCC-configured model; one serial child per task, returns its final text. `jobs=[...]` fans out in parallel (cap 10, results keep your order). No loop, no completion code, no progress file. The model is NOT selectable per call (read from the CCC config); a worker's tool set excludes cheap-worker itself",
3450
3229
  " serenity-skill-creator — creator for serenity-skills (this container's enhanced skill standard): guide (authoring/optimization method — read first) / create <name> (scaffold SKILL.md + skill.ttl + references/, base-chain import path computed) / check <name> (A1 structural + A2 base-chain; three states: modeled-ok / plain-skill / modeled-but-check-could-not-run) / review <name> (C tier: axiom-vs-text compliance question list, half-mechanical) / index <name> (preview the second-layer resource list the model will see) / status (which skills are modeled). A skill is \"modeled\" iff it has a file named exactly skill.ttl next to SKILL.md; a plain skill is NOT an anomaly. Only `create` writes, and it refuses an existing directory",
3451
3230
  " localstore — ACC local credential/config storage (CCC-root localstore.json; git policy localstore.gitTrack default deny)",
3452
3231
  " container_admin — container administration (the maintenance bay): role (Skiff roles: guide/validate/apply/list) / msm (register/deregister/check/guide/catalog/ccc-config) / config (view)",
@@ -3997,15 +3776,6 @@ function unregisterSkiffSession(sessionId) {
3997
3776
  skiffAgents.delete(sessionId);
3998
3777
  unregisterSkiffSession$1(sessionId);
3999
3778
  }
4000
- /** 测试/调试:注册表快照(sessionId → {role} 兼容展示) */
4001
- function skiffSessionSnapshot() {
4002
- const out = /* @__PURE__ */ new Map();
4003
- for (const [id, b] of skiffSessionSnapshot$1()) out.set(id, {
4004
- role: b.role,
4005
- ccc: b.ccc
4006
- });
4007
- return out;
4008
- }
4009
3779
  /** 自动创建的 bound 标记(readLastBound 恢复判定——只恢复本机制建的;手工 use 的旧 bound 不认) */
4010
3780
  const AUTO_BOUND_NOTE_PREFIX = "auto-created for skiff role";
4011
3781
  /**
@@ -4094,7 +3864,7 @@ function ensureWorkspacePromptSection(agent, scope) {
4094
3864
  *
4095
3865
  * 判定链:
4096
3866
  * ⓪ **临时身份不建工作台**(v1.30.9,S142 用户报"SESSION 爆炸"根治):`persistent=false`
4097
- * (ACP/调试页/问答页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
3867
+ * (调试页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
4098
3868
  * 调用都是新 id、无身份延续,为其建 SESSION = 每次问答留一个永久空目录(无界熵增);
4099
3869
  * 用户模型「skiff 本质是临时会话机制」——工作台属于**持久身份**,不属于单次调用。
4100
3870
  * ① 角色未启用 session 能力(trajectory.session !== true)→ null(现状零变化)
@@ -4152,7 +3922,7 @@ function ensureSkiffSession(root, agent, roleName, role, persistent = true) {
4152
3922
  return mdPath;
4153
3923
  }
4154
3924
  /** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
4155
- * handyman 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
3925
+ * cheap-worker 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
4156
3926
  * guard 角色白名单再按角色过滤可见/可用面——白名单外工具仍 deny) */
4157
3927
  const SKIFF_PRESET = "standard";
4158
3928
  /**
@@ -4163,10 +3933,10 @@ const SKIFF_PRESET = "standard";
4163
3933
  * 指定 sessionId 且磁盘已有持久化 log → `ctx.agents.resume`(DSH 持久化语义:
4164
3934
  * sessionId 即身份,已持久化会话必须 resume——历史 + turn 续号延续,同用户长期记忆保留);
4165
3935
  * 无 log(首次)/持久化未配置 → `ctx.agents.create`(新建)。不指定 sessionId →
4166
- * 随机 id 恒 create(ACP/调试页默认路径)。
3936
+ * 随机 id 恒 create(调试页默认路径)。
4167
3937
  *
4168
3938
  * @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现用户↔会话长期映射——
4169
- * 不传则随机生成 skiff-<role>-<uuid>,ACP/调试页默认路径)
3939
+ * 不传则随机生成 skiff-<role>-<uuid>,调试页默认路径)
4170
3940
  */
4171
3941
  async function createSkiffAgent(ctx, root, roleName, role, defaultModel, sessionId) {
4172
3942
  if (!ctx.agents) throw new Error("skiff: ctx.agents unavailable");
@@ -4315,8 +4085,8 @@ function sessionEvents(session) {
4315
4085
  if (typeof s.snapshotEvents === "function") return s.snapshotEvents();
4316
4086
  return s.events ?? [];
4317
4087
  }
4318
- /** 读会话最后一个 assistant/message 文本(handyman 同款) */
4319
- function lastAssistantText$2(agent) {
4088
+ /** 读会话最后一个 assistant/message 文本(cheap-worker 同款) */
4089
+ function lastAssistantText$1(agent) {
4320
4090
  const events = sessionEvents(agent.session);
4321
4091
  for (let i = events.length - 1; i >= 0; i--) {
4322
4092
  const e = events[i];
@@ -4377,8 +4147,9 @@ function truncate(s, n) {
4377
4147
  * 提问一轮:followup → 等 idle → 读答案 + 轨迹。
4378
4148
  * @param eventsStart 轨迹起点:显式传 0 = 全量轨迹(会话追问时页面重绘完整时间线,
4379
4149
  * v1.25.10 用户拍板);不传(undefined)= 本轮增量(followup 前 events 之后)
4380
- * @param options.includeTrajectory 是否计算轨迹(v1.26.10:**3100 对外只提供问答**——
4381
- * 公开问答页 / 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 保留)
4382
4153
  */
4383
4154
  async function askSkiff(ctx, agent, question, eventsStart, options) {
4384
4155
  const before = eventsStart === void 0 ? sessionEvents(agent.session).length : eventsStart;
@@ -4390,7 +4161,7 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
4390
4161
  source: PLUGIN_SOURCE
4391
4162
  }));
4392
4163
  await waitAgentIdle(ctx, agent);
4393
- const answer = lastAssistantText$2(agent);
4164
+ const answer = lastAssistantText$1(agent);
4394
4165
  const trajectory = options?.includeTrajectory === false ? [] : eventsToTrajectory(sessionEvents(agent.session).slice(before));
4395
4166
  return {
4396
4167
  answer,
@@ -9306,49 +9077,23 @@ function createSkillCreatorTool() {
9306
9077
  });
9307
9078
  }
9308
9079
  //#endregion
9309
- //#region src/handyman-preset-inherit.ts
9080
+ //#region src/tools/cheap-worker.ts
9310
9081
  /**
9311
- * 解析 handyman worker 从父 agent 继承的 preset,并组装创建 setup 钩子。
9312
- * @param parentCtx - 发起 handyman 的 agent 的 scope ctx;无(headless/非 agent 上下文)时为 undefined。
9313
- * @returns 继承结果:meta 用的 agentPreset 与创建 setup 钩子。
9314
- */
9315
- function handymanPresetInheritance(parentCtx) {
9316
- if (parentCtx === void 0) return { setup: (childCtx) => {
9317
- childCtx.tools.restrict({ deny: ["handyman"] });
9318
- } };
9319
- const agentPreset = parentCtx.get("agentPresets")?.composedPreset(parentCtx);
9320
- if (agentPreset === void 0) return { setup: (childCtx) => {
9321
- childCtx.tools.restrict({ deny: ["handyman"] });
9322
- } };
9323
- return {
9324
- agentPreset,
9325
- setup: (childCtx) => {
9326
- childCtx.get("agentPresets")?.composeFrom(childCtx, parentCtx);
9327
- childCtx.tools.restrict({ deny: ["handyman"] });
9328
- }
9329
- };
9330
- }
9331
- //#endregion
9332
- //#region src/tools/handyman.ts
9333
- /**
9334
- * handyman.ts — handyman(杂工)真实 DSH 工具(v1.24.0:loop 牛马 → handyman 杂工)
9082
+ * cheap-worker.ts — cheap-worker(杂工)真实 DSH 工具
9335
9083
  *
9336
- * 用户需求(S142 拍板):
9337
- * - **同步**(用户明确:这个事一般不会异步——工具阻塞到完成)
9338
- * - **指定模型**(CCC 白名单内——handyman.models,未配置报错)
9339
- * - **自主循环到完成**(stop-token 唯一完成判据,对齐 osp loop)
9340
- * - **内部递归同样低能 subagent**(worker 工具面含 subagent,DSH 原生模型继承)
9341
- * - **workflow 能力**(jobs 编排:主 agent 一次并行多个杂工,maxParallel 默认 10)
9342
- * - **worker 不含 handyman 本身**(递归编排归主 agent,防无限嵌套)
9343
- * - **不兼容旧 loop 进度文件**(仅 handyman- 前缀)
9084
+ * v1.24.0:loop(牛马)→ handyman(杂工);2026-10-14:handyman → cheap-worker。
9085
+ * 🆕 2026-10-14(owner 令「删除后台运行能力」):**background 模式整条退场** ——
9086
+ * 轮次循环 / stop-token 完成码 / 进度文件 / 重启计数 / maxRounds/maxParallel 全部删除。
9087
+ * 现存只有**前台委派**一种形态:宿主起子 agent、等它跑完、取回最终文本。
9344
9088
  *
9345
- * 机制:ctx.agents.create()(带 setup 钩子)创建专用 agent(进程内),
9346
- * 每轮 followup → agent/status idle → 读 session.events 响应 → 写进度 → stop token 检查 →
9347
- * 未完成继续下一轮;followup/等待空闲抛错(非正常停止)→ dispose 并重新 create agent
9348
- * (重启计数,≤100),同一轮重试。工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
9089
+ * 机制:走宿主**委派正门** `ctx.subagents.start('spawn', …)`(深度上限、子 agent 所有权、
9090
+ * `subagent/start|end` 事件、`dispose()` 收尾全部沿用宿主实现),模型经 `agentOptions` 注入。
9091
+ * 前置:`exec.agent` 必须存在(前台委派要有发起方)。
9349
9092
  *
9350
- * preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
9351
- * subagent 先例)+ tools.restrict deny handyman(worker 内部看不到 handyman 工具)。
9093
+ * 🔴 **模型不可由调用方指定**(owner 令):**没有 `model` 参数**,一律读 CCC 配置
9094
+ * `cheap-worker.models` / `cheap-worker.defaultModel`(单一真相源)。
9095
+ *
9096
+ * 递归防护:子 agent 的工具面 deny `cheap-worker` 本身(编排归主 agent,防无限嵌套)。
9352
9097
  */
9353
9098
  function renderText$4(value) {
9354
9099
  return [{
@@ -9356,52 +9101,18 @@ function renderText$4(value) {
9356
9101
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
9357
9102
  }];
9358
9103
  }
9359
- /** 读取会话最后一个 assistant/message 文本 */
9360
- function lastAssistantText$1(agent) {
9361
- const s = agent.session;
9362
- const events = typeof s.snapshotEvents === "function" ? s.snapshotEvents() : s.events ?? [];
9363
- for (let i = events.length - 1; i >= 0; i--) {
9364
- const e = events[i];
9365
- if (e && e.type === "assistant/message") {
9366
- const data = e.data;
9367
- const text = (data?.message?.content ?? data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
9368
- if (text) return text;
9369
- }
9370
- }
9371
- return "";
9372
- }
9373
- /** 非正常停止时重启 agent 的次数上限(防死循环保险阀):对话轮次有上限,重启有上限 */
9374
- const HANDYMAN_MAX_RESTARTS = 100;
9375
- /** 运行时解析 jobs 参数(DSH schema 不支持 object items 的 required/校验 → 手工校验类型) */
9376
- function parseJobs(raw) {
9377
- if (!Array.isArray(raw) || raw.length === 0) return null;
9378
- const jobs = [];
9379
- for (const item of raw) {
9380
- if (typeof item !== "object" || item === null) return null;
9381
- const rec = item;
9382
- if (typeof rec.task !== "string" || typeof rec.label !== "string") return null;
9383
- jobs.push({
9384
- task: rec.task,
9385
- label: rec.label,
9386
- model: typeof rec.model === "string" ? rec.model : void 0
9387
- });
9388
- }
9389
- return jobs;
9390
- }
9391
9104
  /**
9392
- * 🔴 **foreground 并行上限 = 10**(owner 2026-09-29 具名令:「这个版本顺手帮我把 handyman 并行度从 5 调整成 10」;
9105
+ * 🔴 **前台并行上限 = 10**(owner 2026-09-29 具名令:「这个版本顺手帮我把 handyman 并行度从 5 调整成 10」;
9393
9106
  * 原值 5 出自 owner 2026-09-27 的具名令「最大 5;就当是支持自定模型的 subagent 用了,急需」)。
9394
9107
  *
9395
9108
  * 为什么是**硬编码**而不是又一个配置键:owner 两次都是**直接给数**(同 `REBUILD-TODO` 那次的裁决口径
9396
9109
  * —— 设定项能不加就不加,见 S142 的「可砍设定项」)。若将来还要调,仍按具名令改这个常量。
9397
9110
  *
9398
- * 为什么 foreground 仍与 background 分开(后台 = `handyman.maxParallel`,缺省 **10**):**foreground 会占住
9399
- * 主 agent 的这一轮**(调用方在等结果),并发越高越容易把宿主的并发面与用量推上去。
9400
- * ⚠️ 现在两档**数值相等但语义不同**(前台 = 占住本轮 / 后台 = 不占)⇒ **刻意不合并成一个常量**:
9401
- * 合并会让"哪天要给前台单独降档"变成一次语义手术。
9111
+ * 为什么是 10 而不是更高:**前台委派会占住主 agent 的这一轮**(调用方在等结果),
9112
+ * 并发越高越容易把宿主的并发面与用量推上去。
9402
9113
  */
9403
9114
  const FOREGROUND_MAX_PARALLEL = 10;
9404
- /** foreground 的 jobs 解析(与 background 的差别:**label 可选** —— 前台 label 只是显示名,不命名进度文件) */
9115
+ /** foreground 的 jobs 解析(label 可选 —— 前台 label 只是显示名,不命名任何文件) */
9405
9116
  function parseForegroundJobs(raw) {
9406
9117
  if (!Array.isArray(raw) || raw.length === 0) return null;
9407
9118
  const jobs = [];
@@ -9411,8 +9122,7 @@ function parseForegroundJobs(raw) {
9411
9122
  if (typeof rec.task !== "string" || rec.task.trim() === "") return null;
9412
9123
  jobs.push({
9413
9124
  task: rec.task,
9414
- ...typeof rec.label === "string" && rec.label !== "" ? { label: rec.label } : {},
9415
- ...typeof rec.model === "string" && rec.model !== "" ? { model: rec.model } : {}
9125
+ ...typeof rec.label === "string" && rec.label !== "" ? { label: rec.label } : {}
9416
9126
  });
9417
9127
  }
9418
9128
  return jobs;
@@ -9429,8 +9139,8 @@ function subagentOutputText(output) {
9429
9139
  * ⇒ 一律**不阻断**,保持旧行为 —— 增强不得变成新的失败源)。
9430
9140
  *
9431
9141
  * 为什么必须放在"创建之前":宿主对未知 provider 抛 `no adapter registered for provider "…"`
9432
- * (`dsh-llm`,code `NO_ADAPTER`)。若照常创建:前台只拿回 `stopReason:"error"` + **空
9433
- * `diagnostic`**(实测),后台整条失败且**不带原因** ⇒ 调用方(LLM)无法自我修正配置。
9142
+ * (`dsh-llm`,code `NO_ADAPTER`)。若照常创建:只拿回 `stopReason:"error"` + **空
9143
+ * `diagnostic`**(实测)⇒ 调用方(LLM)无法自我修正配置。
9434
9144
  */
9435
9145
  function providerPreflight(ctx, model) {
9436
9146
  const llm = hostLlm(ctx);
@@ -9455,14 +9165,13 @@ function providerPreflight(ctx, model) {
9455
9165
  });
9456
9166
  }
9457
9167
  /**
9458
- * foreground 模式:单次前台串行委派(用户裁决"本次所需的简单实现")。
9168
+ * 单次前台串行委派。
9459
9169
  *
9460
- * 与 background 的差别(R↓):
9461
9170
  * - 走宿主**委派正门** `ctx.subagents.start('spawn', …)`——深度上限、子 agent 所有权、
9462
9171
  * `subagent/start|end` 生命周期事件、`dispose()` 收尾全部沿用宿主实现(不重写 agent 创建);
9463
9172
  * - 模型经 `agentOptions` 注入(spawn 后端能力位 `agentOptions: true`),因此**不依赖**
9464
9173
  * 宿主未放开的 `subagent-model-selection` 设置;
9465
- * - 不循环、不写进度文件、不做完成码校验——"一次调用一次结果"。
9174
+ * - 不循环、不写文件、不做完成码校验——"一次调用一次结果"。
9466
9175
  *
9467
9176
  * 前置条件:`opts.parent` 必须是发起 agent(由调用方 `execute` 在解析 CCC 之前校验;
9468
9177
  * 本函数不再重复检查——单一真相源)。
@@ -9471,7 +9180,7 @@ async function runForegroundJob(ctx, opts) {
9471
9180
  const { task, label, model, models, parent, signal } = opts;
9472
9181
  requireWhitelistedModel(model, models);
9473
9182
  const subagents = hostSubagents(ctx);
9474
- if (!subagents?.start) throw new Error("handyman foreground: host subagents service unavailable — neither ctx.subagents nor ctx.get(\"subagents\") yielded a start() function. Check `dashboard health` (host-contract section, service \"subagents\"); mode=\"background\" does not need this service and still works.");
9183
+ if (!subagents?.start) throw new Error("cheap-worker: host subagents service unavailable — neither ctx.subagents nor ctx.get(\"subagents\") yielded a start() function. Check `dashboard health` (host-contract section, service \"subagents\").");
9475
9184
  const preflight = providerPreflight(ctx, model);
9476
9185
  if (preflight !== null) return {
9477
9186
  mode: "foreground",
@@ -9494,7 +9203,7 @@ async function runForegroundJob(ctx, opts) {
9494
9203
  ...provider === void 0 ? {} : { provider },
9495
9204
  model: modelName
9496
9205
  },
9497
- toolFilter: { deny: ["handyman"] }
9206
+ toolFilter: { deny: ["cheap-worker"] }
9498
9207
  });
9499
9208
  try {
9500
9209
  const result = run.result === void 0 ? {} : await run.result;
@@ -9512,165 +9221,28 @@ async function runForegroundJob(ctx, opts) {
9512
9221
  if (run.dispose) await run.dispose().catch(() => {});
9513
9222
  }
9514
9223
  }
9515
- /**
9516
- * 驱动单个 handyman worker:创建 agent → while 循环(stop-token 完成判据)→ dispose。
9517
- * 同步阻塞直到完成/保险阀。jobs 编排时多个 worker 并行(Promise.all)。
9518
- */
9519
- async function runHandymanJob(ctx, opts) {
9520
- const { root, job, defaultModel, models, maxRounds, parentSession, parentCtx, session } = opts;
9521
- const label = job.label;
9522
- const model = job.model ?? defaultModel;
9523
- requireWhitelistedModel(model, models);
9524
- const preflightError = providerPreflight(ctx, model);
9525
- if (preflightError !== null) throw new Error(preflightError);
9526
- const { provider, model: modelName } = splitModel(model);
9527
- const stopToken = newStopToken();
9528
- let progress = readProgress(root, label);
9529
- const startRound = progress ? progress.round + 1 : 1;
9530
- const inherited = handymanPresetInheritance(parentCtx);
9531
- if (!ctx.agents) throw new Error("handyman: ctx.agents unavailable");
9532
- let handle;
9533
- let workerAgent;
9534
- const spawnAgent = async () => {
9535
- const sessionId = `handyman-${label}-${randomUUID()}`;
9536
- handle = await ctx.agents.create({
9537
- sessionId,
9538
- meta: {
9539
- cwd: root,
9540
- origin: "subagent",
9541
- ...parentSession === void 0 ? {} : { parentSession },
9542
- ...inherited.agentPreset === void 0 ? {} : { agentPreset: inherited.agentPreset }
9543
- },
9544
- agentOptions: {
9545
- provider,
9546
- model: modelName
9547
- },
9548
- ...inherited.setup === void 0 ? {} : { setup: inherited.setup }
9549
- });
9550
- workerAgent = handle.agent;
9551
- };
9552
- await spawnAgent();
9553
- let done = false;
9554
- let lastResponse = progress?.lastResponse ?? "";
9555
- let finalRound = startRound - 1;
9556
- let restarts = 0;
9557
- let finishReason = "done";
9558
- try {
9559
- let round = startRound;
9560
- while (true) {
9561
- if (round > maxRounds) {
9562
- finishReason = "max_rounds";
9563
- break;
9564
- }
9565
- finalRound = round;
9566
- const prompt = buildRoundPrompt({
9567
- root,
9568
- session,
9569
- label,
9570
- round,
9571
- stopToken,
9572
- progress,
9573
- task: job.task
9574
- });
9575
- try {
9576
- workerAgent.followup(createUserMessage({
9577
- content: [{
9578
- type: "text",
9579
- text: prompt
9580
- }],
9581
- source: PLUGIN_SOURCE
9582
- }));
9583
- await waitAgentIdle(ctx, workerAgent);
9584
- lastResponse = lastAssistantText$1(workerAgent);
9585
- } catch {
9586
- restarts++;
9587
- if (restarts > HANDYMAN_MAX_RESTARTS) {
9588
- finishReason = "restart_exceeded";
9589
- break;
9590
- }
9591
- await handle.dispose().catch(() => {});
9592
- await spawnAgent();
9593
- continue;
9594
- }
9595
- progress = {
9596
- round,
9597
- done: false,
9598
- label,
9599
- model,
9600
- updated: isoLocal(),
9601
- lastResponse
9602
- };
9603
- writeProgress(root, label, progress);
9604
- if (lastResponse.includes(stopToken)) {
9605
- done = true;
9606
- finishReason = "done";
9607
- break;
9608
- }
9609
- round++;
9610
- }
9611
- writeProgress(root, label, {
9612
- round: finalRound,
9613
- done,
9614
- label,
9615
- model,
9616
- updated: isoLocal(),
9617
- lastResponse,
9618
- status: done ? "done" : "running"
9619
- });
9620
- if (finishReason !== "done") writeFailedStatus(root, label, {
9621
- errorCode: finishReason,
9622
- errorMessage: finishReason === "max_rounds" ? `Reached ${maxRounds} rounds without completion — resume with same label to continue.` : `Agent restarted ${HANDYMAN_MAX_RESTARTS} times without progress — resume with same label to continue.`
9623
- });
9624
- } finally {
9625
- await handle.dispose().catch(() => {});
9626
- }
9627
- const { json } = handymanProgressPaths(root, label);
9628
- return {
9629
- label,
9630
- done,
9631
- rounds: finalRound,
9632
- finishReason,
9633
- restarts,
9634
- model,
9635
- lastResponse: lastResponse.slice(0, 2e3),
9636
- progressFile: json
9637
- };
9638
- }
9639
- /** 创建 handyman 工具(闭包捕获插件 ctx → 可访问 ctx.agents) */
9640
- function createHandymanTool(ctx) {
9224
+ /** 创建 cheap-worker 工具(闭包捕获插件 ctx → 可访问 ctx.subagents) */
9225
+ function createCheapWorkerTool(ctx) {
9641
9226
  return defineTool({
9642
- name: "handyman",
9643
- description: "The handyman (杂工): delegate work to a worker agent on a CCC-configured model. Two modes (v1.31.3).\nMode \"foreground\" (DEFAULT): one serial child agent runs the task once and returns its final text — no loop, no completion-code validation, no progress file; use it for one-shot serial delegation (e.g. per-case testers that just gather evidence). Usage: handyman(task, [label], [model]).\nMode \"background\": the loop-validated worker — internal hard while-loop whose ONLY completion condition is the worker echoing this round's random completion code (stop token, prevents low-intelligence models from finishing early), round cap (default 100, resumable), automatic restart on abnormal stop (≤100), progress file AGENT_SESSIONS/handyman-<label>.md/.json (same label resumes), and parallel jobs (handyman.maxParallel, default 10). Usage: handyman(mode=\"background\", task, label, [model]) or handyman(mode=\"background\", jobs=[{task,label,model?},...]).\nChoosing: need exactly one result back now → foreground; need anti-early-finish guarantees or resumability → background; need N results back now (parallel, per-job model) → foreground with jobs (cap 10).\nModel: only models whitelisted in .opencode/serenity.json \"handyman.models\" (missing config = error); default reads handyman.defaultModel. Both modes share the same whitelist and default.\nProvider check: the whitelist lives in the CCC (it travels with the repo) while provider adapters are registered per machine by the host — so before spawning, the provider of the resolved model is verified against the adapters present on THIS machine. If it is missing you get the available providers plus the exact fix (foreground: in `diagnostic`; background: thrown), never an opaque failure.\nRecursion: a worker's tool set excludes handyman itself (orchestration belongs to the main agent).\nGuide: handyman(guide=true) prints the scale-up usage guide.",
9227
+ name: "cheap-worker",
9228
+ description: "The cheap-worker (杂工): delegate work to a worker agent on the CCC-configured model, and return its final text. One serial child per task; `jobs` fans out in parallel.\nUsage: cheap-worker(task, [label]) for a single delegation; cheap-worker(jobs=[{task,label?},...]) for parallel fan-out (cap 10, results keep your order).\nThe child does NOT share this conversation — the task must be self-contained.\nNo loop, no completion-code validation, no progress file, no unattended/background run.\nModel: NOT selectable per call — it is read from .opencode/serenity.json \"cheap-worker.models\" (missing config = error); \"cheap-worker.defaultModel\" names the one used, falling back to models[0].\nProvider check: the whitelist lives in the CCC (it travels with the repo) while provider adapters are registered per machine by the host — so before spawning, the provider of the resolved model is verified against the adapters present on THIS machine. If it is missing you get the available providers plus the exact fix in `diagnostic`, and no child is created.\nRecursion: a worker's tool set excludes cheap-worker itself (orchestration belongs to the main agent).\nGuide: cheap-worker(guide=true) prints the scale-up usage guide.",
9644
9229
  parameters: {
9645
- mode: {
9646
- type: "string",
9647
- description: "Delegation mode: \"foreground\" (default — child agent(s) run once and return their final text; accepts `jobs` for parallel fan-out, cap 10) or \"background\" (loop-validated worker with completion-code check, round cap, auto-restart, progress file, parallel jobs)"
9648
- },
9649
9230
  task: {
9650
9231
  type: "string",
9651
- description: "The task goal to complete (required in both modes; foreground needs a self-contained prompt — the child does not share this conversation)"
9232
+ description: "The task goal to complete (must be self-contained — the child does not share this conversation)"
9652
9233
  },
9653
9234
  label: {
9654
9235
  type: "string",
9655
- description: "Task label (background: 1-50 chars, names the progress file; foreground: optional child display name)"
9656
- },
9657
- session: {
9658
- type: "string",
9659
- description: "Work session S### (context hint, progress reference)"
9660
- },
9661
- model: {
9662
- type: "string",
9663
- description: "provider/model — must be in the CCC whitelist handyman.models; default reads handyman.defaultModel"
9236
+ description: "Optional child display name"
9664
9237
  },
9665
9238
  jobs: {
9666
9239
  type: "array",
9667
- description: "Multi-job orchestration (workflow capability): [{task, label?, model?}, ...] — runs jobs in parallel. Cap: foreground 10 (fixed) / background handyman.maxParallel (default 10). Results keep the order you passed them in.",
9240
+ description: "Multi-job orchestration (workflow capability): [{task, label?}, ...] — runs jobs in parallel, cap 10 (fixed). Results keep the order you passed them in.",
9668
9241
  items: {
9669
9242
  type: "object",
9670
9243
  properties: {
9671
9244
  task: { type: "string" },
9672
- label: { type: "string" },
9673
- model: { type: "string" }
9245
+ label: { type: "string" }
9674
9246
  },
9675
9247
  additionalProperties: false
9676
9248
  }
@@ -9685,93 +9257,51 @@ function createHandymanTool(ctx) {
9685
9257
  render: (_args, value) => renderText$4(value)
9686
9258
  },
9687
9259
  async execute(args, exec) {
9688
- if (args.guide) return { guide: HANDYMAN_GUIDE };
9689
- if ((args.mode ?? "foreground") === "foreground" && !exec.agent) throw new Error("handyman foreground: requires a calling agent (exec.agent was undefined)");
9260
+ if (args.guide) return { guide: CHEAP_WORKER_GUIDE };
9261
+ if (!exec.agent) throw new Error("cheap-worker: requires a calling agent (exec.agent was undefined)");
9690
9262
  const root = cccRootForExec(exec);
9691
9263
  if (!root) throw new Error(NO_CCC_FROM_AGENT_CWD);
9692
- const hc = readHandymanConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
9693
- if (hc === null) throw new Error("handyman requires a model whitelist: configure .opencode/serenity.json \"handyman.models\" (e.g. {\"handyman\": {\"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"}})");
9694
- const mode = args.mode === "background" ? "background" : "foreground";
9695
- if (args.mode !== void 0 && args.mode !== "background" && args.mode !== "foreground") throw new Error("handyman: mode must be \"foreground\" (default) or \"background\"");
9696
- if (mode === "foreground") {
9697
- if (args.jobs !== void 0) {
9698
- const jobs = parseForegroundJobs(args.jobs);
9699
- if (jobs === null) throw new Error("handyman foreground: jobs must be [{task, label?, model?}, ...] (every job requires a non-empty task)");
9700
- if (jobs.length > FOREGROUND_MAX_PARALLEL) throw new Error(`handyman foreground: ${jobs.length} jobs exceed the foreground parallel cap ${FOREGROUND_MAX_PARALLEL} (split into batches, or use mode="background" whose cap is handyman.maxParallel)`);
9701
- const results = await Promise.all(jobs.map((job) => runForegroundJob(ctx, {
9702
- task: job.task,
9703
- ...job.label === void 0 ? {} : { label: job.label },
9704
- model: job.model ?? (typeof args.model === "string" && args.model !== "" ? args.model : hc.defaultModel),
9705
- models: hc.models,
9706
- parent: exec.agent,
9707
- signal: exec.signal
9708
- })));
9709
- const done = results.every((r) => r.done);
9710
- return {
9711
- mode: "foreground",
9712
- done,
9713
- jobs: results,
9714
- usage: {
9715
- how: `foreground parallel mode starts ${results.length} child agents at once through the host delegation service (ctx.subagents.start "spawn"), awaits all of them, and returns each child's final text. No loop, no completion-code validation, no progress file. Cap = ${FOREGROUND_MAX_PARALLEL}.`,
9716
- model: "restricted to the CCC whitelist handyman.models; per-job model overrides the call-level model; default reads handyman.defaultModel",
9717
- next: done ? "All children completed; use each job's output directly (results are in the same order as the jobs you passed)" : "Some children did not complete — inspect each job's diagnostic/output (results keep the order you passed); for anti-early-finish guarantees use mode=\"background\""
9718
- }
9719
- };
9720
- }
9721
- if (typeof args.task !== "string" || args.task.trim() === "") throw new Error("handyman foreground: task is required (and must be self-contained — the child does not share this conversation)");
9722
- const model = typeof args.model === "string" && args.model !== "" ? args.model : hc.defaultModel;
9723
- const res = await runForegroundJob(ctx, {
9724
- task: args.task,
9725
- ...typeof args.label === "string" && args.label !== "" ? { label: args.label } : {},
9726
- model,
9264
+ const hc = readCheapWorkerConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
9265
+ if (hc === null) throw new Error("cheap-worker requires a model whitelist: configure .opencode/serenity.json \"cheap-worker.models\" (e.g. {\"cheap-worker\": {\"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"}})");
9266
+ if (args.jobs !== void 0) {
9267
+ const jobs = parseForegroundJobs(args.jobs);
9268
+ if (jobs === null) throw new Error("cheap-worker: jobs must be [{task, label?}, ...] (every job requires a non-empty task)");
9269
+ if (jobs.length > FOREGROUND_MAX_PARALLEL) throw new Error(`cheap-worker: ${jobs.length} jobs exceed the parallel cap ${FOREGROUND_MAX_PARALLEL} (split into batches)`);
9270
+ const results = await Promise.all(jobs.map((job) => runForegroundJob(ctx, {
9271
+ task: job.task,
9272
+ ...job.label === void 0 ? {} : { label: job.label },
9273
+ model: hc.defaultModel,
9727
9274
  models: hc.models,
9728
9275
  parent: exec.agent,
9729
9276
  signal: exec.signal
9730
- });
9277
+ })));
9278
+ const done = results.every((r) => r.done);
9731
9279
  return {
9732
- ...res,
9280
+ mode: "foreground",
9281
+ done,
9282
+ jobs: results,
9733
9283
  usage: {
9734
- how: "foreground mode starts ONE serial child agent through the host delegation service (ctx.subagents.start \"spawn\") with the CCC-configured model; it awaits the run and returns the child's final text. No loop, no completion-code validation, no progress file.",
9735
- model: "restricted to the CCC whitelist handyman.models; default reads handyman.defaultModel",
9736
- next: res.done ? "Child completed; use its output directly" : `Child did not complete (stopReason=${res.stopReason}); inspect diagnostic/output and decide whether to retry, switch model, or use mode="background"`
9284
+ how: `foreground parallel mode starts ${results.length} child agents at once through the host delegation service (ctx.subagents.start "spawn"), awaits all of them, and returns each child's final text. No loop, no completion-code validation, no progress file. Cap = ${FOREGROUND_MAX_PARALLEL}.`,
9285
+ model: "NOT selectable per call — resolved from the CCC config cheap-worker.models (cheap-worker.defaultModel, else models[0])",
9286
+ next: done ? "All children completed; use each job's output directly (results are in the same order as the jobs you passed)" : "Some children did not complete — inspect each job's diagnostic/output (results keep the order you passed)"
9737
9287
  }
9738
9288
  };
9739
9289
  }
9740
- const jobs = args.jobs !== void 0 ? (() => {
9741
- const parsed = parseJobs(args.jobs);
9742
- if (parsed === null) throw new Error("handyman: jobs must be [{task, label, model?}, ...] (every job requires task and label)");
9743
- return parsed;
9744
- })() : (() => {
9745
- if (typeof args.task !== "string" || typeof args.label !== "string") throw new Error("handyman: task and label are required (single-job mode)");
9746
- return [{
9747
- task: args.task,
9748
- label: args.label,
9749
- ...typeof args.model === "string" && args.model !== "" ? { model: args.model } : {}
9750
- }];
9751
- })();
9752
- if (jobs.length === 0) throw new Error("handyman: no jobs provided");
9753
- if (jobs.length > hc.maxParallel) throw new Error(`handyman: ${jobs.length} jobs exceed maxParallel ${hc.maxParallel} (configure handyman.maxParallel to raise)`);
9754
- if (jobs.some((j) => !j.task || !j.label)) throw new Error("handyman: every job requires task and label");
9755
- const parentCtx = exec.agent?.ctx;
9756
- const parentSession = (exec.agent?.session)?.id;
9757
- const results = await Promise.all(jobs.map((job) => runHandymanJob(ctx, {
9758
- root,
9759
- job,
9760
- defaultModel: hc.defaultModel,
9290
+ if (typeof args.task !== "string" || args.task.trim() === "") throw new Error("cheap-worker: task is required (and must be self-contained — the child does not share this conversation)");
9291
+ const res = await runForegroundJob(ctx, {
9292
+ task: args.task,
9293
+ ...typeof args.label === "string" && args.label !== "" ? { label: args.label } : {},
9294
+ model: hc.defaultModel,
9761
9295
  models: hc.models,
9762
- maxRounds: hc.maxRounds,
9763
- parentSession,
9764
- parentCtx,
9765
- session: typeof args.session === "string" ? args.session : void 0
9766
- })));
9296
+ parent: exec.agent,
9297
+ signal: exec.signal
9298
+ });
9767
9299
  return {
9768
- done: results.every((r) => r.done),
9769
- jobs: results.map((r) => ({ ...r })),
9300
+ ...res,
9770
9301
  usage: {
9771
- how: "handyman drives each worker agent round by round via an internal hard while-loop; the worker terminates by echoing the random completion code; round cap (default 100, osp fail-safe), automatic restart on abnormal stop (≤100 times); model restricted to the CCC whitelist handyman.models",
9772
- progress: "Progress in AGENT_SESSIONS/handyman-<label>.md and .json; calling handyman again with the same label resumes from the next round (no redo)",
9773
- constraints: "handyman workers carry full Serenity constraints (ACC identity / entry-skill system prompt / guards / trajectory-steward); workers include the subagent tool (same-model inheritance) but NOT handyman itself",
9774
- next: results.every((r) => r.done) ? "All jobs completed; check the progress files to wrap up" : "Some jobs incomplete (max_rounds / restart_exceeded fail-safe); resume with the same labels"
9302
+ how: "foreground mode starts ONE serial child agent through the host delegation service (ctx.subagents.start \"spawn\") with the CCC-configured model; it awaits the run and returns the child's final text. No loop, no completion-code validation, no progress file.",
9303
+ model: "NOT selectable per call — resolved from the CCC config cheap-worker.models (cheap-worker.defaultModel, else models[0])",
9304
+ next: res.done ? "Child completed; use its output directly" : `Child did not complete (stopReason=${res.stopReason}); inspect diagnostic/output and decide whether to retry`
9775
9305
  }
9776
9306
  };
9777
9307
  }
@@ -12032,7 +11562,7 @@ function renderCroSampleSnapshot() {
12032
11562
  /**
12033
11563
  * CRO 编写指南正文(ACC 内置手册;由工具的 guide 动作输出)。
12034
11564
  *
12035
- * 结构照既有 guide 惯例(`MSM_GUIDE` / `handyman(guide=true)`):
11565
+ * 结构照既有 guide 惯例(`MSM_GUIDE` / `cheap-worker(guide=true)`):
12036
11566
  * 用途 → 位置 → 我给什么 → 你给我什么 → 骨架 → 常见模式 → 坑 → **自测**。
12037
11567
  */
12038
11568
  const CRO_GUIDE = `CRO(Continuous Re-Occurrence · 持续再发生)编写指南
@@ -12559,7 +12089,7 @@ function renderText$2(value) {
12559
12089
  }
12560
12090
  const localstoreTool = defineTool({
12561
12091
  name: "localstore",
12562
- description: "ACC standard local credential/config storage: one tool manages two namespaces — credential (credentials) and config (local preferences). Stored in localstore.json at the CCC root (JSON format, directly readable by MSMs). Git policy: .opencode/serenity.json localstore.gitTrack (allow commits / deny commits, default deny; .dsh fallback) — when deny, writes ensure .gitignore contains this file (physical guarantee), container_git commit checks and refuses. Subcommands: list (list keys, credentials never return values) / get <name> (read value) / set <name> <value> (write) / unset <name> (delete) / show <name> (metadata, credentials never print values) / doc (output the storage spec — path/format/key conventions/git policy; agents may operate the file directly with read/write per the spec). Default scope=credential; config requires --scope config (path is section.key, e.g. handyman.models).",
12092
+ description: "ACC standard local credential/config storage: one tool manages two namespaces — credential (credentials) and config (local preferences). Stored in localstore.json at the CCC root (JSON format, directly readable by MSMs). Git policy: .opencode/serenity.json localstore.gitTrack (allow commits / deny commits, default deny; .dsh fallback) — when deny, writes ensure .gitignore contains this file (physical guarantee), container_git commit checks and refuses. Subcommands: list (list keys, credentials never return values) / get <name> (read value) / set <name> <value> (write) / unset <name> (delete) / show <name> (metadata, credentials never print values) / doc (output the storage spec — path/format/key conventions/git policy; agents may operate the file directly with read/write per the spec). Default scope=credential; config requires --scope config (path is section.key, e.g. cheap-worker.models).",
12563
12093
  parameters: {
12564
12094
  action: {
12565
12095
  type: "string",
@@ -12603,7 +12133,7 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
12603
12133
  ── 角色配置 schema ──
12604
12134
  { "skiff": { "roles": {
12605
12135
  "<role-name>": {
12606
- "model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退 handyman.defaultModel)
12136
+ "model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退 cheap-worker.defaultModel)
12607
12137
  "msms": ["msm-a", "msm-b"], // MSM 白名单(独立):msm 工具只能执行这些;register/deregister 必拒
12608
12138
  "tools": ["read","grep","glob",...], // 非 MSM 工具白名单(独立):白名单外工具一律不可用(guard 强制)
12609
12139
  "trajectory": { "session": false, "keeper": false, "rebuild": false }, // 轨迹纪律子集(缺省全关 = 完全独立)
@@ -12636,18 +12166,17 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
12636
12166
 
12637
12167
  ── 运行 ──
12638
12168
  调试:设置面板「Serenity」页 Skiff 区块开启 → http://127.0.0.1:<debugPort> 问答页实测
12639
- ACP 程序化面(v1.26.0):设置面板「ACP」区块开启 → HTTP JSON-RPC 端点(默认 3100)——
12640
- POST / {jsonrpc, id, method, params}
12641
- session/new {ccc, role, sessionId?} → 创建/延续会话(sessionId 可选=进程内追问)
12642
- session/prompt {sessionId, question} → {answer, trajectory}
12643
- 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 的接入指南文档。
12644
12173
  启停 = 人工(设置面板开关,不随插件加载自动启动);未开启零资源占用
12645
12174
  生效机制:角色配置**实时读取**(改 .opencode/serenity.json → 刷新调试页即生效);
12646
12175
  改配置后用 container_admin role apply 做显式校验 + 应用确认(绑定 CCC + 角色清单)
12647
12176
  会话追问(v1.25.10):调试页持有当前会话,追问自动续接(同会话上下文延续);
12648
12177
  「新对话」按钮开新会话;进程重启后旧会话不可续(WebUI 仍可见历史)
12649
12178
  注意:skill 加载对 skiff **恒可用**(不设白名单——读知识面,无写能力)`;
12650
- /** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ handyman.models / systemPrompt 非空 */
12179
+ /** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ cheap-worker.models / systemPrompt 非空 */
12651
12180
  function validateSkiffConfig(root) {
12652
12181
  const roles = readSkiffRoles(root);
12653
12182
  const issues = [];
@@ -12658,7 +12187,7 @@ function validateSkiffConfig(root) {
12658
12187
  note: "no skiff roles defined (skiff disabled — zero impact)"
12659
12188
  };
12660
12189
  const registered = new Set(loadMsmEntries(root).map((e) => e.name));
12661
- const hc = readHandymanConfig(root);
12190
+ const hc = readCheapWorkerConfig(root);
12662
12191
  const allowedModels = hc ? new Set(hc.models) : null;
12663
12192
  for (const [name, role] of roles) {
12664
12193
  try {
@@ -12667,7 +12196,7 @@ function validateSkiffConfig(root) {
12667
12196
  issues.push(`role "${name}": ${String(err?.message ?? err)}`);
12668
12197
  }
12669
12198
  for (const m of role.msms ?? []) if (!registered.has(m)) issues.push(`role "${name}": msms "${m}" is not registered in mech-registry.json`);
12670
- if (role.model && allowedModels && !allowedModels.has(role.model)) issues.push(`role "${name}": model "${role.model}" not in handyman.models (${[...allowedModels].join(", ") || "(none)"})`);
12199
+ if (role.model && allowedModels && !allowedModels.has(role.model)) issues.push(`role "${name}": model "${role.model}" not in cheap-worker.models (${[...allowedModels].join(", ") || "(none)"})`);
12671
12200
  }
12672
12201
  return {
12673
12202
  ok: issues.length === 0,
@@ -12684,7 +12213,7 @@ function listSkiffRoles(root) {
12684
12213
  };
12685
12214
  const items = [...roles.entries()].map(([name, role]) => ({
12686
12215
  name,
12687
- model: role.model ?? "(handyman default)",
12216
+ model: role.model ?? "(cheap-worker default)",
12688
12217
  msms: role.msms ?? [],
12689
12218
  tools: role.tools ?? [],
12690
12219
  trajectory: {
@@ -12914,7 +12443,7 @@ function createContainerAdminTool(ctx) {
12914
12443
  * 注入文本**自带**"请回复 ABCD" ⇒ 模型**复述指令**即**假完成**(高概率,不是小概率)
12915
12444
  * ⇒ 用**每轮新生成**的 nonce + **包装 + 独占一行**(`[[UNATTENDED-DONE:<hex>]]`),
12916
12445
  * 明写「**旧码作废**」,校验**只认当前轮**(防上下文里的旧码复活)。
12917
- * 先例 = `handyman` background 的 `SERENITY_HANDYMAN_DONE_<16hex>`(同族实现)。
12446
+ * 先例 = `cheap-worker` background 的 `SERENITY_CHEAP_WORKER_DONE_<16hex>`(同族实现)。
12918
12447
  */
12919
12448
  /** 完成码包装前缀(**独占一行**才有意义;见文件头"禁用 4 位字母码") */
12920
12449
  const UNATTENDED_DONE_PREFIX = "[[UNATTENDED-DONE:";
@@ -13021,14 +12550,16 @@ function claimsUserNotification(text) {
13021
12550
  /**
13022
12551
  * **不许开**的会话(设计 §3 的"建议写死",本版**确实写死**):
13023
12552
  * `skiff-`(F4 问答 / **微信桥会话也是这个前缀** —— 桥那侧的"用户"是**家人**,
13024
- * 代理替家人说话 = **冒充**)| `acp-`(对外程序化面)| `handyman-`(自主 worker,已有自己的完成码循环)。
12553
+ * 代理替家人说话 = **冒充**)| `acp-`(对外程序化面 —— ⚠️ **2026-10-15 起已成死判据**:
12554
+ * 该前缀从无生产者且面 E 已退役;理由与"为何保留"见 `output-guard-seam.ts` 文件头)|
12555
+ * `cheap-worker-`(自主 worker,已有自己的完成码循环)。
13025
12556
  *
13026
12557
  * ⚠️ **`rebuild-` 不在排除面**(与 `output-guard-seam.ts` 的取向**刻意不同**):重建会话是
13027
12558
  * **同一轨迹的载体**(内部维护面),不是对外面;把它排除会让"重建后就没人推了"。
13028
12559
  */
13029
12560
  function isProxyExcludedSessionId(sessionId) {
13030
12561
  if (typeof sessionId !== "string" || sessionId === "") return true;
13031
- return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("handyman-");
12562
+ return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("cheap-worker-");
13032
12563
  }
13033
12564
  /**
13034
12565
  * 一次判定的**唯一入口**(纯函数)。
@@ -14138,7 +13669,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
14138
13669
  promoted: true
14139
13670
  };
14140
13671
  const loopSid = session.id;
14141
- if (typeof loopSid === "string" && (loopSid.startsWith("handyman-") || isSkiffSessionId(loopSid))) return {
13672
+ if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || isSkiffSessionId(loopSid))) return {
14142
13673
  boundary: -1,
14143
13674
  promoted: true
14144
13675
  };
@@ -14242,7 +13773,7 @@ function registerBootstrap(ctx) {
14242
13773
  const session = agent.session;
14243
13774
  const depth = session?.header?.delegationDepth ?? 0;
14244
13775
  const sid = typeof session?.id === "string" ? session.id : void 0;
14245
- if (sid !== void 0 && (sid.startsWith("handyman-") || isSkiffSessionId(sid))) return;
13776
+ if (sid !== void 0 && (sid.startsWith("cheap-worker-") || isSkiffSessionId(sid))) return;
14246
13777
  if (depth === 0) {
14247
13778
  if (hasUserMessageHistory(session)) return;
14248
13779
  } else if (sid !== void 0 && anchoredSessions.has(sid)) return;
@@ -14783,7 +14314,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
14783
14314
  }
14784
14315
  /**
14785
14316
  * ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + Phase 2)。
14786
- * ⚠️ 原文案含「+ handyman 模型」,该注入已于 2026-09-16 删除(§0e-G)⇒ 此处同步订正(散文级残留)。
14317
+ * ⚠️ 原文案含「+ cheap-worker 模型」,该注入已于 2026-09-16 删除(§0e-G)⇒ 此处同步订正(散文级残留)。
14787
14318
  * 完整身份(ACC 5 块 + CCE + Constraints + EAP + SKILL 全文 + Session 块)由**系统提示词层**
14788
14319
  * (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
14789
14320
  * 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
@@ -14811,7 +14342,7 @@ function agentScope(agent) {
14811
14342
  * 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
14812
14343
  * - subagent:session header `origin === 'subagent'`(DSH 路由语义,agent-lookup.ts)
14813
14344
  * - 派生会话:`parentSession` 存在(任何子会话)
14814
- * - handyman 杂工:sessionId 固定 `handyman-` 前缀(tools/handyman.ts 生成)
14345
+ * - cheap-worker 杂工:sessionId 固定 `cheap-worker-` 前缀(tools/cheap-worker.ts 生成)
14815
14346
  * 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
14816
14347
  */
14817
14348
  function shouldAutoRestore(agent) {
@@ -14819,7 +14350,7 @@ function shouldAutoRestore(agent) {
14819
14350
  if (!session) return false;
14820
14351
  if (session.header?.origin === "subagent") return false;
14821
14352
  if (session.header?.parentSession) return false;
14822
- if (session.id?.startsWith("handyman-")) return false;
14353
+ if (session.id?.startsWith("cheap-worker-")) return false;
14823
14354
  if (isSkiffSessionId(session.id)) return false;
14824
14355
  return true;
14825
14356
  }
@@ -14980,7 +14511,7 @@ function registerCompactRetention(ctx, opts = {}) {
14980
14511
  /**
14981
14512
  * status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
14982
14513
  *
14983
- * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / handyman 模型。
14514
+ * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / cheap-worker 模型。
14984
14515
  * setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
14985
14516
  */
14986
14517
  /** 宿主包名 —— 从运行入口反推包根时的**身份核对**用(见 {@link dshVersionFromEntry}) */
@@ -15065,7 +14596,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
15065
14596
  safeModeOn: false,
15066
14597
  blacklist: [],
15067
14598
  threshold: null,
15068
- handymanModel: null,
14599
+ cheapWorkerModel: null,
15069
14600
  restrict
15070
14601
  };
15071
14602
  const cfg = loadSerenityConfig(root, configPaths);
@@ -15075,7 +14606,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
15075
14606
  safeModeOn: isSafeModeOn(root),
15076
14607
  blacklist: readBlacklist(root, configPaths),
15077
14608
  threshold: cfg.sessionKeeper?.threshold ?? null,
15078
- handymanModel: readHandymanConfig(root, configPaths)?.defaultModel ?? null,
14609
+ cheapWorkerModel: readCheapWorkerConfig(root, configPaths)?.defaultModel ?? null,
15079
14610
  restrict
15080
14611
  };
15081
14612
  }
@@ -15090,12 +14621,10 @@ function setSafeMode(root, on) {
15090
14621
  //#endregion
15091
14622
  //#region src/api.ts
15092
14623
  const ROUTE_PATH = "/serenity/status";
15093
- const HANDYMEN_PATH = "/serenity/handymen";
15094
14624
  const UPLOAD_PATH = "/serenity/image-upload";
15095
14625
  const FILE_UPLOAD_PATH = "/serenity/file-upload";
15096
14626
  const CONFIG_PATH = "/serenity/config";
15097
14627
  const CCCS_PATH = "/serenity/cccs";
15098
- const PUBLIC_ASK_PATH = "/serenity/public-ask";
15099
14628
  const WEIXIN_PATH = "/serenity/weixin";
15100
14629
  /** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
15101
14630
  const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
@@ -15179,7 +14708,7 @@ function saveFileToTmp(root, fileName, data) {
15179
14708
  writeFileSync(join(dir, filename), bytes);
15180
14709
  return `${FILE_UPLOAD_DIR}/${filename}`;
15181
14710
  }
15182
- function readBody$4(req, maxBytes) {
14711
+ function readBody$3(req, maxBytes) {
15183
14712
  return new Promise((resolve, reject) => {
15184
14713
  let data = "";
15185
14714
  req.on("data", (chunk) => {
@@ -15193,7 +14722,7 @@ function readBody$4(req, maxBytes) {
15193
14722
  req.on("error", reject);
15194
14723
  });
15195
14724
  }
15196
- function sendJson$4(res, code, body) {
14725
+ function sendJson$3(res, code, body) {
15197
14726
  const payload = JSON.stringify(body);
15198
14727
  res.writeHead(code, {
15199
14728
  "content-type": "application/json; charset=utf-8",
@@ -15218,7 +14747,7 @@ function senderIsWebUi(req) {
15218
14747
  */
15219
14748
  function requireWebUi(req, res, message) {
15220
14749
  if (senderIsWebUi(req)) return true;
15221
- sendJson$4(res, 403, { error: message });
14750
+ sendJson$3(res, 403, { error: message });
15222
14751
  return false;
15223
14752
  }
15224
14753
  /**
@@ -15245,42 +14774,17 @@ function resolveWorkspace(ctx, params) {
15245
14774
  }
15246
14775
  function registerStatusApi(ctx, opts = {}) {
15247
14776
  const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
15248
- ctx.webServer.register({
15249
- kind: "exact",
15250
- path: HANDYMEN_PATH,
15251
- handler: async (req, res) => {
15252
- try {
15253
- if (req.method !== "GET") {
15254
- sendJson$4(res, 405, { error: "method not allowed" });
15255
- return;
15256
- }
15257
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
15258
- const workspace = resolveWorkspace(ctx, {
15259
- sessionId: url.searchParams.get("sessionId") ?? void 0,
15260
- workspace: url.searchParams.get("workspace") ?? void 0
15261
- });
15262
- const root = cccRootForCwd(workspace);
15263
- if (!root) {
15264
- sendJson$4(res, 200, { handymen: [] });
15265
- return;
15266
- }
15267
- sendJson$4(res, 200, { handymen: listActiveHandymen(root) });
15268
- } catch (err) {
15269
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15270
- }
15271
- }
15272
- });
15273
14777
  ctx.webServer.register({
15274
14778
  kind: "exact",
15275
14779
  path: UPLOAD_PATH,
15276
14780
  handler: async (req, res) => {
15277
14781
  try {
15278
14782
  if (req.method !== "POST") {
15279
- sendJson$4(res, 405, { error: "method not allowed" });
14783
+ sendJson$3(res, 405, { error: "method not allowed" });
15280
14784
  return;
15281
14785
  }
15282
14786
  if (!requireWebUi(req, res, "图片落盘仅限 WebUI(client 专用)")) return;
15283
- const raw = await readBody$4(req, 20971520);
14787
+ const raw = await readBody$3(req, 20971520);
15284
14788
  const body = JSON.parse(raw);
15285
14789
  const workspace = resolveWorkspace(ctx, {
15286
14790
  sessionId: body.sessionId,
@@ -15288,12 +14792,12 @@ function registerStatusApi(ctx, opts = {}) {
15288
14792
  });
15289
14793
  const root = cccRootForCwd(workspace);
15290
14794
  if (!root) {
15291
- sendJson$4(res, 404, { error: `no CCC found from workspace: ${workspace}` });
14795
+ sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15292
14796
  return;
15293
14797
  }
15294
- sendJson$4(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
14798
+ sendJson$3(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
15295
14799
  } catch (err) {
15296
- sendJson$4(res, 400, { error: err.message ?? String(err) });
14800
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15297
14801
  }
15298
14802
  }
15299
14803
  });
@@ -15303,11 +14807,11 @@ function registerStatusApi(ctx, opts = {}) {
15303
14807
  handler: async (req, res) => {
15304
14808
  try {
15305
14809
  if (req.method !== "POST") {
15306
- sendJson$4(res, 405, { error: "method not allowed" });
14810
+ sendJson$3(res, 405, { error: "method not allowed" });
15307
14811
  return;
15308
14812
  }
15309
14813
  if (!requireWebUi(req, res, "文件落盘仅限 WebUI(client 专用)")) return;
15310
- const raw = await readBody$4(req, 20971520);
14814
+ const raw = await readBody$3(req, 20971520);
15311
14815
  const body = JSON.parse(raw);
15312
14816
  const workspace = resolveWorkspace(ctx, {
15313
14817
  sessionId: body.sessionId,
@@ -15315,12 +14819,12 @@ function registerStatusApi(ctx, opts = {}) {
15315
14819
  });
15316
14820
  const root = cccRootForCwd(workspace);
15317
14821
  if (!root) {
15318
- sendJson$4(res, 404, { error: `no CCC found from workspace: ${workspace}` });
14822
+ sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15319
14823
  return;
15320
14824
  }
15321
- sendJson$4(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
14825
+ sendJson$3(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
15322
14826
  } catch (err) {
15323
- sendJson$4(res, 400, { error: err.message ?? String(err) });
14827
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15324
14828
  }
15325
14829
  }
15326
14830
  });
@@ -15336,7 +14840,7 @@ function registerStatusApi(ctx, opts = {}) {
15336
14840
  workspace: url.searchParams.get("workspace") ?? void 0
15337
14841
  }), configPaths);
15338
14842
  const runtime = ctx.get("codeRuntime");
15339
- sendJson$4(res, 200, {
14843
+ sendJson$3(res, 200, {
15340
14844
  ...status,
15341
14845
  ...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
15342
14846
  language: runtime.language,
@@ -15347,7 +14851,7 @@ function registerStatusApi(ctx, opts = {}) {
15347
14851
  }
15348
14852
  if (req.method === "POST") {
15349
14853
  if (!requireWebUi(req, res, "safe-mode 切换仅限 WebUI(agent 不可自行开关)")) return;
15350
- const raw = await readBody$4(req, 65536);
14854
+ const raw = await readBody$3(req, 65536);
15351
14855
  const body = JSON.parse(raw);
15352
14856
  const workspace = resolveWorkspace(ctx, {
15353
14857
  sessionId: body.sessionId,
@@ -15355,19 +14859,19 @@ function registerStatusApi(ctx, opts = {}) {
15355
14859
  });
15356
14860
  const root = cccRootForCwd(workspace);
15357
14861
  if (!root) {
15358
- sendJson$4(res, 404, { error: `no CCC found from workspace: ${workspace}` });
14862
+ sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
15359
14863
  return;
15360
14864
  }
15361
14865
  const result = setSafeMode(root, body.on === true);
15362
- sendJson$4(res, 200, {
14866
+ sendJson$3(res, 200, {
15363
14867
  ...getStatus(workspace, configPaths),
15364
14868
  ...result
15365
14869
  });
15366
14870
  return;
15367
14871
  }
15368
- sendJson$4(res, 405, { error: "method not allowed" });
14872
+ sendJson$3(res, 405, { error: "method not allowed" });
15369
14873
  } catch (err) {
15370
- sendJson$4(res, 400, { error: err.message ?? String(err) });
14874
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15371
14875
  }
15372
14876
  }
15373
14877
  });
@@ -15383,24 +14887,24 @@ function registerStatusApi(ctx, opts = {}) {
15383
14887
  try {
15384
14888
  known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
15385
14889
  } catch {}
15386
- sendJson$4(res, 200, {
14890
+ sendJson$3(res, 200, {
15387
14891
  config: toWire(settings),
15388
14892
  knownWorkspaces: known
15389
14893
  });
15390
14894
  return;
15391
14895
  }
15392
14896
  if (req.method === "PUT") {
15393
- const raw = await readBody$4(req, 131072);
14897
+ const raw = await readBody$3(req, 131072);
15394
14898
  const saved = applyWirePatch(JSON.parse(raw).config ?? {});
15395
14899
  try {
15396
14900
  ctx.emit?.("serenity/config-updated");
15397
14901
  } catch {}
15398
- sendJson$4(res, 200, { config: toWire(saved) });
14902
+ sendJson$3(res, 200, { config: toWire(saved) });
15399
14903
  return;
15400
14904
  }
15401
- sendJson$4(res, 405, { error: "method not allowed" });
14905
+ sendJson$3(res, 405, { error: "method not allowed" });
15402
14906
  } catch (err) {
15403
- sendJson$4(res, 400, { error: err.message ?? String(err) });
14907
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15404
14908
  }
15405
14909
  }
15406
14910
  });
@@ -15410,7 +14914,7 @@ function registerStatusApi(ctx, opts = {}) {
15410
14914
  handler: async (req, res) => {
15411
14915
  try {
15412
14916
  if (req.method !== "GET") {
15413
- sendJson$4(res, 405, { error: "method not allowed" });
14917
+ sendJson$3(res, 405, { error: "method not allowed" });
15414
14918
  return;
15415
14919
  }
15416
14920
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -15419,54 +14923,12 @@ function registerStatusApi(ctx, opts = {}) {
15419
14923
  workspace: url.searchParams.get("workspace") ?? void 0
15420
14924
  });
15421
14925
  const root = cccRootForCwd(workspace) ?? "";
15422
- sendJson$4(res, 200, { cccs: await listCccs(ctx, {
14926
+ sendJson$3(res, 200, { cccs: await listCccs(ctx, {
15423
14927
  defaultRoot: root,
15424
14928
  withRoles: true
15425
14929
  }) });
15426
14930
  } catch (err) {
15427
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15428
- }
15429
- }
15430
- });
15431
- ctx.webServer.register({
15432
- kind: "exact",
15433
- path: PUBLIC_ASK_PATH,
15434
- handler: async (req, res) => {
15435
- try {
15436
- if (!requireWebUi(req, res, "仅限 WebUI(key 属敏感凭据)")) return;
15437
- if (req.method === "PUT") {
15438
- const raw = await readBody$4(req, 16384);
15439
- if (JSON.parse(raw).action !== "rotate") {
15440
- sendJson$4(res, 400, { error: "unsupported action (expected \"rotate\")" });
15441
- return;
15442
- }
15443
- sendJson$4(res, 200, { key: rotatePublicAskKey() });
15444
- return;
15445
- }
15446
- if (req.method !== "GET") {
15447
- sendJson$4(res, 405, { error: "method not allowed" });
15448
- return;
15449
- }
15450
- const settings = readAdvancedSettings();
15451
- const { readSimpleSettings } = await import("./settings-section-xHABl8nk.js").then((n) => n.i);
15452
- const simple = readSimpleSettings();
15453
- const allowed = settings.publicAsk.allowed;
15454
- const port = simple.acpHttpPort ?? ACP_HTTP_PORT;
15455
- const base = `http://127.0.0.1:${port}`;
15456
- const containerUrls = allowed.map((name) => ({
15457
- name,
15458
- url: `${base}/c/${encodeURIComponent(name)}`
15459
- }));
15460
- sendJson$4(res, 200, {
15461
- enabled: simple.publicAskEnabled ?? false,
15462
- port,
15463
- key: ensurePublicAskKey(),
15464
- allowed,
15465
- urls: containerUrls,
15466
- listUrl: `${base}/`
15467
- });
15468
- } catch (err) {
15469
- sendJson$4(res, 400, { error: err.message ?? String(err) });
14931
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15470
14932
  }
15471
14933
  }
15472
14934
  });
@@ -15480,21 +14942,21 @@ function registerStatusApi(ctx, opts = {}) {
15480
14942
  if (req.method === "GET") {
15481
14943
  const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
15482
14944
  if (!chk.ok) {
15483
- sendJson$4(res, 400, { error: chk.error });
14945
+ sendJson$3(res, 400, { error: chk.error });
15484
14946
  return;
15485
14947
  }
15486
- const { readWeixinSettings } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
14948
+ const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15487
14949
  const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
15488
14950
  const settings = readWeixinSettings(chk.root);
15489
14951
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
15490
- const { readWeixinCredential } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
14952
+ const { readWeixinCredential } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15491
14953
  const accounts = (settings.accounts ?? []).map((a) => ({
15492
14954
  accountId: a.accountId,
15493
14955
  name: a.name ?? void 0,
15494
14956
  enabled: a.enabled !== false,
15495
14957
  bound: readWeixinCredential(chk.root, a.accountId) !== null
15496
14958
  }));
15497
- sendJson$4(res, 200, {
14959
+ sendJson$3(res, 200, {
15498
14960
  enabled: settings.enabled === true,
15499
14961
  botType: settings.botType ?? void 0,
15500
14962
  accounts,
@@ -15504,14 +14966,14 @@ function registerStatusApi(ctx, opts = {}) {
15504
14966
  return;
15505
14967
  }
15506
14968
  if (req.method !== "POST") {
15507
- sendJson$4(res, 405, { error: "method not allowed" });
14969
+ sendJson$3(res, 405, { error: "method not allowed" });
15508
14970
  return;
15509
14971
  }
15510
- const raw = await readBody$4(req, 131072);
14972
+ const raw = await readBody$3(req, 131072);
15511
14973
  const body = JSON.parse(raw);
15512
14974
  const chk = requireCcc(body.ccc);
15513
14975
  if (!chk.ok) {
15514
- sendJson$4(res, 400, { error: chk.error });
14976
+ sendJson$3(res, 400, { error: chk.error });
15515
14977
  return;
15516
14978
  }
15517
14979
  const root = chk.root;
@@ -15519,7 +14981,7 @@ function registerStatusApi(ctx, opts = {}) {
15519
14981
  const { fetchQRCode } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
15520
14982
  const { randomUUID } = await import("node:crypto");
15521
14983
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
15522
- const { readWeixinSettings } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
14984
+ const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15523
14985
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
15524
14986
  const loginKey = randomUUID();
15525
14987
  weixinLogins.set(loginKey, {
@@ -15528,7 +14990,7 @@ function registerStatusApi(ctx, opts = {}) {
15528
14990
  startedAt: Date.now(),
15529
14991
  polling: false
15530
14992
  });
15531
- sendJson$4(res, 200, {
14993
+ sendJson$3(res, 200, {
15532
14994
  qrcode: qr.qrcode,
15533
14995
  qrcode_img_content: qr.qrcode_img_content,
15534
14996
  loginKey
@@ -15537,40 +14999,40 @@ function registerStatusApi(ctx, opts = {}) {
15537
14999
  }
15538
15000
  if (body.action === "remove-account") {
15539
15001
  if (!body.accountId) {
15540
- sendJson$4(res, 400, { error: "missing accountId" });
15002
+ sendJson$3(res, 400, { error: "missing accountId" });
15541
15003
  return;
15542
15004
  }
15543
- const { removeWeixinAccount } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15005
+ const { removeWeixinAccount } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15544
15006
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15545
15007
  removeWeixinAccount(root, body.accountId);
15546
15008
  syncCccBridge(ctx, root);
15547
- sendJson$4(res, 200, { removed: body.accountId });
15009
+ sendJson$3(res, 200, { removed: body.accountId });
15548
15010
  return;
15549
15011
  }
15550
15012
  if (body.action === "save-routes") {
15551
15013
  const routes = body.routes ?? [];
15552
15014
  if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
15553
- sendJson$4(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
15015
+ sendJson$3(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
15554
15016
  return;
15555
15017
  }
15556
- const { saveWeixinRoutes } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15557
- const { readSkiffRoles } = await import("./skiff-role-Det66X16.js").then((n) => n.u);
15018
+ const { saveWeixinRoutes } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15019
+ const { readSkiffRoles } = await import("./skiff-role-B615L9MV.js").then((n) => n.u);
15558
15020
  const roles = readSkiffRoles(root);
15559
15021
  for (const r of routes) if (!roles.has(r.role)) {
15560
- 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)` });
15561
15023
  return;
15562
15024
  }
15563
15025
  saveWeixinRoutes(root, routes);
15564
- sendJson$4(res, 200, { saved: routes.length });
15026
+ sendJson$3(res, 200, { saved: routes.length });
15565
15027
  return;
15566
15028
  }
15567
15029
  if (body.action === "set-enabled") {
15568
15030
  const enabled = body.enabled === true;
15569
- const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15031
+ const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15570
15032
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15571
15033
  const settings = readWeixinSettings(root);
15572
15034
  if (enabled && (settings.accounts ?? []).length === 0) {
15573
- sendJson$4(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
15035
+ sendJson$3(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
15574
15036
  return;
15575
15037
  }
15576
15038
  setWeixinEnabled(root, enabled);
@@ -15579,12 +15041,12 @@ function registerStatusApi(ctx, opts = {}) {
15579
15041
  const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15580
15042
  stopCccBridge(root);
15581
15043
  }
15582
- sendJson$4(res, 200, { enabled });
15044
+ sendJson$3(res, 200, { enabled });
15583
15045
  return;
15584
15046
  }
15585
- sendJson$4(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
15047
+ sendJson$3(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
15586
15048
  } catch (err) {
15587
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15049
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15588
15050
  }
15589
15051
  }
15590
15052
  });
@@ -15595,22 +15057,22 @@ function registerStatusApi(ctx, opts = {}) {
15595
15057
  try {
15596
15058
  if (!requireWebUi(req, res, "微信桥配置仅限 WebUI(client 专用)")) return;
15597
15059
  if (req.method !== "GET") {
15598
- sendJson$4(res, 405, { error: "method not allowed" });
15060
+ sendJson$3(res, 405, { error: "method not allowed" });
15599
15061
  return;
15600
15062
  }
15601
15063
  const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
15602
15064
  const login = weixinLogins.get(key);
15603
15065
  if (!login) {
15604
- sendJson$4(res, 404, { error: "login not found or expired" });
15066
+ sendJson$3(res, 404, { error: "login not found or expired" });
15605
15067
  return;
15606
15068
  }
15607
15069
  if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
15608
15070
  weixinLogins.delete(key);
15609
- sendJson$4(res, 200, { status: "expired" });
15071
+ sendJson$3(res, 200, { status: "expired" });
15610
15072
  return;
15611
15073
  }
15612
15074
  const { pollQRStatus } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
15613
- const { readWeixinSettings } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15075
+ const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15614
15076
  const settings = readWeixinSettings(login.root);
15615
15077
  const status = await pollQRStatus({
15616
15078
  baseUrl: void 0,
@@ -15619,13 +15081,13 @@ function registerStatusApi(ctx, opts = {}) {
15619
15081
  switch (status.status) {
15620
15082
  case "confirmed": {
15621
15083
  if (!status.bot_token) {
15622
- sendJson$4(res, 200, {
15084
+ sendJson$3(res, 200, {
15623
15085
  status: "error",
15624
15086
  error: "confirmed but no bot_token"
15625
15087
  });
15626
15088
  return;
15627
15089
  }
15628
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15090
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15629
15091
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15630
15092
  const accountId = nextWeixinAccountId(settings);
15631
15093
  upsertWeixinAccount(login.root, {
@@ -15640,7 +15102,7 @@ function registerStatusApi(ctx, opts = {}) {
15640
15102
  });
15641
15103
  syncCccBridge(ctx, login.root);
15642
15104
  weixinLogins.delete(key);
15643
- sendJson$4(res, 200, {
15105
+ sendJson$3(res, 200, {
15644
15106
  status: "confirmed",
15645
15107
  accountId,
15646
15108
  tokenSaved: true
@@ -15649,14 +15111,14 @@ function registerStatusApi(ctx, opts = {}) {
15649
15111
  }
15650
15112
  case "expired":
15651
15113
  weixinLogins.delete(key);
15652
- sendJson$4(res, 200, { status: "expired" });
15114
+ sendJson$3(res, 200, { status: "expired" });
15653
15115
  return;
15654
15116
  default:
15655
- sendJson$4(res, 200, { status: status.status ?? "wait" });
15117
+ sendJson$3(res, 200, { status: status.status ?? "wait" });
15656
15118
  return;
15657
15119
  }
15658
15120
  } catch (err) {
15659
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15121
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15660
15122
  }
15661
15123
  }
15662
15124
  });
@@ -15678,7 +15140,7 @@ function registerStatusApi(ctx, opts = {}) {
15678
15140
  const root = sessionsRootDir();
15679
15141
  if (req.method === "GET") {
15680
15142
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
15681
- sendJson$4(res, 200, {
15143
+ sendJson$3(res, 200, {
15682
15144
  root,
15683
15145
  olderThanDays: days,
15684
15146
  dryRun: true,
@@ -15692,12 +15154,12 @@ function registerStatusApi(ctx, opts = {}) {
15692
15154
  return;
15693
15155
  }
15694
15156
  if (req.method !== "POST") {
15695
- sendJson$4(res, 405, { error: "method not allowed" });
15157
+ sendJson$3(res, 405, { error: "method not allowed" });
15696
15158
  return;
15697
15159
  }
15698
15160
  if (!requireWebUi(req, res, "会话清理仅限 WebUI(client 专用)")) return;
15699
15161
  const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
15700
- sendJson$4(res, 200, {
15162
+ sendJson$3(res, 200, {
15701
15163
  root,
15702
15164
  olderThanDays: days,
15703
15165
  dryRun: false,
@@ -15705,7 +15167,7 @@ function registerStatusApi(ctx, opts = {}) {
15705
15167
  errors: result.errors
15706
15168
  });
15707
15169
  } catch (err) {
15708
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15170
+ sendJson$3(res, 400, { error: err.message ?? String(err) });
15709
15171
  }
15710
15172
  }
15711
15173
  });
@@ -15758,7 +15220,7 @@ function loadVoyageHtml(assetPath = voyageAssetPath()) {
15758
15220
  return readFileSync(assetPath, "utf-8");
15759
15221
  }
15760
15222
  /** 发 JSON(错误分支用;与 api.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
15761
- function sendJson$3(res, status, body) {
15223
+ function sendJson$2(res, status, body) {
15762
15224
  res.statusCode = status;
15763
15225
  res.setHeader("content-type", "application/json; charset=utf-8");
15764
15226
  res.end(JSON.stringify(body));
@@ -15773,14 +15235,14 @@ function registerVoyageApi(ctx, opts = {}) {
15773
15235
  path: VOYAGE_PATH,
15774
15236
  handler: (req, res) => {
15775
15237
  if (req.method !== "GET") {
15776
- sendJson$3(res, 405, { error: "method not allowed" });
15238
+ sendJson$2(res, 405, { error: "method not allowed" });
15777
15239
  return;
15778
15240
  }
15779
15241
  let html;
15780
15242
  try {
15781
15243
  html = loadVoyageHtml(opts.assetPath ?? voyageAssetPath());
15782
15244
  } catch (err) {
15783
- 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)}` });
15784
15246
  return;
15785
15247
  }
15786
15248
  res.statusCode = 200;
@@ -15989,7 +15451,7 @@ const HOST_SERVICES = [
15989
15451
  name: "get",
15990
15452
  kind: "function"
15991
15453
  }],
15992
- impact: "skiff/handyman 无法创建或复用 agent",
15454
+ impact: "skiff/cheap-worker 无法创建或复用 agent",
15993
15455
  required: true
15994
15456
  },
15995
15457
  {
@@ -16071,7 +15533,7 @@ const HOST_SERVICES = [
16071
15533
  name: "agentLoop",
16072
15534
  access: "injected",
16073
15535
  members: [],
16074
- impact: "handyman worker agent 无法装配 preset",
15536
+ impact: "cheap-worker worker agent 无法装配 preset",
16075
15537
  required: false
16076
15538
  },
16077
15539
  {
@@ -16082,7 +15544,7 @@ const HOST_SERVICES = [
16082
15544
  name: "start",
16083
15545
  kind: "function"
16084
15546
  }],
16085
- impact: "handyman foreground 模式不可用(响亮报错,不影响 background 模式)",
15547
+ impact: "cheap-worker 委派不可用(前台委派的正门,响亮报错)",
16086
15548
  required: false
16087
15549
  },
16088
15550
  {
@@ -16375,14 +15837,16 @@ function summarizeHostContract(report) {
16375
15837
  *
16376
15838
  * ## 现状 → 本模块要收掉什么(取证:`…/acc-c4c5-current-state.md` §①/§③ 3.1)
16377
15839
  *
16378
- * 插件自己 spawn 了**四个** node:http 监听器(B 3081 网关 / C 3082 微信发送 / D 3099 Skiff 调试
16379
- * / 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')` +
16380
15844
  * `listen(port, host)` + 模块级 `active` 变量 + `close()`;拆卸也分裂成**两套**——
16381
15845
  * `seams/lifecycle.ts` 有一个聚合 disposer,而 `gateway.ts` 与 `weixin-send-api.ts`
16382
15846
  * **各自另注册**一个(D/E 干脆没有自己的 disposer)。
16383
15847
  *
16384
15848
  * 本模块把"listener 生命周期"收成一处:**active 表 + 统一 listen/close + 统一错误与端口占用
16385
- * 处理**。四个面各自只提供 `{ name, port, host, enabled, handler }`。
15849
+ * 处理**。各面各自只提供 `{ name, port, host, enabled, handler }`。
16386
15850
  *
16387
15851
  * ## 🔴 共享的是「生命周期与样板」,**绝不是**「路由 / 监听器 / 鉴权」
16388
15852
  *
@@ -16390,12 +15854,12 @@ function summarizeHostContract(report) {
16390
15854
  * · **A ↔ C 解耦**(`weixin-send-api.ts:4-7`):3080 会被 3081 原样反代(连请求头),
16391
15855
  * 把 3082 的端点挂到 3080 上 = 已登录的外部家庭账号即可冒充 bot 发消息
16392
15856
  * ⇒ 本模块**不提供**"把面挂到别的面"的能力,也不共享路由表;
16393
- * · **E ↔ D** 共用会话核心(acp-core → skiff-core)但各自独立 listener + 独立路由/HTML;
15857
+ * · (原 **E ↔ D** 共用会话核心但各自独立 listener 一条,**随 E 退役删除**);
16394
15858
  * · **B 是 A 的代理宿主**(把请求打到 `127.0.0.1:<主端口>`),不是另一个路由面。
16395
15859
  *
16396
15860
  * ## 面 A(3080)不在本模块之列
16397
15861
  * 它是**宿主 DSH 主 WebUI**,插件只往上挂 `/serenity/*` 路由(`ctx.webServer.register`),
16398
- * **插件不起这个 listener** ⇒ 面宿主只持有 B/C/D/E 四个自起面。
15862
+ * **插件不起这个 listener** ⇒ 面宿主只持有 B/C/D 三个自起面。
16399
15863
  *
16400
15864
  * ## 两个入口的分工(这是本模块唯一的"抽象决策",别混)
16401
15865
  * · {@link startFace} —— **机械启动**:把一个 handler 绑到 `host:port` 上。同名面若已在监听,
@@ -17020,7 +16484,7 @@ function createDshCookieProvider(connection, authority) {
17020
16484
  /** 面名(C4 块 A:listener 生命周期归 `face-host`) */
17021
16485
  const FACE_GATEWAY = "gateway";
17022
16486
  /** 读取请求体(≤ maxBytes;超限 reject) */
17023
- function readBody$3(req, maxBytes) {
16487
+ function readBody$2(req, maxBytes) {
17024
16488
  return new Promise((resolve, reject) => {
17025
16489
  let data = "";
17026
16490
  req.on("data", (chunk) => {
@@ -17147,7 +16611,7 @@ async function startGateway(config, getAccounts, getDshCookie) {
17147
16611
  const session = authed(req);
17148
16612
  if (session !== void 0) {
17149
16613
  if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
17150
- readBody$3(req, 131072).then((body) => {
16614
+ readBody$2(req, 131072).then((body) => {
17151
16615
  const { rpcId, path } = parseWorkspaceCreateBody(body);
17152
16616
  if (!allowWorkspaceCreate) {
17153
16617
  res.writeHead(403, { "content-type": "application/json" });
@@ -19477,7 +18941,7 @@ const SKIFF_DEBUG_HOST = FACE_PORTS.skiffDebug.host ?? "127.0.0.1";
19477
18941
  function skiffDebugActive() {
19478
18942
  return faceActive(FACE_SKIFF_DEBUG);
19479
18943
  }
19480
- function readBody$2(req) {
18944
+ function readBody$1(req) {
19481
18945
  return new Promise((resolve, reject) => {
19482
18946
  const chunks = [];
19483
18947
  req.on("data", (c) => chunks.push(c));
@@ -19485,7 +18949,7 @@ function readBody$2(req) {
19485
18949
  req.on("error", reject);
19486
18950
  });
19487
18951
  }
19488
- function sendJson$2(res, status, payload) {
18952
+ function sendJson$1(res, status, payload) {
19489
18953
  const body = JSON.stringify(payload);
19490
18954
  res.writeHead(status, {
19491
18955
  "Content-Type": "application/json; charset=utf-8",
@@ -19493,7 +18957,7 @@ function sendJson$2(res, status, payload) {
19493
18957
  });
19494
18958
  res.end(body);
19495
18959
  }
19496
- function sendHtml$1(res, html) {
18960
+ function sendHtml(res, html) {
19497
18961
  res.writeHead(200, {
19498
18962
  "Content-Type": "text/html; charset=utf-8",
19499
18963
  "Cache-Control": "no-store"
@@ -19690,8 +19154,8 @@ function escapeHtml(s) {
19690
19154
  * 在 JSON **文本层**把它们替换为 `\uXXXX` 转义序列:JSON.parse 后语义完全一致(还原原字符),
19691
19155
  * 且 JSC 正则看到的是常规 ASCII 转义(与 JSON.stringify 对控制字符的输出同形态,安全)。
19692
19156
  *
19693
- * 3100 问答页客户端 `await res.json()`(acp-http.ts)与页面内嵌 JSON(skiff-debug/acp-http)
19694
- * 均需此兼容层——iOS Safari 用户实测"复杂回答"触发。
19157
+ * 3099 调试页客户端 `await res.json()` 与页面内嵌 JSON 均需此兼容层
19158
+ * ——iOS Safari 用户实测"复杂回答"触发。(原同批还列了 3100 问答页,该面已退役,2026-10-15)
19695
19159
  *
19696
19160
  * @param jsonText JSON.stringify 的输出文本;原地等价替换,返回 JSC 安全文本
19697
19161
  */
@@ -19819,7 +19283,7 @@ function skiffDebugSpec(ctx, root, port, webPort) {
19819
19283
  port,
19820
19284
  host: SKIFF_DEBUG_HOST,
19821
19285
  enabled: () => readSimpleSettings().skiffEnabled,
19822
- handler: (req, res) => handle$1(ctx, root, webPort, req, res)
19286
+ handler: (req, res) => handle(ctx, root, webPort, req, res)
19823
19287
  };
19824
19288
  }
19825
19289
  /**
@@ -19843,11 +19307,11 @@ async function startSkiffDebugServer(ctx, root, port, webPort) {
19843
19307
  function stopSkiffDebugServer() {
19844
19308
  stopFace(FACE_SKIFF_DEBUG);
19845
19309
  }
19846
- async function handle$1(ctx, defaultRoot, webPort, req, res) {
19310
+ async function handle(ctx, defaultRoot, webPort, req, res) {
19847
19311
  try {
19848
19312
  const url = (req.url ?? "/").split("?")[0] ?? "/";
19849
19313
  if (req.method === "GET" && url === "/") {
19850
- sendHtml$1(res, skiffDebugPage(await listCccs(ctx, {
19314
+ sendHtml(res, skiffDebugPage(await listCccs(ctx, {
19851
19315
  defaultRoot,
19852
19316
  withRoles: true
19853
19317
  }), defaultRoot, webPort));
@@ -19856,11 +19320,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19856
19320
  if (req.method === "POST" && url === "/ask") {
19857
19321
  let body;
19858
19322
  try {
19859
- const parsed = JSON.parse(await readBody$2(req));
19323
+ const parsed = JSON.parse(await readBody$1(req));
19860
19324
  if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
19861
19325
  body = parsed;
19862
19326
  } catch {
19863
- sendJson$2(res, 400, { error: "invalid JSON body" });
19327
+ sendJson$1(res, 400, { error: "invalid JSON body" });
19864
19328
  return;
19865
19329
  }
19866
19330
  const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot;
@@ -19869,11 +19333,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19869
19333
  const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
19870
19334
  const role = readSkiffRoles(ccc).get(roleName);
19871
19335
  if (!roleName || !role) {
19872
- sendJson$2(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
19336
+ sendJson$1(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
19873
19337
  return;
19874
19338
  }
19875
19339
  if (!question.trim()) {
19876
- sendJson$2(res, 400, { error: "empty question" });
19340
+ sendJson$1(res, 400, { error: "empty question" });
19877
19341
  return;
19878
19342
  }
19879
19343
  let agent;
@@ -19882,20 +19346,20 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19882
19346
  const info = skiffSessionInfo(sessionId);
19883
19347
  const live = getSkiffAgent(sessionId);
19884
19348
  if (!info || !live) {
19885
- 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" });
19886
19350
  return;
19887
19351
  }
19888
19352
  if (info.role !== roleName || info.ccc !== ccc) {
19889
- 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` });
19890
19354
  return;
19891
19355
  }
19892
19356
  agent = live;
19893
19357
  continued = true;
19894
19358
  }
19895
- const hc = readHandymanConfig(ccc);
19359
+ const hc = readCheapWorkerConfig(ccc);
19896
19360
  if (!agent) agent = (await createSkiffAgent(ctx, ccc, roleName, role, hc?.defaultModel)).agent;
19897
19361
  const result = await askSkiff(ctx, agent, question, 0);
19898
- sendJson$2(res, 200, {
19362
+ sendJson$1(res, 200, {
19899
19363
  answer: result.answer,
19900
19364
  answer_html: renderSkiffMarkdown(result.answer),
19901
19365
  sessionId: result.sessionId,
@@ -19904,9 +19368,9 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19904
19368
  });
19905
19369
  return;
19906
19370
  }
19907
- sendJson$2(res, 404, { error: "not found" });
19371
+ sendJson$1(res, 404, { error: "not found" });
19908
19372
  } catch (err) {
19909
- sendJson$2(res, 500, { error: err?.message ?? String(err) });
19373
+ sendJson$1(res, 500, { error: err?.message ?? String(err) });
19910
19374
  }
19911
19375
  }
19912
19376
  //#endregion
@@ -20050,7 +19514,9 @@ const rebukeStates = /* @__PURE__ */ new Map();
20050
19514
  //#endregion
20051
19515
  //#region src/output-guard-seam.ts
20052
19516
  /** 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免):
20053
- * skiff-(F4 问答)/ acp-(F4c 程序化)/ rebuild-(v1.22.4 重建会话)前缀 */
19517
+ * skiff-(F4 问答)/ rebuild-(v1.22.4 重建会话)前缀。
19518
+ * 🔴 `acp-` 分支保留但**已是死判据**(无生产者;面 E 已于 2026-10-15 退役)——
19519
+ * 理由与"为何不擅自删"见文件头注释。 */
20054
19520
  function isExternalFaceSession(sessionId) {
20055
19521
  if (!sessionId) return false;
20056
19522
  return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
@@ -20345,760 +19811,6 @@ function registerUnattendedSeam(ctx) {
20345
19811
  });
20346
19812
  }
20347
19813
  //#endregion
20348
- //#region src/acp-core.ts
20349
- /** JSON-RPC 错误码(对齐标准) */
20350
- const RPC_ERROR = {
20351
- PARSE: -32700,
20352
- INVALID_REQUEST: -32600,
20353
- METHOD_NOT_FOUND: -32601,
20354
- INVALID_PARAMS: -32602,
20355
- INTERNAL: -32603
20356
- };
20357
- /**
20358
- * ACP 会话管理器:持有 skiff agent 会话(与 skiff-core 注册表同源——
20359
- * 创建走 createSkiffAgent(自动注册),关闭走 unregisterSkiffSession)。
20360
- * 传输无关:HTTP 端点/企业微信桥/未来 stdio 都调 handle(method, params)。
20361
- */
20362
- var AcpServer = class {
20363
- ctx;
20364
- constructor(ctx) {
20365
- this.ctx = ctx;
20366
- }
20367
- /** 实现级方法调度(返回 result 或抛 JSON-RPC 语义错误) */
20368
- async handle(method, params) {
20369
- switch (method) {
20370
- case "initialize": return this.initialize();
20371
- case "authenticate": return {};
20372
- case "session/new": return this.newSession(params);
20373
- case "session/list": return this.listSessions();
20374
- case "session/prompt": return this.prompt(params);
20375
- case "session/cancel": return this.cancel(params);
20376
- case "session/close": return this.closeSession(params);
20377
- case "session/request_permission": return { allow: true };
20378
- default: throw new RpcMethodError(`method not found: ${method}`);
20379
- }
20380
- }
20381
- initialize() {
20382
- return {
20383
- protocolVersion: 1,
20384
- capabilities: {
20385
- sessionId: true,
20386
- ccc: true,
20387
- role: true
20388
- },
20389
- serverInfo: {
20390
- name: "dsh-serenity-hooks-acp",
20391
- version: "1.26.0"
20392
- }
20393
- };
20394
- }
20395
- /**
20396
- * session/new:{ ccc, role, sessionId? } → 创建或延续 skiff 会话。
20397
- * 无 sessionId → createSkiffAgent(新会话,continued:false);
20398
- * 有 → 进程内延续(getSkiffAgent 命中 + role/ccc 绑定校验;未命中 → 错误)。
20399
- */
20400
- async newSession(params) {
20401
- const ccc = typeof params?.ccc === "string" && params.ccc !== "" ? params.ccc : void 0;
20402
- const roleName = typeof params?.role === "string" ? params.role : void 0;
20403
- const sessionId = typeof params?.sessionId === "string" && params.sessionId !== "" ? params.sessionId : void 0;
20404
- if (!ccc) throw new RpcInvalidParams("session/new requires ccc (CCC root path)");
20405
- if (!roleName) throw new RpcInvalidParams("session/new requires role");
20406
- const role = readSkiffRoles(ccc).get(roleName);
20407
- if (!role) throw new RpcInvalidParams(`unknown role: ${roleName} (ccc: ${ccc})`);
20408
- if (sessionId) {
20409
- const info = skiffSessionInfo(sessionId);
20410
- const live = getSkiffAgent(sessionId);
20411
- if (!info || !live) throw new RpcInvalidParams("session is not recoverable (process restarted or session unknown) — start a new conversation");
20412
- if (info.role !== roleName || info.ccc !== ccc) throw new RpcInvalidParams(`session "${sessionId}" belongs to role "${info.role}" in another CCC`);
20413
- return {
20414
- sessionId,
20415
- role: roleName,
20416
- ccc,
20417
- continued: true
20418
- };
20419
- }
20420
- const hc = readHandymanConfig(ccc);
20421
- return {
20422
- sessionId: (await createSkiffAgent(this.ctx, ccc, roleName, role, hc?.defaultModel)).sessionId,
20423
- role: roleName,
20424
- ccc,
20425
- continued: false
20426
- };
20427
- }
20428
- listSessions() {
20429
- const sessions = [];
20430
- for (const [id, b] of skiffSessionSnapshot()) sessions.push({
20431
- sessionId: id,
20432
- role: b.role,
20433
- ccc: b.ccc
20434
- });
20435
- return { sessions };
20436
- }
20437
- /**
20438
- * session/prompt:{ sessionId, question } → 答案(**v1.26.10:不返回 trajectory**——
20439
- * 3100 对外只提供问答,轨迹含工具结果等内部信息;3099 调试页另走 /ask 保留)
20440
- */
20441
- async prompt(params) {
20442
- const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
20443
- const question = typeof params?.question === "string" ? params.question : void 0;
20444
- if (!sessionId) throw new RpcInvalidParams("session/prompt requires sessionId");
20445
- if (!question?.trim()) throw new RpcInvalidParams("session/prompt requires question");
20446
- const agent = getSkiffAgent(sessionId);
20447
- if (!agent) throw new RpcInvalidParams(`unknown session: ${sessionId} (not recoverable — process restarted?)`);
20448
- const result = await askSkiff(this.ctx, agent, question, void 0, { includeTrajectory: false });
20449
- return {
20450
- answer: result.answer,
20451
- sessionId: result.sessionId
20452
- };
20453
- }
20454
- /** session/cancel:{ sessionId } → 取消该会话的当前活动(Agent.cancel,宿主 rc.1 契约) */
20455
- cancel(params) {
20456
- const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
20457
- if (!sessionId) return { cancelled: false };
20458
- const agent = getSkiffAgent(sessionId);
20459
- if (!agent) return { cancelled: false };
20460
- try {
20461
- agent.cancel({ kind: "user" });
20462
- } catch {}
20463
- return { cancelled: true };
20464
- }
20465
- /** session/close:{ sessionId } → 释放 agent + 注册表清理(进程内) */
20466
- closeSession(params) {
20467
- const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
20468
- if (!sessionId) return { closed: false };
20469
- if (!getSkiffAgent(sessionId)) return { closed: false };
20470
- unregisterSkiffSession(sessionId);
20471
- return { closed: true };
20472
- }
20473
- };
20474
- /** 方法不存在错误(JSON-RPC -32601) */
20475
- var RpcMethodError = class extends Error {
20476
- constructor(message) {
20477
- super(message);
20478
- this.name = "RpcMethodError";
20479
- }
20480
- };
20481
- /** 参数错误(JSON-RPC -32602) */
20482
- var RpcInvalidParams = class extends Error {
20483
- constructor(message) {
20484
- super(message);
20485
- this.name = "RpcInvalidParams";
20486
- }
20487
- };
20488
- /**
20489
- * JSON-RPC 2.0 批处理(单帧):解析请求/通知 → 调 AcpServer.handle → 响应。
20490
- * 传输无关:acp-http 端点 / 未来 stdio 共用。
20491
- * @returns 响应帧数组(请求有响应;通知无;解析失败 → 单错误响应)
20492
- */
20493
- async function dispatchRpc(server, frame) {
20494
- if (frame === null || typeof frame !== "object") return [rpcError(null, RPC_ERROR.INVALID_REQUEST, "invalid request")];
20495
- const req = frame;
20496
- if (req.jsonrpc !== "2.0" || typeof req.method !== "string") return [rpcError(req.id ?? null, RPC_ERROR.INVALID_REQUEST, "invalid request")];
20497
- if (req.id === void 0) {
20498
- try {
20499
- await server.handle(req.method, req.params);
20500
- } catch {}
20501
- return [];
20502
- }
20503
- try {
20504
- const result = await server.handle(req.method, req.params);
20505
- return [{
20506
- jsonrpc: "2.0",
20507
- id: req.id,
20508
- result
20509
- }];
20510
- } catch (error) {
20511
- return [rpcError(req.id, rpcCodeOf(error), error instanceof Error ? error.message : String(error))];
20512
- }
20513
- }
20514
- function rpcCodeOf(error) {
20515
- if (error instanceof RpcMethodError) return RPC_ERROR.METHOD_NOT_FOUND;
20516
- if (error instanceof RpcInvalidParams) return RPC_ERROR.INVALID_PARAMS;
20517
- return RPC_ERROR.INTERNAL;
20518
- }
20519
- function rpcError(id, code, message) {
20520
- return {
20521
- jsonrpc: "2.0",
20522
- id,
20523
- error: {
20524
- code,
20525
- message
20526
- }
20527
- };
20528
- }
20529
- //#endregion
20530
- //#region src/acp-http.ts
20531
- /** 面名(C4 块 A) */
20532
- const FACE_ACP_HTTP = "acp-http";
20533
- /** 监听地址:**只绑 loopback**(JSON-RPC 子面无认证——仅靠 loopback;问答页另有 key 认证) */
20534
- const ACP_HTTP_HOST = FACE_PORTS.acpHttp.host ?? "127.0.0.1";
20535
- function acpHttpActive() {
20536
- return faceActive(FACE_ACP_HTTP);
20537
- }
20538
- function readBody$1(req) {
20539
- return new Promise((resolve, reject) => {
20540
- const chunks = [];
20541
- req.on("data", (c) => chunks.push(c));
20542
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
20543
- req.on("error", reject);
20544
- });
20545
- }
20546
- function sendJson$1(res, status, payload) {
20547
- const body = jscSafeJsonText(JSON.stringify(payload));
20548
- res.writeHead(status, {
20549
- "Content-Type": "application/json; charset=utf-8",
20550
- "Cache-Control": "no-store"
20551
- });
20552
- res.end(body);
20553
- }
20554
- function sendHtml(res, html) {
20555
- res.writeHead(200, {
20556
- "Content-Type": "text/html; charset=utf-8",
20557
- "Cache-Control": "no-store"
20558
- });
20559
- res.end(html);
20560
- }
20561
- /**
20562
- * 本面的规格(C4 块 A/④:**只提供** name/port/host/enabled/handler)。
20563
- * 两个子面(ACP JSON-RPC / 对外问答页)的路由与鉴权仍全部归 {@link handle}——
20564
- * 面宿主不介入面语义,也**不合并** D/E 两面(两者共用会话核心但各有独立 listener/路由/HTML)。
20565
- */
20566
- function acpHttpSpec(ctx, port, defaultRoot) {
20567
- return {
20568
- name: FACE_ACP_HTTP,
20569
- port,
20570
- host: ACP_HTTP_HOST,
20571
- enabled: () => {
20572
- const s = readSimpleSettings();
20573
- return s.acpEnabled || s.publicAskEnabled;
20574
- },
20575
- handler: (req, res) => handle(ctx, defaultRoot ?? "", req, res)
20576
- };
20577
- }
20578
- /**
20579
- * 启动 ACP HTTP JSON-RPC + 建议问答页服务(单实例;重复启动幂等返回既有实例)。
20580
- * C4 块 A:listener 的创建/监听/错误/端口占用/关闭归 `face-host`。
20581
- * @param ctx 插件上下文(AcpServer 内部经 skiff-core 创建 agent)
20582
- * @param port 监听端口(仅 127.0.0.1)
20583
- * @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
20584
- */
20585
- async function startAcpHttpServer(ctx, port, defaultRoot) {
20586
- if (acpHttpActive()) return;
20587
- const handle = await startFace(acpHttpSpec(ctx, port, defaultRoot));
20588
- const s = readSimpleSettings();
20589
- const faces = [];
20590
- if (s.acpEnabled) faces.push("JSON-RPC");
20591
- if (s.publicAskEnabled) faces.push("问答页(key 认证)");
20592
- console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${handle.port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
20593
- }
20594
- function stopAcpHttpServer() {
20595
- stopFace(FACE_ACP_HTTP);
20596
- }
20597
- async function handle(ctx, defaultRoot, req, res) {
20598
- try {
20599
- const url = (req.url ?? "/").split("?")[0] ?? "/";
20600
- const s = readSimpleSettings();
20601
- const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
20602
- if (req.method === "POST" && url === "/") {
20603
- if (!s.acpEnabled) {
20604
- sendJson$1(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
20605
- return;
20606
- }
20607
- let body;
20608
- try {
20609
- body = JSON.parse(await readBody$1(req));
20610
- } catch {
20611
- sendJson$1(res, 400, [{
20612
- jsonrpc: "2.0",
20613
- id: null,
20614
- error: {
20615
- code: -32700,
20616
- message: "parse error"
20617
- }
20618
- }]);
20619
- return;
20620
- }
20621
- sendJson$1(res, 200, await dispatchRpc(new AcpServer(ctx), body));
20622
- return;
20623
- }
20624
- if (!s.publicAskEnabled) {
20625
- if (req.method === "GET" && (url === "/" || url.startsWith("/c/"))) {
20626
- sendHtml(res, publicAskDisabledHtml());
20627
- return;
20628
- }
20629
- if (req.method === "POST" && (url === "/ask" || url.startsWith("/c/"))) {
20630
- sendJson$1(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
20631
- return;
20632
- }
20633
- sendJson$1(res, 404, { error: "not found" });
20634
- return;
20635
- }
20636
- const containerAllowed = (name) => {
20637
- const allowed = readAdvancedSettings().publicAsk.allowed;
20638
- return allowed.length === 0 || allowed.includes(name);
20639
- };
20640
- let cccsCache = null;
20641
- const cccsOf = async () => {
20642
- if (cccsCache === null) cccsCache = await listCccs(ctx, {
20643
- defaultRoot: defaultRoot || (cccRootForCwd(process.cwd()) ?? ""),
20644
- withRoles: true
20645
- });
20646
- return cccsCache;
20647
- };
20648
- const cMatch = /^\/c\/([^/]+)$/.exec(url);
20649
- if (req.method === "GET" && cMatch) {
20650
- const name = decodeURIComponent(cMatch[1] ?? "");
20651
- const ccc = (await cccsOf()).find((c) => c.name === name);
20652
- if (!ccc) {
20653
- sendHtml(res, publicAskContainerUnknownHtml(name));
20654
- return;
20655
- }
20656
- if (!containerAllowed(name)) {
20657
- sendHtml(res, publicAskContainerClosedHtml(name));
20658
- return;
20659
- }
20660
- sendHtml(res, publicAskContainerPage(ccc));
20661
- return;
20662
- }
20663
- if (req.method === "GET" && url === "/") {
20664
- sendHtml(res, publicAskListPage((await cccsOf()).filter((c) => containerAllowed(c.name))));
20665
- return;
20666
- }
20667
- const aMatch = /^\/c\/([^/]+)\/ask$/.exec(url);
20668
- if (req.method === "POST" && aMatch) {
20669
- const name = decodeURIComponent(aMatch[1] ?? "");
20670
- const ccc = (await cccsOf()).find((c) => c.name === name);
20671
- if (!ccc || !containerAllowed(name)) {
20672
- sendJson$1(res, 403, { error: `container "${name}" is not open for public ask` });
20673
- return;
20674
- }
20675
- let body;
20676
- try {
20677
- const parsed = JSON.parse(await readBody$1(req));
20678
- if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
20679
- body = parsed;
20680
- } catch {
20681
- sendJson$1(res, 400, { error: "invalid JSON body" });
20682
- return;
20683
- }
20684
- await handleAskParsed(ctx, ccc, body, res, ip);
20685
- return;
20686
- }
20687
- if (req.method === "POST" && url === "/ask") {
20688
- let body;
20689
- try {
20690
- const parsed = JSON.parse(await readBody$1(req));
20691
- if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
20692
- body = parsed;
20693
- } catch {
20694
- sendJson$1(res, 400, { error: "invalid JSON body" });
20695
- return;
20696
- }
20697
- const cccValue = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : void 0;
20698
- const nameValue = typeof body.name === "string" && body.name !== "" ? body.name : void 0;
20699
- let ccc;
20700
- if (nameValue) ccc = (await cccsOf()).find((c) => c.name === nameValue);
20701
- else if (cccValue) {
20702
- ccc = (await cccsOf()).find((c) => c.root === cccValue);
20703
- if (!ccc && cccValue !== "") ccc = {
20704
- root: cccValue,
20705
- name: cccValue.split("/").filter(Boolean).pop() ?? cccValue,
20706
- roles: [...readSkiffRoles(cccValue).keys()]
20707
- };
20708
- }
20709
- if (!ccc || !containerAllowed(ccc.name)) {
20710
- sendJson$1(res, 403, { error: "container is not open for public ask" });
20711
- return;
20712
- }
20713
- await handleAskParsed(ctx, ccc, body, res, ip);
20714
- return;
20715
- }
20716
- sendJson$1(res, 404, { error: "not found" });
20717
- } catch (err) {
20718
- sendJson$1(res, 500, { error: err?.message ?? String(err) });
20719
- }
20720
- }
20721
- /**
20722
- * 单容器问答核心(/c/<name>/ask 与 /ask 共用):
20723
- * body { key, role?, question, sessionId? } → key 校验(401 + IP 失败锁定)→ 角色缺省取 CCC 第一个
20724
- * → 会话延续(v1.25.10 语义,静默新建)→ askSkiff(0 全量轨迹) → { answer, answer_html, sessionId, continued, trajectory }
20725
- * @param ip 请求来源 IP(v1.26.5 公网 key 失败锁定:X-Forwarded-For 优先——公网反代,回退 remoteAddress)
20726
- */
20727
- async function handleAskParsed(ctx, ccc, body, res, ip) {
20728
- const key = typeof body.key === "string" ? body.key : void 0;
20729
- if (isPublicAskIpLocked(ip)) {
20730
- sendJson$1(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
20731
- return;
20732
- }
20733
- if (!verifyPublicAskKey(key)) {
20734
- const remaining = recordPublicAskFail(ip);
20735
- 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" });
20736
- return;
20737
- }
20738
- resetPublicAskIpFail(ip);
20739
- const root = ccc.root;
20740
- const roleName = typeof body.role === "string" && body.role !== "" ? body.role : void 0;
20741
- const question = typeof body.question === "string" ? body.question : "";
20742
- const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
20743
- if (!question.trim()) {
20744
- sendJson$1(res, 400, { error: "empty question" });
20745
- return;
20746
- }
20747
- const roles = readSkiffRoles(root);
20748
- const explicit = roleName ? roles.get(roleName) : void 0;
20749
- const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
20750
- const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
20751
- if (!effectiveRole || !effectiveCfg) {
20752
- sendJson$1(res, 400, { error: `no skiff role available in ccc: ${root}` });
20753
- return;
20754
- }
20755
- let agent;
20756
- let continued = false;
20757
- if (sessionId) {
20758
- const info = skiffSessionInfo(sessionId);
20759
- const live = getSkiffAgent(sessionId);
20760
- if (info && live && info.role === effectiveRole && info.ccc === root) {
20761
- agent = live;
20762
- continued = true;
20763
- }
20764
- }
20765
- if (!agent) agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, readHandymanConfig(root)?.defaultModel)).agent;
20766
- const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
20767
- sendJson$1(res, 200, {
20768
- answer: result.answer,
20769
- answer_html: renderSkiffMarkdown(result.answer, true),
20770
- sessionId: result.sessionId,
20771
- continued
20772
- });
20773
- }
20774
- /** 问答页未启用提示页 */
20775
- function publicAskDisabledHtml() {
20776
- return `<!DOCTYPE html>
20777
- <html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
20778
- <body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
20779
- <h2>Serenity Public Ask</h2>
20780
- <p>问答页未启用(请在 dsh 设置面板「Serenity」页开启「建议问答」)。</p>
20781
- </body></html>`;
20782
- }
20783
- /** 容器不存在提示页(URL 容器名不在已发现 CCC 中) */
20784
- function publicAskContainerUnknownHtml(name) {
20785
- return `<!DOCTYPE html>
20786
- <html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
20787
- <body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
20788
- <h2>Serenity Public Ask</h2>
20789
- <p>认知容器「${name}」不存在。</p>
20790
- <p style="font-size:13px"><a href="/" style="color:#0ba875">返回容器列表</a></p>
20791
- </body></html>`;
20792
- }
20793
- /** 容器未开放提示页(白名单外) */
20794
- function publicAskContainerClosedHtml(name) {
20795
- return `<!DOCTYPE html>
20796
- <html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
20797
- <body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
20798
- <h2>Serenity Public Ask</h2>
20799
- <p>认知容器「${name}」当前未开放问答(管理员在设置面板「开放容器」中配置)。</p>
20800
- <p style="font-size:13px"><a href="/" style="color:#0ba875">返回容器列表</a></p>
20801
- </body></html>`;
20802
- }
20803
- /** 页面基础样式(列表页 + 单容器页共用;v1.26.4 聊天 UI) */
20804
- const ASK_CSS = `
20805
- :root { color-scheme: light dark; --sp-green: #0ba875; --sp-green-dim: #059669; }
20806
- * { box-sizing: border-box; }
20807
- html, body { height: 100%; }
20808
- body { font-family: system-ui, -apple-system, sans-serif; margin: 0; background: #f6f7f9; color: #1f2328; }
20809
- @media (prefers-color-scheme: dark) { body { background: #16181c; color: #e6e6e6; } }
20810
- main { max-width: 720px; margin: 0 auto; }
20811
- h1 { font-size: 18px; margin: 0 0 4px; }
20812
- .sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
20813
- .card { background: #fff; border: 1px solid #d0d7de; border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; display: block; text-decoration: none; color: inherit; }
20814
- @media (prefers-color-scheme: dark) { .card { background: #26282c; border-color: #3a3d42; } }
20815
- .card:hover { border-color: var(--sp-green); }
20816
- .card .cname { font-weight: 600; font-size: 15px; }
20817
- .card .cmeta { opacity: .6; font-size: 12px; margin-top: 2px; }
20818
- .empty { text-align: center; opacity: .7; padding: 24px 0; }
20819
- .ccname { font-weight: 600; color: var(--sp-green); }
20820
- /* key 门(v1.26.6:选容器前必填) */
20821
- .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; }
20822
- .keyGate label { font-size: 12px; opacity: .7; white-space: nowrap; }
20823
- .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; }
20824
- .keyGate input:focus { outline: none; border-color: var(--sp-green); }
20825
-
20826
- /* ── 聊天布局(v1.26.4 体验升级:参考 ChatGPT/Claude 会话形态)── */
20827
- .chat { display: flex; flex-direction: column; height: 100vh; max-width: 720px; margin: 0 auto; }
20828
- .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; }
20829
- .chatHead .back { color: var(--sp-green); text-decoration: none; font-size: 13px; white-space: nowrap; }
20830
- .chatHead .title { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
20831
- .chatHead .spacer { flex: 1; }
20832
- .chatHead select { border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 4px 8px; font-size: 12px; }
20833
- .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; }
20834
- .chatHead .newBtn:hover { border-color: var(--sp-green); color: var(--sp-green); }
20835
- /* 消息流 */
20836
- .msgList { flex: 1; overflow-y: auto; padding: 18px 16px; display: flex; flex-direction: column; gap: 16px; }
20837
- .msg { display: flex; flex-direction: column; max-width: 88%; }
20838
- .msg.user { align-self: flex-end; align-items: flex-end; }
20839
- .msg.assistant { align-self: flex-start; align-items: flex-start; }
20840
- .msg .who { font-size: 11px; opacity: .55; margin-bottom: 4px; padding: 0 4px; }
20841
- .msg .bubble { padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.65; word-break: break-word; white-space: normal; }
20842
- .msg.user .bubble { background: var(--sp-green); color: #fff; border-bottom-right-radius: 4px; }
20843
- .msg.assistant .bubble { background: #fff; border: 1px solid #d0d7de; border-bottom-left-radius: 4px; }
20844
- @media (prefers-color-scheme: dark) { .msg.assistant .bubble { background: #26282c; border-color: #3a3d42; } }
20845
- .bubble p { margin: 6px 0; }
20846
- .bubble p:first-child { margin-top: 0; }
20847
- .bubble p:last-child { margin-bottom: 0; }
20848
- .bubble h1, .bubble h2, .bubble h3, .bubble h4 { margin: 12px 0 6px; font-size: 15px; line-height: 1.4; }
20849
- .bubble ul, .bubble ol { margin: 6px 0; padding-left: 22px; }
20850
- .bubble code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, monospace; }
20851
- .bubble pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 8px 0; }
20852
- .bubble pre code { background: none; padding: 0; }
20853
- .bubble blockquote { margin: 6px 0; padding: 2px 12px; border-left: 3px solid rgba(127,127,127,.3); opacity: .85; }
20854
- .bubble a { color: var(--sp-green); }
20855
- .bubble table { border-collapse: collapse; margin: 8px 0; }
20856
- .bubble th, .bubble td { border: 1px solid rgba(127,127,127,.25); padding: 4px 10px; font-size: 13px; }
20857
- .bubble img { max-width: 100%; border-radius: 8px; }
20858
- .bubble hr { border: 0; border-top: 1px solid rgba(127,127,127,.2); margin: 10px 0; }
20859
- .typing { display: inline-flex; align-items: center; gap: 4px; padding: 4px 2px; }
20860
- .typing i { width: 6px; height: 6px; border-radius: 50%; background: rgba(127,127,127,.5); animation: blink 1.2s infinite; }
20861
- .typing i:nth-child(2) { animation-delay: .2s; }
20862
- .typing i:nth-child(3) { animation-delay: .4s; }
20863
- @keyframes blink { 0%, 80%, 100% { opacity: .3 } 40% { opacity: 1 } }
20864
- .err { color: #cf222e; white-space: pre-wrap; font-size: 13px; }
20865
- .msg .err { padding: 0 4px; }
20866
- /* 输入区 */
20867
- .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); }
20868
- .inputRow { display: flex; align-items: flex-end; gap: 8px; }
20869
- .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; }
20870
- @media (prefers-color-scheme: dark) { .inputRow textarea { background: #26282c; border-color: #3a3d42; } }
20871
- .inputRow textarea:focus { outline: none; border-color: var(--sp-green); }
20872
- .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; }
20873
- .inputRow .sendBtn:disabled { opacity: .5; cursor: wait; }
20874
- .inputRow .sendBtn:hover:not(:disabled) { background: var(--sp-green-dim); }
20875
- .hint { text-align: center; font-size: 12px; opacity: .55; padding: 20px 0; }
20876
-
20877
- /* ── 移动端适配(v1.26.6:手机/平板——iOS 防放大 16px / 触控 ≥44px / 100dvh / safe-area)── */
20878
- @media (max-width: 640px) {
20879
- main { padding: 20px 16px calc(28px + env(safe-area-inset-bottom)); }
20880
- .sub { font-size: 14px; margin-bottom: 14px; }
20881
- .keyGate { flex-wrap: wrap; gap: 6px; }
20882
- .keyGate input { flex: 1 1 100%; font-size: 16px; padding: 12px 14px; min-height: 48px; } /* 16px 防 iOS 聚焦放大 + 触控 ≥44px */
20883
- .card { padding: 16px; border-radius: 12px; }
20884
- .card .cname { font-size: 16px; }
20885
- .card .cmeta { font-size: 13px; }
20886
- .chat { height: 100vh; height: 100dvh; } /* dvh:iOS 地址栏收起/展开不跳动 */
20887
- .chatHead { gap: 8px; padding: 8px 12px calc(8px + env(safe-area-inset-top)); }
20888
- .chatHead .back { font-size: 14px; padding: 10px 4px; } /* 触控目标 ≥44px */
20889
- .chatHead .title { font-size: 14px; }
20890
- .chatHead select { font-size: 16px; padding: 8px 6px; min-height: 44px; } /* 16px 防放大 + 触控 */
20891
- .chatHead .newBtn { font-size: 14px; padding: 10px 12px; min-height: 44px; }
20892
- .msgList { padding: 14px 12px calc(20px + env(safe-area-inset-bottom)); gap: 14px; }
20893
- .msg { max-width: 94%; }
20894
- .msg .bubble { font-size: 15px; padding: 12px 14px; }
20895
- .msg .who { font-size: 12px; }
20896
- .chatInput { padding: 8px 12px calc(10px + env(safe-area-inset-bottom)); }
20897
- .inputRow textarea { font-size: 16px; padding: 12px 14px; min-height: 48px; } /* 16px 防放大 + 触控 */
20898
- .inputRow .sendBtn { font-size: 16px; padding: 12px 20px; min-height: 48px; }
20899
- }
20900
- `;
20901
- /** 容器列表页(v1.26.2 → v1.26.6):key 在选容器前填写(v1.26.6 用户拍板:不填 key 不让选容器) */
20902
- function publicAskListPage(cccs) {
20903
- const cards = cccs.map((c) => `
20904
- <a class="card" data-name="${c.name}" href="/c/${encodeURIComponent(c.name)}">
20905
- <div class="cname">${c.name}</div>
20906
- <div class="cmeta">${c.roles.length} 个问答角色 · 点击进入</div>
20907
- </a>`).join("") || "<div class=\"empty\">暂无可问答的认知容器(管理员尚未开放)</div>";
20908
- return `<!DOCTYPE html>
20909
- <html lang="zh">
20910
- <head>
20911
- <meta charset="utf-8">
20912
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
20913
- <title>Serenity Public Ask</title>
20914
- <style>${ASK_CSS}</style>
20915
- </head>
20916
- <body>
20917
- <main>
20918
- <h1>Serenity Public Ask</h1>
20919
- <div class="sub">先填写访问 key,再选择认知容器开始提问</div>
20920
- <div class="keyGate">
20921
- <label for="gateKey">访问 Key</label>
20922
- <input id="gateKey" type="password" placeholder="请输入访问 key(仅本次浏览器记忆)" autocomplete="off" autocapitalize="none" enterkeyhint="done">
20923
- </div>
20924
- <div id="cards">${cards}</div>
20925
- </main>
20926
- <script>
20927
- const KEY_STORE = 'serenity-public-ask-key'
20928
- const gateKey = document.getElementById('gateKey')
20929
- const cardList = document.getElementById('cards')
20930
- // 容器卡片(key 未填时禁用——v1.26.6 用户拍板:key 在选容器前填写)
20931
- function applyGate() {
20932
- const ok = gateKey.value.trim().length > 0
20933
- for (const card of cardList.querySelectorAll('a.card')) {
20934
- card.classList.toggle('locked', !ok)
20935
- card.setAttribute('aria-disabled', ok ? 'false' : 'true')
20936
- if (!ok) { card.removeAttribute('href'); card.style.pointerEvents = 'none'; card.style.opacity = '.4' }
20937
- else { card.setAttribute('href', '/c/' + encodeURIComponent(card.dataset.name)); card.style.pointerEvents = ''; card.style.opacity = '' }
20938
- }
20939
- }
20940
- gateKey.addEventListener('input', () => {
20941
- // 即存 localStorage(v1.26.7 修复:v1.26.6 漏掉保存——填一次就记录,对话页读取)
20942
- try { localStorage.setItem(KEY_STORE, gateKey.value.trim()) } catch {}
20943
- applyGate()
20944
- })
20945
- // key 自动从 localStorage 恢复
20946
- let hasSavedKey = false
20947
- try { const saved = localStorage.getItem(KEY_STORE); if (saved) { gateKey.value = saved; hasSavedKey = true } } catch {}
20948
- if (hasSavedKey) gateKey.placeholder = '访问 Key(已记忆,可修改)'
20949
- applyGate()
20950
- gateKey.focus()
20951
- <\/script>
20952
- </body>
20953
- </html>`;
20954
- }
20955
- /** 单容器问答页(v1.26.2→v1.26.6):URL 锁定容器;聊天 UI——消息流 + 连续对话 + key 从列表页记忆(v1.26.6 用户拍板:对话页不再填 key) */
20956
- function publicAskContainerPage(ccc) {
20957
- const data = jscSafeJsonText(JSON.stringify({
20958
- name: ccc.name,
20959
- root: ccc.root,
20960
- roles: ccc.roles
20961
- }).replace(/</g, "\\u003c"));
20962
- return `<!DOCTYPE html>
20963
- <html lang="zh">
20964
- <head>
20965
- <meta charset="utf-8">
20966
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
20967
- <title>Serenity · ${ccc.name}</title>
20968
- <style>${ASK_CSS}</style>
20969
- </head>
20970
- <body>
20971
- <div class="chat">
20972
- <div class="chatHead">
20973
- <a class="back" href="/">← 容器</a>
20974
- <span class="title">Serenity · <span class="ccname">${ccc.name}</span></span>
20975
- <span class="spacer"></span>
20976
- <select id="role" title="问答角色"></select>
20977
- <button id="newChat" class="newBtn" title="清空当前对话(不存储,刷新即失)">新对话</button>
20978
- </div>
20979
-
20980
- <div id="msgs" class="msgList"></div>
20981
-
20982
- <div class="chatInput">
20983
- <div class="inputRow">
20984
- <textarea id="q" placeholder="向 ${ccc.name} 提问…(Enter 发送,Shift+Enter 换行)" rows="1" enterkeyhint="send" autocapitalize="sentences"></textarea>
20985
- <button id="ask" class="sendBtn">发送</button>
20986
- </div>
20987
- </div>
20988
- </div>
20989
- <script id="ask-data" type="application/json">${data}<\/script>
20990
- <script>
20991
- const CCC = JSON.parse(document.getElementById('ask-data').textContent)
20992
- const KEY_STORE = 'serenity-public-ask-key'
20993
- const roleSel = document.getElementById('role')
20994
- const btn = document.getElementById('ask')
20995
- const msgs = document.getElementById('msgs')
20996
- const qInput = document.getElementById('q')
20997
- let sessionId = ''
20998
- let busy = false
20999
-
21000
- // 角色下拉
21001
- roleSel.innerHTML = (CCC.roles || []).map((r) => '<option value="' + r + '">' + r + '</option>').join('') || '<option value="">(无角色)</option>'
21002
- // key 从 localStorage 读取(列表页填写时记忆;对话页不填——v1.26.6 用户拍板)
21003
- function getStoredKey() {
21004
- try { return (localStorage.getItem(KEY_STORE) || '').trim() } catch { return '' }
21005
- }
21006
-
21007
- /** 追加一条消息气泡(role: user|assistant) */
21008
- function addMsg(role, htmlOrText, isHtml) {
21009
- const m = document.createElement('div')
21010
- m.className = 'msg ' + role
21011
- const who = document.createElement('div')
21012
- who.className = 'who'
21013
- who.textContent = role === 'user' ? '你' : CCC.name
21014
- const bubble = document.createElement('div')
21015
- bubble.className = 'bubble'
21016
- if (isHtml) bubble.innerHTML = htmlOrText
21017
- else bubble.textContent = htmlOrText
21018
- m.appendChild(who)
21019
- m.appendChild(bubble)
21020
- msgs.appendChild(m)
21021
- msgs.scrollTop = msgs.scrollHeight
21022
- return bubble
21023
- }
21024
-
21025
- /** 打字指示器(等待回答时显示) */
21026
- function addTyping() {
21027
- const m = document.createElement('div')
21028
- m.className = 'msg assistant'
21029
- const who = document.createElement('div')
21030
- who.className = 'who'
21031
- who.textContent = CCC.name
21032
- const bubble = document.createElement('div')
21033
- bubble.className = 'bubble'
21034
- bubble.innerHTML = '<span class="typing"><i></i><i></i><i></i></span>'
21035
- m.appendChild(who)
21036
- m.appendChild(bubble)
21037
- msgs.appendChild(m)
21038
- msgs.scrollTop = msgs.scrollHeight
21039
- return bubble
21040
- }
21041
-
21042
- async function ask() {
21043
- const q = qInput.value.trim()
21044
- const key = getStoredKey()
21045
- const role = roleSel.value || undefined
21046
- if (!q) return
21047
- if (!key) { addMsg('assistant', '⚠️ 未找到访问 key,请先回到 <a href="/">容器列表页</a> 填写 key。', true); return }
21048
- if (busy) return
21049
- // 追加用户消息 + 打字指示
21050
- addMsg('user', q, false)
21051
- qInput.value = ''
21052
- autoGrow()
21053
- const typing = addTyping()
21054
- busy = true
21055
- btn.disabled = true
21056
- try {
21057
- 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 }) })
21058
- const data = await res.json()
21059
- typing.remove() // 移除打字指示,替换为真实回答
21060
- if (!res.ok) {
21061
- sessionId = ''
21062
- addMsg('assistant', '⚠️ ' + (data.error || ('HTTP ' + res.status)), true)
21063
- return
21064
- }
21065
- addMsg('assistant', data.answer_html || data.answer || '(空回答)', true)
21066
- sessionId = data.sessionId || ''
21067
- } catch (err) {
21068
- typing.remove()
21069
- addMsg('assistant', '⚠️ ' + String(err.message || err), true)
21070
- } finally {
21071
- busy = false
21072
- btn.disabled = false
21073
- qInput.focus()
21074
- }
21075
- }
21076
-
21077
- function autoGrow() {
21078
- qInput.style.height = 'auto'
21079
- qInput.style.height = Math.min(qInput.scrollHeight, 140) + 'px'
21080
- }
21081
-
21082
- btn.addEventListener('click', () => void ask())
21083
- qInput.addEventListener('keydown', (e) => {
21084
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void ask() }
21085
- })
21086
- qInput.addEventListener('input', autoGrow)
21087
- // 新对话:重置 sessionId + 清空消息流(前端内存,不存储——刷新即失)
21088
- document.getElementById('newChat').addEventListener('click', () => {
21089
- sessionId = ''
21090
- msgs.innerHTML = ''
21091
- qInput.focus()
21092
- })
21093
-
21094
- // 初始欢迎消息(外层模板求值注入容器名;textContent 渲染防注入)
21095
- addMsg('assistant', '你好,我是「${ccc.name}」的认知助手。有什么可以帮你的?(本对话仅当前页面有效,刷新即失)', false)
21096
- qInput.focus()
21097
- <\/script>
21098
- </body>
21099
- </html>`;
21100
- }
21101
- //#endregion
21102
19814
  //#region src/human-channel.ts
21103
19815
  /**
21104
19816
  * human-channel.ts — 人机信道的**机制层**(机器级;2026-09-26 owner 定案 ⇒ 方案 §2.6)
@@ -21420,7 +20132,7 @@ function buildManualOutputRebuke(session) {
21420
20132
  }
21421
20133
  /**
21422
20134
  * 装配闸门(index.ts apply 调用)。
21423
- * 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff/ACP 临时会话)零干预。
20135
+ * 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff 会话)零干预。
21424
20136
  */
21425
20137
  function registerWeixinOutputGuard(ctx) {
21426
20138
  try {
@@ -21781,7 +20493,7 @@ function manualOutputFallbackNeeded(input) {
21781
20493
  */
21782
20494
  async function resolveCccRootByAlias(ctx, alias) {
21783
20495
  try {
21784
- const { listCccs } = await import("./ccc-roots-CDW2dO0n.js").then((n) => n.a);
20496
+ const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
21785
20497
  const entries = await listCccs(ctx);
21786
20498
  const byName = entries.find((e) => e.name === alias);
21787
20499
  if (byName) return byName.root;
@@ -21930,7 +20642,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21930
20642
  try {
21931
20643
  const sessionId = weixinSessionIdFor(fromUserId);
21932
20644
  const existing = getSkiffAgent(sessionId);
21933
- const hc = readHandymanConfig(root);
20645
+ const hc = readCheapWorkerConfig(root);
21934
20646
  if (existing) try {
21935
20647
  ensureSkiffSession(root, existing, roleName, role, true);
21936
20648
  } catch (err) {
@@ -22280,7 +20992,7 @@ function syncHumanChannel(ctx) {
22280
20992
  (async () => {
22281
20993
  try {
22282
20994
  stopMachineLoops();
22283
- const { listCccs } = await import("./ccc-roots-CDW2dO0n.js").then((n) => n.a);
20995
+ const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
22284
20996
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
22285
20997
  } catch {}
22286
20998
  })();
@@ -22516,7 +21228,7 @@ function matchCcc(input, candidates) {
22516
21228
  }
22517
21229
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
22518
21230
  async function collectCandidates(ctx) {
22519
- const { listCccs } = await import("./ccc-roots-CDW2dO0n.js").then((n) => n.a);
21231
+ const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
22520
21232
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
22521
21233
  const seen = /* @__PURE__ */ new Set();
22522
21234
  const out = [];
@@ -22782,7 +21494,7 @@ function registerLifecycle(ctx) {
22782
21494
  stopAllBridges();
22783
21495
  } catch {}
22784
21496
  };
22785
- 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);
22786
21498
  }
22787
21499
  //#endregion
22788
21500
  //#region src/web-fetch-provider.ts
@@ -24984,7 +23696,7 @@ function renderDsl(model) {
24984
23696
  * 🔴 **为什么 PNG 是必需的**:宿主的对话内图片通路**白名单只有光栅格式**
24985
23697
  * (`image/png|jpeg|webp|gif`,见 `dsh-attachment/types.d.ts`)⇒ **SVG 进不去**。
24986
23698
  *
24987
- * 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 skiff/acp 口径。
23699
+ * 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 skiff 口径。
24988
23700
  *
24989
23701
  * ## 🔴 两条**宿主契约**(2026-09-29 现场取证;第 ① 条是本文件第一版写错的地方)
24990
23702
  *
@@ -25358,10 +24070,6 @@ const Config = z.object({
25358
24070
  enabled: z.boolean().default(false),
25359
24071
  debugPort: z.number().min(1024).max(65535).default(SKIFF_DEBUG_PORT)
25360
24072
  }),
25361
- acp: z.object({
25362
- enabled: z.boolean().default(false),
25363
- httpPort: z.number().min(1024).max(65535).default(ACP_HTTP_PORT)
25364
- }),
25365
24073
  webFetch: z.object({ enabled: z.boolean().default(true) }),
25366
24074
  opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
25367
24075
  visionPatch: z.object({ enabled: z.boolean().default(true) }),
@@ -25376,9 +24084,6 @@ const Config = z.object({
25376
24084
  rebuildThresholdK: z.number().min(50).max(4e3).volatile(),
25377
24085
  skiffEnabled: z.boolean().volatile(),
25378
24086
  skiffDebugPort: z.number().min(1024).max(65535).volatile(),
25379
- acpEnabled: z.boolean().volatile(),
25380
- acpHttpPort: z.number().min(1024).max(65535).volatile(),
25381
- publicAskEnabled: z.boolean().volatile(),
25382
24087
  croEnabled: z.boolean().volatile(),
25383
24088
  unattendedEnabled: z.boolean().volatile(),
25384
24089
  diagramEnabled: z.boolean().volatile()
@@ -25407,7 +24112,7 @@ function apply(ctx, config) {
25407
24112
  ctx.tools.register(praxisTool);
25408
24113
  ctx.tools.register(createOntologyTool());
25409
24114
  ctx.tools.register(createSkillCreatorTool());
25410
- ctx.tools.register(createHandymanTool(ctx));
24115
+ ctx.tools.register(createCheapWorkerTool(ctx));
25411
24116
  ctx.tools.register(localstoreTool);
25412
24117
  ctx.tools.register(createContainerAdminTool(ctx));
25413
24118
  ctx.tools.register(createImBridgeTool());
@@ -25441,7 +24146,6 @@ function apply(ctx, config) {
25441
24146
  if (config.opencodeSkills) registerOpencodeSkills(ctx);
25442
24147
  registerBootstrap(ctx);
25443
24148
  registerSkiff(ctx);
25444
- registerAcp(ctx);
25445
24149
  registerCroTurnTracking(ctx);
25446
24150
  registerWakeScheduler(ctx);
25447
24151
  registerWeixinBridge(ctx);
@@ -25465,7 +24169,7 @@ function apply(ctx, config) {
25465
24169
  * ① 听宿主的**真事件** `settings/document-updated`(ns = 我们那张设置页)——顺带说明为什么不用
25466
24170
  * 我们自己的 `serenity/settings-changed`:那个事件**全仓没有任何 `emit` 点**(只有 `ctx.on` 侧
25467
24171
  * 在用)=**死通道**,照着它接线等于接了个永远不响的铃;
25468
- * ② 兜底再挂 `agent/created` / `session/created`(与 skiff/acp 同款做法,那两条在实测里确实会 fire)
24172
+ * ② 兜底再挂 `agent/created` / `session/created`(与 skiff 同款做法,那两条在实测里确实会 fire)
25469
24173
  * —— 宿主事件面若换名,至少还有这一条能自愈;
25470
24174
  * ③ 启动时同步一次(apply 时按当前配置定初始态)。
25471
24175
  * 🔴 **关的时候要注销**(不是"留着但不响应"):与"关时不注册"同一口径 —— 工具不出现在模型清单里。
@@ -25512,7 +24216,8 @@ function registerDiagramTool(ctx) {
25512
24216
  * 同步一次——此刻通常还没有 live 会话,进程 cwd(服务启动目录)也不在 CCC 内 →
25513
24217
  * `resolveSkiffRoot` 返回 null → 打印一行警告后**永不重试**(实证:重启日志
25514
24218
  * `✗ Skiff 调试服务未启动:无法定位 CCC root`,`ss -ltn` 无 3099;同批 ACP 因容忍
25515
- * `root ?? undefined` 而正常启动)。
24219
+ * `root ?? undefined` 而正常启动 —— ⚠️ **该对照已随面 E 退役而失效**(2026-10-15),
24220
+ * 但本条记录的**病因与修法不受影响**(Skiff 侧仍是"首次未定位 → 曾永不重试")。
25516
24221
  *
25517
24222
  * v1.30.14(重启日志实证后的收敛):**并发启动守卫** + 首次未定位降级为信息级日志。
25518
24223
  * - 实证(v1.30.13 重启日志):定时触发与"会话就绪"事件几乎同时到达 → 两次 sync 都
@@ -25636,29 +24341,5 @@ function readWebPort(ctx) {
25636
24341
  return MAIN_WEB_PORT;
25637
24342
  }
25638
24343
  }
25639
- /**
25640
- * F4c ACP HTTP + F4d 建议问答页装配(v1.26.x 实验性):启停 = 人工(设置面板
25641
- * 「Serenity」页 ACP / 问答页开关,settings 持久化)。任一面开启即启动服务
25642
- * (同一端口:POST / = JSON-RPC 需 acpEnabled;GET / + /ask = 问答页需 publicAskEnabled)。
25643
- * 会话创建/延续走 acp-core → skiff-core;仅监听 127.0.0.1;未开启零资源占用。
25644
- */
25645
- function registerAcp(ctx) {
25646
- const sync = async () => {
25647
- const s = readSimpleSettings();
25648
- const anyFace = s.acpEnabled || s.publicAskEnabled;
25649
- if (anyFace && !acpHttpActive()) {
25650
- const root = await resolveSkiffRoot(ctx);
25651
- return startAcpHttpServer(ctx, s.acpHttpPort, root ?? void 0).catch((err) => {
25652
- console.error(`[serenity-hooks] ✗ ACP HTTP 服务启动失败: ${String(err?.message ?? err)}`);
25653
- });
25654
- } else if (!anyFace && acpHttpActive()) stopAcpHttpServer();
25655
- };
25656
- try {
25657
- ctx.on("serenity/settings-changed", () => {
25658
- sync();
25659
- });
25660
- } catch {}
25661
- sync();
25662
- }
25663
24344
  //#endregion
25664
24345
  export { Config, apply, humanChannelAccountSchema, humanChannelSubscriptionSchema, inject, name };