@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.
- package/README.md +25 -11
- package/dsh.plugin.json +3 -3
- package/lib/agent-idle.d.ts +2 -2
- package/lib/api.d.ts +0 -1
- package/lib/{ccc-DXky9UZu.js → ccc-B0ZGL9lx.js} +30 -13
- package/lib/{ccc-roots-CDW2dO0n.js → ccc-roots-GIJ3A_tw.js} +82 -5
- package/lib/ccc-roots.d.ts +3 -2
- package/lib/ccc.d.ts +23 -10
- package/lib/cheap-worker-ops.d.ts +49 -0
- package/lib/cheap-worker-preset-inherit.d.ts +35 -0
- package/lib/client.js +47 -471
- package/lib/config-ops.d.ts +3 -34
- package/lib/cro-guide.d.ts +1 -1
- package/lib/face-host.d.ts +7 -5
- package/lib/host/access.d.ts +2 -2
- package/lib/index.d.ts +0 -31
- package/lib/index.js +280 -1599
- package/lib/invariant.d.ts +1 -1
- package/lib/invariant.js +1 -1
- package/lib/localstore-ops.d.ts +1 -1
- package/lib/{message-source-CQNfQaTx.js → message-source-B4P3Uw6D.js} +1 -1
- package/lib/output-guard-seam.d.ts +9 -2
- package/lib/ports.d.ts +6 -6
- package/lib/seams/context.d.ts +2 -2
- package/lib/seams/lifecycle.d.ts +3 -3
- package/lib/{settings-section-xHABl8nk.js → settings-section-DQIVsTqE.js} +45 -30
- package/lib/settings-section.d.ts +3 -16
- package/lib/skiff-core.d.ts +6 -5
- package/lib/skiff-debug.d.ts +5 -4
- package/lib/{skiff-role-Det66X16.js → skiff-role-B615L9MV.js} +2 -2
- package/lib/skiff-role.d.ts +2 -2
- package/lib/status.d.ts +2 -2
- package/lib/tools/cheap-worker.d.ts +21 -0
- package/lib/tools/skiff-admin.d.ts +2 -2
- package/lib/unattended-ops.d.ts +6 -4
- package/lib/weixin-bridge.d.ts +9 -2
- package/lib/weixin-output-guard.d.ts +1 -1
- package/lib/{weixin-route-CxCciqQl.js → weixin-route-B9gU1YXz.js} +5 -5
- package/package.json +2 -2
- package/lib/access-CNccNtgf.js +0 -79
- package/lib/acp-core.d.ts +0 -80
- package/lib/acp-http.d.ts +0 -37
- package/lib/handyman-ops.d.ts +0 -97
- package/lib/handyman-preset-inherit.d.ts +0 -35
- package/lib/skiff-registry-BjIX_KD1.js +0 -43
- 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 {
|
|
3
|
-
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as
|
|
4
|
-
import { i as
|
|
5
|
-
import {
|
|
6
|
-
import { a as
|
|
7
|
-
import { a as
|
|
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
|
-
|
|
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
|
-
|
|
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 段:
|
|
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.
|
|
1162
|
-
|
|
1163
|
-
缺省模型 = models[0](可用
|
|
1164
|
-
**CCC 应把该白名单指向低成本模型**——
|
|
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
|
-
|
|
1167
|
-
|
|
1166
|
+
Behaviour(2026-10-14:background 模式已删除,只剩前台委派):
|
|
1167
|
+
cheap-worker(task, [label]):一次前台委派(宿主委派服务 ctx.subagents.start("spawn") +
|
|
1168
1168
|
agentOptions 注入模型)——不循环、不校验完成码、不写进度文件;返回子 agent 最终文本。
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
{ "
|
|
1177
|
+
{ "cheap-worker": { "models": ["provider/model-name"] } }
|
|
1176
1178
|
|
|
1177
1179
|
Example:
|
|
1178
|
-
{ "
|
|
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/
|
|
1826
|
+
//#region src/cheap-worker-ops.ts
|
|
1825
1827
|
/**
|
|
1826
|
-
*
|
|
1828
|
+
* cheap-worker-ops.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
|
|
1827
1829
|
*
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
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(`
|
|
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** 上实测):`
|
|
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
|
-
`
|
|
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} → "
|
|
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
|
-
" (\"
|
|
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
|
-
*
|
|
1995
|
-
*
|
|
1996
|
-
*
|
|
1906
|
+
* cheap-worker 规模化使用指引(guide 子命令输出)。
|
|
1907
|
+
*
|
|
1908
|
+
* 🆕 2026-10-14:**双模式段已整段退场**(owner 令「删除后台运行能力」)。本指引只描述现行机制:
|
|
1909
|
+
* 前台委派(单任务)/前台并行扇出(jobs)/白名单与 provider 预检。
|
|
1997
1910
|
*/
|
|
1998
|
-
const
|
|
1911
|
+
const CHEAP_WORKER_GUIDE = `# cheap-worker — Scale-Up Usage Guide (guide)
|
|
1999
1912
|
|
|
2000
|
-
##
|
|
2001
|
-
|
|
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
|
-
|
|
|
2004
|
-
|
|
|
2005
|
-
|
|
|
2006
|
-
|
|
|
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
|
|
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
|
|
1930
|
+
- Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel via \`jobs\`
|
|
2017
1931
|
|
|
2018
|
-
### 2. Prompt design (
|
|
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
|
-
-
|
|
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
|
|
1938
|
+
the cheap-worker-internal agent is also required to load eap and organize output per the EAP standard
|
|
2025
1939
|
|
|
2026
|
-
### 3. Model
|
|
2027
|
-
-
|
|
2028
|
-
|
|
2029
|
-
-
|
|
2030
|
-
-
|
|
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,
|
|
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 —
|
|
2040
|
-
- ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "
|
|
2041
|
-
(and "
|
|
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
|
|
2045
|
-
- Independent subtasks
|
|
2046
|
-
|
|
2047
|
-
|
|
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
|
-
-
|
|
2057
|
-
|
|
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
|
-
"
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
3936
|
+
* 随机 id 恒 create(调试页默认路径)。
|
|
4167
3937
|
*
|
|
4168
3938
|
* @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现用户↔会话长期映射——
|
|
4169
|
-
* 不传则随机生成 skiff-<role>-<uuid
|
|
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 文本(
|
|
4319
|
-
function lastAssistantText$
|
|
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
|
|
4381
|
-
* 公开问答页 / ACP JSON-RPC
|
|
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$
|
|
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/
|
|
9080
|
+
//#region src/tools/cheap-worker.ts
|
|
9310
9081
|
/**
|
|
9311
|
-
*
|
|
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
|
-
*
|
|
9337
|
-
*
|
|
9338
|
-
*
|
|
9339
|
-
*
|
|
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
|
-
*
|
|
9346
|
-
*
|
|
9347
|
-
*
|
|
9348
|
-
* (重启计数,≤100),同一轮重试。工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
|
|
9089
|
+
* 机制:走宿主**委派正门** `ctx.subagents.start('spawn', …)`(深度上限、子 agent 所有权、
|
|
9090
|
+
* `subagent/start|end` 事件、`dispose()` 收尾全部沿用宿主实现),模型经 `agentOptions` 注入。
|
|
9091
|
+
* 前置:`exec.agent` 必须存在(前台委派要有发起方)。
|
|
9349
9092
|
*
|
|
9350
|
-
*
|
|
9351
|
-
*
|
|
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
|
-
* 🔴
|
|
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
|
-
*
|
|
9399
|
-
*
|
|
9400
|
-
* ⚠️ 现在两档**数值相等但语义不同**(前台 = 占住本轮 / 后台 = 不占)⇒ **刻意不合并成一个常量**:
|
|
9401
|
-
* 合并会让"哪天要给前台单独降档"变成一次语义手术。
|
|
9111
|
+
* 为什么是 10 而不是更高:**前台委派会占住主 agent 的这一轮**(调用方在等结果),
|
|
9112
|
+
* 并发越高越容易把宿主的并发面与用量推上去。
|
|
9402
9113
|
*/
|
|
9403
9114
|
const FOREGROUND_MAX_PARALLEL = 10;
|
|
9404
|
-
/** foreground 的 jobs
|
|
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
|
|
9433
|
-
* `diagnostic
|
|
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
|
-
*
|
|
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("
|
|
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: ["
|
|
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
|
-
|
|
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: "
|
|
9643
|
-
description: "The
|
|
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 (
|
|
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: "
|
|
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
|
|
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:
|
|
9689
|
-
if (
|
|
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 =
|
|
9693
|
-
if (hc === null) throw new Error("
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
if (
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
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
|
-
|
|
9280
|
+
mode: "foreground",
|
|
9281
|
+
done,
|
|
9282
|
+
jobs: results,
|
|
9733
9283
|
usage: {
|
|
9734
|
-
how:
|
|
9735
|
-
model: "
|
|
9736
|
-
next:
|
|
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
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
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
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
session: typeof args.session === "string" ? args.session : void 0
|
|
9766
|
-
})));
|
|
9296
|
+
parent: exec.agent,
|
|
9297
|
+
signal: exec.signal
|
|
9298
|
+
});
|
|
9767
9299
|
return {
|
|
9768
|
-
|
|
9769
|
-
jobs: results.map((r) => ({ ...r })),
|
|
9300
|
+
...res,
|
|
9770
9301
|
usage: {
|
|
9771
|
-
how: "
|
|
9772
|
-
|
|
9773
|
-
|
|
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` / `
|
|
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.
|
|
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 直接指定,无白名单校验;缺省回退
|
|
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
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
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 ∈
|
|
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 =
|
|
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
|
|
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 ?? "(
|
|
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
|
-
* 先例 = `
|
|
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
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
* ⚠️ 原文案含「+
|
|
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
|
-
* -
|
|
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("
|
|
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 阈值 /
|
|
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
|
-
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
14795
|
+
sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
15292
14796
|
return;
|
|
15293
14797
|
}
|
|
15294
|
-
sendJson$
|
|
14798
|
+
sendJson$3(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
15295
14799
|
} catch (err) {
|
|
15296
|
-
sendJson$
|
|
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$
|
|
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$
|
|
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$
|
|
14822
|
+
sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
15319
14823
|
return;
|
|
15320
14824
|
}
|
|
15321
|
-
sendJson$
|
|
14825
|
+
sendJson$3(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
15322
14826
|
} catch (err) {
|
|
15323
|
-
sendJson$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
14866
|
+
sendJson$3(res, 200, {
|
|
15363
14867
|
...getStatus(workspace, configPaths),
|
|
15364
14868
|
...result
|
|
15365
14869
|
});
|
|
15366
14870
|
return;
|
|
15367
14871
|
}
|
|
15368
|
-
sendJson$
|
|
14872
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15369
14873
|
} catch (err) {
|
|
15370
|
-
sendJson$
|
|
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$
|
|
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$
|
|
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$
|
|
14902
|
+
sendJson$3(res, 200, { config: toWire(saved) });
|
|
15399
14903
|
return;
|
|
15400
14904
|
}
|
|
15401
|
-
sendJson$
|
|
14905
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15402
14906
|
} catch (err) {
|
|
15403
|
-
sendJson$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
14945
|
+
sendJson$3(res, 400, { error: chk.error });
|
|
15484
14946
|
return;
|
|
15485
14947
|
}
|
|
15486
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
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-
|
|
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$
|
|
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$
|
|
14969
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15508
14970
|
return;
|
|
15509
14971
|
}
|
|
15510
|
-
const raw = await readBody$
|
|
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$
|
|
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-
|
|
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$
|
|
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$
|
|
15002
|
+
sendJson$3(res, 400, { error: "missing accountId" });
|
|
15541
15003
|
return;
|
|
15542
15004
|
}
|
|
15543
|
-
const { removeWeixinAccount } = await import("./weixin-route-
|
|
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$
|
|
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$
|
|
15015
|
+
sendJson$3(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
15554
15016
|
return;
|
|
15555
15017
|
}
|
|
15556
|
-
const { saveWeixinRoutes } = await import("./weixin-route-
|
|
15557
|
-
const { readSkiffRoles } = await import("./skiff-role-
|
|
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$
|
|
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$
|
|
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-
|
|
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$
|
|
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$
|
|
15044
|
+
sendJson$3(res, 200, { enabled });
|
|
15583
15045
|
return;
|
|
15584
15046
|
}
|
|
15585
|
-
sendJson$
|
|
15047
|
+
sendJson$3(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
|
|
15586
15048
|
} catch (err) {
|
|
15587
|
-
sendJson$
|
|
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$
|
|
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$
|
|
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$
|
|
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-
|
|
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$
|
|
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-
|
|
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$
|
|
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$
|
|
15114
|
+
sendJson$3(res, 200, { status: "expired" });
|
|
15653
15115
|
return;
|
|
15654
15116
|
default:
|
|
15655
|
-
sendJson$
|
|
15117
|
+
sendJson$3(res, 200, { status: status.status ?? "wait" });
|
|
15656
15118
|
return;
|
|
15657
15119
|
}
|
|
15658
15120
|
} catch (err) {
|
|
15659
|
-
sendJson$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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/
|
|
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: "
|
|
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: "
|
|
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
|
|
16379
|
-
*
|
|
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
|
-
*
|
|
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**
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
-
*
|
|
19694
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
19336
|
+
sendJson$1(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
|
|
19873
19337
|
return;
|
|
19874
19338
|
}
|
|
19875
19339
|
if (!question.trim()) {
|
|
19876
|
-
sendJson$
|
|
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$
|
|
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$
|
|
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 =
|
|
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$
|
|
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$
|
|
19371
|
+
sendJson$1(res, 404, { error: "not found" });
|
|
19908
19372
|
} catch (err) {
|
|
19909
|
-
sendJson$
|
|
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 问答)/
|
|
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
|
|
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-
|
|
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 =
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 };
|