@shgroup/dsh-serenity-hooks 1.72.0 → 1.73.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/dsh.plugin.json +1 -1
- package/lib/{ccc-Dkoa2oW3.js → ccc-CZUOq3NY.js} +1 -1
- package/lib/{ccc-roots-jZYR2GVE.js → ccc-roots-BhoA7G-0.js} +3 -3
- package/lib/client.js +50 -58
- package/lib/{wake/cro.d.ts → cro/core.d.ts} +2 -2
- 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 +1 -1
- package/lib/{ilink-DEpmE3yi.js → ilink-BMk2w2KY.js} +1 -1
- package/lib/im/bridge.d.ts +33 -62
- package/lib/im/endpoint.d.ts +1 -1
- package/lib/im/feishu-account.d.ts +92 -0
- package/lib/im/inbound-event.d.ts +162 -0
- package/lib/im/route.d.ts +13 -0
- package/lib/index.d.ts +0 -22
- package/lib/index.js +700 -914
- package/lib/kernel/ccc.d.ts +2 -2
- package/lib/kernel/ports.d.ts +2 -4
- package/lib/lib-TxscFKeo.js +119355 -0
- package/lib/{message-source-kVoNK0C5.js → message-source-ChyVfev9.js} +2 -2
- package/lib/{profile-DyMJ_bfr.js → profile-BIQ2widI.js} +14 -11
- package/lib/rolldown-runtime-B4iAMlE-.js +35 -0
- package/lib/{route-f7JlinIt.js → route-ItCgAsnz.js} +19 -4
- package/lib/seams/guards.d.ts +1 -1
- package/lib/{settings-lt1i3OZv.js → settings-UcwIPJhM.js} +13 -62
- package/lib/skills/resource-index.d.ts +1 -1
- package/lib/surface/settings.d.ts +0 -6
- package/lib/surface/text.d.ts +72 -0
- package/lib/surface/usage.d.ts +2 -2
- package/lib/{role/worker/idle.d.ts → trajectory/carrier.d.ts} +12 -2
- package/lib/{role/worker → trajectory/delegate}/core.d.ts +7 -1
- package/lib/{role/worker → trajectory/delegate}/preset.d.ts +9 -2
- package/lib/{role/skiff → trajectory/policy}/profile.d.ts +9 -2
- package/lib/{role/skiff/config.d.ts → trajectory/policy/rules.d.ts} +9 -3
- package/lib/{role/skiff/registry.d.ts → trajectory/policy/window.d.ts} +7 -1
- package/lib/trajectory/prompt.d.ts +1 -1
- package/lib/{role/skiff/core.d.ts → trajectory/session.d.ts} +13 -4
- package/package.json +2 -1
- package/lib/role/skiff/debug.d.ts +0 -88
- package/lib/rolldown-runtime-8H4AJuhK.js +0 -14
package/lib/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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-BhoA7G-0.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-CZUOq3NY.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 readSkiffProfileAt, c as registerSkiffSession$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 skiffTrajectoryDirNameFor, m as archiveSession, n as isSkiffSession, o as readSkiffProfileByCode, p as UID_FILENAME, r as listSkiffProfiles, s as skiffProfileForSession, t as SKIFF_PROFILE_FILENAME, u as unregisterSkiffSession$1, v as findSession, w as parseSessionContextFromEvents, x as listActiveTrajectories, y as getActiveSessionInfo } from "./profile-BIQ2widI.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-ItCgAsnz.js";
|
|
6
|
+
import { a as buildSkiffBasePrompt, c as profileMsmWhitelist, d as resolveProfileSystemPrompt, f as ACC_VERSION, i as SKIFF_PROFILE_HINT, l as profileSystemPromptSource, n as readSimpleSettings, o as createProfilePromptReader, p as findPackageRoot, r as registerSettingsSection, s as profileCognitiveTrajectory, u as profileToolWhitelist } from "./settings-UcwIPJhM.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-ChyVfev9.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
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import z from "@deepseek-ai/schemastery";
|
|
12
12
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -981,7 +981,7 @@ const gitTool = defineTool({
|
|
|
981
981
|
* 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
|
|
982
982
|
* (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
|
|
983
983
|
*
|
|
984
|
-
* 同款先例:`host/effect.ts`(拆卸登记)、`
|
|
984
|
+
* 同款先例:`host/effect.ts`(拆卸登记)、`trajectory/carrier.ts`(等待结算)。
|
|
985
985
|
*/
|
|
986
986
|
let endpoint = null;
|
|
987
987
|
/** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
|
|
@@ -1958,7 +1958,7 @@ function trajectoryPromptBlock(text) {
|
|
|
1958
1958
|
].join("\n");
|
|
1959
1959
|
}
|
|
1960
1960
|
//#endregion
|
|
1961
|
-
//#region src/
|
|
1961
|
+
//#region src/trajectory/carrier.ts
|
|
1962
1962
|
/**
|
|
1963
1963
|
* 等待 agent 空闲或销毁。
|
|
1964
1964
|
* @param ctx 宿主插件上下文(事件通道来源;**结构化访问**,故类型为 `unknown`,见文件头 I2 段)
|
|
@@ -2105,7 +2105,7 @@ function truncateContent(content, maxChars) {
|
|
|
2105
2105
|
* 六条硬性规则(逐条对应设计 §4.2,改这里先改那里):
|
|
2106
2106
|
* ① **只枚举,不注入正文** —— 索引里**绝不**出现任何参考文件的正文;
|
|
2107
2107
|
* ② **确定性排序** = 相对路径的 **UTF-8 字节序**(🔴 **不用 mtime**:mtime 随检出漂移,
|
|
2108
|
-
* 会让每请求的注入文本抖动;对照 `src/
|
|
2108
|
+
* 会让每请求的注入文本抖动;对照 `src/cro/core.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
|
|
2109
2109
|
* **同一件事两处的取舍相反,那个差异是判据不是巧合**);
|
|
2110
2110
|
* ③ **有界 + 响亮** —— 条数/字符各有上限,超出**必须显式写出还剩多少项**,**绝不静默截断**;
|
|
2111
2111
|
* ④ **摘要可复现** —— 取该文件首个 `#` 标题(剥 markdown 记号);取不到 ⇒ 留空,**不编**;
|
|
@@ -2600,7 +2600,7 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
|
|
|
2600
2600
|
* ⇒ 本模块让每个 CCC 留下一份**纯计数**账本:**用了哪个、多少次、第一次和最近一次是什么时候**。
|
|
2601
2601
|
* 用途 = 清理/精简的**依据**(没有人用的东西与天天在用的东西,不再靠猜)。
|
|
2602
2602
|
*
|
|
2603
|
-
* ## 🔴 铁律(与 `
|
|
2603
|
+
* ## 🔴 铁律(与 `cro/log.ts` 同族)
|
|
2604
2604
|
*
|
|
2605
2605
|
* **本模块每个导出函数都不抛错**:读盘 / 解析 / 写盘失败一律返回结构化失败,
|
|
2606
2606
|
* 由调用方只记一行日志(或什么都不做)⇒ **统计的问题绝不影响工具执行与既有链路**。
|
|
@@ -2616,7 +2616,7 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
|
|
|
2616
2616
|
*
|
|
2617
2617
|
* 宿主会被重启(本容器实测 24h 内 3 次)⇒ 计数**不能只活在内存里**:
|
|
2618
2618
|
* 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake/registry.ts` /
|
|
2619
|
-
* `
|
|
2619
|
+
* `cro/log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
|
|
2620
2620
|
*
|
|
2621
2621
|
* ## 口径(两个字段,勿合并)
|
|
2622
2622
|
*
|
|
@@ -2899,6 +2899,48 @@ function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
|
|
|
2899
2899
|
return null;
|
|
2900
2900
|
}
|
|
2901
2901
|
//#endregion
|
|
2902
|
+
//#region src/kernel/ports.ts
|
|
2903
|
+
/**
|
|
2904
|
+
* 集中端口表。
|
|
2905
|
+
* A=宿主主面(插件不监听)/ B=网关(可配,默认关)/ C=微信发送(默认开,仅 loopback)
|
|
2906
|
+
* / D=Skiff 调试(默认关)。
|
|
2907
|
+
*/
|
|
2908
|
+
const FACE_PORTS = {
|
|
2909
|
+
main: {
|
|
2910
|
+
port: 3080,
|
|
2911
|
+
host: null,
|
|
2912
|
+
defaultEnabled: true,
|
|
2913
|
+
meaning: "宿主 DSH 主 WebUI + 插件 /serenity/* 路由面(**插件不起此 listener**,端口自宿主读)"
|
|
2914
|
+
},
|
|
2915
|
+
gateway: {
|
|
2916
|
+
port: 3081,
|
|
2917
|
+
host: "0.0.0.0",
|
|
2918
|
+
defaultEnabled: false,
|
|
2919
|
+
meaning: "双端口网关(外部登录 + 全量反代宿主主面;host/port 走 plugin 全局配置)"
|
|
2920
|
+
},
|
|
2921
|
+
weixinSend: {
|
|
2922
|
+
port: 3082,
|
|
2923
|
+
host: "127.0.0.1",
|
|
2924
|
+
defaultEnabled: true,
|
|
2925
|
+
meaning: "微信主动发送面(容器外进程 → POST /send;与 3080 显式解耦,不经网关)"
|
|
2926
|
+
}
|
|
2927
|
+
};
|
|
2928
|
+
FACE_PORTS.main.port;
|
|
2929
|
+
/** 网关端口默认值(面 B) */
|
|
2930
|
+
const GATEWAY_PORT = FACE_PORTS.gateway.port;
|
|
2931
|
+
/** 微信主动发送端口默认值(面 C) */
|
|
2932
|
+
const WEIXIN_SEND_PORT = FACE_PORTS.weixinSend.port;
|
|
2933
|
+
/**
|
|
2934
|
+
* **机制端口**词表(output-guard 用:agent 在**用户可见输出**里提及内部端口即打回)。
|
|
2935
|
+
*
|
|
2936
|
+
* 由 {@link FACE_PORTS} 机械派生 ⇒ 与面清单**永不漂移**。
|
|
2937
|
+
* ⚠️ 修复记录(C4 块 B,2026-09-15):旧常量硬写 `['3080','3081','3099','3100']`
|
|
2938
|
+
* ——**漏 3082**(微信发送面,默认开)⇒ 守卫拦不住"3082"这类提及。现由表派生,四面齐全。
|
|
2939
|
+
* ⚠️ 面变更记录(2026-10-15):**面 E(3100 ACP/问答)已整体退役**(owner 裁「砍掉外部问答」)
|
|
2940
|
+
* ⇒ 本词表随之少一项,**无需另改**(这正是"派生而非手抄"的收益)。
|
|
2941
|
+
*/
|
|
2942
|
+
const MECHANISM_PORTS = Object.keys(FACE_PORTS).map((id) => String(FACE_PORTS[id].port));
|
|
2943
|
+
//#endregion
|
|
2902
2944
|
//#region src/kernel/config.ts
|
|
2903
2945
|
/**
|
|
2904
2946
|
* kernel/config.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
|
|
@@ -3768,13 +3810,19 @@ function registerCognitiveTrajectorySkillSection(agent, root) {
|
|
|
3768
3810
|
}
|
|
3769
3811
|
}
|
|
3770
3812
|
//#endregion
|
|
3771
|
-
//#region src/
|
|
3813
|
+
//#region src/trajectory/delegate/core.ts
|
|
3772
3814
|
/**
|
|
3773
|
-
*
|
|
3815
|
+
* trajectory/delegate/core.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
|
|
3774
3816
|
*
|
|
3775
3817
|
* 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
|
|
3776
3818
|
* 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
|
|
3777
3819
|
* 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
|
|
3820
|
+
*
|
|
3821
|
+
* ## S14(2026-10-19,轨迹中心重构):此前位于 `role/worker/` 下(该目录已消失)
|
|
3822
|
+
*
|
|
3823
|
+
* 落点 = `trajectory/delegate/`。判据(判读表 §3.1):它是**轨迹 × 轨迹**那种合法形态 ——
|
|
3824
|
+
* 主语是「主轨迹把活交给子 agent」,既不是某一条轨迹的切面,也不是容器宪法。
|
|
3825
|
+
* ⚠️ 它**不属于** `policy/`:policy 管"这条轨迹允许什么",本模块管"一条轨迹怎么把活派出去"。
|
|
3778
3826
|
*/
|
|
3779
3827
|
/** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
|
|
3780
3828
|
function splitModel(model) {
|
|
@@ -3910,13 +3958,9 @@ Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up c
|
|
|
3910
3958
|
(and "output" for whatever partial text was produced).
|
|
3911
3959
|
`;
|
|
3912
3960
|
//#endregion
|
|
3913
|
-
//#region src/
|
|
3914
|
-
/** `provider/model` → agentOptions(由
|
|
3961
|
+
//#region src/trajectory/session.ts
|
|
3962
|
+
/** `provider/model` → agentOptions(由 delegate/core 提供,保持单一实现) */
|
|
3915
3963
|
const skiffAgents = /* @__PURE__ */ new Map();
|
|
3916
|
-
/** 查 sessionId 的会话绑定(轨迹 + ccc;无 → null)——追问校验 */
|
|
3917
|
-
function skiffSessionInfo(sessionId) {
|
|
3918
|
-
return skiffSessionInfo$1(sessionId);
|
|
3919
|
-
}
|
|
3920
3964
|
/** 查 sessionId 的活体 agent(进程内会话延续用;未注册/已清理 → undefined) */
|
|
3921
3965
|
function getSkiffAgent(sessionId) {
|
|
3922
3966
|
return skiffAgents.get(sessionId);
|
|
@@ -10043,9 +10087,9 @@ function listWakes(root, opts = {}) {
|
|
|
10043
10087
|
};
|
|
10044
10088
|
}
|
|
10045
10089
|
//#endregion
|
|
10046
|
-
//#region src/
|
|
10090
|
+
//#region src/cro/core.ts
|
|
10047
10091
|
/**
|
|
10048
|
-
*
|
|
10092
|
+
* cro/core.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
|
|
10049
10093
|
*
|
|
10050
10094
|
* ## 这东西用来干什么(白话)
|
|
10051
10095
|
*
|
|
@@ -10669,9 +10713,9 @@ function registerDisposer(ctx, label, cleanup) {
|
|
|
10669
10713
|
}
|
|
10670
10714
|
}
|
|
10671
10715
|
//#endregion
|
|
10672
|
-
//#region src/
|
|
10716
|
+
//#region src/cro/turns.ts
|
|
10673
10717
|
/**
|
|
10674
|
-
*
|
|
10718
|
+
* cro/turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
|
|
10675
10719
|
*
|
|
10676
10720
|
* ## 为什么需要这个模块(CRO 唯一需要 ACC 新增的状态)
|
|
10677
10721
|
*
|
|
@@ -10797,7 +10841,7 @@ function runningCarriersOf(boundSessionIds, nowMs, ttlMs = TURN_TTL_MS) {
|
|
|
10797
10841
|
* 🔴 **本处顺带更正 §2.7 的一处分档**:本文件原被列在"剩余 3 个属**真依赖**档"里,
|
|
10798
10842
|
* 但**逐符号查证后与那个判断不符** ——
|
|
10799
10843
|
* · `Context`:`ctx` 只被**结构化访问**(原码本就写着 `ctx as unknown as { on: … }`)
|
|
10800
|
-
* ⇒ 该注解未提供任何类型保证(与 `
|
|
10844
|
+
* ⇒ 该注解未提供任何类型保证(与 `trajectory/carrier.ts` 同档)。
|
|
10801
10845
|
* · `Agent`:**导入后从未使用** —— 事件 payload 一律按 `{ agent?: unknown }` 解构,
|
|
10802
10846
|
* 再交给 `sessionIdOf(...)`(它收 `unknown`)。⇒ **纯粹是死 import**。
|
|
10803
10847
|
* ⇒ **判据(本件再证一次,且比上一件更硬)**:分档**必须逐符号看"它有没有参与类型检查"**,
|
|
@@ -10845,9 +10889,9 @@ function registerCroTurnTracking(ctx) {
|
|
|
10845
10889
|
});
|
|
10846
10890
|
}
|
|
10847
10891
|
//#endregion
|
|
10848
|
-
//#region src/
|
|
10892
|
+
//#region src/cro/log.ts
|
|
10849
10893
|
/**
|
|
10850
|
-
*
|
|
10894
|
+
* cro/log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
|
|
10851
10895
|
*
|
|
10852
10896
|
* ## 所有者令(本模块的存在理由)
|
|
10853
10897
|
*
|
|
@@ -10883,7 +10927,7 @@ function registerCroTurnTracking(ctx) {
|
|
|
10883
10927
|
* ⇒ 清理与写入**是同一笔操作** ⇒ **结构上不存在"忘了清"的可能**(比"另设一个每 tick 的清理步"更硬:
|
|
10884
10928
|
* 那多一个"可能忘了跑"的面)。窗宽是**常量**,**无配置键、无人工清理动作**。
|
|
10885
10929
|
*
|
|
10886
|
-
* ## 🔴 铁律(与 `
|
|
10930
|
+
* ## 🔴 铁律(与 `cro/core.ts` 同族)
|
|
10887
10931
|
*
|
|
10888
10932
|
* **本模块的每个导出函数都不抛错**:写盘 / 裁剪 / 解析失败一律返回结构化失败,
|
|
10889
10933
|
* 由调用方只记一行 tick 日志 ⇒ **CRO 流水的问题绝不影响投递与既有链路**(设计 §5)。
|
|
@@ -12006,9 +12050,9 @@ function registerWakeScheduler(ctx) {
|
|
|
12006
12050
|
registerDisposer(ctx, "认知轨迹唤醒调度器", () => clock.dispose());
|
|
12007
12051
|
}
|
|
12008
12052
|
//#endregion
|
|
12009
|
-
//#region src/
|
|
12053
|
+
//#region src/cro/guide.ts
|
|
12010
12054
|
/**
|
|
12011
|
-
*
|
|
12055
|
+
* cro/guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
|
|
12012
12056
|
*
|
|
12013
12057
|
* ## 为什么指南在 ACC 而不在 CCC(所有者令)
|
|
12014
12058
|
*
|
|
@@ -15312,7 +15356,7 @@ function saveFileToTmp(root, fileName, data) {
|
|
|
15312
15356
|
writeFileSync(join(dir, filename), bytes);
|
|
15313
15357
|
return `${FILE_UPLOAD_DIR}/${filename}`;
|
|
15314
15358
|
}
|
|
15315
|
-
function readBody$
|
|
15359
|
+
function readBody$2(req, maxBytes) {
|
|
15316
15360
|
return new Promise((resolve, reject) => {
|
|
15317
15361
|
let data = "";
|
|
15318
15362
|
req.on("data", (chunk) => {
|
|
@@ -15326,7 +15370,7 @@ function readBody$3(req, maxBytes) {
|
|
|
15326
15370
|
req.on("error", reject);
|
|
15327
15371
|
});
|
|
15328
15372
|
}
|
|
15329
|
-
function sendJson$
|
|
15373
|
+
function sendJson$2(res, code, body) {
|
|
15330
15374
|
const payload = JSON.stringify(body);
|
|
15331
15375
|
res.writeHead(code, {
|
|
15332
15376
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -15351,7 +15395,7 @@ function senderIsWebUi(req) {
|
|
|
15351
15395
|
*/
|
|
15352
15396
|
function requireWebUi(req, res, message) {
|
|
15353
15397
|
if (senderIsWebUi(req)) return true;
|
|
15354
|
-
sendJson$
|
|
15398
|
+
sendJson$2(res, 403, { error: message });
|
|
15355
15399
|
return false;
|
|
15356
15400
|
}
|
|
15357
15401
|
/**
|
|
@@ -15409,11 +15453,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15409
15453
|
handler: async (req, res) => {
|
|
15410
15454
|
try {
|
|
15411
15455
|
if (req.method !== "POST") {
|
|
15412
|
-
sendJson$
|
|
15456
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15413
15457
|
return;
|
|
15414
15458
|
}
|
|
15415
15459
|
if (!requireWebUi(req, res, "图片落盘仅限 WebUI(client 专用)")) return;
|
|
15416
|
-
const raw = await readBody$
|
|
15460
|
+
const raw = await readBody$2(req, 20971520);
|
|
15417
15461
|
const body = JSON.parse(raw);
|
|
15418
15462
|
const workspace = resolveWorkspace(ctx, {
|
|
15419
15463
|
sessionId: body.sessionId,
|
|
@@ -15421,12 +15465,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15421
15465
|
});
|
|
15422
15466
|
const root = cccRootForCwd(workspace);
|
|
15423
15467
|
if (!root) {
|
|
15424
|
-
sendJson$
|
|
15468
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
15425
15469
|
return;
|
|
15426
15470
|
}
|
|
15427
|
-
sendJson$
|
|
15471
|
+
sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
15428
15472
|
} catch (err) {
|
|
15429
|
-
sendJson$
|
|
15473
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15430
15474
|
}
|
|
15431
15475
|
}
|
|
15432
15476
|
});
|
|
@@ -15436,11 +15480,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15436
15480
|
handler: async (req, res) => {
|
|
15437
15481
|
try {
|
|
15438
15482
|
if (req.method !== "POST") {
|
|
15439
|
-
sendJson$
|
|
15483
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15440
15484
|
return;
|
|
15441
15485
|
}
|
|
15442
15486
|
if (!requireWebUi(req, res, "文件落盘仅限 WebUI(client 专用)")) return;
|
|
15443
|
-
const raw = await readBody$
|
|
15487
|
+
const raw = await readBody$2(req, 20971520);
|
|
15444
15488
|
const body = JSON.parse(raw);
|
|
15445
15489
|
const workspace = resolveWorkspace(ctx, {
|
|
15446
15490
|
sessionId: body.sessionId,
|
|
@@ -15448,12 +15492,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15448
15492
|
});
|
|
15449
15493
|
const root = cccRootForCwd(workspace);
|
|
15450
15494
|
if (!root) {
|
|
15451
|
-
sendJson$
|
|
15495
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
15452
15496
|
return;
|
|
15453
15497
|
}
|
|
15454
|
-
sendJson$
|
|
15498
|
+
sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
15455
15499
|
} catch (err) {
|
|
15456
|
-
sendJson$
|
|
15500
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15457
15501
|
}
|
|
15458
15502
|
}
|
|
15459
15503
|
});
|
|
@@ -15469,7 +15513,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15469
15513
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
15470
15514
|
}), configPaths);
|
|
15471
15515
|
const runtime = ctx.get("codeRuntime");
|
|
15472
|
-
sendJson$
|
|
15516
|
+
sendJson$2(res, 200, {
|
|
15473
15517
|
...status,
|
|
15474
15518
|
...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
|
|
15475
15519
|
language: runtime.language,
|
|
@@ -15480,7 +15524,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15480
15524
|
}
|
|
15481
15525
|
if (req.method === "POST") {
|
|
15482
15526
|
if (!requireWebUi(req, res, "safe-mode 切换仅限 WebUI(agent 不可自行开关)")) return;
|
|
15483
|
-
const raw = await readBody$
|
|
15527
|
+
const raw = await readBody$2(req, 65536);
|
|
15484
15528
|
const body = JSON.parse(raw);
|
|
15485
15529
|
const workspace = resolveWorkspace(ctx, {
|
|
15486
15530
|
sessionId: body.sessionId,
|
|
@@ -15488,19 +15532,19 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15488
15532
|
});
|
|
15489
15533
|
const root = cccRootForCwd(workspace);
|
|
15490
15534
|
if (!root) {
|
|
15491
|
-
sendJson$
|
|
15535
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
15492
15536
|
return;
|
|
15493
15537
|
}
|
|
15494
15538
|
const result = setSafeMode(root, body.on === true);
|
|
15495
|
-
sendJson$
|
|
15539
|
+
sendJson$2(res, 200, {
|
|
15496
15540
|
...getStatus(workspace, configPaths),
|
|
15497
15541
|
...result
|
|
15498
15542
|
});
|
|
15499
15543
|
return;
|
|
15500
15544
|
}
|
|
15501
|
-
sendJson$
|
|
15545
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15502
15546
|
} catch (err) {
|
|
15503
|
-
sendJson$
|
|
15547
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15504
15548
|
}
|
|
15505
15549
|
}
|
|
15506
15550
|
});
|
|
@@ -15516,24 +15560,24 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15516
15560
|
try {
|
|
15517
15561
|
known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
|
|
15518
15562
|
} catch {}
|
|
15519
|
-
sendJson$
|
|
15563
|
+
sendJson$2(res, 200, {
|
|
15520
15564
|
config: toWire(settings),
|
|
15521
15565
|
knownWorkspaces: known
|
|
15522
15566
|
});
|
|
15523
15567
|
return;
|
|
15524
15568
|
}
|
|
15525
15569
|
if (req.method === "PUT") {
|
|
15526
|
-
const raw = await readBody$
|
|
15570
|
+
const raw = await readBody$2(req, 131072);
|
|
15527
15571
|
const saved = applyWirePatch(JSON.parse(raw).config ?? {});
|
|
15528
15572
|
try {
|
|
15529
15573
|
ctx.emit?.("serenity/config-updated");
|
|
15530
15574
|
} catch {}
|
|
15531
|
-
sendJson$
|
|
15575
|
+
sendJson$2(res, 200, { config: toWire(saved) });
|
|
15532
15576
|
return;
|
|
15533
15577
|
}
|
|
15534
|
-
sendJson$
|
|
15578
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15535
15579
|
} catch (err) {
|
|
15536
|
-
sendJson$
|
|
15580
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15537
15581
|
}
|
|
15538
15582
|
}
|
|
15539
15583
|
});
|
|
@@ -15543,7 +15587,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15543
15587
|
handler: async (req, res) => {
|
|
15544
15588
|
try {
|
|
15545
15589
|
if (req.method !== "GET") {
|
|
15546
|
-
sendJson$
|
|
15590
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15547
15591
|
return;
|
|
15548
15592
|
}
|
|
15549
15593
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -15552,12 +15596,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15552
15596
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
15553
15597
|
});
|
|
15554
15598
|
const root = cccRootForCwd(workspace) ?? "";
|
|
15555
|
-
sendJson$
|
|
15599
|
+
sendJson$2(res, 200, { cccs: await listCccs(ctx, {
|
|
15556
15600
|
defaultRoot: root,
|
|
15557
15601
|
withRoles: true
|
|
15558
15602
|
}) });
|
|
15559
15603
|
} catch (err) {
|
|
15560
|
-
sendJson$
|
|
15604
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15561
15605
|
}
|
|
15562
15606
|
}
|
|
15563
15607
|
});
|
|
@@ -15571,21 +15615,21 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15571
15615
|
if (req.method === "GET") {
|
|
15572
15616
|
const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
|
|
15573
15617
|
if (!chk.ok) {
|
|
15574
|
-
sendJson$
|
|
15618
|
+
sendJson$2(res, 400, { error: chk.error });
|
|
15575
15619
|
return;
|
|
15576
15620
|
}
|
|
15577
|
-
const { readWeixinSettings } = await import("./route-
|
|
15621
|
+
const { readWeixinSettings } = await import("./route-ItCgAsnz.js").then((n) => n.c);
|
|
15578
15622
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
|
|
15579
15623
|
const settings = readWeixinSettings(chk.root);
|
|
15580
15624
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
15581
|
-
const { readWeixinCredential } = await import("./route-
|
|
15625
|
+
const { readWeixinCredential } = await import("./route-ItCgAsnz.js").then((n) => n.c);
|
|
15582
15626
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
15583
15627
|
accountId: a.accountId,
|
|
15584
15628
|
name: a.name ?? void 0,
|
|
15585
15629
|
enabled: a.enabled !== false,
|
|
15586
15630
|
bound: readWeixinCredential(chk.root, a.accountId) !== null
|
|
15587
15631
|
}));
|
|
15588
|
-
sendJson$
|
|
15632
|
+
sendJson$2(res, 200, {
|
|
15589
15633
|
enabled: settings.enabled === true,
|
|
15590
15634
|
botType: settings.botType ?? void 0,
|
|
15591
15635
|
accounts,
|
|
@@ -15595,22 +15639,22 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15595
15639
|
return;
|
|
15596
15640
|
}
|
|
15597
15641
|
if (req.method !== "POST") {
|
|
15598
|
-
sendJson$
|
|
15642
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15599
15643
|
return;
|
|
15600
15644
|
}
|
|
15601
|
-
const raw = await readBody$
|
|
15645
|
+
const raw = await readBody$2(req, 131072);
|
|
15602
15646
|
const body = JSON.parse(raw);
|
|
15603
15647
|
const chk = requireCcc(body.ccc);
|
|
15604
15648
|
if (!chk.ok) {
|
|
15605
|
-
sendJson$
|
|
15649
|
+
sendJson$2(res, 400, { error: chk.error });
|
|
15606
15650
|
return;
|
|
15607
15651
|
}
|
|
15608
15652
|
const root = chk.root;
|
|
15609
15653
|
if (body.action === "login-start") {
|
|
15610
|
-
const { fetchQRCode } = await import("./ilink-
|
|
15654
|
+
const { fetchQRCode } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
|
|
15611
15655
|
const { randomUUID } = await import("node:crypto");
|
|
15612
15656
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
15613
|
-
const { readWeixinSettings } = await import("./route-
|
|
15657
|
+
const { readWeixinSettings } = await import("./route-ItCgAsnz.js").then((n) => n.c);
|
|
15614
15658
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
15615
15659
|
const loginKey = randomUUID();
|
|
15616
15660
|
weixinLogins.set(loginKey, {
|
|
@@ -15619,7 +15663,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15619
15663
|
startedAt: Date.now(),
|
|
15620
15664
|
polling: false
|
|
15621
15665
|
});
|
|
15622
|
-
sendJson$
|
|
15666
|
+
sendJson$2(res, 200, {
|
|
15623
15667
|
qrcode: qr.qrcode,
|
|
15624
15668
|
qrcode_img_content: qr.qrcode_img_content,
|
|
15625
15669
|
loginKey
|
|
@@ -15628,20 +15672,20 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15628
15672
|
}
|
|
15629
15673
|
if (body.action === "remove-account") {
|
|
15630
15674
|
if (!body.accountId) {
|
|
15631
|
-
sendJson$
|
|
15675
|
+
sendJson$2(res, 400, { error: "missing accountId" });
|
|
15632
15676
|
return;
|
|
15633
15677
|
}
|
|
15634
|
-
const { removeWeixinAccount } = await import("./route-
|
|
15678
|
+
const { removeWeixinAccount } = await import("./route-ItCgAsnz.js").then((n) => n.c);
|
|
15635
15679
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15636
15680
|
removeWeixinAccount(root, body.accountId);
|
|
15637
15681
|
syncCccBridge(ctx, root);
|
|
15638
|
-
sendJson$
|
|
15682
|
+
sendJson$2(res, 200, { removed: body.accountId });
|
|
15639
15683
|
return;
|
|
15640
15684
|
}
|
|
15641
15685
|
if (body.action === "save-routes") {
|
|
15642
15686
|
const raw = body.routes ?? [];
|
|
15643
15687
|
if (!Array.isArray(raw)) {
|
|
15644
|
-
sendJson$
|
|
15688
|
+
sendJson$2(res, 400, { error: "invalid routes (expected [{user, trajectory}, ...])" });
|
|
15645
15689
|
return;
|
|
15646
15690
|
}
|
|
15647
15691
|
const routes = [];
|
|
@@ -15649,7 +15693,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15649
15693
|
const user = typeof r?.user === "string" ? r.user : "";
|
|
15650
15694
|
const code = typeof r?.trajectory === "string" && r.trajectory !== "" ? r.trajectory : typeof r?.role === "string" ? r.role : "";
|
|
15651
15695
|
if (user === "" || code === "") {
|
|
15652
|
-
sendJson$
|
|
15696
|
+
sendJson$2(res, 400, { error: "invalid routes (each row needs non-empty user and trajectory)" });
|
|
15653
15697
|
return;
|
|
15654
15698
|
}
|
|
15655
15699
|
routes.push({
|
|
@@ -15657,23 +15701,23 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15657
15701
|
trajectory: code
|
|
15658
15702
|
});
|
|
15659
15703
|
}
|
|
15660
|
-
const { saveWeixinRoutes } = await import("./route-
|
|
15661
|
-
const { findDirNameByCode } = await import("./profile-
|
|
15704
|
+
const { saveWeixinRoutes } = await import("./route-ItCgAsnz.js").then((n) => n.c);
|
|
15705
|
+
const { findDirNameByCode } = await import("./profile-BIQ2widI.js").then((n) => n.i);
|
|
15662
15706
|
for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
|
|
15663
|
-
sendJson$
|
|
15707
|
+
sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
|
|
15664
15708
|
return;
|
|
15665
15709
|
}
|
|
15666
15710
|
saveWeixinRoutes(root, routes);
|
|
15667
|
-
sendJson$
|
|
15711
|
+
sendJson$2(res, 200, { saved: routes.length });
|
|
15668
15712
|
return;
|
|
15669
15713
|
}
|
|
15670
15714
|
if (body.action === "set-enabled") {
|
|
15671
15715
|
const enabled = body.enabled === true;
|
|
15672
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./route-
|
|
15716
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./route-ItCgAsnz.js").then((n) => n.c);
|
|
15673
15717
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15674
15718
|
const settings = readWeixinSettings(root);
|
|
15675
15719
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
15676
|
-
sendJson$
|
|
15720
|
+
sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
|
|
15677
15721
|
return;
|
|
15678
15722
|
}
|
|
15679
15723
|
setWeixinEnabled(root, enabled);
|
|
@@ -15682,12 +15726,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15682
15726
|
const { stopCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15683
15727
|
stopCccBridge(root);
|
|
15684
15728
|
}
|
|
15685
|
-
sendJson$
|
|
15729
|
+
sendJson$2(res, 200, { enabled });
|
|
15686
15730
|
return;
|
|
15687
15731
|
}
|
|
15688
|
-
sendJson$
|
|
15732
|
+
sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
|
|
15689
15733
|
} catch (err) {
|
|
15690
|
-
sendJson$
|
|
15734
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15691
15735
|
}
|
|
15692
15736
|
}
|
|
15693
15737
|
});
|
|
@@ -15698,22 +15742,22 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15698
15742
|
try {
|
|
15699
15743
|
if (!requireWebUi(req, res, "微信桥配置仅限 WebUI(client 专用)")) return;
|
|
15700
15744
|
if (req.method !== "GET") {
|
|
15701
|
-
sendJson$
|
|
15745
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15702
15746
|
return;
|
|
15703
15747
|
}
|
|
15704
15748
|
const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
|
|
15705
15749
|
const login = weixinLogins.get(key);
|
|
15706
15750
|
if (!login) {
|
|
15707
|
-
sendJson$
|
|
15751
|
+
sendJson$2(res, 404, { error: "login not found or expired" });
|
|
15708
15752
|
return;
|
|
15709
15753
|
}
|
|
15710
15754
|
if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
|
|
15711
15755
|
weixinLogins.delete(key);
|
|
15712
|
-
sendJson$
|
|
15756
|
+
sendJson$2(res, 200, { status: "expired" });
|
|
15713
15757
|
return;
|
|
15714
15758
|
}
|
|
15715
|
-
const { pollQRStatus } = await import("./ilink-
|
|
15716
|
-
const { readWeixinSettings } = await import("./route-
|
|
15759
|
+
const { pollQRStatus } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
|
|
15760
|
+
const { readWeixinSettings } = await import("./route-ItCgAsnz.js").then((n) => n.c);
|
|
15717
15761
|
const settings = readWeixinSettings(login.root);
|
|
15718
15762
|
const status = await pollQRStatus({
|
|
15719
15763
|
baseUrl: void 0,
|
|
@@ -15722,13 +15766,13 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15722
15766
|
switch (status.status) {
|
|
15723
15767
|
case "confirmed": {
|
|
15724
15768
|
if (!status.bot_token) {
|
|
15725
|
-
sendJson$
|
|
15769
|
+
sendJson$2(res, 200, {
|
|
15726
15770
|
status: "error",
|
|
15727
15771
|
error: "confirmed but no bot_token"
|
|
15728
15772
|
});
|
|
15729
15773
|
return;
|
|
15730
15774
|
}
|
|
15731
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-
|
|
15775
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-ItCgAsnz.js").then((n) => n.c);
|
|
15732
15776
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15733
15777
|
const accountId = nextWeixinAccountId(settings);
|
|
15734
15778
|
upsertWeixinAccount(login.root, {
|
|
@@ -15743,7 +15787,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15743
15787
|
});
|
|
15744
15788
|
syncCccBridge(ctx, login.root);
|
|
15745
15789
|
weixinLogins.delete(key);
|
|
15746
|
-
sendJson$
|
|
15790
|
+
sendJson$2(res, 200, {
|
|
15747
15791
|
status: "confirmed",
|
|
15748
15792
|
accountId,
|
|
15749
15793
|
tokenSaved: true
|
|
@@ -15752,14 +15796,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15752
15796
|
}
|
|
15753
15797
|
case "expired":
|
|
15754
15798
|
weixinLogins.delete(key);
|
|
15755
|
-
sendJson$
|
|
15799
|
+
sendJson$2(res, 200, { status: "expired" });
|
|
15756
15800
|
return;
|
|
15757
15801
|
default:
|
|
15758
|
-
sendJson$
|
|
15802
|
+
sendJson$2(res, 200, { status: status.status ?? "wait" });
|
|
15759
15803
|
return;
|
|
15760
15804
|
}
|
|
15761
15805
|
} catch (err) {
|
|
15762
|
-
sendJson$
|
|
15806
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15763
15807
|
}
|
|
15764
15808
|
}
|
|
15765
15809
|
});
|
|
@@ -15781,7 +15825,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15781
15825
|
const root = sessionsRootDir();
|
|
15782
15826
|
if (req.method === "GET") {
|
|
15783
15827
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
15784
|
-
sendJson$
|
|
15828
|
+
sendJson$2(res, 200, {
|
|
15785
15829
|
root,
|
|
15786
15830
|
olderThanDays: days,
|
|
15787
15831
|
dryRun: true,
|
|
@@ -15795,12 +15839,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15795
15839
|
return;
|
|
15796
15840
|
}
|
|
15797
15841
|
if (req.method !== "POST") {
|
|
15798
|
-
sendJson$
|
|
15842
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15799
15843
|
return;
|
|
15800
15844
|
}
|
|
15801
15845
|
if (!requireWebUi(req, res, "会话清理仅限 WebUI(client 专用)")) return;
|
|
15802
15846
|
const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
|
|
15803
|
-
sendJson$
|
|
15847
|
+
sendJson$2(res, 200, {
|
|
15804
15848
|
root,
|
|
15805
15849
|
olderThanDays: days,
|
|
15806
15850
|
dryRun: false,
|
|
@@ -15808,7 +15852,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15808
15852
|
errors: result.errors
|
|
15809
15853
|
});
|
|
15810
15854
|
} catch (err) {
|
|
15811
|
-
sendJson$
|
|
15855
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
15812
15856
|
}
|
|
15813
15857
|
}
|
|
15814
15858
|
});
|
|
@@ -15987,7 +16031,7 @@ function loadVoyageHtml(assetPath = voyageAssetPath()) {
|
|
|
15987
16031
|
return readFileSync(assetPath, "utf-8");
|
|
15988
16032
|
}
|
|
15989
16033
|
/** 发 JSON(错误分支用;与 surface/http.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
|
|
15990
|
-
function sendJson$
|
|
16034
|
+
function sendJson$1(res, status, body) {
|
|
15991
16035
|
res.statusCode = status;
|
|
15992
16036
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
15993
16037
|
res.end(JSON.stringify(body));
|
|
@@ -16005,14 +16049,14 @@ function registerVoyageApi(ctx, opts = {}) {
|
|
|
16005
16049
|
path: VOYAGE_PATH,
|
|
16006
16050
|
handler: (req, res) => {
|
|
16007
16051
|
if (req.method !== "GET") {
|
|
16008
|
-
sendJson$
|
|
16052
|
+
sendJson$1(res, 405, { error: "method not allowed" });
|
|
16009
16053
|
return;
|
|
16010
16054
|
}
|
|
16011
16055
|
let html;
|
|
16012
16056
|
try {
|
|
16013
16057
|
html = loadVoyageHtml(opts.assetPath ?? voyageAssetPath());
|
|
16014
16058
|
} catch (err) {
|
|
16015
|
-
sendJson$
|
|
16059
|
+
sendJson$1(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
|
|
16016
16060
|
return;
|
|
16017
16061
|
}
|
|
16018
16062
|
let root = null;
|
|
@@ -16650,10 +16694,6 @@ var FaceStartCancelled = class extends Error {
|
|
|
16650
16694
|
this.name = "FaceStartCancelled";
|
|
16651
16695
|
}
|
|
16652
16696
|
};
|
|
16653
|
-
/** 面是否在监听 */
|
|
16654
|
-
function faceActive(name) {
|
|
16655
|
-
return faces.has(name);
|
|
16656
|
-
}
|
|
16657
16697
|
/** 面的实际监听端口(未在监听 → null) */
|
|
16658
16698
|
function facePort(name) {
|
|
16659
16699
|
return faces.get(name)?.handle.port ?? null;
|
|
@@ -17240,7 +17280,7 @@ function createDshCookieProvider(connection, authority) {
|
|
|
17240
17280
|
/** 面名(C4 块 A:listener 生命周期归 `face-host`) */
|
|
17241
17281
|
const FACE_GATEWAY = "gateway";
|
|
17242
17282
|
/** 读取请求体(≤ maxBytes;超限 reject) */
|
|
17243
|
-
function readBody$
|
|
17283
|
+
function readBody$1(req, maxBytes) {
|
|
17244
17284
|
return new Promise((resolve, reject) => {
|
|
17245
17285
|
let data = "";
|
|
17246
17286
|
req.on("data", (chunk) => {
|
|
@@ -17367,7 +17407,7 @@ async function startGateway(config, getAccounts, getDshCookie) {
|
|
|
17367
17407
|
const session = authed(req);
|
|
17368
17408
|
if (session !== void 0) {
|
|
17369
17409
|
if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
|
|
17370
|
-
readBody$
|
|
17410
|
+
readBody$1(req, 131072).then((body) => {
|
|
17371
17411
|
const { rpcId, path } = parseWorkspaceCreateBody(body);
|
|
17372
17412
|
if (!allowWorkspaceCreate) {
|
|
17373
17413
|
res.writeHead(403, { "content-type": "application/json" });
|
|
@@ -19687,234 +19727,16 @@ f.parseInline;
|
|
|
19687
19727
|
b.parse;
|
|
19688
19728
|
x.lex;
|
|
19689
19729
|
//#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
|
-
}
|
|
19730
|
+
//#region src/surface/text.ts
|
|
19900
19731
|
/**
|
|
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 安全文本
|
|
19732
|
+
* 剥离 `<think>…</think>` 块(v1.27.1,微信桥用户反馈:"过微信桥回复给用户的消息,
|
|
19733
|
+
* 要去掉 think 标签,用户不应看到")。复用 extractThinkBlocks 状态机(v1.26.8,
|
|
19734
|
+
* 弃正则)——提取后**只保留正文**(占位符替换为空,think 内容丢弃),
|
|
19735
|
+
* 未闭合 think 优雅截断。适用于纯文本输出面(微信等不支持 think 折叠的通道)。
|
|
19915
19736
|
*/
|
|
19916
|
-
function
|
|
19917
|
-
|
|
19737
|
+
function stripThink(raw) {
|
|
19738
|
+
const { body } = extractThinkBlocks(raw);
|
|
19739
|
+
return body.replace(/\u0001T(\d+)\u0001/g, "");
|
|
19918
19740
|
}
|
|
19919
19741
|
/**
|
|
19920
19742
|
* 提取 `<think>…</think>` 块(v1.26.8:**状态机扫描,弃正则**——用户批评"老用正则不是个办法")。
|
|
@@ -19992,140 +19814,6 @@ function matchCloseThink(s, contentStart) {
|
|
|
19992
19814
|
}
|
|
19993
19815
|
return -1;
|
|
19994
19816
|
}
|
|
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
19817
|
//#endregion
|
|
20130
19818
|
//#region src/guard/filter.ts
|
|
20131
19819
|
/**
|
|
@@ -20597,131 +20285,6 @@ function appendHumanChannelAudit(event) {
|
|
|
20597
20285
|
})}\n`, "utf-8");
|
|
20598
20286
|
} catch {}
|
|
20599
20287
|
}
|
|
20600
|
-
const manualSessions = /* @__PURE__ */ new Map();
|
|
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
|
-
}
|
|
20642
|
-
/**
|
|
20643
|
-
* 工具调用是否为「成功的微信发送」。
|
|
20644
|
-
*
|
|
20645
|
-
* v1.31.0 起有两种合法形态(标记与闸门同时认,迁移期并存):
|
|
20646
|
-
* - `im-bridge({channel:"weixin", action:"send"|"send-file", …})` —— ACC 工具(推荐)
|
|
20647
|
-
* - `msm("weixin-send", ["send"|"send-file", …])` —— CCC MSM(v1.30.x 通道,逐步退役)
|
|
20648
|
-
*/
|
|
20649
|
-
function isSuccessfulWeixinSend(exec, result) {
|
|
20650
|
-
const e = exec;
|
|
20651
|
-
if (result?.isError === true) return false;
|
|
20652
|
-
const args = e?.arguments;
|
|
20653
|
-
if (e?.name === "im-bridge") {
|
|
20654
|
-
if ((typeof args?.channel === "string" ? args.channel : "") !== "weixin") return false;
|
|
20655
|
-
const action = typeof args?.action === "string" ? args.action : "";
|
|
20656
|
-
return action === "send" || action === "send-file";
|
|
20657
|
-
}
|
|
20658
|
-
if (e?.name === "msm") return args?.name === "weixin-send";
|
|
20659
|
-
return false;
|
|
20660
|
-
}
|
|
20661
|
-
/** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
|
|
20662
|
-
function buildManualOutputRebuke(session) {
|
|
20663
|
-
return [
|
|
20664
|
-
"[serenity:weixin-manual-output] no send detected in this turn — the user has received NOTHING.",
|
|
20665
|
-
`ccc=${session.root} account=${session.accountId} user=${session.userId}`,
|
|
20666
|
-
`im-bridge({channel:"weixin", action:"send", account:"${session.accountId}", user:"${session.userId}", text:"<回复>"})`
|
|
20667
|
-
].join("\n");
|
|
20668
|
-
}
|
|
20669
|
-
/**
|
|
20670
|
-
* 装配闸门(index.ts apply 调用)。
|
|
20671
|
-
* 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff 会话)零干预。
|
|
20672
|
-
*/
|
|
20673
|
-
function registerWeixinOutputGuard(ctx) {
|
|
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
20288
|
//#endregion
|
|
20726
20289
|
//#region src/im/hook.ts
|
|
20727
20290
|
/**
|
|
@@ -20980,138 +20543,145 @@ function installWeixinAccountImpl(d) {
|
|
|
20980
20543
|
setWeixinLoopDeps(d);
|
|
20981
20544
|
registerImAccountImpl(weixinAccountImpl);
|
|
20982
20545
|
}
|
|
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();
|
|
20546
|
+
const manualSessions = /* @__PURE__ */ new Map();
|
|
20999
20547
|
/**
|
|
21000
|
-
*
|
|
21001
|
-
*
|
|
21002
|
-
* 机器级 poller **不属于任何 CCC**(这正是"上移 ACC 层"的含义:谁轮询不再由 CCC 数量决定)。
|
|
21003
|
-
* 用途有二:① `runAccountLoop` 的 `root` 形参在机器级模式下不被使用,传它以示"无根";
|
|
21004
|
-
* ② 归属表(`im/inbound.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
|
|
21005
|
-
* 与任何**真实** CCC 根都不会撞(真根一律是绝对路径)。
|
|
20548
|
+
* 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
|
|
20549
|
+
* v1.30.17 起桥要在 `askSkiff` 返回后读它决定是否兜底,故 turn-stopping 不再结算清空)。
|
|
21006
20550
|
*/
|
|
21007
|
-
const
|
|
21008
|
-
/**
|
|
21009
|
-
const
|
|
21010
|
-
/**
|
|
21011
|
-
|
|
21012
|
-
/**
|
|
21013
|
-
|
|
21014
|
-
|
|
21015
|
-
|
|
20551
|
+
const sentThisTurn = /* @__PURE__ */ new Set();
|
|
20552
|
+
/** 连续打回计数(成功发送即清零) */
|
|
20553
|
+
const rebukeCounts = /* @__PURE__ */ new Map();
|
|
20554
|
+
/** 闸门是否装配成功(v1.30.17:桥据此判断能否信任"未发送"这一判定——未装配时不兜底,防重复发送) */
|
|
20555
|
+
let guardActive = false;
|
|
20556
|
+
/** 桥:登记手动模式会话(每轮 incoming 都调用,幂等覆盖) */
|
|
20557
|
+
function noteManualOutputSession(sessionId, session) {
|
|
20558
|
+
if (typeof sessionId !== "string" || sessionId === "") return;
|
|
20559
|
+
manualSessions.set(sessionId, session);
|
|
21016
20560
|
}
|
|
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
|
-
}
|
|
20561
|
+
/** 桥:非手动模式 / 会话失效 → 注销(闸门随之失效,零干预) */
|
|
20562
|
+
function forgetManualOutputSession(sessionId) {
|
|
20563
|
+
manualSessions.delete(sessionId);
|
|
20564
|
+
sentThisTurn.delete(sessionId);
|
|
20565
|
+
rebukeCounts.delete(sessionId);
|
|
21043
20566
|
}
|
|
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
|
-
}
|
|
20567
|
+
/** 本轮是否已成功发送(测试/诊断用;桥在每轮结束时读它决定是否兜底) */
|
|
20568
|
+
function hasSentThisTurn(sessionId) {
|
|
20569
|
+
return sentThisTurn.has(sessionId);
|
|
21059
20570
|
}
|
|
21060
20571
|
/**
|
|
21061
|
-
*
|
|
21062
|
-
*
|
|
21063
|
-
*
|
|
21064
|
-
* 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
|
|
21065
|
-
* owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
|
|
21066
|
-
* 归**账号自己的实现**,装配处只按 `channel` 查派发表。
|
|
21067
|
-
* ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
|
|
21068
|
-
* `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
|
|
20572
|
+
* 桥:**每轮开始时**清空本会话的"已发送"标记(turn 边界由桥掌握——v1.30.17)。
|
|
20573
|
+
* 闸门侧不再于 turn-stopping 结算清空,否则桥读不到本轮结果、无法兜底。
|
|
21069
20574
|
*/
|
|
20575
|
+
function clearSentThisTurn(sessionId) {
|
|
20576
|
+
sentThisTurn.delete(sessionId);
|
|
20577
|
+
}
|
|
20578
|
+
/** 闸门是否装配成功(未装配 → 桥不做兜底:宁可静默也不冒重复发送的风险) */
|
|
20579
|
+
function isWeixinOutputGuardActive() {
|
|
20580
|
+
return guardActive;
|
|
20581
|
+
}
|
|
20582
|
+
/** 从 agent / 会话对象取会话 id(形状宽容) */
|
|
20583
|
+
function sessionIdOf$1(value) {
|
|
20584
|
+
const v = value;
|
|
20585
|
+
const id = v?.session?.id ?? v?.id;
|
|
20586
|
+
return typeof id === "string" && id !== "" ? id : null;
|
|
20587
|
+
}
|
|
21070
20588
|
/**
|
|
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 管内容"的归属二分。
|
|
20589
|
+
* 工具调用是否为「成功的微信发送」。
|
|
21082
20590
|
*
|
|
21083
|
-
*
|
|
21084
|
-
*
|
|
21085
|
-
*
|
|
21086
|
-
* im-bridge({channel:"weixin", action:"send", account:"<account>", user:"<user>", text:"<回复>"})
|
|
21087
|
-
* ```
|
|
20591
|
+
* v1.31.0 起有两种合法形态(标记与闸门同时认,迁移期并存):
|
|
20592
|
+
* - `im-bridge({channel:"weixin", action:"send"|"send-file", …})` —— ACC 工具(推荐)
|
|
20593
|
+
* - `msm("weixin-send", ["send"|"send-file", …])` —— CCC MSM(v1.30.x 通道,逐步退役)
|
|
21088
20594
|
*/
|
|
21089
|
-
function
|
|
21090
|
-
|
|
20595
|
+
function isSuccessfulWeixinSend(exec, result) {
|
|
20596
|
+
const e = exec;
|
|
20597
|
+
if (result?.isError === true) return false;
|
|
20598
|
+
const args = e?.arguments;
|
|
20599
|
+
if (e?.name === "im-bridge") {
|
|
20600
|
+
if ((typeof args?.channel === "string" ? args.channel : "") !== "weixin") return false;
|
|
20601
|
+
const action = typeof args?.action === "string" ? args.action : "";
|
|
20602
|
+
return action === "send" || action === "send-file";
|
|
20603
|
+
}
|
|
20604
|
+
if (e?.name === "msm") return args?.name === "weixin-send";
|
|
20605
|
+
return false;
|
|
20606
|
+
}
|
|
20607
|
+
/** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
|
|
20608
|
+
function buildManualOutputRebuke(session) {
|
|
20609
|
+
return [
|
|
20610
|
+
"[serenity:weixin-manual-output] no send detected in this turn — the user has received NOTHING.",
|
|
20611
|
+
`ccc=${session.root} account=${session.accountId} user=${session.userId}`,
|
|
20612
|
+
`im-bridge({channel:"weixin", action:"send", account:"${session.accountId}", user:"${session.userId}", text:"<回复>"})`
|
|
20613
|
+
].join("\n");
|
|
21091
20614
|
}
|
|
21092
20615
|
/**
|
|
21093
|
-
*
|
|
21094
|
-
*
|
|
21095
|
-
* 四个条件同时成立才兜底:
|
|
21096
|
-
* ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默,向后兼容)
|
|
21097
|
-
* ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
|
|
21098
|
-
* ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
|
|
21099
|
-
* ④ 有最终文本可转发(空文本无意义)
|
|
20616
|
+
* 装配闸门(index.ts apply 调用)。
|
|
20617
|
+
* 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff 会话)零干预。
|
|
21100
20618
|
*/
|
|
21101
|
-
function
|
|
21102
|
-
|
|
20619
|
+
function registerWeixinOutputGuard(ctx) {
|
|
20620
|
+
try {
|
|
20621
|
+
ctx.on("tools/post-execute", async (exec, result, next) => {
|
|
20622
|
+
const downstream = await next();
|
|
20623
|
+
try {
|
|
20624
|
+
const sessionId = sessionIdOf$1(exec?.agent);
|
|
20625
|
+
if (sessionId && manualSessions.has(sessionId) && isSuccessfulWeixinSend(exec, result)) {
|
|
20626
|
+
sentThisTurn.add(sessionId);
|
|
20627
|
+
rebukeCounts.delete(sessionId);
|
|
20628
|
+
}
|
|
20629
|
+
} catch {}
|
|
20630
|
+
return downstream;
|
|
20631
|
+
});
|
|
20632
|
+
} catch {
|
|
20633
|
+
console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:tools/post-execute 不可用");
|
|
20634
|
+
return;
|
|
20635
|
+
}
|
|
20636
|
+
try {
|
|
20637
|
+
ctx.on("agent/turn-stopping", (payload) => {
|
|
20638
|
+
const agent = payload?.agent;
|
|
20639
|
+
if (!agent) return;
|
|
20640
|
+
const sessionId = sessionIdOf$1(agent);
|
|
20641
|
+
if (!sessionId) return;
|
|
20642
|
+
const session = manualSessions.get(sessionId);
|
|
20643
|
+
if (!session) return;
|
|
20644
|
+
if (sentThisTurn.has(sessionId)) return;
|
|
20645
|
+
const count = (rebukeCounts.get(sessionId) ?? 0) + 1;
|
|
20646
|
+
if (count > 2) {
|
|
20647
|
+
rebukeCounts.delete(sessionId);
|
|
20648
|
+
console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role})——本轮用户将收不到 agent 自己发的消息`);
|
|
20649
|
+
return;
|
|
20650
|
+
}
|
|
20651
|
+
rebukeCounts.set(sessionId, count);
|
|
20652
|
+
try {
|
|
20653
|
+
agent.steer(createUserMessage({
|
|
20654
|
+
content: [{
|
|
20655
|
+
type: "text",
|
|
20656
|
+
text: buildManualOutputRebuke(session)
|
|
20657
|
+
}],
|
|
20658
|
+
source: PLUGIN_SOURCE
|
|
20659
|
+
}));
|
|
20660
|
+
console.log(`[serenity-hooks] weixin 输出闸门:打回 ${count}/2(session=${sessionId}, turn=${payload.turn ?? "?"})`);
|
|
20661
|
+
} catch (error) {
|
|
20662
|
+
console.warn(`[serenity-hooks] ✗ weixin 输出闸门 steer 失败: ${String(error?.message ?? error)}`);
|
|
20663
|
+
}
|
|
20664
|
+
});
|
|
20665
|
+
} catch {
|
|
20666
|
+
console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:agent/turn-stopping 不可用");
|
|
20667
|
+
return;
|
|
20668
|
+
}
|
|
20669
|
+
guardActive = true;
|
|
21103
20670
|
}
|
|
20671
|
+
//#endregion
|
|
20672
|
+
//#region src/im/inbound-event.ts
|
|
20673
|
+
/** 媒体大小上限(M6:20MB,对齐 vlm-describe;超限降级告知不落盘) */
|
|
20674
|
+
const MEDIA_MAX_BYTES = 20971520;
|
|
21104
20675
|
/**
|
|
21105
20676
|
* 订阅表里的 `ccc` **别名 → CCC 根**(P0-3)。
|
|
21106
20677
|
*
|
|
21107
|
-
*
|
|
21108
|
-
*
|
|
21109
|
-
*
|
|
21110
|
-
* @returns 命中的 CCC 根;枚举里没有 ⇒ `null`(调用方**响亮报错**,不静默丢弃)
|
|
20678
|
+
* 别名取值与面板下拉**同源** ⇒ 先按 `CccEntry.name`(= CCC 根目录名)匹配;
|
|
20679
|
+
* 再退一步接受**绝对路径**(手写配置时人会直接粘路径)。两条都试
|
|
20680
|
+
* ⇒ 两种写法都可用,且**不引入第二张名字表**。
|
|
21111
20681
|
*/
|
|
21112
20682
|
async function resolveCccRootByAlias(ctx, alias) {
|
|
21113
20683
|
try {
|
|
21114
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20684
|
+
const { listCccs } = await import("./ccc-roots-BhoA7G-0.js").then((n) => n.a);
|
|
21115
20685
|
const entries = await listCccs(ctx);
|
|
21116
20686
|
const byName = entries.find((e) => e.name === alias);
|
|
21117
20687
|
if (byName) return byName.root;
|
|
@@ -21128,11 +20698,9 @@ async function resolveCccRootByAlias(ctx, alias) {
|
|
|
21128
20698
|
* 判据**,差别只有一格 —— **有没有 CCC 级回退**(`cccFallback`)。写两遍必然分叉。
|
|
21129
20699
|
*
|
|
21130
20700
|
* 三档(方案 §3.1,**刻意分档**):
|
|
21131
|
-
* - **未命中** ⇒ 不投递 +
|
|
21132
|
-
* - **表残缺** ⇒
|
|
21133
|
-
* - **目标不存在**(CCC / **轨迹**)⇒
|
|
21134
|
-
*
|
|
21135
|
-
* @param input.cccFallback 过渡期回退(方案 Q5);**机器级模式传 `null`** ⇒ 未命中即不投递
|
|
20701
|
+
* - **未命中** ⇒ 不投递 + **留痕**(静默档:这是"表里没有你",不是错误)
|
|
20702
|
+
* - **表残缺** ⇒ **响亮出声**(且**独立于是否命中**)
|
|
20703
|
+
* - **目标不存在**(CCC / **轨迹**)⇒ **响亮报错**(不静默丢弃)
|
|
21136
20704
|
*/
|
|
21137
20705
|
async function resolveInboundTarget(ctx, input) {
|
|
21138
20706
|
const route = planInboundRoute({
|
|
@@ -21181,7 +20749,7 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
21181
20749
|
});
|
|
21182
20750
|
return null;
|
|
21183
20751
|
}
|
|
21184
|
-
const { findDirNameByCode } = await import("./profile-
|
|
20752
|
+
const { findDirNameByCode } = await import("./profile-BIQ2widI.js").then((n) => n.i);
|
|
21185
20753
|
if (findDirNameByCode(root, target.role) === null) {
|
|
21186
20754
|
const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
|
|
21187
20755
|
console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
|
|
@@ -21203,33 +20771,21 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
21203
20771
|
};
|
|
21204
20772
|
}
|
|
21205
20773
|
/**
|
|
21206
|
-
*
|
|
20774
|
+
* **一条入站消息的主流程**(通道中立)。
|
|
21207
20775
|
*
|
|
21208
|
-
*
|
|
21209
|
-
*
|
|
21210
|
-
* 固定 id resume-or-create(v1.27.2):磁盘已有持久化 log → resume(历史延续,
|
|
21211
|
-
* 重启后记忆保留——真正的"同用户长期延续");无 log(首次)→ create。
|
|
21212
|
-
* "新的对话已开始"通知仅真正首次(create)时发送;resume/进程内延续不发。
|
|
20776
|
+
* 职责:路由 → 固定会话 id 创建/延续 → 提问 → 回复(或被闸门拦下后兜底)。
|
|
20777
|
+
* 语料(媒体)由事件自带 `fetch()`;出站走的 `io`。
|
|
21213
20778
|
*
|
|
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)。
|
|
20779
|
+
* 🔴 **本函数没有任何 iLink 概念** —— 这是 B1 的判据。任何 `context_token` /
|
|
20780
|
+
* `item_list` / `message_type` 出现在这里,就说明抽缝没抽干净。
|
|
21222
20781
|
*
|
|
21223
|
-
*
|
|
21224
|
-
* **回退目标**)。两档语义刻意分开(方案 §3.1):**未命中 ⇒ 不投递 + 留痕**(静默档);
|
|
21225
|
-
* **目标不存在(CCC / 角色)⇒ 响亮报错**(`console.error` + 机器级留痕)。两者都
|
|
21226
|
-
* **不是**"投给默认角色"。
|
|
20782
|
+
* @param preset 机器级模式:目标已解析好 ⇒ **不再自行路由**,且**不看本 CCC 的 `enabled`**
|
|
21227
20783
|
*/
|
|
21228
|
-
async function
|
|
21229
|
-
const fromUserId =
|
|
20784
|
+
async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
|
|
20785
|
+
const fromUserId = event.userId;
|
|
21230
20786
|
if (!fromUserId) return;
|
|
21231
|
-
const text =
|
|
21232
|
-
const mediaRefs =
|
|
20787
|
+
const text = event.text;
|
|
20788
|
+
const mediaRefs = event.media;
|
|
21233
20789
|
const settings = readWeixinSettings(root);
|
|
21234
20790
|
let resolved;
|
|
21235
20791
|
if (preset) resolved = preset.target;
|
|
@@ -21249,12 +20805,10 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21249
20805
|
const { trajectory, profile } = resolved;
|
|
21250
20806
|
root = resolved.root;
|
|
21251
20807
|
if (!text && mediaRefs.length === 0) {
|
|
21252
|
-
if (
|
|
21253
|
-
baseUrl: cred.baseUrl,
|
|
21254
|
-
token: cred.token,
|
|
20808
|
+
if (event.hasVoice) await io.sendText({
|
|
21255
20809
|
toUserId: fromUserId,
|
|
21256
20810
|
text: "(抱歉,暂时无法解析这条语音消息,请尝试发送文字)",
|
|
21257
|
-
|
|
20811
|
+
replyRef: event.replyRef
|
|
21258
20812
|
}).catch(() => {});
|
|
21259
20813
|
return;
|
|
21260
20814
|
}
|
|
@@ -21278,25 +20832,22 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21278
20832
|
rebuild: true
|
|
21279
20833
|
}
|
|
21280
20834
|
}, hc?.defaultModel, sessionId);
|
|
21281
|
-
if (!ref.resumed) await
|
|
21282
|
-
baseUrl: cred.baseUrl,
|
|
21283
|
-
token: cred.token,
|
|
20835
|
+
if (!ref.resumed) await io.sendText({
|
|
21284
20836
|
toUserId: fromUserId,
|
|
21285
20837
|
text: "(新的对话已开始)",
|
|
21286
|
-
|
|
20838
|
+
replyRef: event.replyRef
|
|
21287
20839
|
}).catch(() => {});
|
|
21288
|
-
await
|
|
20840
|
+
await io.typingStart?.({
|
|
20841
|
+
toUserId: fromUserId,
|
|
20842
|
+
replyRef: event.replyRef
|
|
20843
|
+
});
|
|
21289
20844
|
try {
|
|
21290
20845
|
const mediaNotes = [];
|
|
21291
20846
|
const degradedNotes = [];
|
|
21292
20847
|
const hookMedia = [];
|
|
21293
20848
|
for (const mediaRef of mediaRefs) {
|
|
21294
|
-
const mediaType = mediaRef.kind === "image" ? "image_item" : "file_item";
|
|
21295
20849
|
const label = mediaRef.kind === "image" ? "一张图片" : `文件 ${mediaRef.fileName ?? "(未命名)"}`;
|
|
21296
|
-
const result = await
|
|
21297
|
-
item: mediaRef.item,
|
|
21298
|
-
mediaType
|
|
21299
|
-
});
|
|
20850
|
+
const result = await mediaRef.fetch();
|
|
21300
20851
|
if (!result) {
|
|
21301
20852
|
hookMedia.push({
|
|
21302
20853
|
kind: mediaRef.kind,
|
|
@@ -21316,7 +20867,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21316
20867
|
try {
|
|
21317
20868
|
const inboundDir = weixinInboundDir(root, fromUserId);
|
|
21318
20869
|
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")}`;
|
|
20870
|
+
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
20871
|
const abs = join(inboundDir, fname);
|
|
21321
20872
|
writeFileSync(abs, result.data);
|
|
21322
20873
|
const rel = relative(root, abs);
|
|
@@ -21330,7 +20881,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21330
20881
|
kind: mediaRef.kind,
|
|
21331
20882
|
relPath: null
|
|
21332
20883
|
});
|
|
21333
|
-
degradedNotes.push(
|
|
20884
|
+
degradedNotes.push("(媒体保存失败,请重试)");
|
|
21334
20885
|
}
|
|
21335
20886
|
}
|
|
21336
20887
|
const parts = [];
|
|
@@ -21349,7 +20900,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21349
20900
|
userId: fromUserId,
|
|
21350
20901
|
role: trajectory
|
|
21351
20902
|
});
|
|
21352
|
-
parts.push(
|
|
20903
|
+
parts.push(manualOutputMarker(accountId, fromUserId));
|
|
21353
20904
|
} else forgetManualOutputSession(sessionId);
|
|
21354
20905
|
const question = parts.join("\n");
|
|
21355
20906
|
const hookRel = settings.hook;
|
|
@@ -21362,7 +20913,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21362
20913
|
text,
|
|
21363
20914
|
media: hookMedia
|
|
21364
20915
|
})).catch((err) => {
|
|
21365
|
-
console.log(`[serenity-hooks]
|
|
20916
|
+
console.log(`[serenity-hooks] im hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
|
|
21366
20917
|
});
|
|
21367
20918
|
const answer = (await askSkiff(ctx, ref.agent, question)).answer ?? "";
|
|
21368
20919
|
if (manualOutput) {
|
|
@@ -21373,14 +20924,12 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21373
20924
|
answer
|
|
21374
20925
|
})) return;
|
|
21375
20926
|
const reply = markdownToPlainText(stripThink(answer));
|
|
21376
|
-
await
|
|
21377
|
-
baseUrl: cred.baseUrl,
|
|
21378
|
-
token: cred.token,
|
|
20927
|
+
await io.sendText({
|
|
21379
20928
|
toUserId: fromUserId,
|
|
21380
20929
|
text: reply,
|
|
21381
|
-
|
|
20930
|
+
replyRef: event.replyRef
|
|
21382
20931
|
});
|
|
21383
|
-
console.warn(`[serenity-hooks] ⚠
|
|
20932
|
+
console.warn(`[serenity-hooks] ⚠ im 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
|
|
21384
20933
|
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
21385
20934
|
cccRoot: root,
|
|
21386
20935
|
accountId,
|
|
@@ -21390,18 +20939,16 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21390
20939
|
reply,
|
|
21391
20940
|
source: "reply-fallback"
|
|
21392
20941
|
})).catch((err) => {
|
|
21393
|
-
console.log(`[serenity-hooks]
|
|
20942
|
+
console.log(`[serenity-hooks] im hook outgoing(reply-fallback) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
21394
20943
|
});
|
|
21395
20944
|
return;
|
|
21396
20945
|
}
|
|
21397
20946
|
if (answer === "") return;
|
|
21398
20947
|
const reply = markdownToPlainText(stripThink(answer));
|
|
21399
|
-
await
|
|
21400
|
-
baseUrl: cred.baseUrl,
|
|
21401
|
-
token: cred.token,
|
|
20948
|
+
await io.sendText({
|
|
21402
20949
|
toUserId: fromUserId,
|
|
21403
20950
|
text: reply,
|
|
21404
|
-
|
|
20951
|
+
replyRef: event.replyRef
|
|
21405
20952
|
});
|
|
21406
20953
|
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
21407
20954
|
cccRoot: root,
|
|
@@ -21411,28 +20958,369 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21411
20958
|
role: trajectory,
|
|
21412
20959
|
reply
|
|
21413
20960
|
})).catch((err) => {
|
|
21414
|
-
console.log(`[serenity-hooks]
|
|
20961
|
+
console.log(`[serenity-hooks] im hook outgoing error: ${err instanceof Error ? err.message : String(err)}`);
|
|
21415
20962
|
});
|
|
21416
20963
|
} finally {
|
|
21417
|
-
await
|
|
20964
|
+
await io.typingStop?.({ toUserId: fromUserId });
|
|
21418
20965
|
}
|
|
21419
20966
|
} catch (err) {
|
|
21420
20967
|
const msg = err instanceof Error ? err.message : String(err);
|
|
21421
|
-
console.log(`[serenity-hooks]
|
|
20968
|
+
console.log(`[serenity-hooks] im-bridge error (ccc=${root}): ${msg}`);
|
|
21422
20969
|
const stack = err instanceof Error ? err.stack : void 0;
|
|
21423
|
-
if (stack) console.log(`[serenity-hooks]
|
|
20970
|
+
if (stack) console.log(`[serenity-hooks] im-bridge stack:\n${stack.slice(0, 1500)}`);
|
|
20971
|
+
}
|
|
20972
|
+
}
|
|
20973
|
+
/**
|
|
20974
|
+
* 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按「ACC 管机制、CCC 管措辞」重构)。
|
|
20975
|
+
*
|
|
20976
|
+
* 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值。
|
|
20977
|
+
* **纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在轨迹的 `PROMPT.md` 里。
|
|
20978
|
+
*/
|
|
20979
|
+
function manualOutputMarker(accountId, userId) {
|
|
20980
|
+
return ["[serenity:weixin-manual-output]", `im-bridge({channel:"weixin", action:"send", account:"${accountId}", user:"${userId}", text:"<回复>"})`].join("\n");
|
|
20981
|
+
}
|
|
20982
|
+
/**
|
|
20983
|
+
* 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
|
|
20984
|
+
*
|
|
20985
|
+
* 四个条件同时成立才兜底:
|
|
20986
|
+
* ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默)
|
|
20987
|
+
* ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
|
|
20988
|
+
* ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
|
|
20989
|
+
* ④ 有最终文本可转发(空文本无意义)
|
|
20990
|
+
*/
|
|
20991
|
+
function manualOutputFallbackNeeded(input) {
|
|
20992
|
+
return input.fallbackOnNoSend === true && input.guardActive && !input.sentThisTurn && input.answer !== "";
|
|
20993
|
+
}
|
|
20994
|
+
//#endregion
|
|
20995
|
+
//#region src/im/feishu-account.ts
|
|
20996
|
+
/** 去重集合上限(飞书重推窗口有限;限量防内存无界增长) */
|
|
20997
|
+
const SEEN_LIMIT = 2e3;
|
|
20998
|
+
/** 已处理过的 message_id(超时重推去重;按账号隔离) */
|
|
20999
|
+
const seenByAccount = /* @__PURE__ */ new Map();
|
|
21000
|
+
/** 取 SDK(未注入 ⇒ 动态 import 真包) */
|
|
21001
|
+
async function loadFeishuSdk() {
|
|
21002
|
+
return await import("./lib-TxscFKeo.js").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
|
|
21003
|
+
}
|
|
21004
|
+
/**
|
|
21005
|
+
* 账号 → 长连接客户端(`ImAccountLoop` 的形状是面板既有契约,**不加字段** ——
|
|
21006
|
+
* 加字段会逼面板与 `im-bridge status` 认识一个通道特有的东西)。
|
|
21007
|
+
* 用途:`stopped` 置位后能真正 `close()` 掉连接(只置标志位而不关连接 = 假停止)。
|
|
21008
|
+
*/
|
|
21009
|
+
const wsByAccount = /* @__PURE__ */ new Map();
|
|
21010
|
+
function seenSet(accountId) {
|
|
21011
|
+
let s = seenByAccount.get(accountId);
|
|
21012
|
+
if (!s) {
|
|
21013
|
+
s = /* @__PURE__ */ new Set();
|
|
21014
|
+
seenByAccount.set(accountId, s);
|
|
21015
|
+
}
|
|
21016
|
+
return s;
|
|
21017
|
+
}
|
|
21018
|
+
/**
|
|
21019
|
+
* 飞书凭据就绪判据:**appId 与 appSecret 都非空**。
|
|
21020
|
+
* (微信只需 token;飞书是双要素,缺任一起不了长连接。)
|
|
21021
|
+
*/
|
|
21022
|
+
function feishuCredentialReady(cred) {
|
|
21023
|
+
const appId = typeof cred.appId === "string" ? cred.appId.trim() : "";
|
|
21024
|
+
const secret = typeof cred.token === "string" ? cred.token.trim() : "";
|
|
21025
|
+
return appId !== "" && secret !== "";
|
|
21026
|
+
}
|
|
21027
|
+
/**
|
|
21028
|
+
* 从飞书 `im.message.receive_v1` 事件里取出**中立入站事件**。
|
|
21029
|
+
*
|
|
21030
|
+
* 🔴 这是 P3 唯一的"形状翻译"处,与 `bridge.ts` 里 iLink 的那一处对称。
|
|
21031
|
+
* ⚠️ 用户标识用 **open_id**(不是 chat_id):路由表是"人 → 轨迹",
|
|
21032
|
+
* 而 chat_id 在群里会变、在单聊里也与人不是一一对应。
|
|
21033
|
+
* @returns 无可用发送者/消息体 ⇒ `null`(调用方丢弃并**出声**,不静默)
|
|
21034
|
+
*/
|
|
21035
|
+
function feishuEventToInbound(data) {
|
|
21036
|
+
const sender = data.sender;
|
|
21037
|
+
const message = data.message;
|
|
21038
|
+
const userId = sender?.sender_id?.open_id;
|
|
21039
|
+
if (!userId || !message?.message_id) return null;
|
|
21040
|
+
let text = null;
|
|
21041
|
+
if (message.message_type === "text" && typeof message.content === "string") try {
|
|
21042
|
+
const parsed = JSON.parse(message.content);
|
|
21043
|
+
text = (typeof parsed.text === "string" ? parsed.text.trim() : "").replace(/@_user_\d+/g, "").trim();
|
|
21044
|
+
if (text === "") text = null;
|
|
21045
|
+
} catch {
|
|
21046
|
+
text = null;
|
|
21047
|
+
}
|
|
21048
|
+
return {
|
|
21049
|
+
userId,
|
|
21050
|
+
text,
|
|
21051
|
+
media: [],
|
|
21052
|
+
hasVoice: false,
|
|
21053
|
+
replyRef: void 0
|
|
21054
|
+
};
|
|
21055
|
+
}
|
|
21056
|
+
/**
|
|
21057
|
+
* 造出站动作面(飞书)。
|
|
21058
|
+
*
|
|
21059
|
+
* 🔴 `content` 必须是 **JSON 字符串**:`JSON.stringify({ text })` —— 这是官方形态,
|
|
21060
|
+
* 直接给裸文本会被服务端拒绝(且错误码不直观)。
|
|
21061
|
+
*/
|
|
21062
|
+
function feishuIo(client) {
|
|
21063
|
+
const c = client;
|
|
21064
|
+
return { sendText: async ({ toUserId, text }) => {
|
|
21065
|
+
const create = c.im?.v1?.message?.create;
|
|
21066
|
+
if (typeof create !== "function") throw new Error("飞书 SDK 缺少 im.v1.message.create");
|
|
21067
|
+
await create({
|
|
21068
|
+
params: { receive_id_type: "open_id" },
|
|
21069
|
+
data: {
|
|
21070
|
+
receive_id: toUserId,
|
|
21071
|
+
msg_type: "text",
|
|
21072
|
+
content: JSON.stringify({ text: markdownToPlainText(text) })
|
|
21073
|
+
}
|
|
21074
|
+
});
|
|
21075
|
+
} };
|
|
21076
|
+
}
|
|
21077
|
+
/**
|
|
21078
|
+
* 单账号的飞书长连接循环。
|
|
21079
|
+
*
|
|
21080
|
+
* 与微信的**关键结构差别**:微信是 `while` 轮询,飞书是**事件回调**(长连接推)。
|
|
21081
|
+
* 所以这里的"循环"其实是"建连一次,然后等回调";`stopped` 靠 `close()` 兑现。
|
|
21082
|
+
*/
|
|
21083
|
+
async function runFeishuAccountLoop(input, loop) {
|
|
21084
|
+
const cred = input.cred;
|
|
21085
|
+
const appId = cred.appId ?? "";
|
|
21086
|
+
const appSecret = cred.token ?? "";
|
|
21087
|
+
if (!appId || !appSecret) {
|
|
21088
|
+
loop.lastError = "飞书凭据不全(缺 appId 或 appSecret)";
|
|
21089
|
+
return;
|
|
21090
|
+
}
|
|
21091
|
+
const Lark = await loadFeishuSdk();
|
|
21092
|
+
const client = new Lark.Client({
|
|
21093
|
+
appId,
|
|
21094
|
+
appSecret
|
|
21095
|
+
});
|
|
21096
|
+
const dispatcher = new Lark.EventDispatcher({});
|
|
21097
|
+
const queue = [];
|
|
21098
|
+
let draining = false;
|
|
21099
|
+
const drain = async () => {
|
|
21100
|
+
if (draining) return;
|
|
21101
|
+
draining = true;
|
|
21102
|
+
try {
|
|
21103
|
+
while (queue.length > 0 && !loop.stopped) {
|
|
21104
|
+
const item = queue.shift();
|
|
21105
|
+
loop.lastPollAt = Date.now();
|
|
21106
|
+
try {
|
|
21107
|
+
const event = feishuEventToInbound(item.data);
|
|
21108
|
+
if (!event) {
|
|
21109
|
+
loop.lastError = "收到飞书事件但取不出发送者/消息体";
|
|
21110
|
+
continue;
|
|
21111
|
+
}
|
|
21112
|
+
const msgId = item.data.message?.message_id ?? "";
|
|
21113
|
+
if (msgId !== "") {
|
|
21114
|
+
const seen = seenSet(input.accountId);
|
|
21115
|
+
if (seen.has(msgId)) continue;
|
|
21116
|
+
seen.add(msgId);
|
|
21117
|
+
if (seen.size > SEEN_LIMIT) {
|
|
21118
|
+
const oldest = seen.values().next().value;
|
|
21119
|
+
if (oldest !== void 0) seen.delete(oldest);
|
|
21120
|
+
}
|
|
21121
|
+
}
|
|
21122
|
+
const ctx = input.ctx;
|
|
21123
|
+
const io = feishuIo(client);
|
|
21124
|
+
if (input.machine) {
|
|
21125
|
+
const target = await resolveInboundTarget(ctx, {
|
|
21126
|
+
account: input.accountId,
|
|
21127
|
+
user: event.userId,
|
|
21128
|
+
cccFallback: null
|
|
21129
|
+
});
|
|
21130
|
+
if (!target) continue;
|
|
21131
|
+
await handleInboundEvent(ctx, target.root, input.accountId, io, event, { target });
|
|
21132
|
+
} else await handleInboundEvent(ctx, input.root, input.accountId, io, event);
|
|
21133
|
+
} catch (err) {
|
|
21134
|
+
loop.lastError = err instanceof Error ? err.message : String(err);
|
|
21135
|
+
}
|
|
21136
|
+
}
|
|
21137
|
+
} finally {
|
|
21138
|
+
draining = false;
|
|
21139
|
+
}
|
|
21140
|
+
};
|
|
21141
|
+
dispatcher.register({ "im.message.receive_v1": (data) => {
|
|
21142
|
+
queue.push({ data });
|
|
21143
|
+
drain();
|
|
21144
|
+
} });
|
|
21145
|
+
const ws = new Lark.WSClient({
|
|
21146
|
+
appId,
|
|
21147
|
+
appSecret
|
|
21148
|
+
});
|
|
21149
|
+
wsByAccount.set(input.accountId, ws);
|
|
21150
|
+
try {
|
|
21151
|
+
await ws.start({ eventDispatcher: dispatcher });
|
|
21152
|
+
} catch (err) {
|
|
21153
|
+
loop.lastError = `飞书长连接建立失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
21154
|
+
}
|
|
21155
|
+
}
|
|
21156
|
+
/** 飞书账号实现(登记名 = `feishu`;与配置里的 `channel` 取值一致) */
|
|
21157
|
+
const feishuAccountImpl = {
|
|
21158
|
+
channel: "feishu",
|
|
21159
|
+
isReady: feishuCredentialReady,
|
|
21160
|
+
notReadyReason: () => "**缺 appId 或 appSecret**(未绑定)",
|
|
21161
|
+
startLoop(input) {
|
|
21162
|
+
const loop = {
|
|
21163
|
+
stopped: false,
|
|
21164
|
+
lastPollAt: 0
|
|
21165
|
+
};
|
|
21166
|
+
runFeishuAccountLoop(input, loop);
|
|
21167
|
+
return loop;
|
|
21168
|
+
}
|
|
21169
|
+
};
|
|
21170
|
+
/**
|
|
21171
|
+
* 注册飞书实现(`bridge.ts` 在**模块求值时**调用)。
|
|
21172
|
+
*
|
|
21173
|
+
* ⚠️ 与微信同款教训(续 220 实测 12 例红):注册必须在**模块求值**时发生,
|
|
21174
|
+
* 不能等 `apply` —— 测试与面板会**直接**调 `syncCccBridge` / `syncHumanChannel`。
|
|
21175
|
+
*/
|
|
21176
|
+
function installFeishuAccountImpl() {
|
|
21177
|
+
registerImAccountImpl(feishuAccountImpl);
|
|
21178
|
+
}
|
|
21179
|
+
//#endregion
|
|
21180
|
+
//#region src/im/bridge.ts
|
|
21181
|
+
var bridge_exports = /* @__PURE__ */ __exportAll({
|
|
21182
|
+
handleIncoming: () => handleIncoming,
|
|
21183
|
+
handleMachineIncoming: () => handleMachineIncoming,
|
|
21184
|
+
registerWeixinBridge: () => registerWeixinBridge,
|
|
21185
|
+
resolveInboundTarget: () => resolveInboundTarget,
|
|
21186
|
+
resolveWeixinAccount: () => resolveWeixinAccount,
|
|
21187
|
+
sendProactiveText: () => sendProactiveText,
|
|
21188
|
+
stopAllBridges: () => stopAllBridges,
|
|
21189
|
+
stopCccBridge: () => stopCccBridge,
|
|
21190
|
+
syncCccBridge: () => syncCccBridge,
|
|
21191
|
+
syncHumanChannel: () => syncHumanChannel,
|
|
21192
|
+
weixinBridgeStatus: () => weixinBridgeStatus
|
|
21193
|
+
});
|
|
21194
|
+
const bridges = /* @__PURE__ */ new Map();
|
|
21195
|
+
/**
|
|
21196
|
+
* 🔴 **机器级 poller 的"根"哨兵**(P0-3 A)—— 它**不是一个目录**。
|
|
21197
|
+
*
|
|
21198
|
+
* 机器级 poller **不属于任何 CCC**(这正是"上移 ACC 层"的含义:谁轮询不再由 CCC 数量决定)。
|
|
21199
|
+
* 用途有二:① `runAccountLoop` 的 `root` 形参在机器级模式下不被使用,传它以示"无根";
|
|
21200
|
+
* ② 归属表(`im/inbound.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
|
|
21201
|
+
* 与任何**真实** CCC 根都不会撞(真根一律是绝对路径)。
|
|
21202
|
+
*/
|
|
21203
|
+
const MACHINE_SCOPE = "<machine>";
|
|
21204
|
+
/** 机器级 poller(账号 id → 循环)。与 `bridges` **互斥**:同一时刻只有一种模式在跑。 */
|
|
21205
|
+
const machineLoops = /* @__PURE__ */ new Map();
|
|
21206
|
+
/** typing_ticket 缓存(每 (accountId, fromUserId);getconfig 一次后续复用——对齐参考实现 typingTicketCache) */
|
|
21207
|
+
const typingTicketByUser = /* @__PURE__ */ new Map();
|
|
21208
|
+
function typingCacheKey(accountId, fromUserId) {
|
|
21209
|
+
return `${accountId}|${fromUserId}`;
|
|
21210
|
+
}
|
|
21211
|
+
/** 处理开始:getconfig(无缓存时)→ sendtyping status=1(微信侧显示"正在输入...")。
|
|
21212
|
+
* 任何失败静默(typing 不影响主流程——对齐参考实现 typingCallbacks try/catch 吞错)。 */
|
|
21213
|
+
async function sendTypingStart(cred, accountId, fromUserId, contextToken) {
|
|
21214
|
+
try {
|
|
21215
|
+
const key = typingCacheKey(accountId, fromUserId);
|
|
21216
|
+
let ticket = typingTicketByUser.get(key);
|
|
21217
|
+
if (!ticket) {
|
|
21218
|
+
ticket = (await getConfig({
|
|
21219
|
+
baseUrl: cred.baseUrl,
|
|
21220
|
+
token: cred.token,
|
|
21221
|
+
ilinkUserId: fromUserId,
|
|
21222
|
+
contextToken: contextToken ?? ""
|
|
21223
|
+
})).typing_ticket;
|
|
21224
|
+
if (ticket) typingTicketByUser.set(key, ticket);
|
|
21225
|
+
}
|
|
21226
|
+
if (!ticket) return;
|
|
21227
|
+
await sendTyping({
|
|
21228
|
+
baseUrl: cred.baseUrl,
|
|
21229
|
+
token: cred.token,
|
|
21230
|
+
ilinkUserId: fromUserId,
|
|
21231
|
+
typingTicket: ticket,
|
|
21232
|
+
status: TypingStatus.TYPING
|
|
21233
|
+
});
|
|
21234
|
+
} catch (err) {
|
|
21235
|
+
console.log(`[serenity-hooks] weixin-bridge typing start skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
21236
|
+
}
|
|
21237
|
+
}
|
|
21238
|
+
/** 处理结束(含异常路径 finally):sendtyping status=0;缓存保留(ticket 可复用)。 */
|
|
21239
|
+
async function sendTypingStop(cred, accountId, fromUserId) {
|
|
21240
|
+
try {
|
|
21241
|
+
const ticket = typingTicketByUser.get(typingCacheKey(accountId, fromUserId));
|
|
21242
|
+
if (!ticket) return;
|
|
21243
|
+
await sendTyping({
|
|
21244
|
+
baseUrl: cred.baseUrl,
|
|
21245
|
+
token: cred.token,
|
|
21246
|
+
ilinkUserId: fromUserId,
|
|
21247
|
+
typingTicket: ticket,
|
|
21248
|
+
status: TypingStatus.CANCEL
|
|
21249
|
+
});
|
|
21250
|
+
} catch (err) {
|
|
21251
|
+
console.log(`[serenity-hooks] weixin-bridge typing stop skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
21424
21252
|
}
|
|
21425
21253
|
}
|
|
21426
21254
|
/**
|
|
21255
|
+
* 🔴 **2026-10-19(P2)`runAccountLoop` 已迁出本文件** —— 逐字搬到
|
|
21256
|
+
* `im/weixin-account.ts`,成为 `ImAccountImpl` 的一个实现(微信)。
|
|
21257
|
+
*
|
|
21258
|
+
* 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
|
|
21259
|
+
* owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
|
|
21260
|
+
* 归**账号自己的实现**,装配处只按 `channel` 查派发表。
|
|
21261
|
+
* ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
|
|
21262
|
+
* `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
|
|
21263
|
+
*/
|
|
21264
|
+
/**
|
|
21265
|
+
* 处理单条**微信**消息:把 iLink 形状的 `msg` 归一成**中立入站事件**,
|
|
21266
|
+
* 再交给通道中立的 `handleInboundEvent`(`im/inbound-event.ts`)走主流程。
|
|
21267
|
+
*
|
|
21268
|
+
* ## 🔴 2026-10-19(B1):本函数已退化为**薄适配器**,主流程不在本文件
|
|
21269
|
+
*
|
|
21270
|
+
* 原因:`handleIncoming` 原先同时承担两件不同的事 —— ① "iLink 消息长什么样"
|
|
21271
|
+
* ② "一条已到达的消息该投到哪、怎么答、怎么记账"。接第二家 IM 时只有 ① 需要新写,
|
|
21272
|
+
* 而 ② 应该**被两家共用**。两者混在一个函数里 ⇒ 飞书接进来只能复制一整份主流程。
|
|
21273
|
+
*
|
|
21274
|
+
* ⇒ 主流程搬到 `im/inbound-event.ts`,本函数只做**形状翻译**:
|
|
21275
|
+
* `msg`(iLink)+ `cred`(iLink 凭据)→ `ImInboundEvent` + `ImAccountIo`。
|
|
21276
|
+
*
|
|
21277
|
+
* ## 为什么保留这个签名(而不是让调用方直接调新的主流程)
|
|
21278
|
+
*
|
|
21279
|
+
* 全仓约 50 处测试按 iLink 形状驱动本函数,而 B1 的验收判据是「**微信行为逐字不变**」
|
|
21280
|
+
* (用例数守恒 + 真机一条微信)。保留签名 ⇒ 那 50 处**逐字不动**,判据可复算。
|
|
21281
|
+
*
|
|
21282
|
+
* 会话语义(不变):会话 id 由**轨迹编码**派生 ⇒ 同一条轨迹恒同一条 dsh 会话;
|
|
21283
|
+
* 进程内已有 → 延续;无 → 固定 id resume-or-create。
|
|
21284
|
+
*/
|
|
21285
|
+
async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
21286
|
+
const fromUserId = msg.from_user_id;
|
|
21287
|
+
if (!fromUserId) return;
|
|
21288
|
+
await handleInboundEvent(ctx, root, accountId, {
|
|
21289
|
+
sendText: async ({ toUserId, text, replyRef }) => {
|
|
21290
|
+
await sendTextMessage({
|
|
21291
|
+
baseUrl: cred.baseUrl,
|
|
21292
|
+
token: cred.token,
|
|
21293
|
+
toUserId,
|
|
21294
|
+
text,
|
|
21295
|
+
contextToken: replyRef
|
|
21296
|
+
});
|
|
21297
|
+
},
|
|
21298
|
+
typingStart: ({ toUserId, replyRef }) => sendTypingStart(cred, accountId, toUserId, replyRef),
|
|
21299
|
+
typingStop: ({ toUserId }) => sendTypingStop(cred, accountId, toUserId)
|
|
21300
|
+
}, {
|
|
21301
|
+
userId: fromUserId,
|
|
21302
|
+
text: extractWeixinText(msg),
|
|
21303
|
+
media: extractWeixinMedia(msg).map((ref) => ({
|
|
21304
|
+
kind: ref.kind,
|
|
21305
|
+
fileName: ref.fileName,
|
|
21306
|
+
fetch: () => downloadMedia({
|
|
21307
|
+
item: ref.item,
|
|
21308
|
+
mediaType: ref.kind === "image" ? "image_item" : "file_item"
|
|
21309
|
+
}).then((r) => r ? {
|
|
21310
|
+
data: r.data,
|
|
21311
|
+
fileName: r.fileName
|
|
21312
|
+
} : null)
|
|
21313
|
+
})),
|
|
21314
|
+
hasVoice: hasVoiceItem(msg),
|
|
21315
|
+
replyRef: msg.context_token
|
|
21316
|
+
}, preset);
|
|
21317
|
+
}
|
|
21318
|
+
/**
|
|
21427
21319
|
* **机器级 poller 的单条消息入口**(P0-3 A)。
|
|
21428
21320
|
*
|
|
21429
21321
|
* 与 `handleIncoming` 的唯一差别:**目标由机器级订阅表逐条解析,且没有 CCC 级回退**
|
|
21430
21322
|
* (方案 §3.1:机器级模式下**未命中 = 不投递**,不是"投给默认角色")。
|
|
21431
|
-
* 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset
|
|
21432
|
-
* 也不看目标 CCC 的 `weixin.enabled`)。
|
|
21433
|
-
*
|
|
21434
|
-
* 🔴 为什么"起 poller"与"路由"必须同批(本件即 P0-2 的后半):机器级 poller 收到消息时
|
|
21435
|
-
* **不知道它属于哪个 CCC** —— 那个答案是订阅表给的。没有订阅表 ⇒ **能收不能投**。
|
|
21323
|
+
* 解析出来后把**目标根**当 `root` 交给 `handleIncoming`(并带上 `preset`)。
|
|
21436
21324
|
*
|
|
21437
21325
|
* 🔵 **导出供测试直接驱动**(与 `handleIncoming` 同款:它俩都是"单条消息的入口",
|
|
21438
21326
|
* 生产路径 `runAccountLoop` 只是循环壳)。
|
|
@@ -21449,6 +21337,35 @@ async function handleMachineIncoming(ctx, accountId, cred, msg) {
|
|
|
21449
21337
|
await handleIncoming(ctx, target.root, accountId, cred, msg, { target });
|
|
21450
21338
|
}
|
|
21451
21339
|
/**
|
|
21340
|
+
* **按渠道读凭据**(P3 起的单一入口)。
|
|
21341
|
+
*
|
|
21342
|
+
* 为什么必须抽成一个函数:CCC 级桥(`syncCccBridge`)与机器级 poller(`syncMachinePollers`)
|
|
21343
|
+
* 是**两条装配路径**,原先各自写着"怎么从 localstore 取微信 token"。接第二家渠道时若两边各写一份,
|
|
21344
|
+
* 必然分叉(一家改了另一家忘改,且**没有任何机械信号**)。
|
|
21345
|
+
*
|
|
21346
|
+
* 两种取法的**真实差别**(不是风格差异):
|
|
21347
|
+
* - **CCC 级**:凭据在 localstore(`WEIXIN_<A>_TOKEN` / `FEISHU_<A>_APPID|SECRET`)。
|
|
21348
|
+
* - **机器级**:凭据在插件设置行(`account.token`)——那是宿主设置面,不经 localstore。
|
|
21349
|
+
*
|
|
21350
|
+
* ⇒ 本函数只负责**把"渠道"翻译成"该读哪套字段"**,返回统一形状 `ImAccountCredential`。
|
|
21351
|
+
* 微信的 appId 恒 undefined;飞书把 appSecret 装进 `token`(让两边共用同一个就绪判据位置)。
|
|
21352
|
+
*/
|
|
21353
|
+
function readImCredentialByChannel(root, accountId, channel) {
|
|
21354
|
+
if (channel === "feishu") {
|
|
21355
|
+
const f = readFeishuCredential(root, accountId);
|
|
21356
|
+
return f ? {
|
|
21357
|
+
appId: f.appId,
|
|
21358
|
+
token: f.appSecret
|
|
21359
|
+
} : null;
|
|
21360
|
+
}
|
|
21361
|
+
const w = readWeixinCredential(root, accountId);
|
|
21362
|
+
return w ? {
|
|
21363
|
+
token: w.token,
|
|
21364
|
+
baseUrl: w.baseUrl,
|
|
21365
|
+
userId: w.userId
|
|
21366
|
+
} : null;
|
|
21367
|
+
}
|
|
21368
|
+
/**
|
|
21452
21369
|
* 启动/重建某 CCC 的桥:读配置 → 对每个 enabled + 有凭据的账号启动轮询循环。
|
|
21453
21370
|
* 已存在(配置变化热重建)→ 先停旧循环再启动新的。
|
|
21454
21371
|
*
|
|
@@ -21481,7 +21398,7 @@ function syncCccBridge(ctx, root) {
|
|
|
21481
21398
|
}
|
|
21482
21399
|
continue;
|
|
21483
21400
|
}
|
|
21484
|
-
const cred =
|
|
21401
|
+
const cred = readImCredentialByChannel(root, account.accountId, channel);
|
|
21485
21402
|
if (!cred) continue;
|
|
21486
21403
|
if (!impl.isReady(cred)) continue;
|
|
21487
21404
|
implByAccount.set(account.accountId, impl);
|
|
@@ -21489,7 +21406,7 @@ function syncCccBridge(ctx, root) {
|
|
|
21489
21406
|
requests.push({
|
|
21490
21407
|
root,
|
|
21491
21408
|
accountId: account.accountId,
|
|
21492
|
-
identity: accountIdentity(root, account.accountId, cred.token)
|
|
21409
|
+
identity: accountIdentity(root, account.accountId, cred.token ?? "")
|
|
21493
21410
|
});
|
|
21494
21411
|
}
|
|
21495
21412
|
const plan = planPollerClaims(requests, pollerOwners());
|
|
@@ -21579,10 +21496,12 @@ function syncMachinePollers(ctx, hc) {
|
|
|
21579
21496
|
continue;
|
|
21580
21497
|
}
|
|
21581
21498
|
const token = (account.token ?? "").trim();
|
|
21499
|
+
const appIdRaw = account.appId;
|
|
21582
21500
|
const cred = {
|
|
21583
21501
|
token,
|
|
21584
21502
|
baseUrl: "",
|
|
21585
|
-
userId: account.userId
|
|
21503
|
+
userId: account.userId,
|
|
21504
|
+
...channel === "feishu" && typeof appIdRaw === "string" ? { appId: appIdRaw.trim() } : {}
|
|
21586
21505
|
};
|
|
21587
21506
|
if (!impl.isReady(cred)) {
|
|
21588
21507
|
console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} ${impl.notReadyReason?.(cred) ?? "**凭据不全**"} ⇒ 不起 poller`);
|
|
@@ -21642,7 +21561,7 @@ function syncHumanChannel(ctx) {
|
|
|
21642
21561
|
(async () => {
|
|
21643
21562
|
try {
|
|
21644
21563
|
stopMachineLoops();
|
|
21645
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21564
|
+
const { listCccs } = await import("./ccc-roots-BhoA7G-0.js").then((n) => n.a);
|
|
21646
21565
|
for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
|
|
21647
21566
|
} catch {}
|
|
21648
21567
|
})();
|
|
@@ -21782,6 +21701,14 @@ installWeixinAccountImpl({
|
|
|
21782
21701
|
handleIncoming,
|
|
21783
21702
|
handleMachineIncoming
|
|
21784
21703
|
});
|
|
21704
|
+
/**
|
|
21705
|
+
* 🆕 **P3 装配:飞书实现登记进同一张派发表**(同样是模块求值时)。
|
|
21706
|
+
*
|
|
21707
|
+
* 飞书不需要注入依赖 —— 它的收(`WSClient`)与发(`im.v1.message.create`)都在 SDK 里,
|
|
21708
|
+
* 主流程则复用 `im/inbound-event.ts`。⇒ 这一行就是"微信与飞书成为并列实现"的**最后一块**:
|
|
21709
|
+
* 派发表有了两个条目,装配处一行没改(它只按 `channel` 查表)。
|
|
21710
|
+
*/
|
|
21711
|
+
installFeishuAccountImpl();
|
|
21785
21712
|
//#endregion
|
|
21786
21713
|
//#region src/im/send.ts
|
|
21787
21714
|
/** 请求体上限(文本消息远小于此;防滥用) */
|
|
@@ -21898,7 +21825,7 @@ function matchCcc(input, candidates) {
|
|
|
21898
21825
|
}
|
|
21899
21826
|
/** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
21900
21827
|
async function collectCandidates(ctx) {
|
|
21901
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21828
|
+
const { listCccs } = await import("./ccc-roots-BhoA7G-0.js").then((n) => n.a);
|
|
21902
21829
|
const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
|
|
21903
21830
|
const seen = /* @__PURE__ */ new Set();
|
|
21904
21831
|
const out = [];
|
|
@@ -24741,10 +24668,6 @@ const Config = z.object({
|
|
|
24741
24668
|
opencodeSkills: z.boolean().default(true),
|
|
24742
24669
|
gateway: z.object({ enabled: z.boolean().default(false) }),
|
|
24743
24670
|
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
24671
|
webFetch: z.object({ enabled: z.boolean().default(true) }),
|
|
24749
24672
|
opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
|
|
24750
24673
|
visionPatch: z.object({ enabled: z.boolean().default(true) }),
|
|
@@ -24757,8 +24680,6 @@ const Config = z.object({
|
|
|
24757
24680
|
}),
|
|
24758
24681
|
gatewayEnabled: z.boolean().volatile(),
|
|
24759
24682
|
rebuildThresholdK: z.number().min(50).max(4e3).volatile(),
|
|
24760
|
-
skiffEnabled: z.boolean().volatile(),
|
|
24761
|
-
skiffDebugPort: z.number().min(1024).max(65535).volatile(),
|
|
24762
24683
|
croEnabled: z.boolean().volatile(),
|
|
24763
24684
|
diagramEnabled: z.boolean().volatile()
|
|
24764
24685
|
});
|
|
@@ -24825,7 +24746,6 @@ function apply(ctx, config) {
|
|
|
24825
24746
|
if (config.env) registerEnv(ctx);
|
|
24826
24747
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
24827
24748
|
registerBootstrap(ctx);
|
|
24828
|
-
registerSkiff(ctx);
|
|
24829
24749
|
registerCroTurnTracking(ctx);
|
|
24830
24750
|
registerWakeScheduler(ctx);
|
|
24831
24751
|
registerWeixinBridge(ctx);
|
|
@@ -24887,139 +24807,5 @@ function registerDiagramTool(ctx) {
|
|
|
24887
24807
|
} catch {}
|
|
24888
24808
|
sync();
|
|
24889
24809
|
}
|
|
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
24810
|
//#endregion
|
|
25025
24811
|
export { Config, apply, humanChannelAccountSchema, humanChannelSubscriptionSchema, inject, name };
|