@shgroup/dsh-serenity-hooks 1.72.0 → 1.74.0
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 +12 -11
- package/dsh.plugin.json +2 -2
- package/lib/accounts-BHQbakUh.js +33 -0
- package/lib/{ccc-Dkoa2oW3.js → ccc-CyYJL4F2.js} +9 -9
- package/lib/{ccc-roots-jZYR2GVE.js → ccc-roots-B2t_aM8L.js} +5 -5
- package/lib/client.js +165 -58
- package/lib/{wake/cro.d.ts → cro/core.d.ts} +3 -3
- package/lib/{wake/cro-guide.d.ts → cro/guide.d.ts} +3 -3
- package/lib/{wake/cro-log.d.ts → cro/log.d.ts} +2 -2
- package/lib/{wake/cro-turns.d.ts → cro/turns.d.ts} +2 -2
- package/lib/guard/seam.d.ts +4 -4
- package/lib/host/effect.d.ts +1 -1
- package/lib/{ilink-DEpmE3yi.js → ilink-BMk2w2KY.js} +1 -1
- package/lib/im/bridge.d.ts +36 -65
- package/lib/im/endpoint.d.ts +3 -3
- package/lib/im/feishu-account.d.ts +92 -0
- package/lib/im/guard.d.ts +1 -1
- package/lib/im/hook.d.ts +1 -1
- package/lib/im/inbound-event.d.ts +162 -0
- package/lib/im/route.d.ts +38 -3
- package/lib/index.d.ts +5 -27
- package/lib/index.js +1089 -1208
- package/lib/kernel/ccc-roots.d.ts +4 -4
- package/lib/kernel/ccc.d.ts +19 -19
- package/lib/kernel/ports.d.ts +3 -5
- package/lib/lib-TxscFKeo.js +119355 -0
- package/lib/{message-source-kVoNK0C5.js → message-source-DCpoRHP3.js} +2 -2
- package/lib/patches/opencode-provider.d.ts +1 -1
- package/lib/{profile-DyMJ_bfr.js → profile-BVt84mbi.js} +68 -65
- package/lib/rolldown-runtime-B4iAMlE-.js +35 -0
- package/lib/{route-f7JlinIt.js → route-COOFLTpU.js} +61 -8
- package/lib/seams/guards.d.ts +14 -14
- package/lib/seams/keeper.d.ts +1 -1
- package/lib/seams/lifecycle.d.ts +2 -2
- package/lib/{settings-lt1i3OZv.js → settings-D8hlqxua.js} +49 -84
- package/lib/skills/resource-index.d.ts +1 -1
- package/lib/surface/faces.d.ts +1 -1
- package/lib/surface/http.d.ts +1 -1
- package/lib/surface/settings.d.ts +7 -13
- package/lib/surface/text.d.ts +72 -0
- package/lib/surface/usage.d.ts +2 -2
- package/lib/tools/config-docs.d.ts +53 -0
- package/lib/tools/container-admin.d.ts +19 -12
- package/lib/tools/msm/service.d.ts +7 -0
- package/lib/tools/trajectory-constraint-admin.d.ts +30 -0
- package/lib/{role/worker/idle.d.ts → trajectory/carrier.d.ts} +12 -2
- package/lib/trajectory/core.d.ts +1 -1
- package/lib/{role/worker → trajectory/delegate}/core.d.ts +7 -1
- package/lib/{role/worker → trajectory/delegate}/preset.d.ts +9 -2
- package/lib/trajectory/policy/profile.d.ts +122 -0
- package/lib/trajectory/policy/rules.d.ts +98 -0
- package/lib/{role/skiff/registry.d.ts → trajectory/policy/window.d.ts} +19 -13
- package/lib/trajectory/prompt.d.ts +12 -12
- package/lib/trajectory/rebuild.d.ts +2 -2
- package/lib/{role/skiff/core.d.ts → trajectory/session.d.ts} +58 -49
- package/package.json +3 -2
- package/lib/role/skiff/config.d.ts +0 -79
- package/lib/role/skiff/debug.d.ts +0 -88
- package/lib/role/skiff/profile.d.ts +0 -115
- package/lib/rolldown-runtime-8H4AJuhK.js +0 -14
- package/lib/tools/skiff-admin.d.ts +0 -30
package/lib/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
3
|
-
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, g as readUtf8, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-
|
|
4
|
-
import { A as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { a as pruneMissingBindings, c as setBindingStore, i as listBoundSessionIds, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-
|
|
1
|
+
import { i as __toESM, n as __exportAll } from "./rolldown-runtime-B4iAMlE-.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-B2t_aM8L.js";
|
|
3
|
+
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, g as readUtf8, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CyYJL4F2.js";
|
|
4
|
+
import { A as sessionsRoot, C as namingTitleFor, D as readTrajectoryUid, E as readActiveSessionMd, F as isoLocal, I as localFileStamp, L as localHuman, M as showSession, N as summarize, O as resolveSessionByTitle, P as useSession, R as localIdStamp, S as listSessions, T as parseTrajectoryCode, _ as extractSessionMdPathFromText, a as readTrajectoryConstraintAt, c as registerTrajectorySession$1, d as COGNITIVE_TRAJECTORY_ACTIONS, f as DEFAULT_SESSION_SCOPE, g as createSession, h as clearActiveSessionInfo, j as setActiveSessionInfo, k as sessionEvents$1, l as trajectoryDirNameForSession, m as archiveSession, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, p as UID_FILENAME, r as listTrajectoryConstraints, s as trajectoryConstraintForSession, t as TRAJECTORY_CONSTRAINT_FILENAME, u as unregisterTrajectorySession$1, v as findSession, w as parseSessionContextFromEvents, x as listActiveTrajectories, y as getActiveSessionInfo } from "./profile-BVt84mbi.js";
|
|
5
|
+
import { _ as runLocalStore, a as readFeishuCredential, d as weixinSessionIdForTrajectory, f as LOCALSTORE_SCOPES, g as readStore, h as readGitTrack, i as matchWeixinRoute, l as sanitizeFileName$1, m as localstorePath, n as extractWeixinText, o as readWeixinCredential, p as checkLocalstoreGitCompliance, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-COOFLTpU.js";
|
|
6
|
+
import { a as buildTrajectoryBasePrompt, c as createProfilePromptReader, d as resolveConstraintSystemPrompt, f as ACC_VERSION, i as TRAJECTORY_CONSTRAINT_HINT, l as profileMsmWhitelist, n as readSimpleSettings, o as constraintCognitiveTrajectory, p as findPackageRoot, r as registerSettingsSection, s as constraintSystemPromptSource, u as profileToolWhitelist } from "./settings-D8hlqxua.js";
|
|
7
|
+
import { a as pruneMissingBindings, c as setBindingStore, i as listBoundSessionIds, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-DCpoRHP3.js";
|
|
8
8
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
9
|
-
import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-
|
|
9
|
+
import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-BMk2w2KY.js";
|
|
10
|
+
import { a as registerImAccountImpl, i as normalizeAccountChannel, n as getImAccountImpl, r as imAccountChannels } from "./accounts-BHQbakUh.js";
|
|
10
11
|
import { createRequire } from "node:module";
|
|
11
12
|
import z from "@deepseek-ai/schemastery";
|
|
12
13
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -977,11 +978,11 @@ const gitTool = defineTool({
|
|
|
977
978
|
*
|
|
978
979
|
* 为什么单独一个模块(R↓):地址的读者是 `msm-ops.buildMsmEnv`(把地址注入 MSM 子进程 env),
|
|
979
980
|
* 写者才是 `weixin-send-api`(监听器启停)。若 msm-ops 直接 import weixin-send-api,
|
|
980
|
-
* 静态依赖链会拖进整条微信桥栈(weixin-bridge →
|
|
981
|
-
* 后果实测:`acc-extras` / `ops` / `
|
|
981
|
+
* 静态依赖链会拖进整条微信桥栈(weixin-bridge → trajectory-session → `@deepseek-ai/dsh-llm`)——
|
|
982
|
+
* 后果实测:`acc-extras` / `ops` / `trajectory-constraint-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
|
|
982
983
|
* (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
|
|
983
984
|
*
|
|
984
|
-
* 同款先例:`host/effect.ts`(拆卸登记)、`
|
|
985
|
+
* 同款先例:`host/effect.ts`(拆卸登记)、`trajectory/carrier.ts`(等待结算)。
|
|
985
986
|
*/
|
|
986
987
|
let endpoint = null;
|
|
987
988
|
/** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
|
|
@@ -1083,18 +1084,19 @@ Directory of ACC capabilities — each area lists where to go for details (guide
|
|
|
1083
1084
|
acc_msm 已并入 msm(执行+发现)与 container_admin(管理)——msm("<name>", ["<args>"]);inspect=true 查用法
|
|
1084
1085
|
cheap-worker(杂工 agent 编排——guide 子命令:cheap-worker guide)
|
|
1085
1086
|
专属工具(**默认对所有 CCC 隐藏**,只有在自己 exclusiveTools 里点名的 CCC 可见)→ acc-diag(运行态诊断)
|
|
1086
|
-
④
|
|
1087
|
-
|
|
1087
|
+
④ 轨迹约束与对外面 → container_admin config(轨迹约束四动作 guide/validate/apply/list——**原 role 域已并入 config**,旧名作过渡别名仍可用;说明类配置走**两层菜单** config docs 目录 / config docs <章节> 详情)
|
|
1088
|
+
外部程序化接入用官方 dsh --profile acp
|
|
1088
1089
|
+ 现成 ACP 客户端;本插件不提供 ACP 面
|
|
1089
1090
|
⑤ 自主与接入 → cognitive_trajectory send-now(跨轨迹的**即时**递送,**不排队**:live 目标在跑就当场注入当前轮、空闲则立即起轮;非 live 冷载入——等效于直接 wake;**有同步回执**)
|
|
1090
1091
|
cognitive_trajectory send-later(跨轨迹的"未来时刻 + 一条 message";fire-and-forget,无回执无回收)
|
|
1091
1092
|
weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:msm("weixin-doctor", ["guide"]))
|
|
1092
|
-
⑥ CCC 配置总览 → container_admin
|
|
1093
|
+
⑥ CCC 配置总览 → container_admin config docs config(按章详情——9 段全表)
|
|
1094
|
+
或 container_admin msm ccc-config(同一份正文的旧入口,保留)
|
|
1093
1095
|
⑦ 注册表与安全 → mech-registry.json 由 container_admin msm register/deregister 管理(写保护——不可直接编辑)
|
|
1094
1096
|
dashboard health 输出 registry 完整性检查;损坏恢复指引见 health registry.issues
|
|
1095
1097
|
CCC 级开关:exclusiveTools(声明专属工具)/ safeMode.blacklist(写路径黑名单)
|
|
1096
1098
|
/ safeMode.hiddenTools(**安全模式开启期间**额外隐藏的工具,v1.51.4)
|
|
1097
|
-
CCC 级 skill 供给:cognitiveTrajectory.skills(v1.44.0——**本 CCC 每条轨迹都带**,含
|
|
1099
|
+
CCC 级 skill 供给:cognitiveTrajectory.skills(v1.44.0——**本 CCC 每条轨迹都带**,含 轨迹约束会话;与轨迹自己的 frontmatter 声明取并集)
|
|
1098
1100
|
|
|
1099
1101
|
每区:一句话定位 + 详细入口(工具名 / guide 子命令)。详情永远以对应工具的 guide/ccc-config 为单一真相源。
|
|
1100
1102
|
`;
|
|
@@ -1151,7 +1153,12 @@ msm("<name>", ["--list"]) — list all MSMs
|
|
|
1151
1153
|
msm("<name>", ["--schema", "<n>"]) — view a specific MSM's parameter schema
|
|
1152
1154
|
msm("<name>", ["--format=json"]) — JSON output mode (remaining args passed through losslessly)
|
|
1153
1155
|
`;
|
|
1154
|
-
/**
|
|
1156
|
+
/**
|
|
1157
|
+
* CCC 配置参考(对齐 osp ccc-config action)。
|
|
1158
|
+
*
|
|
1159
|
+
* 🔴 **2026-10-19 起导出**:`container_admin config docs config` 的两层菜单把它作为
|
|
1160
|
+
* 「config」章正文返回。**不另写一份**——同一段说明有第二份副本就会漂(本轨反复踩过)。
|
|
1161
|
+
*/
|
|
1155
1162
|
const CCC_CONFIG_REFERENCE = `═══ CCC Configuration Reference ═══
|
|
1156
1163
|
|
|
1157
1164
|
CCC-level features are configured in .opencode/serenity.json.
|
|
@@ -1227,15 +1234,15 @@ safe-mode 由 WebUI 开关控制(写 .serenity-safe-on 标记);黑名单
|
|
|
1227
1234
|
Config:
|
|
1228
1235
|
{ "safeMode": { "blacklist": [".secrets/"], "hiddenTools": ["workflow", "job_kill"] } }
|
|
1229
1236
|
|
|
1230
|
-
── 6.
|
|
1231
|
-
|
|
1237
|
+
── 6. 轨迹约束.roles(F4 认知子集角色)──
|
|
1238
|
+
轨迹约束角色 = 全知全能 cognitive trajectory 的任意子集(CCC 定义)。每角色独立模型 + 双白名单
|
|
1232
1239
|
(MSM 白名单 msms[] 与非 MSM 工具白名单 tools[],白名单外全隐藏;skill 加载恒可用)。
|
|
1233
1240
|
cognitive trajectory 纪律子集(session/keeper/rebuild 参与项)默认全关 = 完全独立。
|
|
1234
1241
|
systemPrompt 内联 或 systemPromptFile(.md 引用,推荐)——角色会话的完整人格/边界提示词。
|
|
1235
|
-
validate 校验 / apply 生效 / list 查看:container_admin
|
|
1242
|
+
validate 校验 / apply 生效 / list 查看:container_admin config <guide|validate|apply|list>(原 role 域已并入 config,旧名作过渡别名仍可用)。
|
|
1236
1243
|
|
|
1237
1244
|
Config:
|
|
1238
|
-
{ "
|
|
1245
|
+
{ "轨迹约束": { "roles": {
|
|
1239
1246
|
"qa": {
|
|
1240
1247
|
"model": "provider/model",
|
|
1241
1248
|
"msms": ["web-search", "vlm-describe"],
|
|
@@ -1255,12 +1262,12 @@ CCC 的一条 cognitive trajectory = 持久身体(SESSION.md)+ 时间轴(A
|
|
|
1255
1262
|
运行态诊断(ACC 负责人专用):acc-diag(① live 运行态 / ①b 唤醒时钟 / ③ 唤醒注册表)。
|
|
1256
1263
|
|
|
1257
1264
|
▸ cognitiveTrajectory.skills(v1.44.0,**CCC 级 skill 供给**;owner 令 2026-09-20「要求实现我的诉求……
|
|
1258
|
-
注入是动态注入就行;create
|
|
1265
|
+
注入是动态注入就行;create 先不触发吧;**轨迹约束也支持**」):
|
|
1259
1266
|
**本 CCC 的每条轨迹都带这些 skill**——声明的 skill 全文在该轨迹**被绑定期间**注入系统提示词
|
|
1260
1267
|
(**动态注入**:每请求重新求值、不缓存 ⇒ 改配置或改 frontmatter **即时生效**,无需重新 use、无需重启)。
|
|
1261
1268
|
两条声明来源**并集**(不是二选一):本键 **cognitiveTrajectory.skills**(容器给的底座,**在前**)
|
|
1262
1269
|
+ 轨迹自己 SESSION.md 顶部 frontmatter 的 skills:(这条轨迹额外的,**追加**;同名去重)。
|
|
1263
|
-
✅ **含
|
|
1270
|
+
✅ **含 轨迹约束角色会话**(旁路的是 ACC 身份与纪律;skill 是"工作资料",照给)。
|
|
1264
1271
|
守卫:名字先过安全校验才用于拼路径(防穿越)|**合并后**统一 32 KB 上限截断|找不到响亮写 [缺失]。
|
|
1265
1272
|
⚠️ 与 create **无关**(create 刻意不夺绑定,U6)⇒ 注入的门是"**存在绑定**"(cognitive_trajectory use)。
|
|
1266
1273
|
失败语义:未配置 / 字段缺失 / 配置损坏 ⇒ **空数组**(= 无 CCC 级供给;轨迹级声明照旧生效)。
|
|
@@ -1268,7 +1275,7 @@ CCC 的一条 cognitive trajectory = 持久身体(SESSION.md)+ 时间轴(A
|
|
|
1268
1275
|
|
|
1269
1276
|
Config:
|
|
1270
1277
|
{ "cognitiveTrajectory": {
|
|
1271
|
-
"skills": ["acc-session", "cce"], // v1.44.0:本 CCC 的每条轨迹都带(含
|
|
1278
|
+
"skills": ["acc-session", "cce"], // v1.44.0:本 CCC 的每条轨迹都带(含 轨迹约束会话)
|
|
1272
1279
|
"autopilot": { // 原 autopilotTrajectory 段,字段不变(旧键仍回退读)
|
|
1273
1280
|
"enabled": true,
|
|
1274
1281
|
"intervalHours": 2,
|
|
@@ -1282,7 +1289,7 @@ CCC 的一条 cognitive trajectory = 持久身体(SESSION.md)+ 时间轴(A
|
|
|
1282
1289
|
CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC 独立对接微信桥。
|
|
1283
1290
|
账号/路由/开关在此文件;**bot_token 凭据在 CCC localstore credential scope**
|
|
1284
1291
|
(扫码绑定后自动写入,永不进 git 明文面)。
|
|
1285
|
-
路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC
|
|
1292
|
+
路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC 轨迹约束.roles。
|
|
1286
1293
|
面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"|"guide"]) 排查 + 大而全指南。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
|
|
1287
1294
|
▸ 主动发送(v1.31.0:**ACC 工具 im-bridge**):调
|
|
1288
1295
|
im-bridge({channel:"weixin", action:"send", user:"<别名|id>", text:"<文本>"}) 给用户发消息——
|
|
@@ -1290,7 +1297,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1290
1297
|
成功后触发 outgoing hook(source="proactive")。文件用 action:"send-file" + file:"<CCC 内路径>"
|
|
1291
1298
|
(逃逸/缺失/超 20MB 拒绝)+ 可选 caption。**可见性**:本 CCC 配置了 IM 通道
|
|
1292
1299
|
(weixin.enabled=true)才出现在模型工具清单里;未配置 → 由 guards 逐 agent
|
|
1293
|
-
tools.restrict({deny:['im-bridge']}) 移除(不是"看得到但被拒"
|
|
1300
|
+
tools.restrict({deny:['im-bridge']}) 移除(不是"看得到但被拒")。轨迹约束角色另需在
|
|
1294
1301
|
roles.<role>.tools 白名单列出 im-bridge。
|
|
1295
1302
|
历史(v1.30.9~v1.30.x):CCC 侧 MSM weixin-send(走 loopback HTTP 入口 3082)——
|
|
1296
1303
|
v1.31.0 已退役(脚本删除 + 注册表注销);3082 入口保留给**非 agent 调用者**。
|
|
@@ -1834,7 +1841,7 @@ async function runMsmAsync(root, args) {
|
|
|
1834
1841
|
* **轨迹提示词** = 一条认知轨迹的**可编辑章程声明**,由 ACC 按固定顺序装配进系统提示词。
|
|
1835
1842
|
* 三个关键词各是一条判据:
|
|
1836
1843
|
* - **可编辑**:住在 CCC(盘上),改它**不发版**(D23 归属判据的直接推论)
|
|
1837
|
-
* - **声明**:它声明**身份/章程/方法论**,**不声明能力**(能力面归 `
|
|
1844
|
+
* - **声明**:它声明**身份/章程/方法论**,**不声明能力**(能力面归 `trajectory-constraint.json` 白名单与工具 schema)
|
|
1838
1845
|
* - **固定顺序**:装配顺序由 ACC 定死(见 `seams/system-prompt.ts` 的 ⑧ 位)
|
|
1839
1846
|
*
|
|
1840
1847
|
* ## 为什么取代「人格」
|
|
@@ -1849,13 +1856,13 @@ async function runMsmAsync(root, args) {
|
|
|
1849
1856
|
*
|
|
1850
1857
|
* - **跟着轨迹走**:`archive` 时整个目录搬走,章程不会掉队(旧形态 `.opencode/skiff/<name>.md`
|
|
1851
1858
|
* 与轨迹**分居两处** —— 多条轨迹共用一份文件正是那个后果)。
|
|
1852
|
-
* - **每条轨迹都能有**:不只是
|
|
1853
|
-
* - 与 `SESSION.md`(身体:延续/日志)、`
|
|
1859
|
+
* - **每条轨迹都能有**:不只是 轨迹约束。普通轨迹同样需要"我是谁、按什么章程"。
|
|
1860
|
+
* - 与 `SESSION.md`(身体:延续/日志)、`trajectory-constraint.json`(约束面:能力)三件并列,
|
|
1854
1861
|
* 各管一件事,不合并(合并会让"改能力"与"改身份"互相牵连)。
|
|
1855
1862
|
*
|
|
1856
1863
|
* ## 兼容(不静默坏)
|
|
1857
1864
|
*
|
|
1858
|
-
* `PROMPT.md`
|
|
1865
|
+
* `PROMPT.md` 不存在时**不报错**(普通轨迹常态)。轨迹约束的旧字段 `systemPromptFile`
|
|
1859
1866
|
* 在兼容期仍被读取——优先级见 {@link resolveTrajectoryPrompt}。
|
|
1860
1867
|
*/
|
|
1861
1868
|
/** 轨迹提示词的固定文件名(**判据就是它存在**——不是目录名前缀,也不是配置里的一行) */
|
|
@@ -1867,9 +1874,9 @@ function trajectoryPromptPath(root, dirName) {
|
|
|
1867
1874
|
/**
|
|
1868
1875
|
* 该轨迹**有没有**轨迹提示词(单一判据)。
|
|
1869
1876
|
*
|
|
1870
|
-
* 用途:兼容期的"让位"判定 —— 若已有 `PROMPT.md
|
|
1871
|
-
* 必须**让位**,否则同一份章程会在两处各注入一遍(第 ⑧ 段 + `serenity
|
|
1872
|
-
* 🔴 判据只有这一处:别在
|
|
1877
|
+
* 用途:兼容期的"让位"判定 —— 若已有 `PROMPT.md`,轨迹约束 旧字段 `systemPromptFile`
|
|
1878
|
+
* 必须**让位**,否则同一份章程会在两处各注入一遍(第 ⑧ 段 + `serenity-轨迹约束` 段)。
|
|
1879
|
+
* 🔴 判据只有这一处:别在 轨迹约束那边自己拼路径再判(两处会漂)。
|
|
1873
1880
|
*/
|
|
1874
1881
|
function hasTrajectoryPrompt(root, dirName) {
|
|
1875
1882
|
try {
|
|
@@ -1958,7 +1965,7 @@ function trajectoryPromptBlock(text) {
|
|
|
1958
1965
|
].join("\n");
|
|
1959
1966
|
}
|
|
1960
1967
|
//#endregion
|
|
1961
|
-
//#region src/
|
|
1968
|
+
//#region src/trajectory/carrier.ts
|
|
1962
1969
|
/**
|
|
1963
1970
|
* 等待 agent 空闲或销毁。
|
|
1964
1971
|
* @param ctx 宿主插件上下文(事件通道来源;**结构化访问**,故类型为 `unknown`,见文件头 I2 段)
|
|
@@ -2105,7 +2112,7 @@ function truncateContent(content, maxChars) {
|
|
|
2105
2112
|
* 六条硬性规则(逐条对应设计 §4.2,改这里先改那里):
|
|
2106
2113
|
* ① **只枚举,不注入正文** —— 索引里**绝不**出现任何参考文件的正文;
|
|
2107
2114
|
* ② **确定性排序** = 相对路径的 **UTF-8 字节序**(🔴 **不用 mtime**:mtime 随检出漂移,
|
|
2108
|
-
* 会让每请求的注入文本抖动;对照 `src/
|
|
2115
|
+
* 会让每请求的注入文本抖动;对照 `src/cro/core.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
|
|
2109
2116
|
* **同一件事两处的取舍相反,那个差异是判据不是巧合**);
|
|
2110
2117
|
* ③ **有界 + 响亮** —— 条数/字符各有上限,超出**必须显式写出还剩多少项**,**绝不静默截断**;
|
|
2111
2118
|
* ④ **摘要可复现** —— 取该文件首个 `#` 标题(剥 markdown 记号);取不到 ⇒ 留空,**不编**;
|
|
@@ -2600,7 +2607,7 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
|
|
|
2600
2607
|
* ⇒ 本模块让每个 CCC 留下一份**纯计数**账本:**用了哪个、多少次、第一次和最近一次是什么时候**。
|
|
2601
2608
|
* 用途 = 清理/精简的**依据**(没有人用的东西与天天在用的东西,不再靠猜)。
|
|
2602
2609
|
*
|
|
2603
|
-
* ## 🔴 铁律(与 `
|
|
2610
|
+
* ## 🔴 铁律(与 `cro/log.ts` 同族)
|
|
2604
2611
|
*
|
|
2605
2612
|
* **本模块每个导出函数都不抛错**:读盘 / 解析 / 写盘失败一律返回结构化失败,
|
|
2606
2613
|
* 由调用方只记一行日志(或什么都不做)⇒ **统计的问题绝不影响工具执行与既有链路**。
|
|
@@ -2616,7 +2623,7 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
|
|
|
2616
2623
|
*
|
|
2617
2624
|
* 宿主会被重启(本容器实测 24h 内 3 次)⇒ 计数**不能只活在内存里**:
|
|
2618
2625
|
* 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake/registry.ts` /
|
|
2619
|
-
* `
|
|
2626
|
+
* `cro/log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
|
|
2620
2627
|
*
|
|
2621
2628
|
* ## 口径(两个字段,勿合并)
|
|
2622
2629
|
*
|
|
@@ -2899,6 +2906,48 @@ function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
|
|
|
2899
2906
|
return null;
|
|
2900
2907
|
}
|
|
2901
2908
|
//#endregion
|
|
2909
|
+
//#region src/kernel/ports.ts
|
|
2910
|
+
/**
|
|
2911
|
+
* 集中端口表。
|
|
2912
|
+
* A=宿主主面(插件不监听)/ B=网关(可配,默认关)/ C=微信发送(默认开,仅 loopback)
|
|
2913
|
+
* / D=轨迹约束调试(默认关)。
|
|
2914
|
+
*/
|
|
2915
|
+
const FACE_PORTS = {
|
|
2916
|
+
main: {
|
|
2917
|
+
port: 3080,
|
|
2918
|
+
host: null,
|
|
2919
|
+
defaultEnabled: true,
|
|
2920
|
+
meaning: "宿主 DSH 主 WebUI + 插件 /serenity/* 路由面(**插件不起此 listener**,端口自宿主读)"
|
|
2921
|
+
},
|
|
2922
|
+
gateway: {
|
|
2923
|
+
port: 3081,
|
|
2924
|
+
host: "0.0.0.0",
|
|
2925
|
+
defaultEnabled: false,
|
|
2926
|
+
meaning: "双端口网关(外部登录 + 全量反代宿主主面;host/port 走 plugin 全局配置)"
|
|
2927
|
+
},
|
|
2928
|
+
weixinSend: {
|
|
2929
|
+
port: 3082,
|
|
2930
|
+
host: "127.0.0.1",
|
|
2931
|
+
defaultEnabled: true,
|
|
2932
|
+
meaning: "微信主动发送面(容器外进程 → POST /send;与 3080 显式解耦,不经网关)"
|
|
2933
|
+
}
|
|
2934
|
+
};
|
|
2935
|
+
FACE_PORTS.main.port;
|
|
2936
|
+
/** 网关端口默认值(面 B) */
|
|
2937
|
+
const GATEWAY_PORT = FACE_PORTS.gateway.port;
|
|
2938
|
+
/** 微信主动发送端口默认值(面 C) */
|
|
2939
|
+
const WEIXIN_SEND_PORT = FACE_PORTS.weixinSend.port;
|
|
2940
|
+
/**
|
|
2941
|
+
* **机制端口**词表(output-guard 用:agent 在**用户可见输出**里提及内部端口即打回)。
|
|
2942
|
+
*
|
|
2943
|
+
* 由 {@link FACE_PORTS} 机械派生 ⇒ 与面清单**永不漂移**。
|
|
2944
|
+
* ⚠️ 修复记录(C4 块 B,2026-09-15):旧常量硬写 `['3080','3081','3099','3100']`
|
|
2945
|
+
* ——**漏 3082**(微信发送面,默认开)⇒ 守卫拦不住"3082"这类提及。现由表派生,四面齐全。
|
|
2946
|
+
* ⚠️ 面变更记录(2026-10-15):**面 E(3100 ACP/问答)已整体退役**(owner 裁「砍掉外部问答」)
|
|
2947
|
+
* ⇒ 本词表随之少一项,**无需另改**(这正是"派生而非手抄"的收益)。
|
|
2948
|
+
*/
|
|
2949
|
+
const MECHANISM_PORTS = Object.keys(FACE_PORTS).map((id) => String(FACE_PORTS[id].port));
|
|
2950
|
+
//#endregion
|
|
2902
2951
|
//#region src/kernel/config.ts
|
|
2903
2952
|
/**
|
|
2904
2953
|
* kernel/config.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
|
|
@@ -3221,7 +3270,7 @@ function toolsBlock() {
|
|
|
3221
3270
|
" 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",
|
|
3222
3271
|
" 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",
|
|
3223
3272
|
" localstore — ACC local credential/config storage (CCC-root localstore.json; git policy localstore.gitTrack default deny)",
|
|
3224
|
-
" container_admin — container administration (the maintenance bay): role (
|
|
3273
|
+
" container_admin — container administration (the maintenance bay): role (轨迹约束 roles: guide/validate/apply/list) / msm (register/deregister/check/guide/catalog/ccc-config) / config (view)",
|
|
3225
3274
|
"",
|
|
3226
3275
|
"MSM call (registered CCC MSMs, deterministic Mech & Semi-Mech):",
|
|
3227
3276
|
" Execute: msm(\"<name>\", [\"<arg1>\", \"<arg2>\"]) — run a registered MSM directly (partial name returns matching candidates)",
|
|
@@ -3721,7 +3770,7 @@ function registerEntrySkillSection(agent, root) {
|
|
|
3721
3770
|
*
|
|
3722
3771
|
* **声明来源(两条,并集)**:
|
|
3723
3772
|
* · **CCC 级**(v1.44.0,owner 令)= `.opencode/serenity.json` 的 `cognitiveTrajectory.skills`
|
|
3724
|
-
* ——**本 CCC 的每条轨迹都带**(含
|
|
3773
|
+
* ——**本 CCC 的每条轨迹都带**(含 轨迹约束角色会话),是容器给的底座;
|
|
3725
3774
|
* · **轨迹级** = 该会话绑定的轨迹(`readLastBound(session).mdPath` = 它的 `SESSION.md`)
|
|
3726
3775
|
* 顶部 frontmatter 的 `skills:`——这条轨迹额外的。
|
|
3727
3776
|
* 合并规则与顺序见 `cognitive-trajectory-skills.mergeSkillNames`(CCC 级在前,同名去重)。
|
|
@@ -3768,13 +3817,19 @@ function registerCognitiveTrajectorySkillSection(agent, root) {
|
|
|
3768
3817
|
}
|
|
3769
3818
|
}
|
|
3770
3819
|
//#endregion
|
|
3771
|
-
//#region src/
|
|
3820
|
+
//#region src/trajectory/delegate/core.ts
|
|
3772
3821
|
/**
|
|
3773
|
-
*
|
|
3822
|
+
* trajectory/delegate/core.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
|
|
3774
3823
|
*
|
|
3775
3824
|
* 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
|
|
3776
3825
|
* 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
|
|
3777
3826
|
* 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
|
|
3827
|
+
*
|
|
3828
|
+
* ## S14(2026-10-19,轨迹中心重构):此前位于 `role/worker/` 下(该目录已消失)
|
|
3829
|
+
*
|
|
3830
|
+
* 落点 = `trajectory/delegate/`。判据(判读表 §3.1):它是**轨迹 × 轨迹**那种合法形态 ——
|
|
3831
|
+
* 主语是「主轨迹把活交给子 agent」,既不是某一条轨迹的切面,也不是容器宪法。
|
|
3832
|
+
* ⚠️ 它**不属于** `policy/`:policy 管"这条轨迹允许什么",本模块管"一条轨迹怎么把活派出去"。
|
|
3778
3833
|
*/
|
|
3779
3834
|
/** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
|
|
3780
3835
|
function splitModel(model) {
|
|
@@ -3910,42 +3965,38 @@ Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up c
|
|
|
3910
3965
|
(and "output" for whatever partial text was produced).
|
|
3911
3966
|
`;
|
|
3912
3967
|
//#endregion
|
|
3913
|
-
//#region src/
|
|
3914
|
-
/** `provider/model` → agentOptions(由
|
|
3915
|
-
const
|
|
3916
|
-
/** 查 sessionId 的会话绑定(轨迹 + ccc;无 → null)——追问校验 */
|
|
3917
|
-
function skiffSessionInfo(sessionId) {
|
|
3918
|
-
return skiffSessionInfo$1(sessionId);
|
|
3919
|
-
}
|
|
3968
|
+
//#region src/trajectory/session.ts
|
|
3969
|
+
/** `provider/model` → agentOptions(由 delegate/core 提供,保持单一实现) */
|
|
3970
|
+
const trajectoryAgents = /* @__PURE__ */ new Map();
|
|
3920
3971
|
/** 查 sessionId 的活体 agent(进程内会话延续用;未注册/已清理 → undefined) */
|
|
3921
|
-
function
|
|
3922
|
-
return
|
|
3972
|
+
function getTrajectoryAgent(sessionId) {
|
|
3973
|
+
return trajectoryAgents.get(sessionId);
|
|
3923
3974
|
}
|
|
3924
|
-
/** 该会话当前绑定的
|
|
3925
|
-
function
|
|
3975
|
+
/** 该会话当前绑定的 轨迹约束面(不是 轨迹约束 → null)。**守门与提示词的唯一查询入口。** */
|
|
3976
|
+
function trajectoryConstraintFor(root, sessionId) {
|
|
3926
3977
|
if (sessionId === void 0) return null;
|
|
3927
|
-
return
|
|
3978
|
+
return trajectoryConstraintForSession(root, sessionId, trajectoryDirNameForSession(sessionId));
|
|
3928
3979
|
}
|
|
3929
|
-
function
|
|
3930
|
-
|
|
3931
|
-
|
|
3980
|
+
function registerTrajectorySession(sessionId, trajectoryDirName, ccc, agent) {
|
|
3981
|
+
trajectoryAgents.set(sessionId, agent);
|
|
3982
|
+
registerTrajectorySession$1(sessionId, trajectoryDirName, ccc);
|
|
3932
3983
|
}
|
|
3933
|
-
function
|
|
3934
|
-
|
|
3935
|
-
|
|
3984
|
+
function unregisterTrajectorySession(sessionId) {
|
|
3985
|
+
trajectoryAgents.delete(sessionId);
|
|
3986
|
+
unregisterTrajectorySession$1(sessionId);
|
|
3936
3987
|
}
|
|
3937
3988
|
/**
|
|
3938
|
-
* 该
|
|
3989
|
+
* 该 轨迹约束会话的**工作台路径** = 它自己轨迹的 SESSION.md。
|
|
3939
3990
|
*
|
|
3940
|
-
* 🔴 与旧版的差别:旧版会在这里**自动新建**一个 SESSION(`
|
|
3941
|
-
* 新版**没有新建这个动作** —— 轨迹就是工作台,`
|
|
3991
|
+
* 🔴 与旧版的差别:旧版会在这里**自动新建**一个 SESSION(`ensureTrajectorySession`);
|
|
3992
|
+
* 新版**没有新建这个动作** —— 轨迹就是工作台,`trajectory-constraint.json` 与 `SESSION.md` 同处一个目录。
|
|
3942
3993
|
* ⇒ 语义变成纯粹的"查询:我自己的身体在哪"。
|
|
3943
3994
|
*
|
|
3944
|
-
* @returns SESSION.md 绝对路径;不是
|
|
3995
|
+
* @returns SESSION.md 绝对路径;不是 轨迹约束 / 文件不在 → null
|
|
3945
3996
|
*/
|
|
3946
|
-
function
|
|
3997
|
+
function trajectoryWorkspaceMdPath(root, sessionId) {
|
|
3947
3998
|
if (sessionId === void 0) return null;
|
|
3948
|
-
const dirName =
|
|
3999
|
+
const dirName = trajectoryDirNameForSession(sessionId);
|
|
3949
4000
|
if (dirName === null) return null;
|
|
3950
4001
|
const md = join(sessionsRoot(root), dirName, "SESSION.md");
|
|
3951
4002
|
return existsSync(md) ? md : null;
|
|
@@ -3953,12 +4004,12 @@ function skiffWorkspaceMdPath(root, sessionId) {
|
|
|
3953
4004
|
/**
|
|
3954
4005
|
* 系统提示词中的**工作台纪律块**(S142 用户:只给路径 LLM 不会主动用)。
|
|
3955
4006
|
*
|
|
3956
|
-
* SESSION = 工作台 ——
|
|
4007
|
+
* SESSION = 工作台 —— 轨迹约束与主舱同一套机制(**零特调**,用户拍板):
|
|
3957
4008
|
* SESSION.md 是持久记忆载体、rebuild 自动从本 SESSION 续接。
|
|
3958
4009
|
* 注入 agent 系统提示词(用户对话面不可见)。
|
|
3959
4010
|
*
|
|
3960
4011
|
* v1.30.13(S142 诊断 D5):写入纪律**不点名具体工具** —— 旧文案写死 "with write/edit",
|
|
3961
|
-
* 而某个
|
|
4012
|
+
* 而某个 轨迹约束的工具白名单里根本没有 write/edit ⇒ 指令指向它没有的工具
|
|
3962
4013
|
* (注入纪律与能力面矛盾,模型只能猜)。现文案改为「用本轨迹被授权的写入通道」。
|
|
3963
4014
|
*/
|
|
3964
4015
|
function workspaceCognitiveTrajectoryLine(mdPath) {
|
|
@@ -3986,10 +4037,10 @@ const workspaceSectionAgents = /* @__PURE__ */ new WeakSet();
|
|
|
3986
4037
|
/**
|
|
3987
4038
|
* 把**工作台纪律块**挂到 agent 的系统提示词段(单一注入点)。
|
|
3988
4039
|
*
|
|
3989
|
-
* 用户原话(S142,2026-09-08):"
|
|
3990
|
-
* 这样就重复,能否微信桥情况下,注入内容直接取
|
|
4040
|
+
* 用户原话(S142,2026-09-08):"微信桥的注入机制,每个用户消息都会注入,轨迹约束本身也会注入,
|
|
4041
|
+
* 这样就重复,能否微信桥情况下,注入内容直接取 轨迹约束的,这样不用配两遍"。
|
|
3991
4042
|
*
|
|
3992
|
-
* 现形态:工作台行**只**经本函数挂系统提示词段(name `serenity
|
|
4043
|
+
* 现形态:工作台行**只**经本函数挂系统提示词段(name `serenity-轨迹约束-workspace`,
|
|
3993
4044
|
* 动态 `text()` 每轮按 scope 读当前活跃 mdPath)。
|
|
3994
4045
|
*
|
|
3995
4046
|
* @returns true = 段已在位;false = 该 agent 挂不上(调用方降级为 question 前缀注入)
|
|
@@ -4001,39 +4052,39 @@ function ensureWorkspacePromptSection(agent, scope, root) {
|
|
|
4001
4052
|
if (typeof api?.section !== "function") return false;
|
|
4002
4053
|
try {
|
|
4003
4054
|
api.section({
|
|
4004
|
-
name: "serenity
|
|
4055
|
+
name: "serenity-轨迹约束-workspace",
|
|
4005
4056
|
order: -55,
|
|
4006
4057
|
text: () => {
|
|
4007
|
-
const mdPath =
|
|
4058
|
+
const mdPath = trajectoryWorkspaceMdPath(root, scope);
|
|
4008
4059
|
return mdPath ? workspaceCognitiveTrajectoryLine(mdPath) : "";
|
|
4009
4060
|
}
|
|
4010
4061
|
});
|
|
4011
4062
|
workspaceSectionAgents.add(agent);
|
|
4012
4063
|
return true;
|
|
4013
4064
|
} catch (err) {
|
|
4014
|
-
console.warn(`[serenity-hooks]
|
|
4065
|
+
console.warn(`[serenity-hooks] 轨迹约束 工作台提示词段注册失败(回退 question 注入): ${String(err?.message ?? err)}`);
|
|
4015
4066
|
return false;
|
|
4016
4067
|
}
|
|
4017
4068
|
}
|
|
4018
|
-
/**
|
|
4019
|
-
const
|
|
4069
|
+
/** 轨迹约束 agent 挂载的 DSH preset(平台工具面由 preset 决定;白名单再按轨迹过滤) */
|
|
4070
|
+
const TRAJECTORY_PRESET = "standard";
|
|
4020
4071
|
/**
|
|
4021
|
-
* 创建(或复用)一个
|
|
4072
|
+
* 创建(或复用)一个 轨迹约束会话。
|
|
4022
4073
|
*
|
|
4023
4074
|
* **固定 sessionId 的 resume-or-create**(v1.27.2):指定 sessionId 且磁盘已有持久化 log →
|
|
4024
4075
|
* `ctx.agents.resume`(DSH 持久化语义:sessionId 即身份,历史 + turn 续号延续);
|
|
4025
4076
|
* 无 log(首次)→ `ctx.agents.create`。不指定 → 随机 id 恒 create(调试页路径)。
|
|
4026
4077
|
*
|
|
4027
|
-
* @param profile 该
|
|
4078
|
+
* @param profile 该 轨迹约束的约束面(来自 `trajectory-constraint.json`)
|
|
4028
4079
|
* @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现"轨迹 ↔ 会话"长期映射)
|
|
4029
4080
|
*/
|
|
4030
|
-
async function
|
|
4031
|
-
if (!ctx.agents) throw new Error("
|
|
4081
|
+
async function createTrajectoryAgent(ctx, root, profile, defaultModel, sessionId) {
|
|
4082
|
+
if (!ctx.agents) throw new Error("轨迹约束: ctx.agents unavailable");
|
|
4032
4083
|
const model = profile.model?.trim() || defaultModel || "";
|
|
4033
|
-
const id = sessionId ??
|
|
4084
|
+
const id = sessionId ?? `轨迹约束-${profile.code}-${randomUUID()}`;
|
|
4034
4085
|
const setup = async (agentCtx) => {
|
|
4035
4086
|
try {
|
|
4036
|
-
await agentCtx.get("agentPresets")?.mount?.(agentCtx,
|
|
4087
|
+
await agentCtx.get("agentPresets")?.mount?.(agentCtx, TRAJECTORY_PRESET);
|
|
4037
4088
|
} catch {}
|
|
4038
4089
|
};
|
|
4039
4090
|
const handle = await createOrResumeAgent(ctx, id, root, model, setup, sessionId !== void 0);
|
|
@@ -4043,7 +4094,7 @@ async function createSkiffAgent(ctx, root, profile, defaultModel, sessionId) {
|
|
|
4043
4094
|
const livePrompt = () => {
|
|
4044
4095
|
let live = profile;
|
|
4045
4096
|
try {
|
|
4046
|
-
const fresh =
|
|
4097
|
+
const fresh = readTrajectoryConstraintAt(join(sessionsRoot(root), profile.dirName), root);
|
|
4047
4098
|
if (fresh) live = fresh;
|
|
4048
4099
|
} catch {}
|
|
4049
4100
|
const wantPath = live.systemPromptFile?.trim();
|
|
@@ -4056,23 +4107,23 @@ async function createSkiffAgent(ctx, root, profile, defaultModel, sessionId) {
|
|
|
4056
4107
|
livePrompt();
|
|
4057
4108
|
try {
|
|
4058
4109
|
agent.ctx.systemPrompt.section({
|
|
4059
|
-
name: "serenity
|
|
4110
|
+
name: "serenity-轨迹约束",
|
|
4060
4111
|
order: -60,
|
|
4061
4112
|
text: () => {
|
|
4062
4113
|
const legacy = hasTrajectoryPrompt(root, profile.dirName) ? "" : livePrompt();
|
|
4063
|
-
return [
|
|
4114
|
+
return [buildTrajectoryBasePrompt(profile.code), legacy].filter(Boolean).join("\n");
|
|
4064
4115
|
}
|
|
4065
4116
|
});
|
|
4066
4117
|
} catch (err) {
|
|
4067
|
-
console.warn(`[serenity-hooks]
|
|
4118
|
+
console.warn(`[serenity-hooks] 轨迹约束 系统提示词注册失败: ${String(err?.message ?? err)}`);
|
|
4068
4119
|
}
|
|
4069
4120
|
if (agent?.session) try {
|
|
4070
4121
|
registerCognitiveTrajectorySkillSection(agent, root);
|
|
4071
4122
|
} catch (err) {
|
|
4072
|
-
console.warn(`[serenity-hooks]
|
|
4123
|
+
console.warn(`[serenity-hooks] 轨迹约束轨迹 skill 注入注册失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
|
|
4073
4124
|
}
|
|
4074
|
-
if (
|
|
4075
|
-
|
|
4125
|
+
if (trajectoryWorkspaceMdPath(root, id) !== null) ensureWorkspacePromptSection(agent, id, root);
|
|
4126
|
+
registerTrajectorySession(id, profile.dirName, root, agent);
|
|
4076
4127
|
return {
|
|
4077
4128
|
handle,
|
|
4078
4129
|
agent,
|
|
@@ -4111,7 +4162,7 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
|
|
|
4111
4162
|
sessionId: id,
|
|
4112
4163
|
meta: {
|
|
4113
4164
|
cwd: root,
|
|
4114
|
-
agentPreset:
|
|
4165
|
+
agentPreset: TRAJECTORY_PRESET
|
|
4115
4166
|
},
|
|
4116
4167
|
setup,
|
|
4117
4168
|
...model ? { agentOptions: splitModel(model) } : {}
|
|
@@ -4126,7 +4177,7 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
|
|
|
4126
4177
|
sessionId: id,
|
|
4127
4178
|
meta: {
|
|
4128
4179
|
cwd: root,
|
|
4129
|
-
agentPreset:
|
|
4180
|
+
agentPreset: TRAJECTORY_PRESET
|
|
4130
4181
|
},
|
|
4131
4182
|
setup,
|
|
4132
4183
|
...model ? { agentOptions: splitModel(model) } : {}
|
|
@@ -4147,14 +4198,14 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
|
|
|
4147
4198
|
if (liveNow) return liveReuseRef(liveNow, setup);
|
|
4148
4199
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4149
4200
|
const stack = err instanceof Error ? err.stack ?? "" : "";
|
|
4150
|
-
console.log(`[serenity-hooks]
|
|
4151
|
-
if (stack) console.log(`[serenity-hooks]
|
|
4201
|
+
console.log(`[serenity-hooks] 轨迹约束 resume 失败降级 create (id=${id}): ${msg}`);
|
|
4202
|
+
if (stack) console.log(`[serenity-hooks] 轨迹约束 resume stack:\n${stack.slice(0, 1200)}`);
|
|
4152
4203
|
return {
|
|
4153
4204
|
...await ctx.agents.create({
|
|
4154
4205
|
sessionId: id,
|
|
4155
4206
|
meta: {
|
|
4156
4207
|
cwd: root,
|
|
4157
|
-
agentPreset:
|
|
4208
|
+
agentPreset: TRAJECTORY_PRESET
|
|
4158
4209
|
},
|
|
4159
4210
|
setup,
|
|
4160
4211
|
...model ? { agentOptions: splitModel(model) } : {}
|
|
@@ -4218,7 +4269,7 @@ async function waitForTurnAssistant(ctx, agent, beforeAssistantCount, opts = {})
|
|
|
4218
4269
|
*
|
|
4219
4270
|
* @param options.timeoutMs 本轮等待上限(测试注入;生产用缺省)
|
|
4220
4271
|
*/
|
|
4221
|
-
async function
|
|
4272
|
+
async function askTrajectory(ctx, agent, question, options) {
|
|
4222
4273
|
const beforeAssistant = countAssistantMessages(agent.session);
|
|
4223
4274
|
agent.followup(createUserMessage({
|
|
4224
4275
|
content: [{
|
|
@@ -4233,29 +4284,29 @@ async function askSkiff(ctx, agent, question, options) {
|
|
|
4233
4284
|
};
|
|
4234
4285
|
}
|
|
4235
4286
|
/**
|
|
4236
|
-
*
|
|
4237
|
-
*
|
|
4287
|
+
* 轨迹约束会话的轨迹纪律参与判定:**不是 轨迹约束恒 true**(正常参与);
|
|
4288
|
+
* 轨迹约束 按 `trajectory-constraint.json` 的 `cognitiveTrajectory` 子集决定。
|
|
4238
4289
|
*
|
|
4239
|
-
* 注册表缺失(进程刚重启、绑定还没读)⇒ 由 `
|
|
4240
|
-
* 两者都不中 ⇒ 判"不是
|
|
4290
|
+
* 注册表缺失(进程刚重启、绑定还没读)⇒ 由 `trajectoryConstraintForSession` 回落持久绑定;
|
|
4291
|
+
* 两者都不中 ⇒ 判"不是 轨迹约束" ⇒ 恒 true(正常参与)。**fail-open 是有意的**:
|
|
4241
4292
|
* 纪律参与项的失败不该让会话失去 keeper/rebuild(那是能力倒退,比多跑一次更糟)。
|
|
4242
4293
|
*/
|
|
4243
|
-
function
|
|
4294
|
+
function trajectoryConstraintParticipates(root, sessionId, key) {
|
|
4244
4295
|
if (sessionId === void 0) return true;
|
|
4245
|
-
const profile =
|
|
4296
|
+
const profile = trajectoryConstraintFor(root, sessionId);
|
|
4246
4297
|
if (profile === null) return true;
|
|
4247
|
-
return
|
|
4298
|
+
return constraintCognitiveTrajectory(profile)[key];
|
|
4248
4299
|
}
|
|
4249
4300
|
/**
|
|
4250
|
-
* `msm` 工具的
|
|
4301
|
+
* `msm` 工具的 轨迹约束门控:非 轨迹约束恒放行;轨迹约束会话——
|
|
4251
4302
|
* exec 非白名单 MSM 拒绝(**不列名单**)、register/deregister 必拒、
|
|
4252
4303
|
* list 白名单过滤、check/guide/ccc-config 只读放行。
|
|
4253
4304
|
*/
|
|
4254
|
-
function
|
|
4305
|
+
function trajectoryConstraintMsmGate(root, sessionId, action, name) {
|
|
4255
4306
|
if (sessionId === void 0) return {};
|
|
4256
|
-
const profile =
|
|
4307
|
+
const profile = trajectoryConstraintFor(root, sessionId);
|
|
4257
4308
|
if (profile === null) return {};
|
|
4258
|
-
if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in
|
|
4309
|
+
if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in 轨迹约束 sessions" };
|
|
4259
4310
|
if (action === "exec") {
|
|
4260
4311
|
if (!name || !profile.msms.includes(name)) return { reject: "MSM not allowed" };
|
|
4261
4312
|
return {};
|
|
@@ -4342,7 +4393,7 @@ const msmTool = defineTool({
|
|
|
4342
4393
|
if (!root) throw new Error(NO_CCC_FROM_AGENT_CWD);
|
|
4343
4394
|
const sessionId = agentSessionId$1(exec);
|
|
4344
4395
|
const name = args.name;
|
|
4345
|
-
const gate =
|
|
4396
|
+
const gate = trajectoryConstraintMsmGate(root, sessionId, name ? "exec" : "list", name);
|
|
4346
4397
|
if (gate.reject) throw new Error(gate.reject);
|
|
4347
4398
|
if (!name || name.trim() === "") return gate.whitelist ? "MSMs allowed in this role:\n" + [...gate.whitelist].join("\n") : buildMsmIndex(root);
|
|
4348
4399
|
const clean = name.trim();
|
|
@@ -10043,9 +10094,9 @@ function listWakes(root, opts = {}) {
|
|
|
10043
10094
|
};
|
|
10044
10095
|
}
|
|
10045
10096
|
//#endregion
|
|
10046
|
-
//#region src/
|
|
10097
|
+
//#region src/cro/core.ts
|
|
10047
10098
|
/**
|
|
10048
|
-
*
|
|
10099
|
+
* cro/core.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
|
|
10049
10100
|
*
|
|
10050
10101
|
* ## 这东西用来干什么(白话)
|
|
10051
10102
|
*
|
|
@@ -10375,7 +10426,7 @@ function parseCroHostBinding(raw) {
|
|
|
10375
10426
|
* `AGENT_SESSIONS/` 下每个目录查一次 `<目录>/continuous-re-occurrence.ts`。
|
|
10376
10427
|
* · **一次 readdir + 每个目录一次 existsSync**:N 条轨迹的代价是 O(N) 次 `stat`,
|
|
10377
10428
|
* 每 5min 一次 —— 与既有 `listSessions` 同量级,可接受。
|
|
10378
|
-
* · 跳过 **`_` 前缀**(`_archived` / `
|
|
10429
|
+
* · 跳过 **`_` 前缀**(`_archived` / `_轨迹约束-logs` / `_weixin-logs` = 系统与日志目录)
|
|
10379
10430
|
* 与 **`.` 前缀**(隐藏)——它们**不是轨迹**,且扫它们纯属浪费。
|
|
10380
10431
|
* · 🔴 **本函数绝不抛错**(返回 `[]`)——它跑在调度器 tick 内,任何异常都可能影响既有链路
|
|
10381
10432
|
* (设计 §5 铁律)。目录读不到(不存在 / 权限)⇒ `[]` = "没有轨迹启用 CRO",语义正确。
|
|
@@ -10669,9 +10720,9 @@ function registerDisposer(ctx, label, cleanup) {
|
|
|
10669
10720
|
}
|
|
10670
10721
|
}
|
|
10671
10722
|
//#endregion
|
|
10672
|
-
//#region src/
|
|
10723
|
+
//#region src/cro/turns.ts
|
|
10673
10724
|
/**
|
|
10674
|
-
*
|
|
10725
|
+
* cro/turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
|
|
10675
10726
|
*
|
|
10676
10727
|
* ## 为什么需要这个模块(CRO 唯一需要 ACC 新增的状态)
|
|
10677
10728
|
*
|
|
@@ -10797,7 +10848,7 @@ function runningCarriersOf(boundSessionIds, nowMs, ttlMs = TURN_TTL_MS) {
|
|
|
10797
10848
|
* 🔴 **本处顺带更正 §2.7 的一处分档**:本文件原被列在"剩余 3 个属**真依赖**档"里,
|
|
10798
10849
|
* 但**逐符号查证后与那个判断不符** ——
|
|
10799
10850
|
* · `Context`:`ctx` 只被**结构化访问**(原码本就写着 `ctx as unknown as { on: … }`)
|
|
10800
|
-
* ⇒ 该注解未提供任何类型保证(与 `
|
|
10851
|
+
* ⇒ 该注解未提供任何类型保证(与 `trajectory/carrier.ts` 同档)。
|
|
10801
10852
|
* · `Agent`:**导入后从未使用** —— 事件 payload 一律按 `{ agent?: unknown }` 解构,
|
|
10802
10853
|
* 再交给 `sessionIdOf(...)`(它收 `unknown`)。⇒ **纯粹是死 import**。
|
|
10803
10854
|
* ⇒ **判据(本件再证一次,且比上一件更硬)**:分档**必须逐符号看"它有没有参与类型检查"**,
|
|
@@ -10845,9 +10896,9 @@ function registerCroTurnTracking(ctx) {
|
|
|
10845
10896
|
});
|
|
10846
10897
|
}
|
|
10847
10898
|
//#endregion
|
|
10848
|
-
//#region src/
|
|
10899
|
+
//#region src/cro/log.ts
|
|
10849
10900
|
/**
|
|
10850
|
-
*
|
|
10901
|
+
* cro/log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
|
|
10851
10902
|
*
|
|
10852
10903
|
* ## 所有者令(本模块的存在理由)
|
|
10853
10904
|
*
|
|
@@ -10883,7 +10934,7 @@ function registerCroTurnTracking(ctx) {
|
|
|
10883
10934
|
* ⇒ 清理与写入**是同一笔操作** ⇒ **结构上不存在"忘了清"的可能**(比"另设一个每 tick 的清理步"更硬:
|
|
10884
10935
|
* 那多一个"可能忘了跑"的面)。窗宽是**常量**,**无配置键、无人工清理动作**。
|
|
10885
10936
|
*
|
|
10886
|
-
* ## 🔴 铁律(与 `
|
|
10937
|
+
* ## 🔴 铁律(与 `cro/core.ts` 同族)
|
|
10887
10938
|
*
|
|
10888
10939
|
* **本模块的每个导出函数都不抛错**:写盘 / 裁剪 / 解析失败一律返回结构化失败,
|
|
10889
10940
|
* 由调用方只记一行 tick 日志 ⇒ **CRO 流水的问题绝不影响投递与既有链路**(设计 §5)。
|
|
@@ -12006,9 +12057,9 @@ function registerWakeScheduler(ctx) {
|
|
|
12006
12057
|
registerDisposer(ctx, "认知轨迹唤醒调度器", () => clock.dispose());
|
|
12007
12058
|
}
|
|
12008
12059
|
//#endregion
|
|
12009
|
-
//#region src/
|
|
12060
|
+
//#region src/cro/guide.ts
|
|
12010
12061
|
/**
|
|
12011
|
-
*
|
|
12062
|
+
* cro/guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
|
|
12012
12063
|
*
|
|
12013
12064
|
* ## 为什么指南在 ACC 而不在 CCC(所有者令)
|
|
12014
12065
|
*
|
|
@@ -12771,20 +12822,20 @@ const localstoreTool = defineTool({
|
|
|
12771
12822
|
}
|
|
12772
12823
|
});
|
|
12773
12824
|
//#endregion
|
|
12774
|
-
//#region src/tools/
|
|
12775
|
-
/**
|
|
12776
|
-
const
|
|
12825
|
+
//#region src/tools/trajectory-constraint-admin.ts
|
|
12826
|
+
/** 轨迹约束 定义教程:概念 / `trajectory-constraint.json` schema / 双白名单 / 轨迹纪律 / 微信路由 */
|
|
12827
|
+
const TRAJECTORY_CONSTRAINT_GUIDE = `═══ 轨迹约束 Guide (2026-10-19 redesigned) ═══
|
|
12777
12828
|
|
|
12778
|
-
**A
|
|
12829
|
+
**A 轨迹约束 IS a cognitive trajectory — the one whose directory contains \`${TRAJECTORY_CONSTRAINT_FILENAME}\`.**
|
|
12779
12830
|
That is the whole rule. No role table, no name registry: put the file there and the trajectory
|
|
12780
|
-
becomes a
|
|
12831
|
+
becomes a 轨迹约束; delete it and it goes back to being an ordinary trajectory (takes effect at once).
|
|
12781
12832
|
|
|
12782
12833
|
AGENT_SESSIONS/<YYYY-MM-DD--<code>--<desc>>/
|
|
12783
12834
|
├─ SESSION.md ← its persistent body (this IS the workspace — nothing is auto-created)
|
|
12784
|
-
├─ ${
|
|
12835
|
+
├─ ${TRAJECTORY_CONSTRAINT_FILENAME} ← its constraints (this file is what makes it a 轨迹约束)
|
|
12785
12836
|
└─ TRAJECTORY-UID ← legacy, read-only (superseded by the code)
|
|
12786
12837
|
|
|
12787
|
-
── ${
|
|
12838
|
+
── ${TRAJECTORY_CONSTRAINT_FILENAME} schema ──
|
|
12788
12839
|
{
|
|
12789
12840
|
"model": "provider/model", // optional; falls back to cheap-worker.defaultModel
|
|
12790
12841
|
"tools": ["read","grep","msm",...], // non-MSM toolbar whitelist — anything not listed is REMOVED from the schema
|
|
@@ -12795,35 +12846,34 @@ becomes a skiff; delete it and it goes back to being an ordinary trajectory (tak
|
|
|
12795
12846
|
}
|
|
12796
12847
|
|
|
12797
12848
|
── Two whitelists, both default-deny ──
|
|
12798
|
-
- \`tools: []\` + non-empty \`msms\` = pure-MSM
|
|
12849
|
+
- \`tools: []\` + non-empty \`msms\` = pure-MSM 轨迹约束 (the usual knowledge-QA shape)
|
|
12799
12850
|
- non-empty \`msms\` ⇒ the \`msm\` tool is added automatically (the only entry point for MSMs)
|
|
12800
12851
|
- anything outside both lists is invisible in the schema, not merely rejected at call time
|
|
12801
12852
|
|
|
12802
12853
|
── cognitiveTrajectory: why the defaults are TRUE ──
|
|
12803
|
-
The workspace of a
|
|
12854
|
+
The workspace of a 轨迹约束 is its own SESSION.md. Setting these to false would cut it off from
|
|
12804
12855
|
its own body, so \`session\`/\`keeper\`/\`rebuild\` all default to true. Set false explicitly only
|
|
12805
12856
|
when you deliberately want that participation off.
|
|
12806
12857
|
|
|
12807
12858
|
── WeChat routing: user → trajectory code ──
|
|
12808
12859
|
\`weixin.routes\`: \`[{ "user": "<wx id or *>", "trajectory": "<code>" }]\`
|
|
12809
12860
|
A route pointing at a code with no such trajectory is a hard error (the message is NOT delivered).
|
|
12810
|
-
A trajectory WITHOUT \`${
|
|
12861
|
+
A trajectory WITHOUT \`${TRAJECTORY_CONSTRAINT_FILENAME}\` is still a legal target — it is just an ordinary trajectory.
|
|
12811
12862
|
|
|
12812
12863
|
── Operating it ──
|
|
12813
|
-
|
|
12814
|
-
Everything is read live: saving \`${SKIFF_PROFILE_FILENAME}\` takes effect on the next turn, no restart.
|
|
12864
|
+
Everything is read live: saving \`${TRAJECTORY_CONSTRAINT_FILENAME}\` takes effect on the next turn, no restart.
|
|
12815
12865
|
(The prompt FILE hot-reloads by mtime; the JSON itself is re-read every turn.)
|
|
12816
|
-
After editing, run container_admin
|
|
12817
|
-
Skill loading is always available to
|
|
12818
|
-
/** 校验当前 CCC 的
|
|
12819
|
-
function
|
|
12820
|
-
const profiles =
|
|
12866
|
+
After editing, run container_admin config apply for an explicit validation + confirmation pass.
|
|
12867
|
+
Skill loading is always available to 轨迹约束 sessions (skills are not whitelisted — they are read-only knowledge).`;
|
|
12868
|
+
/** 校验当前 CCC 的 轨迹约束面:msms 均已注册 / model ∈ cheap-worker.models / 提示词非空 */
|
|
12869
|
+
function validateTrajectoryConstraint(root) {
|
|
12870
|
+
const profiles = listTrajectoryConstraints(root);
|
|
12821
12871
|
const issues = [];
|
|
12822
12872
|
if (profiles.length === 0) return {
|
|
12823
12873
|
ok: true,
|
|
12824
12874
|
issues: [],
|
|
12825
|
-
|
|
12826
|
-
note: `no
|
|
12875
|
+
constraintCount: 0,
|
|
12876
|
+
note: `no 轨迹约束 trajectories in this CCC (no trajectory has ${TRAJECTORY_CONSTRAINT_FILENAME} — zero impact)`
|
|
12827
12877
|
};
|
|
12828
12878
|
const registered = new Set(loadMsmEntries(root).map((e) => e.name));
|
|
12829
12879
|
const hc = readCheapWorkerConfig(root);
|
|
@@ -12831,7 +12881,7 @@ function validateSkiffConfig(root) {
|
|
|
12831
12881
|
for (const p of profiles) {
|
|
12832
12882
|
if (!(hasTrajectoryPrompt(root, p.dirName) || (() => {
|
|
12833
12883
|
try {
|
|
12834
|
-
return
|
|
12884
|
+
return resolveConstraintSystemPrompt(root, p).trim() !== "";
|
|
12835
12885
|
} catch (err) {
|
|
12836
12886
|
issues.push(`trajectory "${p.code}": ${String(err?.message ?? err)}`);
|
|
12837
12887
|
return true;
|
|
@@ -12843,15 +12893,15 @@ function validateSkiffConfig(root) {
|
|
|
12843
12893
|
return {
|
|
12844
12894
|
ok: issues.length === 0,
|
|
12845
12895
|
issues,
|
|
12846
|
-
|
|
12896
|
+
constraintCount: profiles.length
|
|
12847
12897
|
};
|
|
12848
12898
|
}
|
|
12849
|
-
/** 列出当前 CCC
|
|
12850
|
-
function
|
|
12851
|
-
const profiles =
|
|
12899
|
+
/** 列出当前 CCC 的轨迹约束(编码 / 模型 / msms / tools / 纪律摘要 / 提示词来源) */
|
|
12900
|
+
function listTrajectoryConstraintSummary(root) {
|
|
12901
|
+
const profiles = listTrajectoryConstraints(root);
|
|
12852
12902
|
if (profiles.length === 0) return {
|
|
12853
12903
|
roles: [],
|
|
12854
|
-
note: `no
|
|
12904
|
+
note: `no 轨迹约束 trajectories in this CCC (no trajectory has ${TRAJECTORY_CONSTRAINT_FILENAME})`
|
|
12855
12905
|
};
|
|
12856
12906
|
const items = profiles.map((p) => ({
|
|
12857
12907
|
name: p.code,
|
|
@@ -12859,8 +12909,8 @@ function listSkiffRoles(root) {
|
|
|
12859
12909
|
model: p.model ?? "(cheap-worker default)",
|
|
12860
12910
|
msms: p.msms,
|
|
12861
12911
|
tools: p.tools,
|
|
12862
|
-
cognitiveTrajectory:
|
|
12863
|
-
promptSource:
|
|
12912
|
+
cognitiveTrajectory: constraintCognitiveTrajectory(p),
|
|
12913
|
+
promptSource: constraintSystemPromptSource(p, hasTrajectoryPrompt(root, p.dirName)),
|
|
12864
12914
|
...p.systemPromptFile ? { systemPromptFile: p.systemPromptFile } : {}
|
|
12865
12915
|
}));
|
|
12866
12916
|
return {
|
|
@@ -12869,51 +12919,91 @@ function listSkiffRoles(root) {
|
|
|
12869
12919
|
};
|
|
12870
12920
|
}
|
|
12871
12921
|
/**
|
|
12872
|
-
* 应用当前
|
|
12922
|
+
* 应用当前 轨迹约束 配置(S142 用户:改了 json 应该有个生效机制)。
|
|
12873
12923
|
*
|
|
12874
12924
|
* 语义:约束面是**每轮实时读取**的 ⇒ apply 不是"推送",而是**显式校验 + 应用确认**:
|
|
12875
12925
|
* ① 校验合法(复用 validate:msms 注册 / model 白名单 / 提示词非空)
|
|
12876
12926
|
* ② 非法 → 报告问题清单,不应用(提示修复后重跑 apply)
|
|
12877
12927
|
* ③ 合法 → 应用确认:绑定 CCC + 轨迹清单 + 说明(下一轮即生效)
|
|
12878
12928
|
*/
|
|
12879
|
-
function
|
|
12880
|
-
const validation =
|
|
12881
|
-
const profiles =
|
|
12929
|
+
function applyTrajectoryConstraint(root) {
|
|
12930
|
+
const validation = validateTrajectoryConstraint(root);
|
|
12931
|
+
const profiles = listTrajectoryConstraints(root);
|
|
12882
12932
|
if (!validation.ok) return {
|
|
12883
12933
|
applied: false,
|
|
12884
12934
|
cccRoot: root,
|
|
12885
|
-
|
|
12935
|
+
constraintCount: profiles.length,
|
|
12886
12936
|
issues: validation.issues,
|
|
12887
|
-
hint: "fix the issues above, then run container_admin
|
|
12937
|
+
hint: "fix the issues above, then run container_admin config apply again"
|
|
12888
12938
|
};
|
|
12889
12939
|
return {
|
|
12890
12940
|
applied: true,
|
|
12891
12941
|
cccRoot: root,
|
|
12892
|
-
|
|
12942
|
+
constraintCount: profiles.length,
|
|
12893
12943
|
roles: profiles.map((p) => p.code),
|
|
12894
|
-
note: `constraints are read live from each trajectory's ${
|
|
12944
|
+
note: `constraints are read live from each trajectory's ${TRAJECTORY_CONSTRAINT_FILENAME} — they take effect on the next turn (no restart); ${TRAJECTORY_CONSTRAINT_HINT}`
|
|
12895
12945
|
};
|
|
12896
12946
|
}
|
|
12897
12947
|
//#endregion
|
|
12948
|
+
//#region src/tools/config-docs.ts
|
|
12949
|
+
/**
|
|
12950
|
+
* 渲染**目录**(第一层)。
|
|
12951
|
+
*
|
|
12952
|
+
* 🔴 只调 `summary`,**绝不调 `render`** —— 目录若触发正文,就退化成"整份长文"。
|
|
12953
|
+
* 这条不是性能优化,是**判据**:它保证 `docs` 的输出大小与章数成正比,与正文长度无关。
|
|
12954
|
+
*/
|
|
12955
|
+
function renderDocsIndex(chapters) {
|
|
12956
|
+
const lines = [
|
|
12957
|
+
"═══ CCC 配置说明 · 目录 ═══",
|
|
12958
|
+
"",
|
|
12959
|
+
"说明类内容已超过单次返回的合适篇幅 ⇒ 先读目录,再按章取详情。",
|
|
12960
|
+
"",
|
|
12961
|
+
"用法:container_admin config docs <章节>",
|
|
12962
|
+
""
|
|
12963
|
+
];
|
|
12964
|
+
for (const ch of chapters) lines.push(` ${ch.id.padEnd(14)}${ch.summary}`);
|
|
12965
|
+
lines.push("");
|
|
12966
|
+
lines.push("相关:container_admin msm catalog(工具/能力目录——与本章录互指,不互相复制)");
|
|
12967
|
+
return lines.join("\n");
|
|
12968
|
+
}
|
|
12969
|
+
/**
|
|
12970
|
+
* 解析一个章节参数(第二层)。
|
|
12971
|
+
*
|
|
12972
|
+
* 不存在 ⇒ 返回 `null`,由调用方给出**带候选清单**的拒绝 ——
|
|
12973
|
+
* "不知道这个章节" 与 "这一章是空的" 必须可区分(否则用户以为功能没有)。
|
|
12974
|
+
*/
|
|
12975
|
+
function findDocsChapter(chapters, id) {
|
|
12976
|
+
const want = id.trim().toLowerCase();
|
|
12977
|
+
if (want === "") return null;
|
|
12978
|
+
return chapters.find((c) => c.id.toLowerCase() === want) ?? null;
|
|
12979
|
+
}
|
|
12980
|
+
//#endregion
|
|
12898
12981
|
//#region src/tools/container-admin.ts
|
|
12899
12982
|
/**
|
|
12900
12983
|
* container-admin.ts — container_admin(v1.30:机务舱——容器管理面统一入口)
|
|
12901
12984
|
*
|
|
12902
|
-
*
|
|
12903
|
-
* · skiff_admin(F4 Skiff 认知子集角色:guide/validate/apply/list)
|
|
12904
|
-
* · acc_msm 管理面(register/deregister/check/guide/catalog/ccc-config)
|
|
12905
|
-
* —— msm 工具只管执行+发现;一切「管理容器」的操作归本工具
|
|
12985
|
+
* ## 域结构(2026-10-19 owner 两条令后:**两域**)
|
|
12906
12986
|
*
|
|
12907
|
-
*
|
|
12987
|
+
* | 域 | 管什么 |
|
|
12988
|
+
* |---|---|
|
|
12989
|
+
* | `config` | **本 CCC 的全部配置面**:说明目录/按章详情 + 轨迹约束(原 `role` 域,已并入) |
|
|
12990
|
+
* | `msm` | MSM 注册表管理 + 手册(register/deregister/check/guide/catalog/ccc-config) |
|
|
12991
|
+
*
|
|
12992
|
+
* 🔴 **`role` 域已并入 `config`**(owner 2026-10-19 裁「1 并入」)。为什么不是三个平行域:
|
|
12993
|
+
* `role` 管的本来就是"某条轨迹的约束面"——**一组配置**,与 `config` 是同一抽象层的两半。
|
|
12994
|
+
* 分着放的代价是:用户在 `config` 里看不到"轨迹怎么约束",得先知道 `role` 这个词存在。
|
|
12995
|
+
* ⚠️ **旧 `role` 域仍被接受**(过渡别名,指向同一实现)——存量 CCC 的文档/脚本里到处是
|
|
12996
|
+
* `container_admin role validate`(实测本仓 30 处),硬切会让它们全部变成"未知域"。
|
|
12997
|
+
* 别名**只保留读取路径**:schema 的 `enum` 只列新名,文案只教新名。
|
|
12998
|
+
*
|
|
12999
|
+
* ## 三个动作名为什么是这三个(P3 的实现形态)
|
|
13000
|
+
* - `config docs`(无参)→ **使用说明目录**(第一层;只索引不复制)
|
|
13001
|
+
* - `config docs <chapter>` → **使用说明详情**(第二层;按章获得)
|
|
13002
|
+
* - `config view` → 配置**现值**(原来是唯一动作;现在是"看当前填了什么")
|
|
13003
|
+
*
|
|
13004
|
+
* 归属(D23):**机制在 ACC**(有哪几章、怎么路由、怎么校验),**内容在 CCC**(填什么)。
|
|
13005
|
+
* 语义(用户拍板"轨迹约束_admin 及系列 admin 合并成 container_admin"):
|
|
12908
13006
|
* container = CCC(具体认知容器);admin = 管理操作。
|
|
12909
|
-
* 类比 The Manifest(清单维护——工具注册表)+ Crew(角色管理)+ 机务(配置/手册)。
|
|
12910
|
-
*
|
|
12911
|
-
* 参数极简(LLM 直觉):domain(管理域)+ action(该域子命令)+ 少量业务参数。
|
|
12912
|
-
*
|
|
12913
|
-
* v1.34.1(⑥ C6a):由 `export const containerAdminTool` 改为**工厂**
|
|
12914
|
-
* `createContainerAdminTool(ctx)`——原为 autopilot 域传进程内运行态事实(全局闸 / live 会话 /
|
|
12915
|
-
* agent 可解析性)。该域已于 2026-09-15 随 ACC 侧 autopilot 整段退场删除(所有者裁决 (a)):
|
|
12916
|
-
* `ctx` 现**无消费者**,工厂形态保留只是为了让装配点签名不再变动(见 index.ts 注册处)。
|
|
12917
13007
|
*/
|
|
12918
13008
|
function agentSessionId(exec) {
|
|
12919
13009
|
return exec.agent?.session?.id ?? "";
|
|
@@ -12924,9 +13014,13 @@ function renderText$1(value) {
|
|
|
12924
13014
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
12925
13015
|
}];
|
|
12926
13016
|
}
|
|
13017
|
+
/** 过渡别名表:旧域 → 新域(只读兼容;文案不教它) */
|
|
13018
|
+
const DOMAIN_ALIASES = { role: "config" };
|
|
12927
13019
|
/** 合法 domain 名(拒绝文案与 schema enum 同源——不得各写一份) */
|
|
12928
13020
|
const DOMAIN_NAMES = Object.keys({
|
|
12929
|
-
|
|
13021
|
+
config: [
|
|
13022
|
+
"docs",
|
|
13023
|
+
"view",
|
|
12930
13024
|
"guide",
|
|
12931
13025
|
"validate",
|
|
12932
13026
|
"apply",
|
|
@@ -12939,23 +13033,43 @@ const DOMAIN_NAMES = Object.keys({
|
|
|
12939
13033
|
"guide",
|
|
12940
13034
|
"catalog",
|
|
12941
13035
|
"ccc-config"
|
|
12942
|
-
]
|
|
12943
|
-
config: ["view"]
|
|
13036
|
+
]
|
|
12944
13037
|
});
|
|
13038
|
+
/**
|
|
13039
|
+
* 说明目录的章节(P3 两层菜单的**唯一注册处**)。
|
|
13040
|
+
*
|
|
13041
|
+
* 🔴 加一章 = 在这里加一项(`render` 惰性求值)。**正文不复制**:
|
|
13042
|
+
* 每章的真相源就在它自己的函数里(`CCC_CONFIG_REFERENCE` / `TRAJECTORY_CONSTRAINT_GUIDE`)——
|
|
13043
|
+
* 本表只做**切分与路由**,避免"同一段说明有第二份会漂的副本"。
|
|
13044
|
+
*/
|
|
13045
|
+
const DOCS_CHAPTERS = [{
|
|
13046
|
+
id: "config",
|
|
13047
|
+
summary: "CCC 配置全表——9 段各是什么、怎么填、填错会怎样(.opencode/serenity.json)",
|
|
13048
|
+
render: () => CCC_CONFIG_REFERENCE
|
|
13049
|
+
}, {
|
|
13050
|
+
id: "trajectory-constraint",
|
|
13051
|
+
summary: "轨迹约束:一条轨迹怎么被约束(trajectory-constraint.json 的 schema + 双白名单)",
|
|
13052
|
+
render: () => TRAJECTORY_CONSTRAINT_GUIDE
|
|
13053
|
+
}];
|
|
12945
13054
|
function createContainerAdminTool(ctx) {
|
|
12946
13055
|
return defineTool({
|
|
12947
13056
|
name: "container_admin",
|
|
12948
|
-
description: "Container administration (the ship's maintenance bay — one entry for managing the CCC):
|
|
13057
|
+
description: "Container administration (the ship's maintenance bay — one entry for managing the CCC): config — CCC configuration: docs (usage-manual index) / docs <chapter> (one chapter in full) / view (current values) + 轨迹约束 (guide/validate/apply/list); msm — MSM registry management (register/deregister/check quality DC-M1~M4) + manuals (guide dev manual / catalog ACC capability directory / ccc-config CCC configuration reference). Execution of registered MSMs belongs to the msm tool; this tool manages the container.",
|
|
12949
13058
|
parameters: {
|
|
12950
13059
|
domain: {
|
|
12951
13060
|
type: "string",
|
|
12952
13061
|
enum: DOMAIN_NAMES,
|
|
12953
13062
|
required: true,
|
|
12954
|
-
description: "Management domain:
|
|
13063
|
+
description: "Management domain: config (CCC configuration + 轨迹约束) / msm (MSM registry + manuals)"
|
|
12955
13064
|
},
|
|
12956
13065
|
action: {
|
|
12957
13066
|
type: "string",
|
|
12958
|
-
description: "Domain subcommand
|
|
13067
|
+
description: "Domain subcommand. config: docs / view / guide / validate / apply / list; msm: register / deregister / check / guide / catalog / ccc-config"
|
|
13068
|
+
},
|
|
13069
|
+
/** 第二层菜单参数:`config docs <chapter>`(缺省 = 目录) */
|
|
13070
|
+
chapter: {
|
|
13071
|
+
type: "string",
|
|
13072
|
+
description: "config docs only: chapter id (omit for the index; e.g. config / trajectory-constraint)"
|
|
12959
13073
|
},
|
|
12960
13074
|
name: {
|
|
12961
13075
|
type: "string",
|
|
@@ -12994,18 +13108,35 @@ function createContainerAdminTool(ctx) {
|
|
|
12994
13108
|
const root = cccRootForExec(exec);
|
|
12995
13109
|
if (!root) throw new Error(NO_CCC_FROM_AGENT_CWD);
|
|
12996
13110
|
const sessionId = agentSessionId(exec);
|
|
12997
|
-
const
|
|
13111
|
+
const rawDomain = String(args.domain ?? "");
|
|
13112
|
+
const domain = DOMAIN_ALIASES[rawDomain] ?? rawDomain;
|
|
12998
13113
|
const action = args.action ?? "";
|
|
13114
|
+
if (!DOMAIN_NAMES.includes(domain)) throw new Error(`container_admin requires domain: ${DOMAIN_NAMES.join(" | ")}`);
|
|
12999
13115
|
if (domain === "msm" && (action === "register" || action === "deregister")) {
|
|
13000
|
-
const gate =
|
|
13116
|
+
const gate = trajectoryConstraintMsmGate(root, sessionId, action, args.name);
|
|
13001
13117
|
if (gate.reject) throw new Error(gate.reject);
|
|
13002
13118
|
}
|
|
13003
|
-
if (domain === "
|
|
13004
|
-
case "
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13119
|
+
if (domain === "config") switch (action) {
|
|
13120
|
+
case "docs": {
|
|
13121
|
+
const chapter = args.chapter?.trim() ?? "";
|
|
13122
|
+
if (chapter === "") return {
|
|
13123
|
+
docs: renderDocsIndex(DOCS_CHAPTERS),
|
|
13124
|
+
chapters: DOCS_CHAPTERS.map((c) => c.id)
|
|
13125
|
+
};
|
|
13126
|
+
const found = findDocsChapter(DOCS_CHAPTERS, chapter);
|
|
13127
|
+
if (found === null) throw new Error(`container_admin config docs: unknown chapter "${chapter}". Available: ${DOCS_CHAPTERS.map((c) => c.id).join(" | ")} (omit the chapter for the index)`);
|
|
13128
|
+
return {
|
|
13129
|
+
chapter: found.id,
|
|
13130
|
+
docs: found.render()
|
|
13131
|
+
};
|
|
13132
|
+
}
|
|
13133
|
+
case "view":
|
|
13134
|
+
case "": return { config: runMsm(root, { action: "ccc-config" }) };
|
|
13135
|
+
case "guide": return { guide: TRAJECTORY_CONSTRAINT_GUIDE };
|
|
13136
|
+
case "validate": return validateTrajectoryConstraint(root);
|
|
13137
|
+
case "apply": return applyTrajectoryConstraint(root);
|
|
13138
|
+
case "list": return listTrajectoryConstraintSummary(root);
|
|
13139
|
+
default: throw new Error(`container_admin config requires action: docs | view | guide | validate | apply | list`);
|
|
13009
13140
|
}
|
|
13010
13141
|
if (domain === "msm") {
|
|
13011
13142
|
const mgmtActions = [
|
|
@@ -13028,7 +13159,6 @@ function createContainerAdminTool(ctx) {
|
|
|
13028
13159
|
usage: args.usage
|
|
13029
13160
|
});
|
|
13030
13161
|
}
|
|
13031
|
-
if (domain === "config") return { config: runMsm(root, { action: "ccc-config" }) };
|
|
13032
13162
|
throw new Error(`container_admin requires domain: ${DOMAIN_NAMES.join(" | ")}`);
|
|
13033
13163
|
}
|
|
13034
13164
|
});
|
|
@@ -13280,7 +13410,7 @@ const SENSITIVE_CREDENTIAL_FILES = /* @__PURE__ */ new Set(["localstore.json"]);
|
|
|
13280
13410
|
* MSM 注册表写保护(需求⑤b S142 用户拍板:"msm注册的文件需要写保护起来,避免CCC意外写坏搞崩自己"):
|
|
13281
13411
|
* mech-registry.json 是 CCC 执行层地基(坏 → loadMsmEntries 抛 → container_admin/output-guard 全崩,
|
|
13282
13412
|
* register 也崩 → 自锁)。**写 deny、读 allow**(与 localstore 读 deny 语义区分 R6——注册表是
|
|
13283
|
-
* 结构核心不是秘密,需被 output-guard/
|
|
13413
|
+
* 结构核心不是秘密,需被 output-guard/trajectory-constraint-admin 读取建 MSM 词表)。
|
|
13284
13414
|
*
|
|
13285
13415
|
* 单级化(需求⑤a)后**唯一合法注册表 = cccName 聚合档**
|
|
13286
13416
|
* `.opencode/skills/<cccName>/references/mech-registry.json`(cccName = .serenity 首行)。
|
|
@@ -13318,15 +13448,15 @@ function isWriteTool(toolName, action) {
|
|
|
13318
13448
|
*/
|
|
13319
13449
|
function decideGuard(input) {
|
|
13320
13450
|
const { root, toolName, safeModeOn, blacklist, pathArg, action } = input;
|
|
13321
|
-
if (input.
|
|
13322
|
-
const profile = input.
|
|
13451
|
+
if (input.trajectoryConstraint !== void 0 && input.trajectoryConstraint !== null) {
|
|
13452
|
+
const profile = input.trajectoryConstraint;
|
|
13323
13453
|
if (toolName === "skill") return { kind: "allow" };
|
|
13324
13454
|
if (profile.msms.includes(toolName)) return {
|
|
13325
13455
|
deny: `"${toolName}" is a registered MSM, not a direct tool in this trajectory — call it through the msm tool: msm("${toolName}", ["<args>"]) (pass "--help" or inspect=true for usage)`,
|
|
13326
13456
|
kind: "deny"
|
|
13327
13457
|
};
|
|
13328
13458
|
if (!profileToolWhitelist(profile).has(toolName)) return {
|
|
13329
|
-
deny: "tool not allowed in this
|
|
13459
|
+
deny: "tool not allowed in this 轨迹约束 trajectory",
|
|
13330
13460
|
kind: "deny"
|
|
13331
13461
|
};
|
|
13332
13462
|
}
|
|
@@ -13611,17 +13741,17 @@ const exclusiveRestrictions = /* @__PURE__ */ new Map();
|
|
|
13611
13741
|
* 每次 pre-step 都重算白名单,只有**算出来的结果变了**才重新发 restrict
|
|
13612
13742
|
* —— 配置不变就不重复发(restrict 会重建 schema,没必要每步都做)。
|
|
13613
13743
|
*/
|
|
13614
|
-
const
|
|
13744
|
+
const trajectoryConstraintRestrictions = /* @__PURE__ */ new Map();
|
|
13615
13745
|
/**
|
|
13616
|
-
* 同步
|
|
13746
|
+
* 同步 **轨迹约束会话的工具面**:把白名单之外的工具**从 schema 移除**(不是"看得到但被拒")。
|
|
13617
13747
|
*
|
|
13618
13748
|
* ## 为什么必须有这条(owner 2026-10-19:「工具清单也要热加载」)
|
|
13619
13749
|
*
|
|
13620
|
-
*
|
|
13750
|
+
* 在它之前,轨迹约束白名单**只由运行时守卫拦截**(`decideGuard` 的 轨迹约束 分支):
|
|
13621
13751
|
* 工具全部出现在模型 schema 里,调用时才被打回。⇒ 两个真实后果:
|
|
13622
13752
|
* ① **模型看得见它不该有的工具** —— 它会去试,试了才撞墙(一轮白烧,且错误文案泛化,
|
|
13623
13753
|
* 看不出"是不在清单里"还是"临时性拒绝");
|
|
13624
|
-
* ② **配置变动在 schema 上永远不生效** —— 改 `
|
|
13754
|
+
* ② **配置变动在 schema 上永远不生效** —— 改 `trajectory-constraint.json` 的 `tools` 只影响"调用是否放行"。
|
|
13625
13755
|
*
|
|
13626
13756
|
* ## 判据与降级
|
|
13627
13757
|
*
|
|
@@ -13629,24 +13759,24 @@ const skiffRoleRestrictions = /* @__PURE__ */ new Map();
|
|
|
13629
13759
|
* 不去动宿主的工具 —— 宿主的工具由各自的条件同步管)。
|
|
13630
13760
|
* - ⚠️ **未注册的名字会让整次 restrict 失败**(宿主契约逐字 "unknown names … fail"),
|
|
13631
13761
|
* 而 `diagram` 是按开关注册的 ⇒ 与 `hideAllFor` 同款**两档尝试**(先全覆盖,失败退常驻段)。
|
|
13632
|
-
* - 读不到约束面(不是
|
|
13633
|
-
* ⛔ 不能只是 `return` —— 那会把上一步的隐藏**永久留在**一个已不再是
|
|
13634
|
-
* (这正是 owner 说的"删掉 `
|
|
13762
|
+
* - 读不到约束面(不是 轨迹约束 / 配置删了)⇒ **撤销既有隐藏**(回到"无约束"):
|
|
13763
|
+
* ⛔ 不能只是 `return` —— 那会把上一步的隐藏**永久留在**一个已不再是 轨迹约束的会话上
|
|
13764
|
+
* (这正是 owner 说的"删掉 `trajectory-constraint.json` 就该立刻恢复")。
|
|
13635
13765
|
* - 幂等:算出来的 deny 签名不变就跳过(不每步重建 schema)。
|
|
13636
13766
|
*
|
|
13637
13767
|
* @param agent 目标会话
|
|
13638
13768
|
* @param root CCC 根
|
|
13639
13769
|
*/
|
|
13640
|
-
function
|
|
13770
|
+
function syncTrajectoryConstraintToolVisibility(agent, root) {
|
|
13641
13771
|
const sessionId = agent.session.id ?? "global";
|
|
13642
|
-
const profile =
|
|
13643
|
-
const existing =
|
|
13772
|
+
const profile = trajectoryConstraintForSession(root, sessionId);
|
|
13773
|
+
const existing = trajectoryConstraintRestrictions.get(sessionId);
|
|
13644
13774
|
if (profile === null) {
|
|
13645
13775
|
if (existing !== void 0) {
|
|
13646
13776
|
try {
|
|
13647
13777
|
existing.dispose();
|
|
13648
13778
|
} catch {}
|
|
13649
|
-
|
|
13779
|
+
trajectoryConstraintRestrictions.delete(sessionId);
|
|
13650
13780
|
}
|
|
13651
13781
|
return;
|
|
13652
13782
|
}
|
|
@@ -13662,7 +13792,7 @@ function syncSkiffRoleToolVisibility(agent, root) {
|
|
|
13662
13792
|
try {
|
|
13663
13793
|
existing.dispose();
|
|
13664
13794
|
} catch {}
|
|
13665
|
-
|
|
13795
|
+
trajectoryConstraintRestrictions.delete(sessionId);
|
|
13666
13796
|
}
|
|
13667
13797
|
return;
|
|
13668
13798
|
}
|
|
@@ -13671,10 +13801,10 @@ function syncSkiffRoleToolVisibility(agent, root) {
|
|
|
13671
13801
|
try {
|
|
13672
13802
|
existing.dispose();
|
|
13673
13803
|
} catch {}
|
|
13674
|
-
|
|
13804
|
+
trajectoryConstraintRestrictions.delete(sessionId);
|
|
13675
13805
|
}
|
|
13676
13806
|
const dispose = agent.ctx.tools.restrict({ deny: [...deny] });
|
|
13677
|
-
|
|
13807
|
+
trajectoryConstraintRestrictions.set(sessionId, {
|
|
13678
13808
|
signature,
|
|
13679
13809
|
deny,
|
|
13680
13810
|
dispose
|
|
@@ -13686,13 +13816,13 @@ function syncSkiffRoleToolVisibility(agent, root) {
|
|
|
13686
13816
|
}
|
|
13687
13817
|
}
|
|
13688
13818
|
/** 会话销毁清理(lifecycle 调用) */
|
|
13689
|
-
function
|
|
13690
|
-
const existing =
|
|
13819
|
+
function forgetTrajectoryConstraintToolVisibility(sessionId) {
|
|
13820
|
+
const existing = trajectoryConstraintRestrictions.get(sessionId);
|
|
13691
13821
|
if (!existing) return;
|
|
13692
13822
|
try {
|
|
13693
13823
|
existing.dispose();
|
|
13694
13824
|
} catch {}
|
|
13695
|
-
|
|
13825
|
+
trajectoryConstraintRestrictions.delete(sessionId);
|
|
13696
13826
|
}
|
|
13697
13827
|
/**
|
|
13698
13828
|
* 同步专属工具可见性(v1.33)。
|
|
@@ -13887,7 +14017,7 @@ function registerGuards(ctx, opts = {}) {
|
|
|
13887
14017
|
const pathArg = extractPathArg(exec);
|
|
13888
14018
|
const action = extractAction(exec);
|
|
13889
14019
|
const sessionId = exec?.agent?.session?.id;
|
|
13890
|
-
const
|
|
14020
|
+
const trajectoryConstraint = trajectoryConstraintForSession(root, sessionId);
|
|
13891
14021
|
return decideGuard({
|
|
13892
14022
|
root,
|
|
13893
14023
|
toolName: exec.name,
|
|
@@ -13895,7 +14025,7 @@ function registerGuards(ctx, opts = {}) {
|
|
|
13895
14025
|
blacklist,
|
|
13896
14026
|
pathArg,
|
|
13897
14027
|
action,
|
|
13898
|
-
|
|
14028
|
+
trajectoryConstraint
|
|
13899
14029
|
});
|
|
13900
14030
|
};
|
|
13901
14031
|
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
@@ -14132,7 +14262,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
|
|
|
14132
14262
|
const loopSid = session.id;
|
|
14133
14263
|
const loopCwd = session.header?.cwd ?? process.cwd();
|
|
14134
14264
|
const loopRoot = cccRootForCwd(loopCwd);
|
|
14135
|
-
if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || loopRoot !== null &&
|
|
14265
|
+
if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || loopRoot !== null && isTrajectoryConstrained(loopRoot, loopSid))) return {
|
|
14136
14266
|
boundary: -1,
|
|
14137
14267
|
promoted: true
|
|
14138
14268
|
};
|
|
@@ -14251,7 +14381,7 @@ function registerBootstrap(ctx) {
|
|
|
14251
14381
|
const depth = session?.header?.delegationDepth ?? 0;
|
|
14252
14382
|
const sid = typeof session?.id === "string" ? session.id : void 0;
|
|
14253
14383
|
if (sid === void 0) return;
|
|
14254
|
-
if (sid.startsWith("cheap-worker-") ||
|
|
14384
|
+
if (sid.startsWith("cheap-worker-") || isTrajectoryConstrained(root, sid)) return;
|
|
14255
14385
|
if (depth === 0) {
|
|
14256
14386
|
if (hasUserMessageHistory(session)) return;
|
|
14257
14387
|
} else if (anchoredSessions.has(sid)) return;
|
|
@@ -14565,7 +14695,7 @@ function forgetLogbookCompactionState(scope) {
|
|
|
14565
14695
|
* 解析**活跃** SESSION.md 绝对路径(缓存 60s)。
|
|
14566
14696
|
*
|
|
14567
14697
|
* 候选链(刻意比 rebuild 的完整链短——这里每个工具调用都要跑,贵候选不划算):
|
|
14568
|
-
* ① `getActiveSessionInfo(scope).mdPath`(`cognitive_trajectory use` /
|
|
14698
|
+
* ① `getActiveSessionInfo(scope).mdPath`(`cognitive_trajectory use` / 轨迹约束 绑定 / 重启恢复都会写)
|
|
14569
14699
|
* ② `readLastBound(session).mdPath`(`.bindings.json` 权威绑定——重启后内存空时兜底)
|
|
14570
14700
|
* 两者都拿不到 → null(**不猜**:宁可不提醒,也不去猜别的轨迹——对齐 v1.30.13 D4 的教训)。
|
|
14571
14701
|
*/
|
|
@@ -14694,10 +14824,10 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
14694
14824
|
const root = cccRootForCwd(cwd);
|
|
14695
14825
|
if (!root) return next();
|
|
14696
14826
|
const sessionId = exec.agent?.session?.id;
|
|
14697
|
-
const
|
|
14698
|
-
const
|
|
14827
|
+
const 轨迹约束Keeper = trajectoryConstraintParticipates(root, sessionId, "keeper");
|
|
14828
|
+
const 轨迹约束Rebuild = trajectoryConstraintParticipates(root, sessionId, "rebuild");
|
|
14699
14829
|
const tracker = trackerFor(exec);
|
|
14700
|
-
const shouldRemind =
|
|
14830
|
+
const shouldRemind = 轨迹约束Keeper ? tracker.step(exec.name) : false;
|
|
14701
14831
|
const downstream = await next();
|
|
14702
14832
|
try {
|
|
14703
14833
|
recordToolUsage(root, exec.name, exec.arguments);
|
|
@@ -14710,7 +14840,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
14710
14840
|
text: reminderText(code)
|
|
14711
14841
|
});
|
|
14712
14842
|
}
|
|
14713
|
-
if (
|
|
14843
|
+
if (轨迹约束Rebuild) {
|
|
14714
14844
|
const session = exec.agent?.session;
|
|
14715
14845
|
if (session) {
|
|
14716
14846
|
const pressure = readContextPressure(ctx, session);
|
|
@@ -14730,7 +14860,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
14730
14860
|
}
|
|
14731
14861
|
}
|
|
14732
14862
|
}
|
|
14733
|
-
if (
|
|
14863
|
+
if (trajectoryConstraintParticipates(root, sessionId, "session")) {
|
|
14734
14864
|
const limitKB = readSessionMdMaxKB(root, opts.configPaths);
|
|
14735
14865
|
if (limitKB > 0) {
|
|
14736
14866
|
const scope = sessionId ?? "global";
|
|
@@ -14753,7 +14883,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
14753
14883
|
} else compactionStates.delete(scope);
|
|
14754
14884
|
}
|
|
14755
14885
|
}
|
|
14756
|
-
if (
|
|
14886
|
+
if (trajectoryConstraintParticipates(root, sessionId, "session")) {
|
|
14757
14887
|
const scope = sessionId ?? "global";
|
|
14758
14888
|
const session = exec.agent?.session;
|
|
14759
14889
|
const mdPath = resolveActiveSessionMdPath(root, scope, session);
|
|
@@ -14861,7 +14991,7 @@ function shouldAutoRestore(agent, root) {
|
|
|
14861
14991
|
if (session.header?.parentSession) return false;
|
|
14862
14992
|
if (session.id?.startsWith("cheap-worker-")) return false;
|
|
14863
14993
|
const cccRoot = root !== void 0 ? root : cccRootForCwd(session.header?.cwd ?? process.cwd());
|
|
14864
|
-
if (cccRoot &&
|
|
14994
|
+
if (cccRoot && isTrajectoryConstrained(cccRoot, session.id)) return false;
|
|
14865
14995
|
return true;
|
|
14866
14996
|
}
|
|
14867
14997
|
/**
|
|
@@ -14884,7 +15014,7 @@ function registerContext(ctx, opts = {}) {
|
|
|
14884
15014
|
} catch {}
|
|
14885
15015
|
if (!root) return;
|
|
14886
15016
|
const key = agentKey(agent);
|
|
14887
|
-
if (
|
|
15017
|
+
if (isTrajectoryConstrained(root, key)) return;
|
|
14888
15018
|
registerEntrySkillSection(agent, root);
|
|
14889
15019
|
registerCognitiveTrajectorySkillSection(agent, root);
|
|
14890
15020
|
const scope = agentScope(agent);
|
|
@@ -14968,9 +15098,9 @@ function registerContext(ctx, opts = {}) {
|
|
|
14968
15098
|
const key = agentKey(agent);
|
|
14969
15099
|
const downstream = await next();
|
|
14970
15100
|
if (root) try {
|
|
14971
|
-
|
|
15101
|
+
syncTrajectoryConstraintToolVisibility(agent, root);
|
|
14972
15102
|
} catch {}
|
|
14973
|
-
if (root &&
|
|
15103
|
+
if (root && isTrajectoryConstrained(root, key)) return downstream;
|
|
14974
15104
|
try {
|
|
14975
15105
|
syncCccToolVisibility(agent, root);
|
|
14976
15106
|
} catch {}
|
|
@@ -15019,7 +15149,7 @@ function registerCompactRetention(ctx, opts = {}) {
|
|
|
15019
15149
|
const cwd = agent.session?.header?.cwd ?? process.cwd();
|
|
15020
15150
|
const root = cccRootForCwd(cwd);
|
|
15021
15151
|
if (!root) return;
|
|
15022
|
-
if (
|
|
15152
|
+
if (isTrajectoryConstrained(root, String(session.id ?? ""))) return;
|
|
15023
15153
|
try {
|
|
15024
15154
|
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
15025
15155
|
} catch {}
|
|
@@ -15312,7 +15442,7 @@ function saveFileToTmp(root, fileName, data) {
|
|
|
15312
15442
|
writeFileSync(join(dir, filename), bytes);
|
|
15313
15443
|
return `${FILE_UPLOAD_DIR}/${filename}`;
|
|
15314
15444
|
}
|
|
15315
|
-
function readBody$
|
|
15445
|
+
function readBody$2(req, maxBytes) {
|
|
15316
15446
|
return new Promise((resolve, reject) => {
|
|
15317
15447
|
let data = "";
|
|
15318
15448
|
req.on("data", (chunk) => {
|
|
@@ -15326,7 +15456,7 @@ function readBody$3(req, maxBytes) {
|
|
|
15326
15456
|
req.on("error", reject);
|
|
15327
15457
|
});
|
|
15328
15458
|
}
|
|
15329
|
-
function sendJson$
|
|
15459
|
+
function sendJson$2(res, code, body) {
|
|
15330
15460
|
const payload = JSON.stringify(body);
|
|
15331
15461
|
res.writeHead(code, {
|
|
15332
15462
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -15351,7 +15481,7 @@ function senderIsWebUi(req) {
|
|
|
15351
15481
|
*/
|
|
15352
15482
|
function requireWebUi(req, res, message) {
|
|
15353
15483
|
if (senderIsWebUi(req)) return true;
|
|
15354
|
-
sendJson$
|
|
15484
|
+
sendJson$2(res, 403, { error: message });
|
|
15355
15485
|
return false;
|
|
15356
15486
|
}
|
|
15357
15487
|
/**
|
|
@@ -15360,7 +15490,7 @@ function requireWebUi(req, res, message) {
|
|
|
15360
15490
|
*
|
|
15361
15491
|
* 🔴🔴 **2026-10-18 修缺陷(owner 报「在别的 CCC 点开显示的还是 home-serenity 的信息」)**:
|
|
15362
15492
|
* 旧实现第 ③ 步是「**遍历 live sessions 找第一个 CCC 会话的 cwd**」—— 那是**跨工作区猜测**:
|
|
15363
|
-
* 只要 sessionId 缺失或那条会话查不到(ACP
|
|
15493
|
+
* 只要 sessionId 缺失或那条会话查不到(ACP/轨迹约束会话、尚未 materialize 的会话、远程访问),
|
|
15364
15494
|
* 它就会**静默返回另一个 CCC 的路径**,于是面板把**别人的轨迹/技能/安全模式**当成你的显示出来。
|
|
15365
15495
|
* 这比"显示未激活"危险得多 —— 用户会据此做判断(以为自己的 safe-mode 开着、以为那是自己的轨迹)。
|
|
15366
15496
|
*
|
|
@@ -15409,11 +15539,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15409
15539
|
handler: async (req, res) => {
|
|
15410
15540
|
try {
|
|
15411
15541
|
if (req.method !== "POST") {
|
|
15412
|
-
sendJson$
|
|
15542
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15413
15543
|
return;
|
|
15414
15544
|
}
|
|
15415
15545
|
if (!requireWebUi(req, res, "图片落盘仅限 WebUI(client 专用)")) return;
|
|
15416
|
-
const raw = await readBody$
|
|
15546
|
+
const raw = await readBody$2(req, 20971520);
|
|
15417
15547
|
const body = JSON.parse(raw);
|
|
15418
15548
|
const workspace = resolveWorkspace(ctx, {
|
|
15419
15549
|
sessionId: body.sessionId,
|
|
@@ -15421,12 +15551,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15421
15551
|
});
|
|
15422
15552
|
const root = cccRootForCwd(workspace);
|
|
15423
15553
|
if (!root) {
|
|
15424
|
-
sendJson$
|
|
15554
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
15425
15555
|
return;
|
|
15426
15556
|
}
|
|
15427
|
-
sendJson$
|
|
15557
|
+
sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
15428
15558
|
} catch (err) {
|
|
15429
|
-
sendJson$
|
|
15559
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15430
15560
|
}
|
|
15431
15561
|
}
|
|
15432
15562
|
});
|
|
@@ -15436,11 +15566,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15436
15566
|
handler: async (req, res) => {
|
|
15437
15567
|
try {
|
|
15438
15568
|
if (req.method !== "POST") {
|
|
15439
|
-
sendJson$
|
|
15569
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15440
15570
|
return;
|
|
15441
15571
|
}
|
|
15442
15572
|
if (!requireWebUi(req, res, "文件落盘仅限 WebUI(client 专用)")) return;
|
|
15443
|
-
const raw = await readBody$
|
|
15573
|
+
const raw = await readBody$2(req, 20971520);
|
|
15444
15574
|
const body = JSON.parse(raw);
|
|
15445
15575
|
const workspace = resolveWorkspace(ctx, {
|
|
15446
15576
|
sessionId: body.sessionId,
|
|
@@ -15448,12 +15578,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15448
15578
|
});
|
|
15449
15579
|
const root = cccRootForCwd(workspace);
|
|
15450
15580
|
if (!root) {
|
|
15451
|
-
sendJson$
|
|
15581
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
15452
15582
|
return;
|
|
15453
15583
|
}
|
|
15454
|
-
sendJson$
|
|
15584
|
+
sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
15455
15585
|
} catch (err) {
|
|
15456
|
-
sendJson$
|
|
15586
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15457
15587
|
}
|
|
15458
15588
|
}
|
|
15459
15589
|
});
|
|
@@ -15469,7 +15599,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15469
15599
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
15470
15600
|
}), configPaths);
|
|
15471
15601
|
const runtime = ctx.get("codeRuntime");
|
|
15472
|
-
sendJson$
|
|
15602
|
+
sendJson$2(res, 200, {
|
|
15473
15603
|
...status,
|
|
15474
15604
|
...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
|
|
15475
15605
|
language: runtime.language,
|
|
@@ -15480,7 +15610,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15480
15610
|
}
|
|
15481
15611
|
if (req.method === "POST") {
|
|
15482
15612
|
if (!requireWebUi(req, res, "safe-mode 切换仅限 WebUI(agent 不可自行开关)")) return;
|
|
15483
|
-
const raw = await readBody$
|
|
15613
|
+
const raw = await readBody$2(req, 65536);
|
|
15484
15614
|
const body = JSON.parse(raw);
|
|
15485
15615
|
const workspace = resolveWorkspace(ctx, {
|
|
15486
15616
|
sessionId: body.sessionId,
|
|
@@ -15488,19 +15618,19 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15488
15618
|
});
|
|
15489
15619
|
const root = cccRootForCwd(workspace);
|
|
15490
15620
|
if (!root) {
|
|
15491
|
-
sendJson$
|
|
15621
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
15492
15622
|
return;
|
|
15493
15623
|
}
|
|
15494
15624
|
const result = setSafeMode(root, body.on === true);
|
|
15495
|
-
sendJson$
|
|
15625
|
+
sendJson$2(res, 200, {
|
|
15496
15626
|
...getStatus(workspace, configPaths),
|
|
15497
15627
|
...result
|
|
15498
15628
|
});
|
|
15499
15629
|
return;
|
|
15500
15630
|
}
|
|
15501
|
-
sendJson$
|
|
15631
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15502
15632
|
} catch (err) {
|
|
15503
|
-
sendJson$
|
|
15633
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15504
15634
|
}
|
|
15505
15635
|
}
|
|
15506
15636
|
});
|
|
@@ -15516,24 +15646,24 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15516
15646
|
try {
|
|
15517
15647
|
known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
|
|
15518
15648
|
} catch {}
|
|
15519
|
-
sendJson$
|
|
15649
|
+
sendJson$2(res, 200, {
|
|
15520
15650
|
config: toWire(settings),
|
|
15521
15651
|
knownWorkspaces: known
|
|
15522
15652
|
});
|
|
15523
15653
|
return;
|
|
15524
15654
|
}
|
|
15525
15655
|
if (req.method === "PUT") {
|
|
15526
|
-
const raw = await readBody$
|
|
15656
|
+
const raw = await readBody$2(req, 131072);
|
|
15527
15657
|
const saved = applyWirePatch(JSON.parse(raw).config ?? {});
|
|
15528
15658
|
try {
|
|
15529
15659
|
ctx.emit?.("serenity/config-updated");
|
|
15530
15660
|
} catch {}
|
|
15531
|
-
sendJson$
|
|
15661
|
+
sendJson$2(res, 200, { config: toWire(saved) });
|
|
15532
15662
|
return;
|
|
15533
15663
|
}
|
|
15534
|
-
sendJson$
|
|
15664
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15535
15665
|
} catch (err) {
|
|
15536
|
-
sendJson$
|
|
15666
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15537
15667
|
}
|
|
15538
15668
|
}
|
|
15539
15669
|
});
|
|
@@ -15543,7 +15673,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15543
15673
|
handler: async (req, res) => {
|
|
15544
15674
|
try {
|
|
15545
15675
|
if (req.method !== "GET") {
|
|
15546
|
-
sendJson$
|
|
15676
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15547
15677
|
return;
|
|
15548
15678
|
}
|
|
15549
15679
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -15552,12 +15682,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15552
15682
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
15553
15683
|
});
|
|
15554
15684
|
const root = cccRootForCwd(workspace) ?? "";
|
|
15555
|
-
sendJson$
|
|
15685
|
+
sendJson$2(res, 200, { cccs: await listCccs(ctx, {
|
|
15556
15686
|
defaultRoot: root,
|
|
15557
15687
|
withRoles: true
|
|
15558
15688
|
}) });
|
|
15559
15689
|
} catch (err) {
|
|
15560
|
-
sendJson$
|
|
15690
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15561
15691
|
}
|
|
15562
15692
|
}
|
|
15563
15693
|
});
|
|
@@ -15571,21 +15701,26 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15571
15701
|
if (req.method === "GET") {
|
|
15572
15702
|
const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
|
|
15573
15703
|
if (!chk.ok) {
|
|
15574
|
-
sendJson$
|
|
15704
|
+
sendJson$2(res, 400, { error: chk.error });
|
|
15575
15705
|
return;
|
|
15576
15706
|
}
|
|
15577
|
-
const { readWeixinSettings } = await import("./route-
|
|
15707
|
+
const { readWeixinSettings } = await import("./route-COOFLTpU.js").then((n) => n.c);
|
|
15578
15708
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
|
|
15579
15709
|
const settings = readWeixinSettings(chk.root);
|
|
15580
15710
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
15581
|
-
const {
|
|
15582
|
-
const
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
15587
|
-
|
|
15588
|
-
|
|
15711
|
+
const { imCredentialBound } = await import("./route-COOFLTpU.js").then((n) => n.c);
|
|
15712
|
+
const { normalizeAccountChannel } = await import("./accounts-BHQbakUh.js").then((n) => n.t);
|
|
15713
|
+
const accounts = (settings.accounts ?? []).map((a) => {
|
|
15714
|
+
const channel = normalizeAccountChannel(a.channel);
|
|
15715
|
+
return {
|
|
15716
|
+
accountId: a.accountId,
|
|
15717
|
+
name: a.name ?? void 0,
|
|
15718
|
+
enabled: a.enabled !== false,
|
|
15719
|
+
channel,
|
|
15720
|
+
bound: imCredentialBound(chk.root, a.accountId, channel)
|
|
15721
|
+
};
|
|
15722
|
+
});
|
|
15723
|
+
sendJson$2(res, 200, {
|
|
15589
15724
|
enabled: settings.enabled === true,
|
|
15590
15725
|
botType: settings.botType ?? void 0,
|
|
15591
15726
|
accounts,
|
|
@@ -15595,22 +15730,22 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15595
15730
|
return;
|
|
15596
15731
|
}
|
|
15597
15732
|
if (req.method !== "POST") {
|
|
15598
|
-
sendJson$
|
|
15733
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15599
15734
|
return;
|
|
15600
15735
|
}
|
|
15601
|
-
const raw = await readBody$
|
|
15736
|
+
const raw = await readBody$2(req, 131072);
|
|
15602
15737
|
const body = JSON.parse(raw);
|
|
15603
15738
|
const chk = requireCcc(body.ccc);
|
|
15604
15739
|
if (!chk.ok) {
|
|
15605
|
-
sendJson$
|
|
15740
|
+
sendJson$2(res, 400, { error: chk.error });
|
|
15606
15741
|
return;
|
|
15607
15742
|
}
|
|
15608
15743
|
const root = chk.root;
|
|
15609
15744
|
if (body.action === "login-start") {
|
|
15610
|
-
const { fetchQRCode } = await import("./ilink-
|
|
15745
|
+
const { fetchQRCode } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
|
|
15611
15746
|
const { randomUUID } = await import("node:crypto");
|
|
15612
15747
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
15613
|
-
const { readWeixinSettings } = await import("./route-
|
|
15748
|
+
const { readWeixinSettings } = await import("./route-COOFLTpU.js").then((n) => n.c);
|
|
15614
15749
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
15615
15750
|
const loginKey = randomUUID();
|
|
15616
15751
|
weixinLogins.set(loginKey, {
|
|
@@ -15619,7 +15754,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15619
15754
|
startedAt: Date.now(),
|
|
15620
15755
|
polling: false
|
|
15621
15756
|
});
|
|
15622
|
-
sendJson$
|
|
15757
|
+
sendJson$2(res, 200, {
|
|
15623
15758
|
qrcode: qr.qrcode,
|
|
15624
15759
|
qrcode_img_content: qr.qrcode_img_content,
|
|
15625
15760
|
loginKey
|
|
@@ -15628,20 +15763,20 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15628
15763
|
}
|
|
15629
15764
|
if (body.action === "remove-account") {
|
|
15630
15765
|
if (!body.accountId) {
|
|
15631
|
-
sendJson$
|
|
15766
|
+
sendJson$2(res, 400, { error: "missing accountId" });
|
|
15632
15767
|
return;
|
|
15633
15768
|
}
|
|
15634
|
-
const { removeWeixinAccount } = await import("./route-
|
|
15769
|
+
const { removeWeixinAccount } = await import("./route-COOFLTpU.js").then((n) => n.c);
|
|
15635
15770
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15636
15771
|
removeWeixinAccount(root, body.accountId);
|
|
15637
15772
|
syncCccBridge(ctx, root);
|
|
15638
|
-
sendJson$
|
|
15773
|
+
sendJson$2(res, 200, { removed: body.accountId });
|
|
15639
15774
|
return;
|
|
15640
15775
|
}
|
|
15641
15776
|
if (body.action === "save-routes") {
|
|
15642
15777
|
const raw = body.routes ?? [];
|
|
15643
15778
|
if (!Array.isArray(raw)) {
|
|
15644
|
-
sendJson$
|
|
15779
|
+
sendJson$2(res, 400, { error: "invalid routes (expected [{user, trajectory}, ...])" });
|
|
15645
15780
|
return;
|
|
15646
15781
|
}
|
|
15647
15782
|
const routes = [];
|
|
@@ -15649,7 +15784,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15649
15784
|
const user = typeof r?.user === "string" ? r.user : "";
|
|
15650
15785
|
const code = typeof r?.trajectory === "string" && r.trajectory !== "" ? r.trajectory : typeof r?.role === "string" ? r.role : "";
|
|
15651
15786
|
if (user === "" || code === "") {
|
|
15652
|
-
sendJson$
|
|
15787
|
+
sendJson$2(res, 400, { error: "invalid routes (each row needs non-empty user and trajectory)" });
|
|
15653
15788
|
return;
|
|
15654
15789
|
}
|
|
15655
15790
|
routes.push({
|
|
@@ -15657,23 +15792,23 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15657
15792
|
trajectory: code
|
|
15658
15793
|
});
|
|
15659
15794
|
}
|
|
15660
|
-
const { saveWeixinRoutes } = await import("./route-
|
|
15661
|
-
const { findDirNameByCode } = await import("./profile-
|
|
15795
|
+
const { saveWeixinRoutes } = await import("./route-COOFLTpU.js").then((n) => n.c);
|
|
15796
|
+
const { findDirNameByCode } = await import("./profile-BVt84mbi.js").then((n) => n.i);
|
|
15662
15797
|
for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
|
|
15663
|
-
sendJson$
|
|
15798
|
+
sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
|
|
15664
15799
|
return;
|
|
15665
15800
|
}
|
|
15666
15801
|
saveWeixinRoutes(root, routes);
|
|
15667
|
-
sendJson$
|
|
15802
|
+
sendJson$2(res, 200, { saved: routes.length });
|
|
15668
15803
|
return;
|
|
15669
15804
|
}
|
|
15670
15805
|
if (body.action === "set-enabled") {
|
|
15671
15806
|
const enabled = body.enabled === true;
|
|
15672
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./route-
|
|
15807
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./route-COOFLTpU.js").then((n) => n.c);
|
|
15673
15808
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15674
15809
|
const settings = readWeixinSettings(root);
|
|
15675
15810
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
15676
|
-
sendJson$
|
|
15811
|
+
sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
|
|
15677
15812
|
return;
|
|
15678
15813
|
}
|
|
15679
15814
|
setWeixinEnabled(root, enabled);
|
|
@@ -15682,12 +15817,41 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15682
15817
|
const { stopCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15683
15818
|
stopCccBridge(root);
|
|
15684
15819
|
}
|
|
15685
|
-
sendJson$
|
|
15820
|
+
sendJson$2(res, 200, { enabled });
|
|
15686
15821
|
return;
|
|
15687
15822
|
}
|
|
15688
|
-
|
|
15823
|
+
if (body.action === "add-feishu-account") {
|
|
15824
|
+
const appId = (body.appId ?? "").trim();
|
|
15825
|
+
const appSecret = (body.appSecret ?? "").trim();
|
|
15826
|
+
if (appId === "" || appSecret === "") {
|
|
15827
|
+
sendJson$2(res, 400, { error: "appId 与 appSecret 都必须填(只填一个等于没绑)" });
|
|
15828
|
+
return;
|
|
15829
|
+
}
|
|
15830
|
+
const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-COOFLTpU.js").then((n) => n.c);
|
|
15831
|
+
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15832
|
+
const before = readWeixinSettings(root);
|
|
15833
|
+
const accountId = (body.accountId ?? "").trim() !== "" ? body.accountId.trim() : nextFeishuAccountId(before);
|
|
15834
|
+
upsertWeixinAccount(root, {
|
|
15835
|
+
accountId,
|
|
15836
|
+
name: (body.name ?? "").trim() !== "" ? body.name.trim() : void 0,
|
|
15837
|
+
channel: "feishu",
|
|
15838
|
+
enabled: true
|
|
15839
|
+
});
|
|
15840
|
+
writeFeishuCredential(root, accountId, {
|
|
15841
|
+
appId,
|
|
15842
|
+
appSecret
|
|
15843
|
+
});
|
|
15844
|
+
if (before.enabled) syncCccBridge(ctx, root);
|
|
15845
|
+
sendJson$2(res, 200, {
|
|
15846
|
+
accountId,
|
|
15847
|
+
channel: "feishu",
|
|
15848
|
+
saved: true
|
|
15849
|
+
});
|
|
15850
|
+
return;
|
|
15851
|
+
}
|
|
15852
|
+
sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
|
|
15689
15853
|
} catch (err) {
|
|
15690
|
-
sendJson$
|
|
15854
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15691
15855
|
}
|
|
15692
15856
|
}
|
|
15693
15857
|
});
|
|
@@ -15698,22 +15862,22 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15698
15862
|
try {
|
|
15699
15863
|
if (!requireWebUi(req, res, "微信桥配置仅限 WebUI(client 专用)")) return;
|
|
15700
15864
|
if (req.method !== "GET") {
|
|
15701
|
-
sendJson$
|
|
15865
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15702
15866
|
return;
|
|
15703
15867
|
}
|
|
15704
15868
|
const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
|
|
15705
15869
|
const login = weixinLogins.get(key);
|
|
15706
15870
|
if (!login) {
|
|
15707
|
-
sendJson$
|
|
15871
|
+
sendJson$2(res, 404, { error: "login not found or expired" });
|
|
15708
15872
|
return;
|
|
15709
15873
|
}
|
|
15710
15874
|
if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
|
|
15711
15875
|
weixinLogins.delete(key);
|
|
15712
|
-
sendJson$
|
|
15876
|
+
sendJson$2(res, 200, { status: "expired" });
|
|
15713
15877
|
return;
|
|
15714
15878
|
}
|
|
15715
|
-
const { pollQRStatus } = await import("./ilink-
|
|
15716
|
-
const { readWeixinSettings } = await import("./route-
|
|
15879
|
+
const { pollQRStatus } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
|
|
15880
|
+
const { readWeixinSettings } = await import("./route-COOFLTpU.js").then((n) => n.c);
|
|
15717
15881
|
const settings = readWeixinSettings(login.root);
|
|
15718
15882
|
const status = await pollQRStatus({
|
|
15719
15883
|
baseUrl: void 0,
|
|
@@ -15722,13 +15886,13 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15722
15886
|
switch (status.status) {
|
|
15723
15887
|
case "confirmed": {
|
|
15724
15888
|
if (!status.bot_token) {
|
|
15725
|
-
sendJson$
|
|
15889
|
+
sendJson$2(res, 200, {
|
|
15726
15890
|
status: "error",
|
|
15727
15891
|
error: "confirmed but no bot_token"
|
|
15728
15892
|
});
|
|
15729
15893
|
return;
|
|
15730
15894
|
}
|
|
15731
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-
|
|
15895
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-COOFLTpU.js").then((n) => n.c);
|
|
15732
15896
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15733
15897
|
const accountId = nextWeixinAccountId(settings);
|
|
15734
15898
|
upsertWeixinAccount(login.root, {
|
|
@@ -15743,7 +15907,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15743
15907
|
});
|
|
15744
15908
|
syncCccBridge(ctx, login.root);
|
|
15745
15909
|
weixinLogins.delete(key);
|
|
15746
|
-
sendJson$
|
|
15910
|
+
sendJson$2(res, 200, {
|
|
15747
15911
|
status: "confirmed",
|
|
15748
15912
|
accountId,
|
|
15749
15913
|
tokenSaved: true
|
|
@@ -15752,14 +15916,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15752
15916
|
}
|
|
15753
15917
|
case "expired":
|
|
15754
15918
|
weixinLogins.delete(key);
|
|
15755
|
-
sendJson$
|
|
15919
|
+
sendJson$2(res, 200, { status: "expired" });
|
|
15756
15920
|
return;
|
|
15757
15921
|
default:
|
|
15758
|
-
sendJson$
|
|
15922
|
+
sendJson$2(res, 200, { status: status.status ?? "wait" });
|
|
15759
15923
|
return;
|
|
15760
15924
|
}
|
|
15761
15925
|
} catch (err) {
|
|
15762
|
-
sendJson$
|
|
15926
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15763
15927
|
}
|
|
15764
15928
|
}
|
|
15765
15929
|
});
|
|
@@ -15781,7 +15945,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15781
15945
|
const root = sessionsRootDir();
|
|
15782
15946
|
if (req.method === "GET") {
|
|
15783
15947
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
15784
|
-
sendJson$
|
|
15948
|
+
sendJson$2(res, 200, {
|
|
15785
15949
|
root,
|
|
15786
15950
|
olderThanDays: days,
|
|
15787
15951
|
dryRun: true,
|
|
@@ -15795,12 +15959,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15795
15959
|
return;
|
|
15796
15960
|
}
|
|
15797
15961
|
if (req.method !== "POST") {
|
|
15798
|
-
sendJson$
|
|
15962
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15799
15963
|
return;
|
|
15800
15964
|
}
|
|
15801
15965
|
if (!requireWebUi(req, res, "会话清理仅限 WebUI(client 专用)")) return;
|
|
15802
15966
|
const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
|
|
15803
|
-
sendJson$
|
|
15967
|
+
sendJson$2(res, 200, {
|
|
15804
15968
|
root,
|
|
15805
15969
|
olderThanDays: days,
|
|
15806
15970
|
dryRun: false,
|
|
@@ -15808,7 +15972,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15808
15972
|
errors: result.errors
|
|
15809
15973
|
});
|
|
15810
15974
|
} catch (err) {
|
|
15811
|
-
sendJson$
|
|
15975
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15812
15976
|
}
|
|
15813
15977
|
}
|
|
15814
15978
|
});
|
|
@@ -15987,7 +16151,7 @@ function loadVoyageHtml(assetPath = voyageAssetPath()) {
|
|
|
15987
16151
|
return readFileSync(assetPath, "utf-8");
|
|
15988
16152
|
}
|
|
15989
16153
|
/** 发 JSON(错误分支用;与 surface/http.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
|
|
15990
|
-
function sendJson$
|
|
16154
|
+
function sendJson$1(res, status, body) {
|
|
15991
16155
|
res.statusCode = status;
|
|
15992
16156
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
15993
16157
|
res.end(JSON.stringify(body));
|
|
@@ -16005,14 +16169,14 @@ function registerVoyageApi(ctx, opts = {}) {
|
|
|
16005
16169
|
path: VOYAGE_PATH,
|
|
16006
16170
|
handler: (req, res) => {
|
|
16007
16171
|
if (req.method !== "GET") {
|
|
16008
|
-
sendJson$
|
|
16172
|
+
sendJson$1(res, 405, { error: "method not allowed" });
|
|
16009
16173
|
return;
|
|
16010
16174
|
}
|
|
16011
16175
|
let html;
|
|
16012
16176
|
try {
|
|
16013
16177
|
html = loadVoyageHtml(opts.assetPath ?? voyageAssetPath());
|
|
16014
16178
|
} catch (err) {
|
|
16015
|
-
sendJson$
|
|
16179
|
+
sendJson$1(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
|
|
16016
16180
|
return;
|
|
16017
16181
|
}
|
|
16018
16182
|
let root = null;
|
|
@@ -16170,7 +16334,7 @@ const HOST_SERVICES = [
|
|
|
16170
16334
|
name: "get",
|
|
16171
16335
|
kind: "function"
|
|
16172
16336
|
}],
|
|
16173
|
-
impact: "
|
|
16337
|
+
impact: "轨迹约束/cheap-worker 无法创建或复用 agent",
|
|
16174
16338
|
required: true
|
|
16175
16339
|
},
|
|
16176
16340
|
{
|
|
@@ -16593,7 +16757,7 @@ function summarizeHostContract(report) {
|
|
|
16593
16757
|
*
|
|
16594
16758
|
* ## 现状 → 本模块要收掉什么(取证:`…/acc-c4c5-current-state.md` §①/§③ 3.1)
|
|
16595
16759
|
*
|
|
16596
|
-
* 插件自己 spawn 了**三个** node:http 监听器(B 3081 网关 / C 3082 微信发送 / D 3099
|
|
16760
|
+
* 插件自己 spawn 了**三个** node:http 监听器(B 3081 网关 / C 3082 微信发送 / D 3099 轨迹约束调试)。
|
|
16597
16761
|
* ⚠️ **2026-10-15 变更**:原为四个(含 **E 3100 ACP+问答**),E 已随 owner「砍掉外部问答」令**整面退役**
|
|
16598
16762
|
* ⇒ 仍保有面 B/C/D 三个;本段保留"当年是四个"的记述,因为下方"两套拆卸"的病史由它解释。
|
|
16599
16763
|
* 此前四处各写一份同款样板:`createServer` + `server.once('error')` +
|
|
@@ -16650,10 +16814,6 @@ var FaceStartCancelled = class extends Error {
|
|
|
16650
16814
|
this.name = "FaceStartCancelled";
|
|
16651
16815
|
}
|
|
16652
16816
|
};
|
|
16653
|
-
/** 面是否在监听 */
|
|
16654
|
-
function faceActive(name) {
|
|
16655
|
-
return faces.has(name);
|
|
16656
|
-
}
|
|
16657
16817
|
/** 面的实际监听端口(未在监听 → null) */
|
|
16658
16818
|
function facePort(name) {
|
|
16659
16819
|
return faces.get(name)?.handle.port ?? null;
|
|
@@ -17240,7 +17400,7 @@ function createDshCookieProvider(connection, authority) {
|
|
|
17240
17400
|
/** 面名(C4 块 A:listener 生命周期归 `face-host`) */
|
|
17241
17401
|
const FACE_GATEWAY = "gateway";
|
|
17242
17402
|
/** 读取请求体(≤ maxBytes;超限 reject) */
|
|
17243
|
-
function readBody$
|
|
17403
|
+
function readBody$1(req, maxBytes) {
|
|
17244
17404
|
return new Promise((resolve, reject) => {
|
|
17245
17405
|
let data = "";
|
|
17246
17406
|
req.on("data", (chunk) => {
|
|
@@ -17367,7 +17527,7 @@ async function startGateway(config, getAccounts, getDshCookie) {
|
|
|
17367
17527
|
const session = authed(req);
|
|
17368
17528
|
if (session !== void 0) {
|
|
17369
17529
|
if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
|
|
17370
|
-
readBody$
|
|
17530
|
+
readBody$1(req, 131072).then((body) => {
|
|
17371
17531
|
const { rpcId, path } = parseWorkspaceCreateBody(body);
|
|
17372
17532
|
if (!allowWorkspaceCreate) {
|
|
17373
17533
|
res.writeHead(403, { "content-type": "application/json" });
|
|
@@ -17768,9 +17928,9 @@ function sessionNameFromMdPath(mdPath) {
|
|
|
17768
17928
|
* → ②b **权威绑定**(`AGENT_SESSIONS/.bindings.json`,本会话持久绑定)→ ③ surface 首条锚点
|
|
17769
17929
|
* (events 异常时兜底)。
|
|
17770
17930
|
*
|
|
17771
|
-
* v1.30.13(S142 用户"微信桥 +
|
|
17931
|
+
* v1.30.13(S142 用户"微信桥 + 轨迹约束会话锚定要准确",诊断 D4):**移除**原 ④「全局最新未完成
|
|
17772
17932
|
* 会话」(`findLatestActiveSessionMd`)。理由(R↓):④ 是**跨轨迹**猜测——候选①②③全空时,
|
|
17773
|
-
* 它会把 rebuild 静默接到 AGENT_SESSIONS 里最新的另一个会话(临时
|
|
17933
|
+
* 它会把 rebuild 静默接到 AGENT_SESSIONS 里最新的另一个会话(临时 轨迹约束会话 persistent=false、
|
|
17774
17934
|
* 重启后首条消息前、新会话首条消息都命中该形态),而"接错轨迹"比重建失败危险得多
|
|
17775
17935
|
* (错误轨迹被继续写入 + 正确轨迹静默丢失)。现在全部候选失败 → 返回 null → 调用方
|
|
17776
17936
|
* 报错引导用户显式 `cognitive_trajectory use`(响亮失败优于静默接错)。
|
|
@@ -19687,234 +19847,16 @@ f.parseInline;
|
|
|
19687
19847
|
b.parse;
|
|
19688
19848
|
x.lex;
|
|
19689
19849
|
//#endregion
|
|
19690
|
-
//#region src/
|
|
19691
|
-
/** 面名(C4 块 A) */
|
|
19692
|
-
const FACE_SKIFF_DEBUG = "skiff-debug";
|
|
19693
|
-
/** 监听地址:**只绑 loopback**(本面无认证——仅靠 loopback 挡住外部) */
|
|
19694
|
-
const SKIFF_DEBUG_HOST = FACE_PORTS.skiffDebug.host ?? "127.0.0.1";
|
|
19695
|
-
function skiffDebugActive() {
|
|
19696
|
-
return faceActive(FACE_SKIFF_DEBUG);
|
|
19697
|
-
}
|
|
19698
|
-
function readBody$1(req) {
|
|
19699
|
-
return new Promise((resolve, reject) => {
|
|
19700
|
-
const chunks = [];
|
|
19701
|
-
req.on("data", (c) => chunks.push(c));
|
|
19702
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
19703
|
-
req.on("error", reject);
|
|
19704
|
-
});
|
|
19705
|
-
}
|
|
19706
|
-
function sendJson$1(res, status, payload) {
|
|
19707
|
-
const body = JSON.stringify(payload);
|
|
19708
|
-
res.writeHead(status, {
|
|
19709
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
19710
|
-
"Cache-Control": "no-store"
|
|
19711
|
-
});
|
|
19712
|
-
res.end(body);
|
|
19713
|
-
}
|
|
19714
|
-
function sendHtml(res, html) {
|
|
19715
|
-
res.writeHead(200, {
|
|
19716
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
19717
|
-
"Cache-Control": "no-store"
|
|
19718
|
-
});
|
|
19719
|
-
res.end(html);
|
|
19720
|
-
}
|
|
19721
|
-
/** 问答页 HTML:CCC 切换器 + 角色下拉 + 输入 + 答案区 + 轨迹区(JS 渲染)+ WebUI 链接 */
|
|
19722
|
-
function skiffDebugPage(cccs, defaultRoot, webPort) {
|
|
19723
|
-
const data = jscSafeJsonText(JSON.stringify(cccs).replace(/</g, "\\u003c"));
|
|
19724
|
-
return `<!DOCTYPE html>
|
|
19725
|
-
<html lang="zh">
|
|
19726
|
-
<head>
|
|
19727
|
-
<meta charset="utf-8">
|
|
19728
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19729
|
-
<title>Skiff Debug — CCC cognitive subset roles</title>
|
|
19730
|
-
<style>
|
|
19731
|
-
:root { color-scheme: light dark; }
|
|
19732
|
-
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 24px; background: #f6f7f9; color: #1f2328; }
|
|
19733
|
-
@media (prefers-color-scheme: dark) { body { background: #1a1b1e; color: #e6e6e6; } }
|
|
19734
|
-
main { max-width: 720px; margin: 0 auto; }
|
|
19735
|
-
h1 { font-size: 18px; margin: 0 0 4px; }
|
|
19736
|
-
.sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
|
|
19737
|
-
.ccc { display: inline-block; padding: 3px 10px; border-radius: 999px; background: rgba(11,168,117,.12); color: #0ba875; font-size: 12px; font-weight: 600; margin-bottom: 12px; word-break: break-all; }
|
|
19738
|
-
@media (prefers-color-scheme: dark) { .ccc { color: #3ddc9a; } }
|
|
19739
|
-
.sessionbar { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
|
|
19740
|
-
.session-badge { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 12px; padding: 3px 10px; border-radius: 999px; background: rgba(127,127,127,.12); word-break: break-all; }
|
|
19741
|
-
.session-badge.continued { background: rgba(11,168,117,.12); color: #0ba875; }
|
|
19742
|
-
@media (prefers-color-scheme: dark) { .session-badge.continued { color: #3ddc9a; } }
|
|
19743
|
-
button.ghost { background: transparent; color: #57606a; border: 1px solid #d0d7de; padding: 4px 12px; font-size: 12px; border-radius: 8px; cursor: pointer; margin-top: 0; }
|
|
19744
|
-
@media (prefers-color-scheme: dark) { button.ghost { color: #8b949e; border-color: #3a3d42; } }
|
|
19745
|
-
button.ghost:hover { background: rgba(127,127,127,.1); }
|
|
19746
|
-
label { font-size: 13px; font-weight: 600; display: block; margin: 12px 0 4px; }
|
|
19747
|
-
select, textarea { width: 100%; box-sizing: border-box; padding: 8px 10px; border-radius: 8px; border: 1px solid #d0d7de; background: #fff; color: inherit; font-size: 14px; }
|
|
19748
|
-
@media (prefers-color-scheme: dark) { select, textarea { background: #26282c; border-color: #3a3d42; } }
|
|
19749
|
-
textarea { min-height: 72px; resize: vertical; }
|
|
19750
|
-
button { margin-top: 12px; padding: 9px 18px; border-radius: 8px; border: 0; background: #0ba875; color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
19751
|
-
button:disabled { opacity: .55; cursor: wait; }
|
|
19752
|
-
#answer { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; padding: 12px; margin-top: 16px; font-size: 14px; line-height: 1.6; min-height: 48px; word-break: break-word; }
|
|
19753
|
-
@media (prefers-color-scheme: dark) { #answer { background: #26282c; border-color: #3a3d42; } }
|
|
19754
|
-
/* Markdown 渲染(v1.25.8):代码/标题/列表/引用/think 折叠 */
|
|
19755
|
-
#answer p { margin: 6px 0; }
|
|
19756
|
-
#answer h1, #answer h2, #answer h3 { margin: 12px 0 6px; font-size: 15px; line-height: 1.4; }
|
|
19757
|
-
#answer h1 { font-size: 17px; }
|
|
19758
|
-
#answer code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, SFMono-Regular, monospace; }
|
|
19759
|
-
#answer pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 8px 0; }
|
|
19760
|
-
#answer pre code { background: none; padding: 0; font-size: 13px; line-height: 1.5; }
|
|
19761
|
-
#answer ul, #answer ol { margin: 6px 0; padding-left: 22px; }
|
|
19762
|
-
#answer li { margin: 2px 0; }
|
|
19763
|
-
#answer blockquote { margin: 6px 0; padding: 2px 12px; border-left: 3px solid rgba(127,127,127,.35); opacity: .88; }
|
|
19764
|
-
#answer a { color: #0ba875; text-decoration: underline; }
|
|
19765
|
-
details.think { margin: 8px 0; border: 1px solid rgba(210,153,34,.4); border-radius: 8px; background: rgba(210,153,34,.06); }
|
|
19766
|
-
details.think summary { cursor: pointer; padding: 6px 10px; font-size: 12px; color: #d29922; font-weight: 600; user-select: none; list-style-position: inside; }
|
|
19767
|
-
details.think summary:hover { opacity: .8; }
|
|
19768
|
-
details.think .think-body { padding: 2px 12px 10px; font-size: 13px; opacity: .78; white-space: normal; }
|
|
19769
|
-
#cognitiveTrajectory { margin-top: 12px; font-size: 13px; }
|
|
19770
|
-
.t-entry { border-left: 2px solid #d0d7de; padding: 6px 10px; margin: 6px 0; border-radius: 0 6px 6px 0; background: rgba(127,127,127,.06); white-space: pre-wrap; word-break: break-word; }
|
|
19771
|
-
.t-user { border-left-color: #0ba875; }
|
|
19772
|
-
.t-tool { border-left-color: #d29922; opacity: .85; font-family: ui-monospace, monospace; font-size: 12px; }
|
|
19773
|
-
.t-role { font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; opacity: .6; margin-bottom: 2px; }
|
|
19774
|
-
.muted { opacity: .6; font-size: 13px; }
|
|
19775
|
-
a { color: #0ba875; }
|
|
19776
|
-
.err { color: #cf222e; white-space: pre-wrap; }
|
|
19777
|
-
</style>
|
|
19778
|
-
</head>
|
|
19779
|
-
<body>
|
|
19780
|
-
<main>
|
|
19781
|
-
<h1>Skiff Debug</h1>
|
|
19782
|
-
<div class="sub">宁静号轨迹的子集角色问答页(实验性)。可切换 CCC;同一会话可追问,点「新对话」另起一条;回答在服务端渲染,think 块折叠。</div>
|
|
19783
|
-
<div class="ccc" id="cccBadge"></div>
|
|
19784
|
-
<div class="sessionbar">
|
|
19785
|
-
<span id="sessionBadge" class="session-badge muted">(新会话)</span>
|
|
19786
|
-
<button id="newChat" type="button" class="ghost">新对话</button>
|
|
19787
|
-
</div>
|
|
19788
|
-
<label for="ccc">认知容器</label>
|
|
19789
|
-
<select id="ccc"></select>
|
|
19790
|
-
<label for="role">角色</label>
|
|
19791
|
-
<select id="role"></select>
|
|
19792
|
-
<label for="q">问题</label>
|
|
19793
|
-
<textarea id="q" placeholder="向该角色提问…(追问直接继续输入)"></textarea>
|
|
19794
|
-
<button id="ask">提问</button>
|
|
19795
|
-
<div id="answer" class="muted">等待提问…</div>
|
|
19796
|
-
<div id="cognitiveTrajectory"></div>
|
|
19797
|
-
<p class="muted"><a href="${`http://127.0.0.1:${webPort}`}" target="_blank" rel="noopener">在 dsh WebUI 查看完整会话</a>(会话列表搜索 sessionId;WebUI 有完整交互)</p>
|
|
19798
|
-
</main>
|
|
19799
|
-
<script id="skiff-data" type="application/json" data-default="${escapeHtml(defaultRoot)}">${data}<\/script>
|
|
19800
|
-
<script>
|
|
19801
|
-
const CCCS = JSON.parse(document.getElementById('skiff-data').textContent)
|
|
19802
|
-
const defaultRoot = document.getElementById('skiff-data').dataset.default
|
|
19803
|
-
const cccSel = document.getElementById('ccc')
|
|
19804
|
-
const roleSel = document.getElementById('role')
|
|
19805
|
-
const badge = document.getElementById('cccBadge')
|
|
19806
|
-
const sessionBadge = document.getElementById('sessionBadge')
|
|
19807
|
-
const newChatBtn = document.getElementById('newChat')
|
|
19808
|
-
const btn = document.getElementById('ask')
|
|
19809
|
-
const answer = document.getElementById('answer')
|
|
19810
|
-
const traj = document.getElementById('cognitiveTrajectory')
|
|
19811
|
-
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))
|
|
19812
|
-
let sessionId = ''
|
|
19813
|
-
function shortId(id) { return id.length > 24 ? id.slice(0, 18) + '…' + id.slice(-6) : id }
|
|
19814
|
-
function renderSessionBadge() {
|
|
19815
|
-
sessionBadge.textContent = sessionId ? '会话 ' + shortId(sessionId) + '(追问续接)' : '(新会话)'
|
|
19816
|
-
sessionBadge.className = 'session-badge' + (sessionId ? ' continued' : '')
|
|
19817
|
-
}
|
|
19818
|
-
function newConversation() {
|
|
19819
|
-
sessionId = ''
|
|
19820
|
-
renderSessionBadge()
|
|
19821
|
-
answer.className = 'muted'
|
|
19822
|
-
answer.textContent = '已开启新对话'
|
|
19823
|
-
traj.innerHTML = ''
|
|
19824
|
-
document.getElementById('q').value = ''
|
|
19825
|
-
}
|
|
19826
|
-
function currentCcc() {
|
|
19827
|
-
return CCCS.find((c) => c.root === cccSel.value) || (CCCS[0] || { root: '', name: '', roles: [] })
|
|
19828
|
-
}
|
|
19829
|
-
function fillCccs() {
|
|
19830
|
-
cccSel.innerHTML = CCCS.map((c) => {
|
|
19831
|
-
const n = c.roles.length
|
|
19832
|
-
return '<option value="' + esc(c.root) + '">' + esc(c.name) + (n ? ' (' + n + ' 角色)' : ' (无角色)') + '</option>'
|
|
19833
|
-
}).join('') || '<option value="">(未发现 CCC)</option>'
|
|
19834
|
-
if (defaultRoot) cccSel.value = CCCS.some((c) => c.root === defaultRoot) ? defaultRoot : (CCCS[0] && CCCS[0].root)
|
|
19835
|
-
fillRoles()
|
|
19836
|
-
}
|
|
19837
|
-
function fillRoles() {
|
|
19838
|
-
const c = currentCcc()
|
|
19839
|
-
badge.textContent = 'CCC: ' + c.root
|
|
19840
|
-
roleSel.innerHTML = (c.roles && c.roles.length)
|
|
19841
|
-
? c.roles.map((r) => '<option value="' + esc(r) + '">' + esc(r) + '</option>').join('')
|
|
19842
|
-
: '<option value="">(未配置角色)</option>'
|
|
19843
|
-
}
|
|
19844
|
-
cccSel.addEventListener('change', () => { fillRoles(); newConversation() })
|
|
19845
|
-
roleSel.addEventListener('change', () => newConversation())
|
|
19846
|
-
newChatBtn.addEventListener('click', newConversation)
|
|
19847
|
-
fillCccs()
|
|
19848
|
-
renderSessionBadge()
|
|
19849
|
-
btn.addEventListener('click', async () => {
|
|
19850
|
-
const c = currentCcc()
|
|
19851
|
-
const role = roleSel.value
|
|
19852
|
-
const q = document.getElementById('q').value.trim()
|
|
19853
|
-
if (!c.root || !role || !q) { answer.className = 'err'; answer.textContent = '请选择认知容器与角色并输入问题'; return }
|
|
19854
|
-
btn.disabled = true
|
|
19855
|
-
answer.className = 'muted'
|
|
19856
|
-
answer.textContent = '运行中…'
|
|
19857
|
-
traj.innerHTML = ''
|
|
19858
|
-
try {
|
|
19859
|
-
const res = await fetch('/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ccc: c.root, role, question: q, sessionId: sessionId || undefined }) })
|
|
19860
|
-
const data = await res.json()
|
|
19861
|
-
if (!res.ok) {
|
|
19862
|
-
// 会话不可恢复/绑定不匹配 → 前端清空 sessionId 重建(用户拍板:切换即新对话)
|
|
19863
|
-
sessionId = ''
|
|
19864
|
-
renderSessionBadge()
|
|
19865
|
-
throw new Error(data.error || ('HTTP ' + res.status))
|
|
19866
|
-
}
|
|
19867
|
-
answer.className = ''
|
|
19868
|
-
answer.innerHTML = data.answer_html || esc(data.answer || '') || '(空回答)'
|
|
19869
|
-
sessionId = data.sessionId || sessionId
|
|
19870
|
-
renderSessionBadge()
|
|
19871
|
-
renderCognitiveTrajectory(data.cognitiveTrajectory || [], data.sessionId || '')
|
|
19872
|
-
} catch (err) {
|
|
19873
|
-
answer.className = 'err'
|
|
19874
|
-
answer.textContent = String(err.message || err)
|
|
19875
|
-
} finally {
|
|
19876
|
-
btn.disabled = false
|
|
19877
|
-
}
|
|
19878
|
-
})
|
|
19879
|
-
function renderCognitiveTrajectory(entries, sessionId) {
|
|
19880
|
-
if (entries.length === 0) { traj.innerHTML = '<div class="muted">(本轮无轨迹)</div>'; return }
|
|
19881
|
-
traj.innerHTML = '<div class="sub">本轮轨迹 · ' + esc(sessionId) + '</div>' + entries.map((e) => {
|
|
19882
|
-
const cls = e.role === 'user' ? 't-user' : (e.role === 'tool' ? 't-tool' : '')
|
|
19883
|
-
const role = e.role === 'tool' ? 'tool' + (e.tool ? ' · ' + esc(e.tool) : '') : e.role
|
|
19884
|
-
return '<div class="t-entry ' + cls + '"><div class="t-role">' + role + '</div>' + esc(e.text) + '</div>'
|
|
19885
|
-
}).join('')
|
|
19886
|
-
}
|
|
19887
|
-
<\/script>
|
|
19888
|
-
</body>
|
|
19889
|
-
</html>`;
|
|
19890
|
-
}
|
|
19891
|
-
function escapeHtml(s) {
|
|
19892
|
-
return s.replace(/[&<>"']/g, (c) => ({
|
|
19893
|
-
"&": "&",
|
|
19894
|
-
"<": "<",
|
|
19895
|
-
">": ">",
|
|
19896
|
-
"\"": """,
|
|
19897
|
-
"'": "'"
|
|
19898
|
-
})[c] ?? c);
|
|
19899
|
-
}
|
|
19850
|
+
//#region src/surface/text.ts
|
|
19900
19851
|
/**
|
|
19901
|
-
*
|
|
19902
|
-
*
|
|
19903
|
-
*
|
|
19904
|
-
*
|
|
19905
|
-
* (段分隔符)时正则无法匹配 → 对**合法 JSON** 也抛
|
|
19906
|
-
* `SyntaxError: The string did not match the expected pattern`(sentry-javascript #2487 同源)。
|
|
19907
|
-
* JSON.stringify **不转义** `\u2028`/`\u2029`/`\uFEFF`(它们都是合法 JSON 字符串字符)→
|
|
19908
|
-
* 在 JSON **文本层**把它们替换为 `\uXXXX` 转义序列:JSON.parse 后语义完全一致(还原原字符),
|
|
19909
|
-
* 且 JSC 正则看到的是常规 ASCII 转义(与 JSON.stringify 对控制字符的输出同形态,安全)。
|
|
19910
|
-
*
|
|
19911
|
-
* 3099 调试页客户端 `await res.json()` 与页面内嵌 JSON 均需此兼容层
|
|
19912
|
-
* ——iOS Safari 用户实测"复杂回答"触发。(原同批还列了 3100 问答页,该面已退役,2026-10-15)
|
|
19913
|
-
*
|
|
19914
|
-
* @param jsonText JSON.stringify 的输出文本;原地等价替换,返回 JSC 安全文本
|
|
19852
|
+
* 剥离 `<think>…</think>` 块(v1.27.1,微信桥用户反馈:"过微信桥回复给用户的消息,
|
|
19853
|
+
* 要去掉 think 标签,用户不应看到")。复用 extractThinkBlocks 状态机(v1.26.8,
|
|
19854
|
+
* 弃正则)——提取后**只保留正文**(占位符替换为空,think 内容丢弃),
|
|
19855
|
+
* 未闭合 think 优雅截断。适用于纯文本输出面(微信等不支持 think 折叠的通道)。
|
|
19915
19856
|
*/
|
|
19916
|
-
function
|
|
19917
|
-
|
|
19857
|
+
function stripThink(raw) {
|
|
19858
|
+
const { body } = extractThinkBlocks(raw);
|
|
19859
|
+
return body.replace(/\u0001T(\d+)\u0001/g, "");
|
|
19918
19860
|
}
|
|
19919
19861
|
/**
|
|
19920
19862
|
* 提取 `<think>…</think>` 块(v1.26.8:**状态机扫描,弃正则**——用户批评"老用正则不是个办法")。
|
|
@@ -19992,140 +19934,6 @@ function matchCloseThink(s, contentStart) {
|
|
|
19992
19934
|
}
|
|
19993
19935
|
return -1;
|
|
19994
19936
|
}
|
|
19995
|
-
/**
|
|
19996
|
-
* Markdown 渲染(v1.25.9,正经库 marked 服务端渲染——替代手写正则渲染器,S142 用户要求):
|
|
19997
|
-
* ① 提取 `<think>…</think>` 块(v1.26.8 状态机扫描,占位符 \u0001T<idx>\u0001)
|
|
19998
|
-
* ② 正文与 think 内容**先 escapeHtml 再 marked.parse**(GFM + breaks)——markdown 语法不受
|
|
19999
|
-
* 转义影响,原始 HTML 注入被消除(安全);代码块内 `<` 显示为实体(可接受)
|
|
20000
|
-
* ③ think 占位符:默认还原为 `<details class="think">` 折叠卡(🧠 思考过程,默认收起);
|
|
20001
|
-
* **hideThink=true(v1.26.4,public 口)→ 直接移除**(思考过程对普通用户不展示)
|
|
20002
|
-
* @param hideThink 为 true 时 `<think>` 内容完全不渲染(public 问答页体验)
|
|
20003
|
-
*/
|
|
20004
|
-
function renderSkiffMarkdown(raw, hideThink = false) {
|
|
20005
|
-
const { body, thinks } = extractThinkBlocks(raw);
|
|
20006
|
-
const parsed = f.parse(escapeHtml(body), {
|
|
20007
|
-
breaks: true,
|
|
20008
|
-
gfm: true
|
|
20009
|
-
});
|
|
20010
|
-
return (typeof parsed === "string" ? parsed : "").replace(/\u0001T(\d+)\u0001/g, (_m, idx) => {
|
|
20011
|
-
if (hideThink) return "";
|
|
20012
|
-
const inner = thinks[Number(idx)] ?? "";
|
|
20013
|
-
const innerHtml = inner === "" ? "" : f.parse(escapeHtml(inner), {
|
|
20014
|
-
breaks: true,
|
|
20015
|
-
gfm: true
|
|
20016
|
-
});
|
|
20017
|
-
return `<details class="think"><summary>🧠 思考过程</summary><div class="think-body">${typeof innerHtml === "string" ? innerHtml : ""}</div></details>`;
|
|
20018
|
-
});
|
|
20019
|
-
}
|
|
20020
|
-
/**
|
|
20021
|
-
* 剥离 `<think>…</think>` 块(v1.27.1,微信桥用户反馈:"过微信桥回复给用户的消息,
|
|
20022
|
-
* 要去掉 think 标签,用户不应看到")。复用 extractThinkBlocks 状态机(v1.26.8,
|
|
20023
|
-
* 弃正则)——提取后**只保留正文**(占位符替换为空,think 内容丢弃),
|
|
20024
|
-
* 未闭合 think 优雅截断。适用于纯文本输出面(微信等不支持 think 折叠的通道)。
|
|
20025
|
-
*/
|
|
20026
|
-
function stripThink(raw) {
|
|
20027
|
-
const { body } = extractThinkBlocks(raw);
|
|
20028
|
-
return body.replace(/\u0001T(\d+)\u0001/g, "");
|
|
20029
|
-
}
|
|
20030
|
-
/**
|
|
20031
|
-
* 本面的规格(C4 块 A/④:**只提供** name/port/host/enabled/handler)。
|
|
20032
|
-
* 路由/鉴权/HTML 全部仍归本模块的 {@link handle}(面宿主不介入面语义)。
|
|
20033
|
-
*/
|
|
20034
|
-
function skiffDebugSpec(ctx, root, port, webPort) {
|
|
20035
|
-
return {
|
|
20036
|
-
name: FACE_SKIFF_DEBUG,
|
|
20037
|
-
port,
|
|
20038
|
-
host: SKIFF_DEBUG_HOST,
|
|
20039
|
-
enabled: () => readSimpleSettings().skiffEnabled,
|
|
20040
|
-
handler: (req, res) => handle(ctx, root, webPort, req, res)
|
|
20041
|
-
};
|
|
20042
|
-
}
|
|
20043
|
-
/**
|
|
20044
|
-
* 启动调试问答服务(单实例;重复启动幂等返回既有实例)。
|
|
20045
|
-
*
|
|
20046
|
-
* **CCC 绑定(v1.25.2 用户指出)**:服务绑定一个默认 CCC root(调用方 resolveSkiffRoot
|
|
20047
|
-
* 解析:live 会话中**含 skiff.roles 的 CCC 优先**);v1.25.4 起页面可**手工切换**到
|
|
20048
|
-
* 其它候选 CCC(live 会话发现的全部 CCC);角色配置**每次请求实时读取**(不缓存快照)。
|
|
20049
|
-
*
|
|
20050
|
-
* C4 块 A:listener 的创建/监听/错误/端口占用/关闭归 `face-host`(本函数只提供规格 + 记日志)。
|
|
20051
|
-
*
|
|
20052
|
-
* @param root 默认绑定的 CCC 根(首次加载选中;角色配置读取 + skiff agent cwd)
|
|
20053
|
-
* @param port 调试端口(仅 127.0.0.1)
|
|
20054
|
-
* @param webPort 主 WebUI 端口(WebUI 链接)
|
|
20055
|
-
*/
|
|
20056
|
-
async function startSkiffDebugServer(ctx, root, port, webPort) {
|
|
20057
|
-
if (skiffDebugActive()) return;
|
|
20058
|
-
const handle = await startFace(skiffDebugSpec(ctx, root, port, webPort));
|
|
20059
|
-
console.log(`[serenity-hooks] ✓ Skiff 调试问答页: http://127.0.0.1:${handle.port}(默认 CCC: ${root},WebUI: ${webPort})`);
|
|
20060
|
-
}
|
|
20061
|
-
function stopSkiffDebugServer() {
|
|
20062
|
-
stopFace(FACE_SKIFF_DEBUG);
|
|
20063
|
-
}
|
|
20064
|
-
async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
20065
|
-
try {
|
|
20066
|
-
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
20067
|
-
if (req.method === "GET" && url === "/") {
|
|
20068
|
-
sendHtml(res, skiffDebugPage(await listCccs(ctx, {
|
|
20069
|
-
defaultRoot,
|
|
20070
|
-
withRoles: true
|
|
20071
|
-
}), defaultRoot, webPort));
|
|
20072
|
-
return;
|
|
20073
|
-
}
|
|
20074
|
-
if (req.method === "POST" && url === "/ask") {
|
|
20075
|
-
let body;
|
|
20076
|
-
try {
|
|
20077
|
-
const parsed = JSON.parse(await readBody$1(req));
|
|
20078
|
-
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
20079
|
-
body = parsed;
|
|
20080
|
-
} catch {
|
|
20081
|
-
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
20082
|
-
return;
|
|
20083
|
-
}
|
|
20084
|
-
const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot;
|
|
20085
|
-
const code = typeof body.role === "string" ? body.role : "";
|
|
20086
|
-
const question = typeof body.question === "string" ? body.question : "";
|
|
20087
|
-
const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
|
|
20088
|
-
const profile = code !== "" ? readSkiffProfileByCode(ccc, code) : null;
|
|
20089
|
-
if (!profile) {
|
|
20090
|
-
sendJson$1(res, 400, { error: `unknown skiff trajectory: ${code} (no trajectory with code "${code}" has ${SKIFF_PROFILE_FILENAME} in ${ccc})` });
|
|
20091
|
-
return;
|
|
20092
|
-
}
|
|
20093
|
-
if (!question.trim()) {
|
|
20094
|
-
sendJson$1(res, 400, { error: "empty question" });
|
|
20095
|
-
return;
|
|
20096
|
-
}
|
|
20097
|
-
let agent;
|
|
20098
|
-
let continued = false;
|
|
20099
|
-
if (sessionId) {
|
|
20100
|
-
const info = skiffSessionInfo(sessionId);
|
|
20101
|
-
const live = getSkiffAgent(sessionId);
|
|
20102
|
-
if (!info || !live) {
|
|
20103
|
-
sendJson$1(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
|
|
20104
|
-
return;
|
|
20105
|
-
}
|
|
20106
|
-
if (info.trajectoryDirName !== profile.dirName || info.ccc !== ccc) {
|
|
20107
|
-
sendJson$1(res, 400, { error: `session "${sessionId}" belongs to trajectory "${info.trajectoryDirName}" in another CCC — start a new conversation` });
|
|
20108
|
-
return;
|
|
20109
|
-
}
|
|
20110
|
-
agent = live;
|
|
20111
|
-
continued = true;
|
|
20112
|
-
}
|
|
20113
|
-
const hc = readCheapWorkerConfig(ccc);
|
|
20114
|
-
if (!agent) agent = (await createSkiffAgent(ctx, ccc, profile, hc?.defaultModel)).agent;
|
|
20115
|
-
const result = await askSkiff(ctx, agent, question);
|
|
20116
|
-
sendJson$1(res, 200, {
|
|
20117
|
-
answer: result.answer,
|
|
20118
|
-
answer_html: renderSkiffMarkdown(result.answer),
|
|
20119
|
-
sessionId: result.sessionId,
|
|
20120
|
-
continued
|
|
20121
|
-
});
|
|
20122
|
-
return;
|
|
20123
|
-
}
|
|
20124
|
-
sendJson$1(res, 404, { error: "not found" });
|
|
20125
|
-
} catch (err) {
|
|
20126
|
-
sendJson$1(res, 500, { error: err?.message ?? String(err) });
|
|
20127
|
-
}
|
|
20128
|
-
}
|
|
20129
19937
|
//#endregion
|
|
20130
19938
|
//#region src/guard/filter.ts
|
|
20131
19939
|
/**
|
|
@@ -20168,9 +19976,9 @@ const MECHANISM_WORDS = [
|
|
|
20168
19976
|
"拦截缝",
|
|
20169
19977
|
"装配层",
|
|
20170
19978
|
"acp-core",
|
|
20171
|
-
"
|
|
20172
|
-
"
|
|
20173
|
-
"
|
|
19979
|
+
"trajectory-session",
|
|
19980
|
+
"trajectory-window",
|
|
19981
|
+
"trajectory-constraint",
|
|
20174
19982
|
"output-guard"
|
|
20175
19983
|
];
|
|
20176
19984
|
/**
|
|
@@ -20270,20 +20078,20 @@ const rebukeStates = /* @__PURE__ */ new Map();
|
|
|
20270
20078
|
/**
|
|
20271
20079
|
* 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免)。
|
|
20272
20080
|
*
|
|
20273
|
-
* 🔴 **2026-10-19
|
|
20274
|
-
* **「该会话绑定的轨迹有 `
|
|
20275
|
-
*
|
|
20276
|
-
* "挂在
|
|
20081
|
+
* 🔴 **2026-10-19 判据更换**:轨迹约束那一条从「sessionId 前缀 `轨迹约束-`」改为
|
|
20082
|
+
* **「该会话绑定的轨迹有 `trajectory-constraint.json`」**(owner 重做令)。前缀判据在新模型下是
|
|
20083
|
+
* **错的**——轨迹约束的身份是轨迹,不再由 id 前缀表达;继续用前缀会漏掉所有
|
|
20084
|
+
* "挂在 轨迹约束轨迹上、但 id 不带前缀"的会话(典型 = 微信桥按轨迹派生的会话 id)。
|
|
20277
20085
|
*
|
|
20278
|
-
* ⚠️ `rebuild-` 前缀保留(那是另一件事:重建会话同属对外面,与
|
|
20086
|
+
* ⚠️ `rebuild-` 前缀保留(那是另一件事:重建会话同属对外面,与 轨迹约束 无关);
|
|
20279
20087
|
* `acp-` 分支保留但**已是死判据**(无生产者;面 E 已于 2026-10-15 退役)——
|
|
20280
20088
|
* 理由与"为何不擅自删"见文件头注释。
|
|
20281
20089
|
*/
|
|
20282
20090
|
function isExternalFaceSession(sessionId, root) {
|
|
20283
20091
|
if (!sessionId) return false;
|
|
20284
|
-
return
|
|
20092
|
+
return isTrajectoryConstrained(root, sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
|
|
20285
20093
|
}
|
|
20286
|
-
/** 读会话最后一个 assistant/message 文本(
|
|
20094
|
+
/** 读会话最后一个 assistant/message 文本(trajectory-session 同款)
|
|
20287
20095
|
* **v1.63.3 起唯一的导出理由消失**:原导出是给 `unattended-seam.ts` 复用的(D77),
|
|
20288
20096
|
* 该文件已随无人值守功能整条退役 ⇒ 现只有本模块自己用(`isLeak` 的调用点)。
|
|
20289
20097
|
* ⚠️ 保持 `export` 无害(它有测试覆盖,且将来"同一读法只留一份"的纪律仍成立),
|
|
@@ -20597,188 +20405,63 @@ function appendHumanChannelAudit(event) {
|
|
|
20597
20405
|
})}\n`, "utf-8");
|
|
20598
20406
|
} catch {}
|
|
20599
20407
|
}
|
|
20600
|
-
|
|
20601
|
-
|
|
20602
|
-
* 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
|
|
20603
|
-
* v1.30.17 起桥要在 `askSkiff` 返回后读它决定是否兜底,故 turn-stopping 不再结算清空)。
|
|
20604
|
-
*/
|
|
20605
|
-
const sentThisTurn = /* @__PURE__ */ new Set();
|
|
20606
|
-
/** 连续打回计数(成功发送即清零) */
|
|
20607
|
-
const rebukeCounts = /* @__PURE__ */ new Map();
|
|
20608
|
-
/** 闸门是否装配成功(v1.30.17:桥据此判断能否信任"未发送"这一判定——未装配时不兜底,防重复发送) */
|
|
20609
|
-
let guardActive = false;
|
|
20610
|
-
/** 桥:登记手动模式会话(每轮 incoming 都调用,幂等覆盖) */
|
|
20611
|
-
function noteManualOutputSession(sessionId, session) {
|
|
20612
|
-
if (typeof sessionId !== "string" || sessionId === "") return;
|
|
20613
|
-
manualSessions.set(sessionId, session);
|
|
20614
|
-
}
|
|
20615
|
-
/** 桥:非手动模式 / 会话失效 → 注销(闸门随之失效,零干预) */
|
|
20616
|
-
function forgetManualOutputSession(sessionId) {
|
|
20617
|
-
manualSessions.delete(sessionId);
|
|
20618
|
-
sentThisTurn.delete(sessionId);
|
|
20619
|
-
rebukeCounts.delete(sessionId);
|
|
20620
|
-
}
|
|
20621
|
-
/** 本轮是否已成功发送(测试/诊断用;桥在每轮结束时读它决定是否兜底) */
|
|
20622
|
-
function hasSentThisTurn(sessionId) {
|
|
20623
|
-
return sentThisTurn.has(sessionId);
|
|
20624
|
-
}
|
|
20625
|
-
/**
|
|
20626
|
-
* 桥:**每轮开始时**清空本会话的"已发送"标记(turn 边界由桥掌握——v1.30.17)。
|
|
20627
|
-
* 闸门侧不再于 turn-stopping 结算清空,否则桥读不到本轮结果、无法兜底。
|
|
20628
|
-
*/
|
|
20629
|
-
function clearSentThisTurn(sessionId) {
|
|
20630
|
-
sentThisTurn.delete(sessionId);
|
|
20631
|
-
}
|
|
20632
|
-
/** 闸门是否装配成功(未装配 → 桥不做兜底:宁可静默也不冒重复发送的风险) */
|
|
20633
|
-
function isWeixinOutputGuardActive() {
|
|
20634
|
-
return guardActive;
|
|
20635
|
-
}
|
|
20636
|
-
/** 从 agent / 会话对象取会话 id(形状宽容) */
|
|
20637
|
-
function sessionIdOf$1(value) {
|
|
20638
|
-
const v = value;
|
|
20639
|
-
const id = v?.session?.id ?? v?.id;
|
|
20640
|
-
return typeof id === "string" && id !== "" ? id : null;
|
|
20641
|
-
}
|
|
20408
|
+
//#endregion
|
|
20409
|
+
//#region src/im/hook.ts
|
|
20642
20410
|
/**
|
|
20643
|
-
*
|
|
20411
|
+
* im/hook.ts — 微信桥消息记录 hook(F4c-3 扩展,v1.27.13 用户需求)
|
|
20644
20412
|
*
|
|
20645
|
-
*
|
|
20646
|
-
*
|
|
20647
|
-
*
|
|
20413
|
+
* 需求:微信桥发生的所有消息记录,支持 hook——允许 CCC 通过自行编写 hook
|
|
20414
|
+
* 脚本进行**持久性保存**(存哪/存成什么/是否入库,全归 CCC 决定——ACC 不绑定存储)。
|
|
20415
|
+
*
|
|
20416
|
+
* 设计(docs/weixin-message-hook-design.md v0.1,三项拍板 H1~H6):
|
|
20417
|
+
* - **双向触发**(H1):incoming(用户→bot,媒体落盘后)+ outgoing(bot→用户,发送成功后)
|
|
20418
|
+
* - **脚本 + stdin JSON**(H2):serenity.json weixin.hook = CCC 根相对脚本路径;
|
|
20419
|
+
* 每次消息事件 spawn 一次,事件 JSON 单行经 stdin 传入(bun 优先 / node 兜底,biasProvider 先例)
|
|
20420
|
+
* - **旁路容忍**(H3):异步 fire-and-forget + 超时 kill + 失败仅日志——微信桥可靠性不受影响
|
|
20421
|
+
* - **不含会话凭据**(H5):事件只有记录所需字段(无 context_token / bot_token——最小暴露面)
|
|
20422
|
+
*
|
|
20423
|
+
* 定位:Mech 纯逻辑(事件构造纯函数 + 执行器确定性),可独立单测。
|
|
20648
20424
|
*/
|
|
20649
|
-
|
|
20650
|
-
|
|
20651
|
-
|
|
20652
|
-
|
|
20653
|
-
|
|
20654
|
-
|
|
20655
|
-
|
|
20656
|
-
|
|
20657
|
-
|
|
20658
|
-
|
|
20659
|
-
|
|
20425
|
+
/** hook 执行超时(旁路记录足够;超时 kill 防挂死脚本) */
|
|
20426
|
+
const WEIXIN_HOOK_TIMEOUT_MS = 15e3;
|
|
20427
|
+
/** hook stderr/stdout 日志截断(防脚本刷屏) */
|
|
20428
|
+
const HOOK_LOG_MAX = 500;
|
|
20429
|
+
/** 构造 incoming 事件对象(纯函数,可测) */
|
|
20430
|
+
function buildIncomingHookEvent(input) {
|
|
20431
|
+
return {
|
|
20432
|
+
event: "incoming",
|
|
20433
|
+
ts: Date.now(),
|
|
20434
|
+
cccRoot: input.cccRoot,
|
|
20435
|
+
accountId: input.accountId,
|
|
20436
|
+
userId: input.userId,
|
|
20437
|
+
sessionId: input.sessionId,
|
|
20438
|
+
role: input.role,
|
|
20439
|
+
message: {
|
|
20440
|
+
text: input.text,
|
|
20441
|
+
media: input.media
|
|
20442
|
+
}
|
|
20443
|
+
};
|
|
20660
20444
|
}
|
|
20661
|
-
/**
|
|
20662
|
-
function
|
|
20663
|
-
return
|
|
20664
|
-
|
|
20665
|
-
|
|
20666
|
-
|
|
20667
|
-
|
|
20445
|
+
/** 构造 outgoing 事件对象(纯函数,可测) */
|
|
20446
|
+
function buildOutgoingHookEvent(input) {
|
|
20447
|
+
return {
|
|
20448
|
+
event: "outgoing",
|
|
20449
|
+
ts: Date.now(),
|
|
20450
|
+
cccRoot: input.cccRoot,
|
|
20451
|
+
accountId: input.accountId,
|
|
20452
|
+
userId: input.userId,
|
|
20453
|
+
sessionId: input.sessionId,
|
|
20454
|
+
role: input.role,
|
|
20455
|
+
reply: input.reply,
|
|
20456
|
+
...input.source && input.source !== "reply" ? { source: input.source } : {},
|
|
20457
|
+
...input.file ? { file: input.file } : {}
|
|
20458
|
+
};
|
|
20668
20459
|
}
|
|
20669
20460
|
/**
|
|
20670
|
-
*
|
|
20671
|
-
*
|
|
20672
|
-
|
|
20673
|
-
|
|
20674
|
-
try {
|
|
20675
|
-
ctx.on("tools/post-execute", async (exec, result, next) => {
|
|
20676
|
-
const downstream = await next();
|
|
20677
|
-
try {
|
|
20678
|
-
const sessionId = sessionIdOf$1(exec?.agent);
|
|
20679
|
-
if (sessionId && manualSessions.has(sessionId) && isSuccessfulWeixinSend(exec, result)) {
|
|
20680
|
-
sentThisTurn.add(sessionId);
|
|
20681
|
-
rebukeCounts.delete(sessionId);
|
|
20682
|
-
}
|
|
20683
|
-
} catch {}
|
|
20684
|
-
return downstream;
|
|
20685
|
-
});
|
|
20686
|
-
} catch {
|
|
20687
|
-
console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:tools/post-execute 不可用");
|
|
20688
|
-
return;
|
|
20689
|
-
}
|
|
20690
|
-
try {
|
|
20691
|
-
ctx.on("agent/turn-stopping", (payload) => {
|
|
20692
|
-
const agent = payload?.agent;
|
|
20693
|
-
if (!agent) return;
|
|
20694
|
-
const sessionId = sessionIdOf$1(agent);
|
|
20695
|
-
if (!sessionId) return;
|
|
20696
|
-
const session = manualSessions.get(sessionId);
|
|
20697
|
-
if (!session) return;
|
|
20698
|
-
if (sentThisTurn.has(sessionId)) return;
|
|
20699
|
-
const count = (rebukeCounts.get(sessionId) ?? 0) + 1;
|
|
20700
|
-
if (count > 2) {
|
|
20701
|
-
rebukeCounts.delete(sessionId);
|
|
20702
|
-
console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role})——本轮用户将收不到 agent 自己发的消息`);
|
|
20703
|
-
return;
|
|
20704
|
-
}
|
|
20705
|
-
rebukeCounts.set(sessionId, count);
|
|
20706
|
-
try {
|
|
20707
|
-
agent.steer(createUserMessage({
|
|
20708
|
-
content: [{
|
|
20709
|
-
type: "text",
|
|
20710
|
-
text: buildManualOutputRebuke(session)
|
|
20711
|
-
}],
|
|
20712
|
-
source: PLUGIN_SOURCE
|
|
20713
|
-
}));
|
|
20714
|
-
console.log(`[serenity-hooks] weixin 输出闸门:打回 ${count}/2(session=${sessionId}, turn=${payload.turn ?? "?"})`);
|
|
20715
|
-
} catch (error) {
|
|
20716
|
-
console.warn(`[serenity-hooks] ✗ weixin 输出闸门 steer 失败: ${String(error?.message ?? error)}`);
|
|
20717
|
-
}
|
|
20718
|
-
});
|
|
20719
|
-
} catch {
|
|
20720
|
-
console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:agent/turn-stopping 不可用");
|
|
20721
|
-
return;
|
|
20722
|
-
}
|
|
20723
|
-
guardActive = true;
|
|
20724
|
-
}
|
|
20725
|
-
//#endregion
|
|
20726
|
-
//#region src/im/hook.ts
|
|
20727
|
-
/**
|
|
20728
|
-
* im/hook.ts — 微信桥消息记录 hook(F4c-3 扩展,v1.27.13 用户需求)
|
|
20729
|
-
*
|
|
20730
|
-
* 需求:微信桥发生的所有消息记录,支持 hook——允许 CCC 通过自行编写 hook
|
|
20731
|
-
* 脚本进行**持久性保存**(存哪/存成什么/是否入库,全归 CCC 决定——ACC 不绑定存储)。
|
|
20732
|
-
*
|
|
20733
|
-
* 设计(docs/weixin-message-hook-design.md v0.1,三项拍板 H1~H6):
|
|
20734
|
-
* - **双向触发**(H1):incoming(用户→bot,媒体落盘后)+ outgoing(bot→用户,发送成功后)
|
|
20735
|
-
* - **脚本 + stdin JSON**(H2):serenity.json weixin.hook = CCC 根相对脚本路径;
|
|
20736
|
-
* 每次消息事件 spawn 一次,事件 JSON 单行经 stdin 传入(bun 优先 / node 兜底,biasProvider 先例)
|
|
20737
|
-
* - **旁路容忍**(H3):异步 fire-and-forget + 超时 kill + 失败仅日志——微信桥可靠性不受影响
|
|
20738
|
-
* - **不含会话凭据**(H5):事件只有记录所需字段(无 context_token / bot_token——最小暴露面)
|
|
20739
|
-
*
|
|
20740
|
-
* 定位:Mech 纯逻辑(事件构造纯函数 + 执行器确定性),可独立单测。
|
|
20741
|
-
*/
|
|
20742
|
-
/** hook 执行超时(旁路记录足够;超时 kill 防挂死脚本) */
|
|
20743
|
-
const WEIXIN_HOOK_TIMEOUT_MS = 15e3;
|
|
20744
|
-
/** hook stderr/stdout 日志截断(防脚本刷屏) */
|
|
20745
|
-
const HOOK_LOG_MAX = 500;
|
|
20746
|
-
/** 构造 incoming 事件对象(纯函数,可测) */
|
|
20747
|
-
function buildIncomingHookEvent(input) {
|
|
20748
|
-
return {
|
|
20749
|
-
event: "incoming",
|
|
20750
|
-
ts: Date.now(),
|
|
20751
|
-
cccRoot: input.cccRoot,
|
|
20752
|
-
accountId: input.accountId,
|
|
20753
|
-
userId: input.userId,
|
|
20754
|
-
sessionId: input.sessionId,
|
|
20755
|
-
role: input.role,
|
|
20756
|
-
message: {
|
|
20757
|
-
text: input.text,
|
|
20758
|
-
media: input.media
|
|
20759
|
-
}
|
|
20760
|
-
};
|
|
20761
|
-
}
|
|
20762
|
-
/** 构造 outgoing 事件对象(纯函数,可测) */
|
|
20763
|
-
function buildOutgoingHookEvent(input) {
|
|
20764
|
-
return {
|
|
20765
|
-
event: "outgoing",
|
|
20766
|
-
ts: Date.now(),
|
|
20767
|
-
cccRoot: input.cccRoot,
|
|
20768
|
-
accountId: input.accountId,
|
|
20769
|
-
userId: input.userId,
|
|
20770
|
-
sessionId: input.sessionId,
|
|
20771
|
-
role: input.role,
|
|
20772
|
-
reply: input.reply,
|
|
20773
|
-
...input.source && input.source !== "reply" ? { source: input.source } : {},
|
|
20774
|
-
...input.file ? { file: input.file } : {}
|
|
20775
|
-
};
|
|
20776
|
-
}
|
|
20777
|
-
/**
|
|
20778
|
-
* 执行一次 hook:spawn CCC 根脚本,事件 JSON 单行经 stdin 传入。
|
|
20779
|
-
* 旁路容忍(H3):超时 kill / 非 0 退出 / spawn 失败 → 仅日志返回 {ok:false},绝不抛(不阻断微信桥)。
|
|
20780
|
-
* 路径逃逸校验:脚本必须解析在 CCC 根内(resolveInside——resolveInside 抛错也吞为 {ok:false})。
|
|
20781
|
-
* 脚本缺失 → {ok:false}(配置了但未实现——日志提示;不视为异常)。
|
|
20461
|
+
* 执行一次 hook:spawn CCC 根脚本,事件 JSON 单行经 stdin 传入。
|
|
20462
|
+
* 旁路容忍(H3):超时 kill / 非 0 退出 / spawn 失败 → 仅日志返回 {ok:false},绝不抛(不阻断微信桥)。
|
|
20463
|
+
* 路径逃逸校验:脚本必须解析在 CCC 根内(resolveInside——resolveInside 抛错也吞为 {ok:false})。
|
|
20464
|
+
* 脚本缺失 → {ok:false}(配置了但未实现——日志提示;不视为异常)。
|
|
20782
20465
|
*/
|
|
20783
20466
|
async function runWeixinHook(root, hookRel, event, timeoutMs = WEIXIN_HOOK_TIMEOUT_MS) {
|
|
20784
20467
|
let scriptAbs;
|
|
@@ -20882,31 +20565,6 @@ function invokeWeixinHook(root, hookRel, event) {
|
|
|
20882
20565
|
return activeRunner(root, hookRel, event);
|
|
20883
20566
|
}
|
|
20884
20567
|
//#endregion
|
|
20885
|
-
//#region src/im/accounts.ts
|
|
20886
|
-
const impls = /* @__PURE__ */ new Map();
|
|
20887
|
-
/** 注册账号实现(`index.ts` 装配时调用;重复 channel 覆盖——测试与热重载友好) */
|
|
20888
|
-
function registerImAccountImpl(impl) {
|
|
20889
|
-
impls.set(impl.channel, impl);
|
|
20890
|
-
}
|
|
20891
|
-
/** 取实现(未注册 ⇒ `null`,由调用方**响亮出声**,不要静默跳过) */
|
|
20892
|
-
function getImAccountImpl(channel) {
|
|
20893
|
-
return impls.get(channel) ?? null;
|
|
20894
|
-
}
|
|
20895
|
-
/** 已注册的实现名(诊断与错误文案用;顺序 = 注册顺序) */
|
|
20896
|
-
function imAccountChannels() {
|
|
20897
|
-
return [...impls.keys()];
|
|
20898
|
-
}
|
|
20899
|
-
/**
|
|
20900
|
-
* 账号行的渠道归一(**单一处**)。
|
|
20901
|
-
*
|
|
20902
|
-
* 缺省 `weixin`:老配置平移到 `im` 段时不必补这个字段——owner 的"统一配置"
|
|
20903
|
-
* 要能直接吃旧数据(与 `route.ts` 的 `normalizeImSection` 保持同一口径)。
|
|
20904
|
-
*/
|
|
20905
|
-
function normalizeAccountChannel(channel) {
|
|
20906
|
-
const v = (channel ?? "").trim();
|
|
20907
|
-
return v === "" ? "weixin" : v;
|
|
20908
|
-
}
|
|
20909
|
-
//#endregion
|
|
20910
20568
|
//#region src/im/weixin-account.ts
|
|
20911
20569
|
/** 轮询间隔(getupdates 失败后重试;成功 = 立即续,长轮询本身 hold 35s) */
|
|
20912
20570
|
const POLL_RETRY_MS = 3e3;
|
|
@@ -20980,138 +20638,145 @@ function installWeixinAccountImpl(d) {
|
|
|
20980
20638
|
setWeixinLoopDeps(d);
|
|
20981
20639
|
registerImAccountImpl(weixinAccountImpl);
|
|
20982
20640
|
}
|
|
20983
|
-
|
|
20984
|
-
//#region src/im/bridge.ts
|
|
20985
|
-
var bridge_exports = /* @__PURE__ */ __exportAll({
|
|
20986
|
-
handleIncoming: () => handleIncoming,
|
|
20987
|
-
handleMachineIncoming: () => handleMachineIncoming,
|
|
20988
|
-
manualOutputFallbackNeeded: () => manualOutputFallbackNeeded,
|
|
20989
|
-
registerWeixinBridge: () => registerWeixinBridge,
|
|
20990
|
-
resolveWeixinAccount: () => resolveWeixinAccount,
|
|
20991
|
-
sendProactiveText: () => sendProactiveText,
|
|
20992
|
-
stopAllBridges: () => stopAllBridges,
|
|
20993
|
-
stopCccBridge: () => stopCccBridge,
|
|
20994
|
-
syncCccBridge: () => syncCccBridge,
|
|
20995
|
-
syncHumanChannel: () => syncHumanChannel,
|
|
20996
|
-
weixinBridgeStatus: () => weixinBridgeStatus
|
|
20997
|
-
});
|
|
20998
|
-
const bridges = /* @__PURE__ */ new Map();
|
|
20641
|
+
const manualSessions = /* @__PURE__ */ new Map();
|
|
20999
20642
|
/**
|
|
21000
|
-
*
|
|
21001
|
-
*
|
|
21002
|
-
* 机器级 poller **不属于任何 CCC**(这正是"上移 ACC 层"的含义:谁轮询不再由 CCC 数量决定)。
|
|
21003
|
-
* 用途有二:① `runAccountLoop` 的 `root` 形参在机器级模式下不被使用,传它以示"无根";
|
|
21004
|
-
* ② 归属表(`im/inbound.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
|
|
21005
|
-
* 与任何**真实** CCC 根都不会撞(真根一律是绝对路径)。
|
|
20643
|
+
* 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
|
|
20644
|
+
* v1.30.17 起桥要在 `askTrajectory` 返回后读它决定是否兜底,故 turn-stopping 不再结算清空)。
|
|
21006
20645
|
*/
|
|
21007
|
-
const
|
|
21008
|
-
/**
|
|
21009
|
-
const
|
|
21010
|
-
/**
|
|
21011
|
-
|
|
21012
|
-
/**
|
|
21013
|
-
|
|
21014
|
-
|
|
21015
|
-
|
|
20646
|
+
const sentThisTurn = /* @__PURE__ */ new Set();
|
|
20647
|
+
/** 连续打回计数(成功发送即清零) */
|
|
20648
|
+
const rebukeCounts = /* @__PURE__ */ new Map();
|
|
20649
|
+
/** 闸门是否装配成功(v1.30.17:桥据此判断能否信任"未发送"这一判定——未装配时不兜底,防重复发送) */
|
|
20650
|
+
let guardActive = false;
|
|
20651
|
+
/** 桥:登记手动模式会话(每轮 incoming 都调用,幂等覆盖) */
|
|
20652
|
+
function noteManualOutputSession(sessionId, session) {
|
|
20653
|
+
if (typeof sessionId !== "string" || sessionId === "") return;
|
|
20654
|
+
manualSessions.set(sessionId, session);
|
|
21016
20655
|
}
|
|
21017
|
-
/**
|
|
21018
|
-
|
|
21019
|
-
|
|
21020
|
-
|
|
21021
|
-
|
|
21022
|
-
let ticket = typingTicketByUser.get(key);
|
|
21023
|
-
if (!ticket) {
|
|
21024
|
-
ticket = (await getConfig({
|
|
21025
|
-
baseUrl: cred.baseUrl,
|
|
21026
|
-
token: cred.token,
|
|
21027
|
-
ilinkUserId: fromUserId,
|
|
21028
|
-
contextToken: contextToken ?? ""
|
|
21029
|
-
})).typing_ticket;
|
|
21030
|
-
if (ticket) typingTicketByUser.set(key, ticket);
|
|
21031
|
-
}
|
|
21032
|
-
if (!ticket) return;
|
|
21033
|
-
await sendTyping({
|
|
21034
|
-
baseUrl: cred.baseUrl,
|
|
21035
|
-
token: cred.token,
|
|
21036
|
-
ilinkUserId: fromUserId,
|
|
21037
|
-
typingTicket: ticket,
|
|
21038
|
-
status: TypingStatus.TYPING
|
|
21039
|
-
});
|
|
21040
|
-
} catch (err) {
|
|
21041
|
-
console.log(`[serenity-hooks] weixin-bridge typing start skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
21042
|
-
}
|
|
20656
|
+
/** 桥:非手动模式 / 会话失效 → 注销(闸门随之失效,零干预) */
|
|
20657
|
+
function forgetManualOutputSession(sessionId) {
|
|
20658
|
+
manualSessions.delete(sessionId);
|
|
20659
|
+
sentThisTurn.delete(sessionId);
|
|
20660
|
+
rebukeCounts.delete(sessionId);
|
|
21043
20661
|
}
|
|
21044
|
-
/**
|
|
21045
|
-
|
|
21046
|
-
|
|
21047
|
-
const ticket = typingTicketByUser.get(typingCacheKey(accountId, fromUserId));
|
|
21048
|
-
if (!ticket) return;
|
|
21049
|
-
await sendTyping({
|
|
21050
|
-
baseUrl: cred.baseUrl,
|
|
21051
|
-
token: cred.token,
|
|
21052
|
-
ilinkUserId: fromUserId,
|
|
21053
|
-
typingTicket: ticket,
|
|
21054
|
-
status: TypingStatus.CANCEL
|
|
21055
|
-
});
|
|
21056
|
-
} catch (err) {
|
|
21057
|
-
console.log(`[serenity-hooks] weixin-bridge typing stop skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
21058
|
-
}
|
|
20662
|
+
/** 本轮是否已成功发送(测试/诊断用;桥在每轮结束时读它决定是否兜底) */
|
|
20663
|
+
function hasSentThisTurn(sessionId) {
|
|
20664
|
+
return sentThisTurn.has(sessionId);
|
|
21059
20665
|
}
|
|
21060
20666
|
/**
|
|
21061
|
-
*
|
|
21062
|
-
*
|
|
21063
|
-
*
|
|
21064
|
-
* 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
|
|
21065
|
-
* owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
|
|
21066
|
-
* 归**账号自己的实现**,装配处只按 `channel` 查派发表。
|
|
21067
|
-
* ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
|
|
21068
|
-
* `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
|
|
20667
|
+
* 桥:**每轮开始时**清空本会话的"已发送"标记(turn 边界由桥掌握——v1.30.17)。
|
|
20668
|
+
* 闸门侧不再于 turn-stopping 结算清空,否则桥读不到本轮结果、无法兜底。
|
|
21069
20669
|
*/
|
|
20670
|
+
function clearSentThisTurn(sessionId) {
|
|
20671
|
+
sentThisTurn.delete(sessionId);
|
|
20672
|
+
}
|
|
20673
|
+
/** 闸门是否装配成功(未装配 → 桥不做兜底:宁可静默也不冒重复发送的风险) */
|
|
20674
|
+
function isWeixinOutputGuardActive() {
|
|
20675
|
+
return guardActive;
|
|
20676
|
+
}
|
|
20677
|
+
/** 从 agent / 会话对象取会话 id(形状宽容) */
|
|
20678
|
+
function sessionIdOf$1(value) {
|
|
20679
|
+
const v = value;
|
|
20680
|
+
const id = v?.session?.id ?? v?.id;
|
|
20681
|
+
return typeof id === "string" && id !== "" ? id : null;
|
|
20682
|
+
}
|
|
21070
20683
|
/**
|
|
21071
|
-
*
|
|
21072
|
-
* 要让 CCC 定义"重构)。
|
|
21073
|
-
*
|
|
21074
|
-
* 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值
|
|
21075
|
-
* (CCC 根 / 账号 / 用户 id)。**纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在
|
|
21076
|
-
* CCC 的角色提示词文件里(如 `.opencode/skiff/<role>.md` 的「输出通道」节),CCC 可随时改,
|
|
21077
|
-
* 且 v1.30.15 起角色提示词热重载——改完立即生效,无需 ACC 发版。
|
|
21078
|
-
*
|
|
21079
|
-
* 为什么不用 ACC 写措辞(历史教训):v1.30.10~v1.30.15 由 ACC 内嵌一段纪律文案,用户实测
|
|
21080
|
-
* "约束不够,LLM 不听"——且措辞迭代要动插件代码 + 发版 + 重启;把措辞放 CCC 才能快速迭代,
|
|
21081
|
-
* 也符合"ACC 管机制、CCC 管内容"的归属二分。
|
|
20684
|
+
* 工具调用是否为「成功的微信发送」。
|
|
21082
20685
|
*
|
|
21083
|
-
*
|
|
21084
|
-
*
|
|
21085
|
-
*
|
|
21086
|
-
* im-bridge({channel:"weixin", action:"send", account:"<account>", user:"<user>", text:"<回复>"})
|
|
21087
|
-
* ```
|
|
20686
|
+
* v1.31.0 起有两种合法形态(标记与闸门同时认,迁移期并存):
|
|
20687
|
+
* - `im-bridge({channel:"weixin", action:"send"|"send-file", …})` —— ACC 工具(推荐)
|
|
20688
|
+
* - `msm("weixin-send", ["send"|"send-file", …])` —— CCC MSM(v1.30.x 通道,逐步退役)
|
|
21088
20689
|
*/
|
|
21089
|
-
function
|
|
21090
|
-
|
|
20690
|
+
function isSuccessfulWeixinSend(exec, result) {
|
|
20691
|
+
const e = exec;
|
|
20692
|
+
if (result?.isError === true) return false;
|
|
20693
|
+
const args = e?.arguments;
|
|
20694
|
+
if (e?.name === "im-bridge") {
|
|
20695
|
+
if ((typeof args?.channel === "string" ? args.channel : "") !== "weixin") return false;
|
|
20696
|
+
const action = typeof args?.action === "string" ? args.action : "";
|
|
20697
|
+
return action === "send" || action === "send-file";
|
|
20698
|
+
}
|
|
20699
|
+
if (e?.name === "msm") return args?.name === "weixin-send";
|
|
20700
|
+
return false;
|
|
20701
|
+
}
|
|
20702
|
+
/** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
|
|
20703
|
+
function buildManualOutputRebuke(session) {
|
|
20704
|
+
return [
|
|
20705
|
+
"[serenity:weixin-manual-output] no send detected in this turn — the user has received NOTHING.",
|
|
20706
|
+
`ccc=${session.root} account=${session.accountId} user=${session.userId}`,
|
|
20707
|
+
`im-bridge({channel:"weixin", action:"send", account:"${session.accountId}", user:"${session.userId}", text:"<回复>"})`
|
|
20708
|
+
].join("\n");
|
|
21091
20709
|
}
|
|
21092
20710
|
/**
|
|
21093
|
-
*
|
|
21094
|
-
*
|
|
21095
|
-
* 四个条件同时成立才兜底:
|
|
21096
|
-
* ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默,向后兼容)
|
|
21097
|
-
* ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
|
|
21098
|
-
* ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
|
|
21099
|
-
* ④ 有最终文本可转发(空文本无意义)
|
|
20711
|
+
* 装配闸门(index.ts apply 调用)。
|
|
20712
|
+
* 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 轨迹约束会话)零干预。
|
|
21100
20713
|
*/
|
|
21101
|
-
function
|
|
21102
|
-
|
|
20714
|
+
function registerWeixinOutputGuard(ctx) {
|
|
20715
|
+
try {
|
|
20716
|
+
ctx.on("tools/post-execute", async (exec, result, next) => {
|
|
20717
|
+
const downstream = await next();
|
|
20718
|
+
try {
|
|
20719
|
+
const sessionId = sessionIdOf$1(exec?.agent);
|
|
20720
|
+
if (sessionId && manualSessions.has(sessionId) && isSuccessfulWeixinSend(exec, result)) {
|
|
20721
|
+
sentThisTurn.add(sessionId);
|
|
20722
|
+
rebukeCounts.delete(sessionId);
|
|
20723
|
+
}
|
|
20724
|
+
} catch {}
|
|
20725
|
+
return downstream;
|
|
20726
|
+
});
|
|
20727
|
+
} catch {
|
|
20728
|
+
console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:tools/post-execute 不可用");
|
|
20729
|
+
return;
|
|
20730
|
+
}
|
|
20731
|
+
try {
|
|
20732
|
+
ctx.on("agent/turn-stopping", (payload) => {
|
|
20733
|
+
const agent = payload?.agent;
|
|
20734
|
+
if (!agent) return;
|
|
20735
|
+
const sessionId = sessionIdOf$1(agent);
|
|
20736
|
+
if (!sessionId) return;
|
|
20737
|
+
const session = manualSessions.get(sessionId);
|
|
20738
|
+
if (!session) return;
|
|
20739
|
+
if (sentThisTurn.has(sessionId)) return;
|
|
20740
|
+
const count = (rebukeCounts.get(sessionId) ?? 0) + 1;
|
|
20741
|
+
if (count > 2) {
|
|
20742
|
+
rebukeCounts.delete(sessionId);
|
|
20743
|
+
console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role})——本轮用户将收不到 agent 自己发的消息`);
|
|
20744
|
+
return;
|
|
20745
|
+
}
|
|
20746
|
+
rebukeCounts.set(sessionId, count);
|
|
20747
|
+
try {
|
|
20748
|
+
agent.steer(createUserMessage({
|
|
20749
|
+
content: [{
|
|
20750
|
+
type: "text",
|
|
20751
|
+
text: buildManualOutputRebuke(session)
|
|
20752
|
+
}],
|
|
20753
|
+
source: PLUGIN_SOURCE
|
|
20754
|
+
}));
|
|
20755
|
+
console.log(`[serenity-hooks] weixin 输出闸门:打回 ${count}/2(session=${sessionId}, turn=${payload.turn ?? "?"})`);
|
|
20756
|
+
} catch (error) {
|
|
20757
|
+
console.warn(`[serenity-hooks] ✗ weixin 输出闸门 steer 失败: ${String(error?.message ?? error)}`);
|
|
20758
|
+
}
|
|
20759
|
+
});
|
|
20760
|
+
} catch {
|
|
20761
|
+
console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:agent/turn-stopping 不可用");
|
|
20762
|
+
return;
|
|
20763
|
+
}
|
|
20764
|
+
guardActive = true;
|
|
21103
20765
|
}
|
|
20766
|
+
//#endregion
|
|
20767
|
+
//#region src/im/inbound-event.ts
|
|
20768
|
+
/** 媒体大小上限(M6:20MB,对齐 vlm-describe;超限降级告知不落盘) */
|
|
20769
|
+
const MEDIA_MAX_BYTES = 20971520;
|
|
21104
20770
|
/**
|
|
21105
20771
|
* 订阅表里的 `ccc` **别名 → CCC 根**(P0-3)。
|
|
21106
20772
|
*
|
|
21107
|
-
*
|
|
21108
|
-
*
|
|
21109
|
-
*
|
|
21110
|
-
* @returns 命中的 CCC 根;枚举里没有 ⇒ `null`(调用方**响亮报错**,不静默丢弃)
|
|
20773
|
+
* 别名取值与面板下拉**同源** ⇒ 先按 `CccEntry.name`(= CCC 根目录名)匹配;
|
|
20774
|
+
* 再退一步接受**绝对路径**(手写配置时人会直接粘路径)。两条都试
|
|
20775
|
+
* ⇒ 两种写法都可用,且**不引入第二张名字表**。
|
|
21111
20776
|
*/
|
|
21112
20777
|
async function resolveCccRootByAlias(ctx, alias) {
|
|
21113
20778
|
try {
|
|
21114
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20779
|
+
const { listCccs } = await import("./ccc-roots-B2t_aM8L.js").then((n) => n.a);
|
|
21115
20780
|
const entries = await listCccs(ctx);
|
|
21116
20781
|
const byName = entries.find((e) => e.name === alias);
|
|
21117
20782
|
if (byName) return byName.root;
|
|
@@ -21128,11 +20793,9 @@ async function resolveCccRootByAlias(ctx, alias) {
|
|
|
21128
20793
|
* 判据**,差别只有一格 —— **有没有 CCC 级回退**(`cccFallback`)。写两遍必然分叉。
|
|
21129
20794
|
*
|
|
21130
20795
|
* 三档(方案 §3.1,**刻意分档**):
|
|
21131
|
-
* - **未命中** ⇒ 不投递 +
|
|
21132
|
-
* - **表残缺** ⇒
|
|
21133
|
-
* - **目标不存在**(CCC / **轨迹**)⇒
|
|
21134
|
-
*
|
|
21135
|
-
* @param input.cccFallback 过渡期回退(方案 Q5);**机器级模式传 `null`** ⇒ 未命中即不投递
|
|
20796
|
+
* - **未命中** ⇒ 不投递 + **留痕**(静默档:这是"表里没有你",不是错误)
|
|
20797
|
+
* - **表残缺** ⇒ **响亮出声**(且**独立于是否命中**)
|
|
20798
|
+
* - **目标不存在**(CCC / **轨迹**)⇒ **响亮报错**(不静默丢弃)
|
|
21136
20799
|
*/
|
|
21137
20800
|
async function resolveInboundTarget(ctx, input) {
|
|
21138
20801
|
const route = planInboundRoute({
|
|
@@ -21181,7 +20844,7 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
21181
20844
|
});
|
|
21182
20845
|
return null;
|
|
21183
20846
|
}
|
|
21184
|
-
const { findDirNameByCode } = await import("./profile-
|
|
20847
|
+
const { findDirNameByCode } = await import("./profile-BVt84mbi.js").then((n) => n.i);
|
|
21185
20848
|
if (findDirNameByCode(root, target.role) === null) {
|
|
21186
20849
|
const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
|
|
21187
20850
|
console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
|
|
@@ -21195,7 +20858,7 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
21195
20858
|
});
|
|
21196
20859
|
return null;
|
|
21197
20860
|
}
|
|
21198
|
-
const profile =
|
|
20861
|
+
const profile = readTrajectoryConstraintByCode(root, target.role);
|
|
21199
20862
|
return {
|
|
21200
20863
|
root,
|
|
21201
20864
|
trajectory: target.role,
|
|
@@ -21203,33 +20866,21 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
21203
20866
|
};
|
|
21204
20867
|
}
|
|
21205
20868
|
/**
|
|
21206
|
-
*
|
|
20869
|
+
* **一条入站消息的主流程**(通道中立)。
|
|
21207
20870
|
*
|
|
21208
|
-
*
|
|
21209
|
-
*
|
|
21210
|
-
* 固定 id resume-or-create(v1.27.2):磁盘已有持久化 log → resume(历史延续,
|
|
21211
|
-
* 重启后记忆保留——真正的"同用户长期延续");无 log(首次)→ create。
|
|
21212
|
-
* "新的对话已开始"通知仅真正首次(create)时发送;resume/进程内延续不发。
|
|
20871
|
+
* 职责:路由 → 固定会话 id 创建/延续 → 提问 → 回复(或被闸门拦下后兜底)。
|
|
20872
|
+
* 语料(媒体)由事件自带 `fetch()`;出站走的 `io`。
|
|
21213
20873
|
*
|
|
21214
|
-
*
|
|
21215
|
-
*
|
|
21216
|
-
* (无需下载/ASR);语音无转写 → 降级提示"暂时无法解析"
|
|
21217
|
-
* - **正在输入**:处理前 sendtyping 1(微信显示"正在输入..."),处理后(含异常)0
|
|
21218
|
-
* - **媒体接收(图片/文件)**:桥侧 CDN 下载 + AES 解密 → 落盘 CCC 根
|
|
21219
|
-
* `_tmp/weixin-inbound/<userhash>/` → question 注入「存在性 + 路径」(ACC 层只保证
|
|
21220
|
-
* 可达性——"让会话知道文件的存在并可以拿到";识别/解析归角色 LLM 决策,不编排)。
|
|
21221
|
-
* 降级不静默:下载失败 / 超 20MB → 注入说明进对话;typing 窗口覆盖下载(M7)。
|
|
20874
|
+
* 🔴 **本函数没有任何 iLink 概念** —— 这是 B1 的判据。任何 `context_token` /
|
|
20875
|
+
* `item_list` / `message_type` 出现在这里,就说明抽缝没抽干净。
|
|
21222
20876
|
*
|
|
21223
|
-
*
|
|
21224
|
-
* **回退目标**)。两档语义刻意分开(方案 §3.1):**未命中 ⇒ 不投递 + 留痕**(静默档);
|
|
21225
|
-
* **目标不存在(CCC / 角色)⇒ 响亮报错**(`console.error` + 机器级留痕)。两者都
|
|
21226
|
-
* **不是**"投给默认角色"。
|
|
20877
|
+
* @param preset 机器级模式:目标已解析好 ⇒ **不再自行路由**,且**不看本 CCC 的 `enabled`**
|
|
21227
20878
|
*/
|
|
21228
|
-
async function
|
|
21229
|
-
const fromUserId =
|
|
20879
|
+
async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
|
|
20880
|
+
const fromUserId = event.userId;
|
|
21230
20881
|
if (!fromUserId) return;
|
|
21231
|
-
const text =
|
|
21232
|
-
const mediaRefs =
|
|
20882
|
+
const text = event.text;
|
|
20883
|
+
const mediaRefs = event.media;
|
|
21233
20884
|
const settings = readWeixinSettings(root);
|
|
21234
20885
|
let resolved;
|
|
21235
20886
|
if (preset) resolved = preset.target;
|
|
@@ -21249,24 +20900,22 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21249
20900
|
const { trajectory, profile } = resolved;
|
|
21250
20901
|
root = resolved.root;
|
|
21251
20902
|
if (!text && mediaRefs.length === 0) {
|
|
21252
|
-
if (
|
|
21253
|
-
baseUrl: cred.baseUrl,
|
|
21254
|
-
token: cred.token,
|
|
20903
|
+
if (event.hasVoice) await io.sendText({
|
|
21255
20904
|
toUserId: fromUserId,
|
|
21256
20905
|
text: "(抱歉,暂时无法解析这条语音消息,请尝试发送文字)",
|
|
21257
|
-
|
|
20906
|
+
replyRef: event.replyRef
|
|
21258
20907
|
}).catch(() => {});
|
|
21259
20908
|
return;
|
|
21260
20909
|
}
|
|
21261
20910
|
try {
|
|
21262
20911
|
const sessionId = weixinSessionIdForTrajectory(trajectory);
|
|
21263
|
-
const existing =
|
|
20912
|
+
const existing = getTrajectoryAgent(sessionId);
|
|
21264
20913
|
const hc = readCheapWorkerConfig(root);
|
|
21265
20914
|
const ref = existing ? {
|
|
21266
20915
|
agent: existing,
|
|
21267
20916
|
sessionId,
|
|
21268
20917
|
resumed: true
|
|
21269
|
-
} : await
|
|
20918
|
+
} : await createTrajectoryAgent(ctx, root, profile ?? {
|
|
21270
20919
|
dirName: trajectory,
|
|
21271
20920
|
code: trajectory,
|
|
21272
20921
|
path: "",
|
|
@@ -21278,25 +20927,22 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21278
20927
|
rebuild: true
|
|
21279
20928
|
}
|
|
21280
20929
|
}, hc?.defaultModel, sessionId);
|
|
21281
|
-
if (!ref.resumed) await
|
|
21282
|
-
baseUrl: cred.baseUrl,
|
|
21283
|
-
token: cred.token,
|
|
20930
|
+
if (!ref.resumed) await io.sendText({
|
|
21284
20931
|
toUserId: fromUserId,
|
|
21285
20932
|
text: "(新的对话已开始)",
|
|
21286
|
-
|
|
20933
|
+
replyRef: event.replyRef
|
|
21287
20934
|
}).catch(() => {});
|
|
21288
|
-
await
|
|
20935
|
+
await io.typingStart?.({
|
|
20936
|
+
toUserId: fromUserId,
|
|
20937
|
+
replyRef: event.replyRef
|
|
20938
|
+
});
|
|
21289
20939
|
try {
|
|
21290
20940
|
const mediaNotes = [];
|
|
21291
20941
|
const degradedNotes = [];
|
|
21292
20942
|
const hookMedia = [];
|
|
21293
20943
|
for (const mediaRef of mediaRefs) {
|
|
21294
|
-
const mediaType = mediaRef.kind === "image" ? "image_item" : "file_item";
|
|
21295
20944
|
const label = mediaRef.kind === "image" ? "一张图片" : `文件 ${mediaRef.fileName ?? "(未命名)"}`;
|
|
21296
|
-
const result = await
|
|
21297
|
-
item: mediaRef.item,
|
|
21298
|
-
mediaType
|
|
21299
|
-
});
|
|
20945
|
+
const result = await mediaRef.fetch();
|
|
21300
20946
|
if (!result) {
|
|
21301
20947
|
hookMedia.push({
|
|
21302
20948
|
kind: mediaRef.kind,
|
|
@@ -21316,7 +20962,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21316
20962
|
try {
|
|
21317
20963
|
const inboundDir = weixinInboundDir(root, fromUserId);
|
|
21318
20964
|
mkdirSync(inboundDir, { recursive: true });
|
|
21319
|
-
const fname = mediaRef.kind === "image" ? `img_${Date.now()}_${randomBytes(4).toString("hex")}.${sniffImageExt(result.data)}` : sanitizeFileName$1(result.fileName ?? "") || `file_${Date.now()}_${randomBytes(4).toString("hex")}`;
|
|
20965
|
+
const fname = mediaRef.kind === "image" ? `img_${Date.now()}_${randomBytes(4).toString("hex")}.${sniffImageExt(result.data)}` : sanitizeFileName$1(result.fileName ?? mediaRef.fileName ?? "") || `file_${Date.now()}_${randomBytes(4).toString("hex")}`;
|
|
21320
20966
|
const abs = join(inboundDir, fname);
|
|
21321
20967
|
writeFileSync(abs, result.data);
|
|
21322
20968
|
const rel = relative(root, abs);
|
|
@@ -21330,11 +20976,11 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21330
20976
|
kind: mediaRef.kind,
|
|
21331
20977
|
relPath: null
|
|
21332
20978
|
});
|
|
21333
|
-
degradedNotes.push(
|
|
20979
|
+
degradedNotes.push("(媒体保存失败,请重试)");
|
|
21334
20980
|
}
|
|
21335
20981
|
}
|
|
21336
20982
|
const parts = [];
|
|
21337
|
-
const workspaceMd =
|
|
20983
|
+
const workspaceMd = trajectoryWorkspaceMdPath(root, sessionId) ?? getActiveSessionInfo(sessionId)?.mdPath ?? null;
|
|
21338
20984
|
if (workspaceMd) {
|
|
21339
20985
|
if (!ensureWorkspacePromptSection(ref.agent, sessionId, root)) parts.push(workspaceCognitiveTrajectoryLine(workspaceMd));
|
|
21340
20986
|
}
|
|
@@ -21349,7 +20995,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21349
20995
|
userId: fromUserId,
|
|
21350
20996
|
role: trajectory
|
|
21351
20997
|
});
|
|
21352
|
-
parts.push(
|
|
20998
|
+
parts.push(manualOutputMarker(accountId, fromUserId));
|
|
21353
20999
|
} else forgetManualOutputSession(sessionId);
|
|
21354
21000
|
const question = parts.join("\n");
|
|
21355
21001
|
const hookRel = settings.hook;
|
|
@@ -21362,9 +21008,9 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21362
21008
|
text,
|
|
21363
21009
|
media: hookMedia
|
|
21364
21010
|
})).catch((err) => {
|
|
21365
|
-
console.log(`[serenity-hooks]
|
|
21011
|
+
console.log(`[serenity-hooks] im hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
|
|
21366
21012
|
});
|
|
21367
|
-
const answer = (await
|
|
21013
|
+
const answer = (await askTrajectory(ctx, ref.agent, question)).answer ?? "";
|
|
21368
21014
|
if (manualOutput) {
|
|
21369
21015
|
if (!manualOutputFallbackNeeded({
|
|
21370
21016
|
fallbackOnNoSend: settings.fallbackOnNoSend,
|
|
@@ -21373,14 +21019,12 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21373
21019
|
answer
|
|
21374
21020
|
})) return;
|
|
21375
21021
|
const reply = markdownToPlainText(stripThink(answer));
|
|
21376
|
-
await
|
|
21377
|
-
baseUrl: cred.baseUrl,
|
|
21378
|
-
token: cred.token,
|
|
21022
|
+
await io.sendText({
|
|
21379
21023
|
toUserId: fromUserId,
|
|
21380
21024
|
text: reply,
|
|
21381
|
-
|
|
21025
|
+
replyRef: event.replyRef
|
|
21382
21026
|
});
|
|
21383
|
-
console.warn(`[serenity-hooks] ⚠
|
|
21027
|
+
console.warn(`[serenity-hooks] ⚠ im 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
|
|
21384
21028
|
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
21385
21029
|
cccRoot: root,
|
|
21386
21030
|
accountId,
|
|
@@ -21390,18 +21034,16 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21390
21034
|
reply,
|
|
21391
21035
|
source: "reply-fallback"
|
|
21392
21036
|
})).catch((err) => {
|
|
21393
|
-
console.log(`[serenity-hooks]
|
|
21037
|
+
console.log(`[serenity-hooks] im hook outgoing(reply-fallback) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
21394
21038
|
});
|
|
21395
21039
|
return;
|
|
21396
21040
|
}
|
|
21397
21041
|
if (answer === "") return;
|
|
21398
21042
|
const reply = markdownToPlainText(stripThink(answer));
|
|
21399
|
-
await
|
|
21400
|
-
baseUrl: cred.baseUrl,
|
|
21401
|
-
token: cred.token,
|
|
21043
|
+
await io.sendText({
|
|
21402
21044
|
toUserId: fromUserId,
|
|
21403
21045
|
text: reply,
|
|
21404
|
-
|
|
21046
|
+
replyRef: event.replyRef
|
|
21405
21047
|
});
|
|
21406
21048
|
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
21407
21049
|
cccRoot: root,
|
|
@@ -21411,28 +21053,369 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21411
21053
|
role: trajectory,
|
|
21412
21054
|
reply
|
|
21413
21055
|
})).catch((err) => {
|
|
21414
|
-
console.log(`[serenity-hooks]
|
|
21056
|
+
console.log(`[serenity-hooks] im hook outgoing error: ${err instanceof Error ? err.message : String(err)}`);
|
|
21415
21057
|
});
|
|
21416
21058
|
} finally {
|
|
21417
|
-
await
|
|
21059
|
+
await io.typingStop?.({ toUserId: fromUserId });
|
|
21418
21060
|
}
|
|
21419
21061
|
} catch (err) {
|
|
21420
21062
|
const msg = err instanceof Error ? err.message : String(err);
|
|
21421
|
-
console.log(`[serenity-hooks]
|
|
21063
|
+
console.log(`[serenity-hooks] im-bridge error (ccc=${root}): ${msg}`);
|
|
21422
21064
|
const stack = err instanceof Error ? err.stack : void 0;
|
|
21423
|
-
if (stack) console.log(`[serenity-hooks]
|
|
21065
|
+
if (stack) console.log(`[serenity-hooks] im-bridge stack:\n${stack.slice(0, 1500)}`);
|
|
21066
|
+
}
|
|
21067
|
+
}
|
|
21068
|
+
/**
|
|
21069
|
+
* 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按「ACC 管机制、CCC 管措辞」重构)。
|
|
21070
|
+
*
|
|
21071
|
+
* 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值。
|
|
21072
|
+
* **纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在轨迹的 `PROMPT.md` 里。
|
|
21073
|
+
*/
|
|
21074
|
+
function manualOutputMarker(accountId, userId) {
|
|
21075
|
+
return ["[serenity:weixin-manual-output]", `im-bridge({channel:"weixin", action:"send", account:"${accountId}", user:"${userId}", text:"<回复>"})`].join("\n");
|
|
21076
|
+
}
|
|
21077
|
+
/**
|
|
21078
|
+
* 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
|
|
21079
|
+
*
|
|
21080
|
+
* 四个条件同时成立才兜底:
|
|
21081
|
+
* ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默)
|
|
21082
|
+
* ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
|
|
21083
|
+
* ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
|
|
21084
|
+
* ④ 有最终文本可转发(空文本无意义)
|
|
21085
|
+
*/
|
|
21086
|
+
function manualOutputFallbackNeeded(input) {
|
|
21087
|
+
return input.fallbackOnNoSend === true && input.guardActive && !input.sentThisTurn && input.answer !== "";
|
|
21088
|
+
}
|
|
21089
|
+
//#endregion
|
|
21090
|
+
//#region src/im/feishu-account.ts
|
|
21091
|
+
/** 去重集合上限(飞书重推窗口有限;限量防内存无界增长) */
|
|
21092
|
+
const SEEN_LIMIT = 2e3;
|
|
21093
|
+
/** 已处理过的 message_id(超时重推去重;按账号隔离) */
|
|
21094
|
+
const seenByAccount = /* @__PURE__ */ new Map();
|
|
21095
|
+
/** 取 SDK(未注入 ⇒ 动态 import 真包) */
|
|
21096
|
+
async function loadFeishuSdk() {
|
|
21097
|
+
return await import("./lib-TxscFKeo.js").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
|
|
21098
|
+
}
|
|
21099
|
+
/**
|
|
21100
|
+
* 账号 → 长连接客户端(`ImAccountLoop` 的形状是面板既有契约,**不加字段** ——
|
|
21101
|
+
* 加字段会逼面板与 `im-bridge status` 认识一个通道特有的东西)。
|
|
21102
|
+
* 用途:`stopped` 置位后能真正 `close()` 掉连接(只置标志位而不关连接 = 假停止)。
|
|
21103
|
+
*/
|
|
21104
|
+
const wsByAccount = /* @__PURE__ */ new Map();
|
|
21105
|
+
function seenSet(accountId) {
|
|
21106
|
+
let s = seenByAccount.get(accountId);
|
|
21107
|
+
if (!s) {
|
|
21108
|
+
s = /* @__PURE__ */ new Set();
|
|
21109
|
+
seenByAccount.set(accountId, s);
|
|
21110
|
+
}
|
|
21111
|
+
return s;
|
|
21112
|
+
}
|
|
21113
|
+
/**
|
|
21114
|
+
* 飞书凭据就绪判据:**appId 与 appSecret 都非空**。
|
|
21115
|
+
* (微信只需 token;飞书是双要素,缺任一起不了长连接。)
|
|
21116
|
+
*/
|
|
21117
|
+
function feishuCredentialReady(cred) {
|
|
21118
|
+
const appId = typeof cred.appId === "string" ? cred.appId.trim() : "";
|
|
21119
|
+
const secret = typeof cred.token === "string" ? cred.token.trim() : "";
|
|
21120
|
+
return appId !== "" && secret !== "";
|
|
21121
|
+
}
|
|
21122
|
+
/**
|
|
21123
|
+
* 从飞书 `im.message.receive_v1` 事件里取出**中立入站事件**。
|
|
21124
|
+
*
|
|
21125
|
+
* 🔴 这是 P3 唯一的"形状翻译"处,与 `bridge.ts` 里 iLink 的那一处对称。
|
|
21126
|
+
* ⚠️ 用户标识用 **open_id**(不是 chat_id):路由表是"人 → 轨迹",
|
|
21127
|
+
* 而 chat_id 在群里会变、在单聊里也与人不是一一对应。
|
|
21128
|
+
* @returns 无可用发送者/消息体 ⇒ `null`(调用方丢弃并**出声**,不静默)
|
|
21129
|
+
*/
|
|
21130
|
+
function feishuEventToInbound(data) {
|
|
21131
|
+
const sender = data.sender;
|
|
21132
|
+
const message = data.message;
|
|
21133
|
+
const userId = sender?.sender_id?.open_id;
|
|
21134
|
+
if (!userId || !message?.message_id) return null;
|
|
21135
|
+
let text = null;
|
|
21136
|
+
if (message.message_type === "text" && typeof message.content === "string") try {
|
|
21137
|
+
const parsed = JSON.parse(message.content);
|
|
21138
|
+
text = (typeof parsed.text === "string" ? parsed.text.trim() : "").replace(/@_user_\d+/g, "").trim();
|
|
21139
|
+
if (text === "") text = null;
|
|
21140
|
+
} catch {
|
|
21141
|
+
text = null;
|
|
21142
|
+
}
|
|
21143
|
+
return {
|
|
21144
|
+
userId,
|
|
21145
|
+
text,
|
|
21146
|
+
media: [],
|
|
21147
|
+
hasVoice: false,
|
|
21148
|
+
replyRef: void 0
|
|
21149
|
+
};
|
|
21150
|
+
}
|
|
21151
|
+
/**
|
|
21152
|
+
* 造出站动作面(飞书)。
|
|
21153
|
+
*
|
|
21154
|
+
* 🔴 `content` 必须是 **JSON 字符串**:`JSON.stringify({ text })` —— 这是官方形态,
|
|
21155
|
+
* 直接给裸文本会被服务端拒绝(且错误码不直观)。
|
|
21156
|
+
*/
|
|
21157
|
+
function feishuIo(client) {
|
|
21158
|
+
const c = client;
|
|
21159
|
+
return { sendText: async ({ toUserId, text }) => {
|
|
21160
|
+
const create = c.im?.v1?.message?.create;
|
|
21161
|
+
if (typeof create !== "function") throw new Error("飞书 SDK 缺少 im.v1.message.create");
|
|
21162
|
+
await create({
|
|
21163
|
+
params: { receive_id_type: "open_id" },
|
|
21164
|
+
data: {
|
|
21165
|
+
receive_id: toUserId,
|
|
21166
|
+
msg_type: "text",
|
|
21167
|
+
content: JSON.stringify({ text: markdownToPlainText(text) })
|
|
21168
|
+
}
|
|
21169
|
+
});
|
|
21170
|
+
} };
|
|
21171
|
+
}
|
|
21172
|
+
/**
|
|
21173
|
+
* 单账号的飞书长连接循环。
|
|
21174
|
+
*
|
|
21175
|
+
* 与微信的**关键结构差别**:微信是 `while` 轮询,飞书是**事件回调**(长连接推)。
|
|
21176
|
+
* 所以这里的"循环"其实是"建连一次,然后等回调";`stopped` 靠 `close()` 兑现。
|
|
21177
|
+
*/
|
|
21178
|
+
async function runFeishuAccountLoop(input, loop) {
|
|
21179
|
+
const cred = input.cred;
|
|
21180
|
+
const appId = cred.appId ?? "";
|
|
21181
|
+
const appSecret = cred.token ?? "";
|
|
21182
|
+
if (!appId || !appSecret) {
|
|
21183
|
+
loop.lastError = "飞书凭据不全(缺 appId 或 appSecret)";
|
|
21184
|
+
return;
|
|
21185
|
+
}
|
|
21186
|
+
const Lark = await loadFeishuSdk();
|
|
21187
|
+
const client = new Lark.Client({
|
|
21188
|
+
appId,
|
|
21189
|
+
appSecret
|
|
21190
|
+
});
|
|
21191
|
+
const dispatcher = new Lark.EventDispatcher({});
|
|
21192
|
+
const queue = [];
|
|
21193
|
+
let draining = false;
|
|
21194
|
+
const drain = async () => {
|
|
21195
|
+
if (draining) return;
|
|
21196
|
+
draining = true;
|
|
21197
|
+
try {
|
|
21198
|
+
while (queue.length > 0 && !loop.stopped) {
|
|
21199
|
+
const item = queue.shift();
|
|
21200
|
+
loop.lastPollAt = Date.now();
|
|
21201
|
+
try {
|
|
21202
|
+
const event = feishuEventToInbound(item.data);
|
|
21203
|
+
if (!event) {
|
|
21204
|
+
loop.lastError = "收到飞书事件但取不出发送者/消息体";
|
|
21205
|
+
continue;
|
|
21206
|
+
}
|
|
21207
|
+
const msgId = item.data.message?.message_id ?? "";
|
|
21208
|
+
if (msgId !== "") {
|
|
21209
|
+
const seen = seenSet(input.accountId);
|
|
21210
|
+
if (seen.has(msgId)) continue;
|
|
21211
|
+
seen.add(msgId);
|
|
21212
|
+
if (seen.size > SEEN_LIMIT) {
|
|
21213
|
+
const oldest = seen.values().next().value;
|
|
21214
|
+
if (oldest !== void 0) seen.delete(oldest);
|
|
21215
|
+
}
|
|
21216
|
+
}
|
|
21217
|
+
const ctx = input.ctx;
|
|
21218
|
+
const io = feishuIo(client);
|
|
21219
|
+
if (input.machine) {
|
|
21220
|
+
const target = await resolveInboundTarget(ctx, {
|
|
21221
|
+
account: input.accountId,
|
|
21222
|
+
user: event.userId,
|
|
21223
|
+
cccFallback: null
|
|
21224
|
+
});
|
|
21225
|
+
if (!target) continue;
|
|
21226
|
+
await handleInboundEvent(ctx, target.root, input.accountId, io, event, { target });
|
|
21227
|
+
} else await handleInboundEvent(ctx, input.root, input.accountId, io, event);
|
|
21228
|
+
} catch (err) {
|
|
21229
|
+
loop.lastError = err instanceof Error ? err.message : String(err);
|
|
21230
|
+
}
|
|
21231
|
+
}
|
|
21232
|
+
} finally {
|
|
21233
|
+
draining = false;
|
|
21234
|
+
}
|
|
21235
|
+
};
|
|
21236
|
+
dispatcher.register({ "im.message.receive_v1": (data) => {
|
|
21237
|
+
queue.push({ data });
|
|
21238
|
+
drain();
|
|
21239
|
+
} });
|
|
21240
|
+
const ws = new Lark.WSClient({
|
|
21241
|
+
appId,
|
|
21242
|
+
appSecret
|
|
21243
|
+
});
|
|
21244
|
+
wsByAccount.set(input.accountId, ws);
|
|
21245
|
+
try {
|
|
21246
|
+
await ws.start({ eventDispatcher: dispatcher });
|
|
21247
|
+
} catch (err) {
|
|
21248
|
+
loop.lastError = `飞书长连接建立失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
21249
|
+
}
|
|
21250
|
+
}
|
|
21251
|
+
/** 飞书账号实现(登记名 = `feishu`;与配置里的 `channel` 取值一致) */
|
|
21252
|
+
const feishuAccountImpl = {
|
|
21253
|
+
channel: "feishu",
|
|
21254
|
+
isReady: feishuCredentialReady,
|
|
21255
|
+
notReadyReason: () => "**缺 appId 或 appSecret**(未绑定)",
|
|
21256
|
+
startLoop(input) {
|
|
21257
|
+
const loop = {
|
|
21258
|
+
stopped: false,
|
|
21259
|
+
lastPollAt: 0
|
|
21260
|
+
};
|
|
21261
|
+
runFeishuAccountLoop(input, loop);
|
|
21262
|
+
return loop;
|
|
21263
|
+
}
|
|
21264
|
+
};
|
|
21265
|
+
/**
|
|
21266
|
+
* 注册飞书实现(`bridge.ts` 在**模块求值时**调用)。
|
|
21267
|
+
*
|
|
21268
|
+
* ⚠️ 与微信同款教训(续 220 实测 12 例红):注册必须在**模块求值**时发生,
|
|
21269
|
+
* 不能等 `apply` —— 测试与面板会**直接**调 `syncCccBridge` / `syncHumanChannel`。
|
|
21270
|
+
*/
|
|
21271
|
+
function installFeishuAccountImpl() {
|
|
21272
|
+
registerImAccountImpl(feishuAccountImpl);
|
|
21273
|
+
}
|
|
21274
|
+
//#endregion
|
|
21275
|
+
//#region src/im/bridge.ts
|
|
21276
|
+
var bridge_exports = /* @__PURE__ */ __exportAll({
|
|
21277
|
+
handleIncoming: () => handleIncoming,
|
|
21278
|
+
handleMachineIncoming: () => handleMachineIncoming,
|
|
21279
|
+
registerWeixinBridge: () => registerWeixinBridge,
|
|
21280
|
+
resolveInboundTarget: () => resolveInboundTarget,
|
|
21281
|
+
resolveWeixinAccount: () => resolveWeixinAccount,
|
|
21282
|
+
sendProactiveText: () => sendProactiveText,
|
|
21283
|
+
stopAllBridges: () => stopAllBridges,
|
|
21284
|
+
stopCccBridge: () => stopCccBridge,
|
|
21285
|
+
syncCccBridge: () => syncCccBridge,
|
|
21286
|
+
syncHumanChannel: () => syncHumanChannel,
|
|
21287
|
+
weixinBridgeStatus: () => weixinBridgeStatus
|
|
21288
|
+
});
|
|
21289
|
+
const bridges = /* @__PURE__ */ new Map();
|
|
21290
|
+
/**
|
|
21291
|
+
* 🔴 **机器级 poller 的"根"哨兵**(P0-3 A)—— 它**不是一个目录**。
|
|
21292
|
+
*
|
|
21293
|
+
* 机器级 poller **不属于任何 CCC**(这正是"上移 ACC 层"的含义:谁轮询不再由 CCC 数量决定)。
|
|
21294
|
+
* 用途有二:① `runAccountLoop` 的 `root` 形参在机器级模式下不被使用,传它以示"无根";
|
|
21295
|
+
* ② 归属表(`im/inbound.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
|
|
21296
|
+
* 与任何**真实** CCC 根都不会撞(真根一律是绝对路径)。
|
|
21297
|
+
*/
|
|
21298
|
+
const MACHINE_SCOPE = "<machine>";
|
|
21299
|
+
/** 机器级 poller(账号 id → 循环)。与 `bridges` **互斥**:同一时刻只有一种模式在跑。 */
|
|
21300
|
+
const machineLoops = /* @__PURE__ */ new Map();
|
|
21301
|
+
/** typing_ticket 缓存(每 (accountId, fromUserId);getconfig 一次后续复用——对齐参考实现 typingTicketCache) */
|
|
21302
|
+
const typingTicketByUser = /* @__PURE__ */ new Map();
|
|
21303
|
+
function typingCacheKey(accountId, fromUserId) {
|
|
21304
|
+
return `${accountId}|${fromUserId}`;
|
|
21305
|
+
}
|
|
21306
|
+
/** 处理开始:getconfig(无缓存时)→ sendtyping status=1(微信侧显示"正在输入...")。
|
|
21307
|
+
* 任何失败静默(typing 不影响主流程——对齐参考实现 typingCallbacks try/catch 吞错)。 */
|
|
21308
|
+
async function sendTypingStart(cred, accountId, fromUserId, contextToken) {
|
|
21309
|
+
try {
|
|
21310
|
+
const key = typingCacheKey(accountId, fromUserId);
|
|
21311
|
+
let ticket = typingTicketByUser.get(key);
|
|
21312
|
+
if (!ticket) {
|
|
21313
|
+
ticket = (await getConfig({
|
|
21314
|
+
baseUrl: cred.baseUrl,
|
|
21315
|
+
token: cred.token,
|
|
21316
|
+
ilinkUserId: fromUserId,
|
|
21317
|
+
contextToken: contextToken ?? ""
|
|
21318
|
+
})).typing_ticket;
|
|
21319
|
+
if (ticket) typingTicketByUser.set(key, ticket);
|
|
21320
|
+
}
|
|
21321
|
+
if (!ticket) return;
|
|
21322
|
+
await sendTyping({
|
|
21323
|
+
baseUrl: cred.baseUrl,
|
|
21324
|
+
token: cred.token,
|
|
21325
|
+
ilinkUserId: fromUserId,
|
|
21326
|
+
typingTicket: ticket,
|
|
21327
|
+
status: TypingStatus.TYPING
|
|
21328
|
+
});
|
|
21329
|
+
} catch (err) {
|
|
21330
|
+
console.log(`[serenity-hooks] weixin-bridge typing start skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
21331
|
+
}
|
|
21332
|
+
}
|
|
21333
|
+
/** 处理结束(含异常路径 finally):sendtyping status=0;缓存保留(ticket 可复用)。 */
|
|
21334
|
+
async function sendTypingStop(cred, accountId, fromUserId) {
|
|
21335
|
+
try {
|
|
21336
|
+
const ticket = typingTicketByUser.get(typingCacheKey(accountId, fromUserId));
|
|
21337
|
+
if (!ticket) return;
|
|
21338
|
+
await sendTyping({
|
|
21339
|
+
baseUrl: cred.baseUrl,
|
|
21340
|
+
token: cred.token,
|
|
21341
|
+
ilinkUserId: fromUserId,
|
|
21342
|
+
typingTicket: ticket,
|
|
21343
|
+
status: TypingStatus.CANCEL
|
|
21344
|
+
});
|
|
21345
|
+
} catch (err) {
|
|
21346
|
+
console.log(`[serenity-hooks] weixin-bridge typing stop skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
21424
21347
|
}
|
|
21425
21348
|
}
|
|
21426
21349
|
/**
|
|
21350
|
+
* 🔴 **2026-10-19(P2)`runAccountLoop` 已迁出本文件** —— 逐字搬到
|
|
21351
|
+
* `im/weixin-account.ts`,成为 `ImAccountImpl` 的一个实现(微信)。
|
|
21352
|
+
*
|
|
21353
|
+
* 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
|
|
21354
|
+
* owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
|
|
21355
|
+
* 归**账号自己的实现**,装配处只按 `channel` 查派发表。
|
|
21356
|
+
* ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
|
|
21357
|
+
* `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
|
|
21358
|
+
*/
|
|
21359
|
+
/**
|
|
21360
|
+
* 处理单条**微信**消息:把 iLink 形状的 `msg` 归一成**中立入站事件**,
|
|
21361
|
+
* 再交给通道中立的 `handleInboundEvent`(`im/inbound-event.ts`)走主流程。
|
|
21362
|
+
*
|
|
21363
|
+
* ## 🔴 2026-10-19(B1):本函数已退化为**薄适配器**,主流程不在本文件
|
|
21364
|
+
*
|
|
21365
|
+
* 原因:`handleIncoming` 原先同时承担两件不同的事 —— ① "iLink 消息长什么样"
|
|
21366
|
+
* ② "一条已到达的消息该投到哪、怎么答、怎么记账"。接第二家 IM 时只有 ① 需要新写,
|
|
21367
|
+
* 而 ② 应该**被两家共用**。两者混在一个函数里 ⇒ 飞书接进来只能复制一整份主流程。
|
|
21368
|
+
*
|
|
21369
|
+
* ⇒ 主流程搬到 `im/inbound-event.ts`,本函数只做**形状翻译**:
|
|
21370
|
+
* `msg`(iLink)+ `cred`(iLink 凭据)→ `ImInboundEvent` + `ImAccountIo`。
|
|
21371
|
+
*
|
|
21372
|
+
* ## 为什么保留这个签名(而不是让调用方直接调新的主流程)
|
|
21373
|
+
*
|
|
21374
|
+
* 全仓约 50 处测试按 iLink 形状驱动本函数,而 B1 的验收判据是「**微信行为逐字不变**」
|
|
21375
|
+
* (用例数守恒 + 真机一条微信)。保留签名 ⇒ 那 50 处**逐字不动**,判据可复算。
|
|
21376
|
+
*
|
|
21377
|
+
* 会话语义(不变):会话 id 由**轨迹编码**派生 ⇒ 同一条轨迹恒同一条 dsh 会话;
|
|
21378
|
+
* 进程内已有 → 延续;无 → 固定 id resume-or-create。
|
|
21379
|
+
*/
|
|
21380
|
+
async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
21381
|
+
const fromUserId = msg.from_user_id;
|
|
21382
|
+
if (!fromUserId) return;
|
|
21383
|
+
await handleInboundEvent(ctx, root, accountId, {
|
|
21384
|
+
sendText: async ({ toUserId, text, replyRef }) => {
|
|
21385
|
+
await sendTextMessage({
|
|
21386
|
+
baseUrl: cred.baseUrl,
|
|
21387
|
+
token: cred.token,
|
|
21388
|
+
toUserId,
|
|
21389
|
+
text,
|
|
21390
|
+
contextToken: replyRef
|
|
21391
|
+
});
|
|
21392
|
+
},
|
|
21393
|
+
typingStart: ({ toUserId, replyRef }) => sendTypingStart(cred, accountId, toUserId, replyRef),
|
|
21394
|
+
typingStop: ({ toUserId }) => sendTypingStop(cred, accountId, toUserId)
|
|
21395
|
+
}, {
|
|
21396
|
+
userId: fromUserId,
|
|
21397
|
+
text: extractWeixinText(msg),
|
|
21398
|
+
media: extractWeixinMedia(msg).map((ref) => ({
|
|
21399
|
+
kind: ref.kind,
|
|
21400
|
+
fileName: ref.fileName,
|
|
21401
|
+
fetch: () => downloadMedia({
|
|
21402
|
+
item: ref.item,
|
|
21403
|
+
mediaType: ref.kind === "image" ? "image_item" : "file_item"
|
|
21404
|
+
}).then((r) => r ? {
|
|
21405
|
+
data: r.data,
|
|
21406
|
+
fileName: r.fileName
|
|
21407
|
+
} : null)
|
|
21408
|
+
})),
|
|
21409
|
+
hasVoice: hasVoiceItem(msg),
|
|
21410
|
+
replyRef: msg.context_token
|
|
21411
|
+
}, preset);
|
|
21412
|
+
}
|
|
21413
|
+
/**
|
|
21427
21414
|
* **机器级 poller 的单条消息入口**(P0-3 A)。
|
|
21428
21415
|
*
|
|
21429
21416
|
* 与 `handleIncoming` 的唯一差别:**目标由机器级订阅表逐条解析,且没有 CCC 级回退**
|
|
21430
21417
|
* (方案 §3.1:机器级模式下**未命中 = 不投递**,不是"投给默认角色")。
|
|
21431
|
-
* 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset
|
|
21432
|
-
* 也不看目标 CCC 的 `weixin.enabled`)。
|
|
21433
|
-
*
|
|
21434
|
-
* 🔴 为什么"起 poller"与"路由"必须同批(本件即 P0-2 的后半):机器级 poller 收到消息时
|
|
21435
|
-
* **不知道它属于哪个 CCC** —— 那个答案是订阅表给的。没有订阅表 ⇒ **能收不能投**。
|
|
21418
|
+
* 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset`)。
|
|
21436
21419
|
*
|
|
21437
21420
|
* 🔵 **导出供测试直接驱动**(与 `handleIncoming` 同款:它俩都是"单条消息的入口",
|
|
21438
21421
|
* 生产路径 `runAccountLoop` 只是循环壳)。
|
|
@@ -21449,6 +21432,35 @@ async function handleMachineIncoming(ctx, accountId, cred, msg) {
|
|
|
21449
21432
|
await handleIncoming(ctx, target.root, accountId, cred, msg, { target });
|
|
21450
21433
|
}
|
|
21451
21434
|
/**
|
|
21435
|
+
* **按渠道读凭据**(P3 起的单一入口)。
|
|
21436
|
+
*
|
|
21437
|
+
* 为什么必须抽成一个函数:CCC 级桥(`syncCccBridge`)与机器级 poller(`syncMachinePollers`)
|
|
21438
|
+
* 是**两条装配路径**,原先各自写着"怎么从 localstore 取微信 token"。接第二家渠道时若两边各写一份,
|
|
21439
|
+
* 必然分叉(一家改了另一家忘改,且**没有任何机械信号**)。
|
|
21440
|
+
*
|
|
21441
|
+
* 两种取法的**真实差别**(不是风格差异):
|
|
21442
|
+
* - **CCC 级**:凭据在 localstore(`WEIXIN_<A>_TOKEN` / `FEISHU_<A>_APPID|SECRET`)。
|
|
21443
|
+
* - **机器级**:凭据在插件设置行(`account.token`)——那是宿主设置面,不经 localstore。
|
|
21444
|
+
*
|
|
21445
|
+
* ⇒ 本函数只负责**把"渠道"翻译成"该读哪套字段"**,返回统一形状 `ImAccountCredential`。
|
|
21446
|
+
* 微信的 appId 恒 undefined;飞书把 appSecret 装进 `token`(让两边共用同一个就绪判据位置)。
|
|
21447
|
+
*/
|
|
21448
|
+
function readImCredentialByChannel(root, accountId, channel) {
|
|
21449
|
+
if (channel === "feishu") {
|
|
21450
|
+
const f = readFeishuCredential(root, accountId);
|
|
21451
|
+
return f ? {
|
|
21452
|
+
appId: f.appId,
|
|
21453
|
+
token: f.appSecret
|
|
21454
|
+
} : null;
|
|
21455
|
+
}
|
|
21456
|
+
const w = readWeixinCredential(root, accountId);
|
|
21457
|
+
return w ? {
|
|
21458
|
+
token: w.token,
|
|
21459
|
+
baseUrl: w.baseUrl,
|
|
21460
|
+
userId: w.userId
|
|
21461
|
+
} : null;
|
|
21462
|
+
}
|
|
21463
|
+
/**
|
|
21452
21464
|
* 启动/重建某 CCC 的桥:读配置 → 对每个 enabled + 有凭据的账号启动轮询循环。
|
|
21453
21465
|
* 已存在(配置变化热重建)→ 先停旧循环再启动新的。
|
|
21454
21466
|
*
|
|
@@ -21481,7 +21493,7 @@ function syncCccBridge(ctx, root) {
|
|
|
21481
21493
|
}
|
|
21482
21494
|
continue;
|
|
21483
21495
|
}
|
|
21484
|
-
const cred =
|
|
21496
|
+
const cred = readImCredentialByChannel(root, account.accountId, channel);
|
|
21485
21497
|
if (!cred) continue;
|
|
21486
21498
|
if (!impl.isReady(cred)) continue;
|
|
21487
21499
|
implByAccount.set(account.accountId, impl);
|
|
@@ -21489,7 +21501,7 @@ function syncCccBridge(ctx, root) {
|
|
|
21489
21501
|
requests.push({
|
|
21490
21502
|
root,
|
|
21491
21503
|
accountId: account.accountId,
|
|
21492
|
-
identity: accountIdentity(root, account.accountId, cred.token)
|
|
21504
|
+
identity: accountIdentity(root, account.accountId, cred.token ?? "")
|
|
21493
21505
|
});
|
|
21494
21506
|
}
|
|
21495
21507
|
const plan = planPollerClaims(requests, pollerOwners());
|
|
@@ -21579,10 +21591,12 @@ function syncMachinePollers(ctx, hc) {
|
|
|
21579
21591
|
continue;
|
|
21580
21592
|
}
|
|
21581
21593
|
const token = (account.token ?? "").trim();
|
|
21594
|
+
const appIdRaw = account.appId;
|
|
21582
21595
|
const cred = {
|
|
21583
21596
|
token,
|
|
21584
21597
|
baseUrl: "",
|
|
21585
|
-
userId: account.userId
|
|
21598
|
+
userId: account.userId,
|
|
21599
|
+
...channel === "feishu" && typeof appIdRaw === "string" ? { appId: appIdRaw.trim() } : {}
|
|
21586
21600
|
};
|
|
21587
21601
|
if (!impl.isReady(cred)) {
|
|
21588
21602
|
console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} ${impl.notReadyReason?.(cred) ?? "**凭据不全**"} ⇒ 不起 poller`);
|
|
@@ -21642,7 +21656,7 @@ function syncHumanChannel(ctx) {
|
|
|
21642
21656
|
(async () => {
|
|
21643
21657
|
try {
|
|
21644
21658
|
stopMachineLoops();
|
|
21645
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21659
|
+
const { listCccs } = await import("./ccc-roots-B2t_aM8L.js").then((n) => n.a);
|
|
21646
21660
|
for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
|
|
21647
21661
|
} catch {}
|
|
21648
21662
|
})();
|
|
@@ -21782,6 +21796,14 @@ installWeixinAccountImpl({
|
|
|
21782
21796
|
handleIncoming,
|
|
21783
21797
|
handleMachineIncoming
|
|
21784
21798
|
});
|
|
21799
|
+
/**
|
|
21800
|
+
* 🆕 **P3 装配:飞书实现登记进同一张派发表**(同样是模块求值时)。
|
|
21801
|
+
*
|
|
21802
|
+
* 飞书不需要注入依赖 —— 它的收(`WSClient`)与发(`im.v1.message.create`)都在 SDK 里,
|
|
21803
|
+
* 主流程则复用 `im/inbound-event.ts`。⇒ 这一行就是"微信与飞书成为并列实现"的**最后一块**:
|
|
21804
|
+
* 派发表有了两个条目,装配处一行没改(它只按 `channel` 查表)。
|
|
21805
|
+
*/
|
|
21806
|
+
installFeishuAccountImpl();
|
|
21785
21807
|
//#endregion
|
|
21786
21808
|
//#region src/im/send.ts
|
|
21787
21809
|
/** 请求体上限(文本消息远小于此;防滥用) */
|
|
@@ -21898,7 +21920,7 @@ function matchCcc(input, candidates) {
|
|
|
21898
21920
|
}
|
|
21899
21921
|
/** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
21900
21922
|
async function collectCandidates(ctx) {
|
|
21901
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21923
|
+
const { listCccs } = await import("./ccc-roots-B2t_aM8L.js").then((n) => n.a);
|
|
21902
21924
|
const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
|
|
21903
21925
|
const seen = /* @__PURE__ */ new Set();
|
|
21904
21926
|
const out = [];
|
|
@@ -22118,7 +22140,7 @@ function sessionIdOf(value) {
|
|
|
22118
22140
|
/** 单会话内存态清理(幂等;无对应状态即 no-op) */
|
|
22119
22141
|
function cleanupSessionState(sessionId) {
|
|
22120
22142
|
try {
|
|
22121
|
-
|
|
22143
|
+
unregisterTrajectorySession(sessionId);
|
|
22122
22144
|
} catch {}
|
|
22123
22145
|
try {
|
|
22124
22146
|
clearActiveSessionInfo(sessionId);
|
|
@@ -22139,7 +22161,7 @@ function cleanupSessionState(sessionId) {
|
|
|
22139
22161
|
forgetHostScheduleToolHiding(sessionId);
|
|
22140
22162
|
} catch {}
|
|
22141
22163
|
try {
|
|
22142
|
-
|
|
22164
|
+
forgetTrajectoryConstraintToolVisibility(sessionId);
|
|
22143
22165
|
} catch {}
|
|
22144
22166
|
try {
|
|
22145
22167
|
forgetLogbookCompactionState(sessionId);
|
|
@@ -22170,7 +22192,7 @@ function registerLifecycle(ctx) {
|
|
|
22170
22192
|
stopAllBridges();
|
|
22171
22193
|
} catch {}
|
|
22172
22194
|
};
|
|
22173
|
-
registerDisposer(ctx, "self-started resources (gateway/weixin/
|
|
22195
|
+
registerDisposer(ctx, "self-started resources (gateway/weixin/trajectory-debug faces + weixin bridges)", disposeAll);
|
|
22174
22196
|
}
|
|
22175
22197
|
//#endregion
|
|
22176
22198
|
//#region src/patches/web-fetch.ts
|
|
@@ -22764,7 +22786,7 @@ async function applyOpencodeAutoConfigOnce(ctx, env) {
|
|
|
22764
22786
|
*
|
|
22765
22787
|
* 为什么要重试:`llm-pi-ai` 只在**它自己的** `ctx.inject(['settings'])` 回调里注册命名空间,
|
|
22766
22788
|
* 与本插件 apply **无先后保证**(两者都依赖 settings 服务)。首次读取拿到 undefined 是常态而非错误
|
|
22767
|
-
* (与
|
|
22789
|
+
* (与 轨迹约束的 CCC root 重试同款问题、同款处置:v1.30.13 实证"只在 apply 时试一次 → 永不重试")。
|
|
22768
22790
|
*/
|
|
22769
22791
|
const NAMESPACE_RETRY_DELAYS_MS$1 = [
|
|
22770
22792
|
1e3,
|
|
@@ -22776,7 +22798,7 @@ const NAMESPACE_RETRY_DELAYS_MS$1 = [
|
|
|
22776
22798
|
/**
|
|
22777
22799
|
* 装配 opencode 路由自动配置。
|
|
22778
22800
|
*
|
|
22779
|
-
* 触发面(三层,与
|
|
22801
|
+
* 触发面(三层,与 轨迹约束 同款;R↓:单点触发在同装载竞态下必然漏):
|
|
22780
22802
|
* ① apply 时立即试一次
|
|
22781
22803
|
* ② 命名空间尚未注册 → 退避重试(1s/3s/8s/20s/40s)
|
|
22782
22804
|
* ③ `settings/updated` → 再试(用户热改 settings.yaml 加路由时自愈;自身写入触发的复评是幂等空转)
|
|
@@ -24371,7 +24393,7 @@ function renderDsl(model) {
|
|
|
24371
24393
|
* 🔴 **为什么 PNG 是必需的**:宿主的对话内图片通路**白名单只有光栅格式**
|
|
24372
24394
|
* (`image/png|jpeg|webp|gif`,见 `dsh-attachment/types.d.ts`)⇒ **SVG 进不去**。
|
|
24373
24395
|
*
|
|
24374
|
-
* 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同
|
|
24396
|
+
* 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 轨迹约束 口径。
|
|
24375
24397
|
*
|
|
24376
24398
|
* ## 🔴 两条**宿主契约**(2026-09-29 现场取证;第 ① 条是本文件第一版写错的地方)
|
|
24377
24399
|
*
|
|
@@ -24741,10 +24763,6 @@ const Config = z.object({
|
|
|
24741
24763
|
opencodeSkills: z.boolean().default(true),
|
|
24742
24764
|
gateway: z.object({ enabled: z.boolean().default(false) }),
|
|
24743
24765
|
rebuild: z.object({ thresholdK: z.number().min(50).max(4e3).default(400) }),
|
|
24744
|
-
skiff: z.object({
|
|
24745
|
-
enabled: z.boolean().default(false),
|
|
24746
|
-
debugPort: z.number().min(1024).max(65535).default(SKIFF_DEBUG_PORT)
|
|
24747
|
-
}),
|
|
24748
24766
|
webFetch: z.object({ enabled: z.boolean().default(true) }),
|
|
24749
24767
|
opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
|
|
24750
24768
|
visionPatch: z.object({ enabled: z.boolean().default(true) }),
|
|
@@ -24757,8 +24775,6 @@ const Config = z.object({
|
|
|
24757
24775
|
}),
|
|
24758
24776
|
gatewayEnabled: z.boolean().volatile(),
|
|
24759
24777
|
rebuildThresholdK: z.number().min(50).max(4e3).volatile(),
|
|
24760
|
-
skiffEnabled: z.boolean().volatile(),
|
|
24761
|
-
skiffDebugPort: z.number().min(1024).max(65535).volatile(),
|
|
24762
24778
|
croEnabled: z.boolean().volatile(),
|
|
24763
24779
|
diagramEnabled: z.boolean().volatile()
|
|
24764
24780
|
});
|
|
@@ -24825,7 +24841,6 @@ function apply(ctx, config) {
|
|
|
24825
24841
|
if (config.env) registerEnv(ctx);
|
|
24826
24842
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
24827
24843
|
registerBootstrap(ctx);
|
|
24828
|
-
registerSkiff(ctx);
|
|
24829
24844
|
registerCroTurnTracking(ctx);
|
|
24830
24845
|
registerWakeScheduler(ctx);
|
|
24831
24846
|
registerWeixinBridge(ctx);
|
|
@@ -24849,7 +24864,7 @@ function apply(ctx, config) {
|
|
|
24849
24864
|
* ① 听宿主的**真事件** `settings/document-updated`(ns = 我们那张设置页)——顺带说明为什么不用
|
|
24850
24865
|
* 我们自己的 `serenity/settings-changed`:那个事件**全仓没有任何 `emit` 点**(只有 `ctx.on` 侧
|
|
24851
24866
|
* 在用)=**死通道**,照着它接线等于接了个永远不响的铃;
|
|
24852
|
-
* ② 兜底再挂 `agent/created` / `session/created`(与
|
|
24867
|
+
* ② 兜底再挂 `agent/created` / `session/created`(与 轨迹约束 同款做法,那两条在实测里确实会 fire)
|
|
24853
24868
|
* —— 宿主事件面若换名,至少还有这一条能自愈;
|
|
24854
24869
|
* ③ 启动时同步一次(apply 时按当前配置定初始态)。
|
|
24855
24870
|
* 🔴 **关的时候要注销**(不是"留着但不响应"):与"关时不注册"同一口径 —— 工具不出现在模型清单里。
|
|
@@ -24887,139 +24902,5 @@ function registerDiagramTool(ctx) {
|
|
|
24887
24902
|
} catch {}
|
|
24888
24903
|
sync();
|
|
24889
24904
|
}
|
|
24890
|
-
/**
|
|
24891
|
-
* F4 Skiff 调试服务装配:启停 = 人工(设置面板 Skiff 区块开关,settings 持久化)。
|
|
24892
|
-
* settings-changed 事件触发同步(skiffEnabled 开 → 启动调试服务;关 → 停止)。
|
|
24893
|
-
* 角色配置(skiff.roles)从当前 CCC 根读取(含 skiff.roles 的 CCC 优先,进程 cwd 次之)。
|
|
24894
|
-
*
|
|
24895
|
-
* v1.30.13(S142 诊断 D1):**CCC root 解析失败不再"永不重试"**。旧实现只在 apply 时
|
|
24896
|
-
* 同步一次——此刻通常还没有 live 会话,进程 cwd(服务启动目录)也不在 CCC 内 →
|
|
24897
|
-
* `resolveSkiffRoot` 返回 null → 打印一行警告后**永不重试**(实证:重启日志
|
|
24898
|
-
* `✗ Skiff 调试服务未启动:无法定位 CCC root`,`ss -ltn` 无 3099;同批 ACP 因容忍
|
|
24899
|
-
* `root ?? undefined` 而正常启动 —— ⚠️ **该对照已随面 E 退役而失效**(2026-10-15),
|
|
24900
|
-
* 但本条记录的**病因与修法不受影响**(Skiff 侧仍是"首次未定位 → 曾永不重试")。
|
|
24901
|
-
*
|
|
24902
|
-
* v1.30.14(重启日志实证后的收敛):**并发启动守卫** + 首次未定位降级为信息级日志。
|
|
24903
|
-
* - 实证(v1.30.13 重启日志):定时触发与"会话就绪"事件几乎同时到达 → 两次 sync 都
|
|
24904
|
-
* 看到 `started === false` → **两次 start**(第一次绑定成功、第二次 `EADDRINUSE` 刷错误日志)。
|
|
24905
|
-
* → 加 `starting` 在飞标志(启动 Promise settle 前不再重复发起)。
|
|
24906
|
-
* - 首次未定位**不是失败**(apply 时无 live 会话是常态)→ 用 `console.log` 说明"等待 live 会话"。
|
|
24907
|
-
*
|
|
24908
|
-
* v1.35(C2,S142 2026-09-15 Q5):**退避重试定时器退场**。原形态是 ① 启动同步
|
|
24909
|
-
* ② 退避重试(1s/3s/8s/20s/40s) ③ 会话就绪事件再触发。C2 把 CCC 发现面归一后,
|
|
24910
|
-
* ② 的病灶("apply 时刻解析不到根")改由 **`resolveSkiffRoot` 第 ④ 档(ccc-roots 的
|
|
24911
|
-
* **并集**枚举:工作区注册表 ∪ 持久化会话 ∪ live 会话)** 从源头消掉——持久来源不依赖
|
|
24912
|
-
* 任何 live 会话,故 apply 时刻就能解析到,不再需要"等一会儿再试"。
|
|
24913
|
-
*
|
|
24914
|
-
* 🔒 **必须保留**(它们不是"重试",删了会引入新缺陷):
|
|
24915
|
-
* - **反应式再触发**(`agent/created` / `session/created` → sync):宿主重启后
|
|
24916
|
-
* "恢复"旧会话不触发 `session/created`,但**开始对话**会触发 `agent/created`
|
|
24917
|
-
* (v0.1.7 前名 `agent/session-start`)——这是唯一能把"进程起来时还解析不到、稍后才有会话"
|
|
24918
|
-
* 这条路径接上的通道。
|
|
24919
|
-
* - **`starting` 在飞标志**:防同拍双次 start → `EADDRINUSE`。
|
|
24920
|
-
* (C4 块 A 备注:面宿主亦按面名合并在飞启动,是第二道保险;**本标志仍保留**——
|
|
24921
|
-
* 它管的是"解析 root 这段装配流程"的在飞语义,且调试服务的启动替身在测试里被 mock,
|
|
24922
|
-
* 面宿主的 active 表在那种场景下为空,不能当作 `started` 的替代。)
|
|
24923
|
-
* - **`informedRoot` 信息级日志**(只报一次,不刷屏)、`serenity/settings-changed` 监听、
|
|
24924
|
-
* 启动时那次 `sync()`。
|
|
24925
|
-
*/
|
|
24926
|
-
function registerSkiff(ctx) {
|
|
24927
|
-
let started = false;
|
|
24928
|
-
let starting = false;
|
|
24929
|
-
let informedRoot = false;
|
|
24930
|
-
/** 已解析到的根(成功后缓存,避免每次 sync 重复解析) */
|
|
24931
|
-
let resolvedRoot = null;
|
|
24932
|
-
const sync = async () => {
|
|
24933
|
-
const s = readSimpleSettings();
|
|
24934
|
-
if (s.skiffEnabled) {
|
|
24935
|
-
if (started || starting) return;
|
|
24936
|
-
starting = true;
|
|
24937
|
-
try {
|
|
24938
|
-
const root = resolvedRoot ?? await resolveSkiffRoot(ctx);
|
|
24939
|
-
if (!root) {
|
|
24940
|
-
if (!informedRoot) {
|
|
24941
|
-
informedRoot = true;
|
|
24942
|
-
console.log("[serenity-hooks] Skiff 调试服务等待 CCC root(此刻无已知 CCC;会话就绪即试)");
|
|
24943
|
-
}
|
|
24944
|
-
return;
|
|
24945
|
-
}
|
|
24946
|
-
resolvedRoot = root;
|
|
24947
|
-
const webPort = readWebPort(ctx);
|
|
24948
|
-
await startSkiffDebugServer(ctx, root, s.skiffDebugPort, webPort);
|
|
24949
|
-
started = true;
|
|
24950
|
-
} catch (err) {
|
|
24951
|
-
console.error(`[serenity-hooks] ✗ Skiff 调试服务启动失败: ${String(err?.message ?? err)}`);
|
|
24952
|
-
} finally {
|
|
24953
|
-
starting = false;
|
|
24954
|
-
}
|
|
24955
|
-
return;
|
|
24956
|
-
}
|
|
24957
|
-
if (started) {
|
|
24958
|
-
stopSkiffDebugServer();
|
|
24959
|
-
started = false;
|
|
24960
|
-
}
|
|
24961
|
-
resolvedRoot = null;
|
|
24962
|
-
informedRoot = false;
|
|
24963
|
-
};
|
|
24964
|
-
try {
|
|
24965
|
-
ctx.on("serenity/settings-changed", () => {
|
|
24966
|
-
sync();
|
|
24967
|
-
});
|
|
24968
|
-
} catch {}
|
|
24969
|
-
for (const eventName of ["agent/created", "session/created"]) try {
|
|
24970
|
-
ctx.on(eventName, () => {
|
|
24971
|
-
if (!started) sync();
|
|
24972
|
-
});
|
|
24973
|
-
} catch {}
|
|
24974
|
-
sync();
|
|
24975
|
-
}
|
|
24976
|
-
/**
|
|
24977
|
-
* 解析 Skiff 调试服务绑定的 CCC 根(v1.25.2 用户指出:skiff 必须绑定 CCC):
|
|
24978
|
-
* ① live 会话中**配置了 skiff.roles 的 CCC 优先**(用户认知中的绑定目标)
|
|
24979
|
-
* ② 回退进程 cwd 上溯 .serenity
|
|
24980
|
-
* ③ 再回退任一 live 会话的 CCC
|
|
24981
|
-
* ④ **(v1.35 / C2 新增)ccc-roots 枚举出的 CCC——仅在唯一候选时采用**
|
|
24982
|
-
*
|
|
24983
|
-
* 第 ④ 档为什么存在:①②③ 全部依赖 live 会话或进程 cwd;宿主重启、浏览器尚未重连时
|
|
24984
|
-
* 三者可能同时落空,而 `ccc-roots.listCccs` 的**持久来源**(工作区注册表 / 持久化会话)
|
|
24985
|
-
* 不依赖 live 会话 ⇒ 这正是"apply 时刻就能解析到根"的那条路,也是退避重试定时器
|
|
24986
|
-
* 得以退场的原因。
|
|
24987
|
-
*
|
|
24988
|
-
* ④ 的收敛条件(**不猜**):只在候选**唯一**时采用;多个候选 → 返回 `null`
|
|
24989
|
-
* (留着让 ①②③ 的语义与人类的显式选择决定,不代替人拍板)。
|
|
24990
|
-
*
|
|
24991
|
-
* ⚠️ ①②③ 的**逐条优先级不变**;④ 只追加在末尾。
|
|
24992
|
-
*/
|
|
24993
|
-
async function resolveSkiffRoot(ctx) {
|
|
24994
|
-
const liveRoots = [];
|
|
24995
|
-
try {
|
|
24996
|
-
const sessions = hostSessions(ctx);
|
|
24997
|
-
for (const s of sessions?.list?.() ?? []) {
|
|
24998
|
-
const cwd = s?.header?.cwd;
|
|
24999
|
-
if (typeof cwd === "string") {
|
|
25000
|
-
const r = cccRootForCwd(cwd);
|
|
25001
|
-
if (r && !liveRoots.includes(r)) liveRoots.push(r);
|
|
25002
|
-
}
|
|
25003
|
-
}
|
|
25004
|
-
} catch {}
|
|
25005
|
-
for (const r of liveRoots) if (listSkiffProfiles(r).length > 0) return r;
|
|
25006
|
-
const fromCwd = cccRootForCwd(process.cwd());
|
|
25007
|
-
if (fromCwd) return fromCwd;
|
|
25008
|
-
if (liveRoots[0]) return liveRoots[0];
|
|
25009
|
-
try {
|
|
25010
|
-
const entries = await listCccs(ctx);
|
|
25011
|
-
if (entries.length === 1) return entries[0].root;
|
|
25012
|
-
} catch {}
|
|
25013
|
-
return null;
|
|
25014
|
-
}
|
|
25015
|
-
/** 主 WebUI 端口(WebUI 链接;webServer 未装配回退集中端口表的 main 面端口) */
|
|
25016
|
-
function readWebPort(ctx) {
|
|
25017
|
-
try {
|
|
25018
|
-
const ws = hostWebServer(ctx);
|
|
25019
|
-
return typeof ws?.port === "number" ? ws.port : MAIN_WEB_PORT;
|
|
25020
|
-
} catch {
|
|
25021
|
-
return MAIN_WEB_PORT;
|
|
25022
|
-
}
|
|
25023
|
-
}
|
|
25024
24905
|
//#endregion
|
|
25025
24906
|
export { Config, apply, humanChannelAccountSchema, humanChannelSubscriptionSchema, inject, name };
|