@shgroup/dsh-serenity-hooks 1.30.5 → 1.30.9
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/access-CdL6BAYj.js +49 -0
- package/lib/acp-core.d.ts +1 -1
- package/lib/agent-idle.d.ts +27 -0
- package/lib/{ccc-CX48YNUL.js → ccc-DAsSHsub.js} +22 -1
- package/lib/ccc.d.ts +13 -0
- package/lib/config-ops.d.ts +10 -0
- package/lib/gateway-proxy.d.ts +21 -0
- package/lib/gateway.d.ts +1 -1
- package/lib/host/access.d.ts +53 -0
- package/lib/host/contract.d.ts +99 -0
- package/lib/host/effect.d.ts +24 -0
- package/lib/index.js +1126 -231
- package/lib/kit-ops.d.ts +1 -1
- package/lib/localstore-ops.d.ts +2 -0
- package/lib/msm-ops.d.ts +1 -1
- package/lib/seams/lifecycle.d.ts +29 -0
- package/lib/{session-bound-p9oR9dj9.js → session-bound-D2ANqVn-.js} +82 -28
- package/lib/session-bound.d.ts +28 -47
- package/lib/{settings-section-BfVxgCZy.js → settings-section-BPk0e1du.js} +8 -1
- package/lib/settings-section.d.ts +2 -0
- package/lib/skiff-core.d.ts +10 -2
- package/lib/{skiff-debug-gYigSgl_.js → skiff-debug-TONJlpgF.js} +73 -28
- package/lib/{skiff-role-LW4tjF9L.js → skiff-role-DlrbHPLD.js} +4 -2
- package/lib/tools/handyman.d.ts +1 -1
- package/lib/tools/kit.d.ts +6 -1
- package/lib/{weixin-api-OUCuw0Ws.js → weixin-api-BMpljodH.js} +25 -3
- package/lib/weixin-api.d.ts +12 -1
- package/lib/weixin-bridge.d.ts +43 -1
- package/lib/weixin-hook.d.ts +9 -0
- package/lib/{weixin-route-jA0qENOf.js → weixin-route-BJ4g4ZNq.js} +9 -3
- package/lib/weixin-send-api.d.ts +87 -0
- package/lib/weixin-send-endpoint.d.ts +17 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-
|
|
3
|
-
import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-
|
|
4
|
-
import { C as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { n as
|
|
8
|
-
import {
|
|
2
|
+
import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-DAsSHsub.js";
|
|
3
|
+
import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-DlrbHPLD.js";
|
|
4
|
+
import { C as newStopToken, D as writeFailedStatus, E as splitModel, O as writeProgress, S as listActiveHandymen, T as requireWhitelistedModel, _ as workspaceTrajectoryLine, a as startSkiffDebugServer, b as buildRoundPrompt, c as askSkiff, d as getSkiffAgent, f as skiffMsmGate, g as unregisterSkiffSession, h as skiffTrajectoryEnabled, k as skiffRoleFor, l as createSkiffAgent, m as skiffSessionSnapshot, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionInfo, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as waitAgentIdle, w as readProgress, x as handymanProgressPaths, y as HANDYMAN_GUIDE } from "./skiff-debug-TONJlpgF.js";
|
|
5
|
+
import { i as hostSessions, n as hostInjected, o as hostWebServer, r as hostService, t as hostAgents } from "./access-CdL6BAYj.js";
|
|
6
|
+
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-BJ4g4ZNq.js";
|
|
7
|
+
import { C as summarize, S as showSession, _ as readActiveSessionMd, a as archiveSessions, b as sessionsRoot, c as createSession, d as findSession, f as getActiveSessionInfo, g as qaCheck, h as parseSessionContextFromEvents, i as SESSION_ACTIONS, l as extractSessionMdPathFromText, m as listSessions, n as readLastBound, o as clearActiveSessionInfo, p as healthCheck, r as DEFAULT_SESSION_SCOPE, s as closeSession, t as appendBound, u as findLatestActiveSessionMd, v as resolveSessionByTitle, w as useSession, x as setActiveSessionInfo, y as sessionEvents } from "./session-bound-D2ANqVn-.js";
|
|
8
|
+
import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-BPk0e1du.js";
|
|
9
|
+
import { a as markdownToPlainText, c as sniffImageExt, i as getUpdates, n as downloadMedia, o as sendTextMessage, r as getConfig, s as sendTyping, t as TypingStatus } from "./weixin-api-BMpljodH.js";
|
|
9
10
|
import z from "@deepseek-ai/schemastery";
|
|
10
11
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
11
12
|
import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
@@ -836,6 +837,306 @@ function setSafeMode(root, on) {
|
|
|
836
837
|
return { on: isSafeModeOn(root) };
|
|
837
838
|
}
|
|
838
839
|
//#endregion
|
|
840
|
+
//#region src/host/contract.ts
|
|
841
|
+
/** dsp 依赖的宿主服务与成员(对照 AI_LAB/dsh-harness-public @ 0.1.2-rc.1 逐一核对) */
|
|
842
|
+
const HOST_SERVICES = [
|
|
843
|
+
{
|
|
844
|
+
id: "tools",
|
|
845
|
+
name: "tools",
|
|
846
|
+
access: "injected",
|
|
847
|
+
members: [{
|
|
848
|
+
name: "register",
|
|
849
|
+
kind: "function"
|
|
850
|
+
}, {
|
|
851
|
+
name: "guard",
|
|
852
|
+
kind: "function"
|
|
853
|
+
}],
|
|
854
|
+
impact: "10 个 ACC 工具无法注册 / 机械守卫失效",
|
|
855
|
+
required: true
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
id: "sessions",
|
|
859
|
+
name: "sessions",
|
|
860
|
+
access: "injected",
|
|
861
|
+
members: [
|
|
862
|
+
{
|
|
863
|
+
name: "list",
|
|
864
|
+
kind: "function"
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
name: "get",
|
|
868
|
+
kind: "function"
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
name: "create",
|
|
872
|
+
kind: "function"
|
|
873
|
+
}
|
|
874
|
+
],
|
|
875
|
+
impact: "会话定位/rebuild/CCC 解析失效",
|
|
876
|
+
required: true
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
id: "agents",
|
|
880
|
+
name: "agents",
|
|
881
|
+
access: "injected",
|
|
882
|
+
members: [{
|
|
883
|
+
name: "create",
|
|
884
|
+
kind: "function"
|
|
885
|
+
}, {
|
|
886
|
+
name: "get",
|
|
887
|
+
kind: "function"
|
|
888
|
+
}],
|
|
889
|
+
impact: "skiff/handyman 无法创建或复用 agent",
|
|
890
|
+
required: true
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
id: "webServer",
|
|
894
|
+
name: "webServer",
|
|
895
|
+
access: "injected",
|
|
896
|
+
members: [{
|
|
897
|
+
name: "register",
|
|
898
|
+
kind: "function"
|
|
899
|
+
}, {
|
|
900
|
+
name: "port",
|
|
901
|
+
kind: "value"
|
|
902
|
+
}],
|
|
903
|
+
impact: "状态接口/网关端口发现失效",
|
|
904
|
+
required: true
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
id: "settings",
|
|
908
|
+
name: "settings",
|
|
909
|
+
access: "injected",
|
|
910
|
+
members: [{
|
|
911
|
+
name: "installSection",
|
|
912
|
+
kind: "function"
|
|
913
|
+
}],
|
|
914
|
+
impact: "设置面板不安装 → 所有开关静默 no-op",
|
|
915
|
+
required: true
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
id: "systemPrompt",
|
|
919
|
+
name: "systemPrompt",
|
|
920
|
+
access: "injected",
|
|
921
|
+
members: [],
|
|
922
|
+
impact: "Induction(入口 skill 段)注入失效",
|
|
923
|
+
required: true
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
id: "sessionProjections",
|
|
927
|
+
name: "sessionProjections",
|
|
928
|
+
access: "injected",
|
|
929
|
+
members: [{
|
|
930
|
+
name: "snapshot",
|
|
931
|
+
kind: "function"
|
|
932
|
+
}],
|
|
933
|
+
impact: "上下文压力读数失效 → rebuild 提醒不再触发",
|
|
934
|
+
required: false
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
id: "skills",
|
|
938
|
+
name: "skills",
|
|
939
|
+
access: "injected",
|
|
940
|
+
members: [],
|
|
941
|
+
impact: "opencode skill 兼容层(.opencode/skills 注册)失效",
|
|
942
|
+
required: false
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
id: "shellEnv",
|
|
946
|
+
name: "shellEnv",
|
|
947
|
+
access: "injected",
|
|
948
|
+
members: [],
|
|
949
|
+
impact: "DSH_SERENITY_* 环境事实注入失效",
|
|
950
|
+
required: false
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
id: "agentLoop",
|
|
954
|
+
name: "agentLoop",
|
|
955
|
+
access: "injected",
|
|
956
|
+
members: [],
|
|
957
|
+
impact: "handyman worker agent 无法装配 preset",
|
|
958
|
+
required: false
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
id: "sessionTitle",
|
|
962
|
+
name: "sessionTitle",
|
|
963
|
+
access: "lazy",
|
|
964
|
+
members: [{
|
|
965
|
+
name: "rename",
|
|
966
|
+
kind: "function"
|
|
967
|
+
}],
|
|
968
|
+
impact: "会话命名 / rebuild 后重命名跳过(仅告警)",
|
|
969
|
+
required: false
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
id: "tokenMeter",
|
|
973
|
+
name: "tokenMeter",
|
|
974
|
+
access: "lazy",
|
|
975
|
+
members: [{
|
|
976
|
+
name: "estimateMessage",
|
|
977
|
+
kind: "function"
|
|
978
|
+
}],
|
|
979
|
+
impact: "rebuild shadow-price 定价失效(计量不准)",
|
|
980
|
+
required: false
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
id: "workspaceRegistry",
|
|
984
|
+
name: "workspaceRegistry",
|
|
985
|
+
access: "lazy",
|
|
986
|
+
members: [{
|
|
987
|
+
name: "list",
|
|
988
|
+
kind: "function"
|
|
989
|
+
}],
|
|
990
|
+
impact: "工作区白名单下拉为空(外部访问配置受限)",
|
|
991
|
+
required: false
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
id: "sessionPersistence",
|
|
995
|
+
name: "sessionPersistence",
|
|
996
|
+
access: "lazy",
|
|
997
|
+
members: [{
|
|
998
|
+
name: "list",
|
|
999
|
+
kind: "function"
|
|
1000
|
+
}],
|
|
1001
|
+
impact: "CCC 自动发现回落失效(诊断面受限)",
|
|
1002
|
+
required: false
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
id: "connection",
|
|
1006
|
+
name: "connection",
|
|
1007
|
+
access: "lazy",
|
|
1008
|
+
members: [{
|
|
1009
|
+
name: "authenticatedUrl",
|
|
1010
|
+
kind: "function"
|
|
1011
|
+
}, {
|
|
1012
|
+
name: "authorizeIndex",
|
|
1013
|
+
kind: "function"
|
|
1014
|
+
}],
|
|
1015
|
+
impact: "双端口网关无法换取 dsh cookie → 外部反代 401",
|
|
1016
|
+
required: false
|
|
1017
|
+
}
|
|
1018
|
+
];
|
|
1019
|
+
/** dsp 被验证过的宿主版本范围(与 package.json peerDependencies 同源,单一真相源) */
|
|
1020
|
+
const REQUIRED_HOST_RANGE = "^0.1.2-rc.1";
|
|
1021
|
+
/** 最小 semver 比较(无依赖):返回 -1 / 0 / 1;解析失败返回 null */
|
|
1022
|
+
function compareSemver(a, b) {
|
|
1023
|
+
const parse = (v) => {
|
|
1024
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-(.*))?$/.exec(v.trim());
|
|
1025
|
+
if (!m) return null;
|
|
1026
|
+
return [
|
|
1027
|
+
Number(m[1]),
|
|
1028
|
+
Number(m[2]),
|
|
1029
|
+
Number(m[3]),
|
|
1030
|
+
m[4] ?? ""
|
|
1031
|
+
];
|
|
1032
|
+
};
|
|
1033
|
+
const pa = parse(a);
|
|
1034
|
+
const pb = parse(b);
|
|
1035
|
+
if (!pa || !pb) return null;
|
|
1036
|
+
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
|
|
1037
|
+
const ra = pa[3];
|
|
1038
|
+
const rb = pb[3];
|
|
1039
|
+
if (ra === rb) return 0;
|
|
1040
|
+
if (ra === "") return 1;
|
|
1041
|
+
if (rb === "") return -1;
|
|
1042
|
+
return ra < rb ? -1 : 1;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* 宿主版本是否落在 dsp 被验证的范围(`^0.1.2-rc.1`)。
|
|
1046
|
+
* 无版本信息 → 视为未知(ok=false,detail 说明),不猜测。
|
|
1047
|
+
*/
|
|
1048
|
+
function checkHostVersion(version) {
|
|
1049
|
+
if (version === null) return {
|
|
1050
|
+
ok: false,
|
|
1051
|
+
detail: `host version unknown (required ${REQUIRED_HOST_RANGE})`
|
|
1052
|
+
};
|
|
1053
|
+
const floor = compareSemver(version, "0.1.2-rc.1");
|
|
1054
|
+
const ceiling = compareSemver(version, "0.2.0");
|
|
1055
|
+
if (floor === null || ceiling === null) return {
|
|
1056
|
+
ok: false,
|
|
1057
|
+
detail: `host version "${version}" not parseable (required ${REQUIRED_HOST_RANGE})`
|
|
1058
|
+
};
|
|
1059
|
+
if (floor < 0) return {
|
|
1060
|
+
ok: false,
|
|
1061
|
+
detail: `host ${version} is below the verified floor (required ${REQUIRED_HOST_RANGE})`
|
|
1062
|
+
};
|
|
1063
|
+
if (ceiling >= 0) return {
|
|
1064
|
+
ok: false,
|
|
1065
|
+
detail: `host ${version} is outside the verified range (required ${REQUIRED_HOST_RANGE})`
|
|
1066
|
+
};
|
|
1067
|
+
return {
|
|
1068
|
+
ok: true,
|
|
1069
|
+
detail: `host ${version} within ${REQUIRED_HOST_RANGE}`
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
/** 结构化读取宿主服务(lazy → ctx.get;injected → 直接属性,属性缺失再试 ctx.get) */
|
|
1073
|
+
function readService(ctx, name, access) {
|
|
1074
|
+
return access === "lazy" ? hostService(ctx, name) : hostInjected(ctx, name);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* 探针:核对运行中的宿主是否仍满足 dsp 依赖的服务与成员。
|
|
1078
|
+
*
|
|
1079
|
+
* @param ctx 宿主插件上下文(真实 cordis Context;测试可传结构化替身)
|
|
1080
|
+
* @param hostVersion 运行中宿主版本(`readDshVersion()` 结果;null = 未知)
|
|
1081
|
+
* @returns 报告(`ok:false` 时 `issues` 逐条给出缺失项与后果)
|
|
1082
|
+
*/
|
|
1083
|
+
function probeHostContract(ctx, hostVersion = null) {
|
|
1084
|
+
const issues = [];
|
|
1085
|
+
let checked = 0;
|
|
1086
|
+
for (const svc of HOST_SERVICES) {
|
|
1087
|
+
checked += 1;
|
|
1088
|
+
let value;
|
|
1089
|
+
try {
|
|
1090
|
+
value = readService(ctx, svc.name, svc.access);
|
|
1091
|
+
} catch {
|
|
1092
|
+
value = void 0;
|
|
1093
|
+
}
|
|
1094
|
+
if (value === void 0 || value === null) {
|
|
1095
|
+
issues.push({
|
|
1096
|
+
id: svc.id,
|
|
1097
|
+
kind: "service",
|
|
1098
|
+
detail: `service "${svc.name}" not available (${svc.access})`,
|
|
1099
|
+
impact: svc.impact,
|
|
1100
|
+
required: svc.required
|
|
1101
|
+
});
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
for (const member of svc.members) {
|
|
1105
|
+
checked += 1;
|
|
1106
|
+
const actual = value[member.name];
|
|
1107
|
+
if (!(member.kind === "function" ? typeof actual === "function" : actual !== void 0)) issues.push({
|
|
1108
|
+
id: `${svc.id}.${member.name}`,
|
|
1109
|
+
kind: "member",
|
|
1110
|
+
detail: `service "${svc.name}" is missing ${member.kind} "${member.name}"`,
|
|
1111
|
+
impact: svc.impact,
|
|
1112
|
+
required: svc.required
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const version = checkHostVersion(hostVersion);
|
|
1117
|
+
if (!version.ok) issues.push({
|
|
1118
|
+
id: "host.version",
|
|
1119
|
+
kind: "version",
|
|
1120
|
+
detail: version.detail,
|
|
1121
|
+
impact: "契约未经该宿主版本验证(漂移风险)",
|
|
1122
|
+
required: false
|
|
1123
|
+
});
|
|
1124
|
+
return {
|
|
1125
|
+
ok: issues.every((i) => !i.required),
|
|
1126
|
+
checked,
|
|
1127
|
+
hostVersion,
|
|
1128
|
+
versionOk: version.ok,
|
|
1129
|
+
issues
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
/** 单行摘要(启动告警用) */
|
|
1133
|
+
function summarizeHostContract(report) {
|
|
1134
|
+
if (report.ok && report.issues.length === 0) return `host contract ok (${report.checked} checks, host ${report.hostVersion ?? "unknown"})`;
|
|
1135
|
+
const required = report.issues.filter((i) => i.required);
|
|
1136
|
+
const optional = report.issues.filter((i) => !i.required);
|
|
1137
|
+
return `${required.length > 0 ? `host contract BROKEN (${required.length} required)` : `host contract degraded (${optional.length} optional)`}: ${report.issues.map((i) => i.detail).join("; ")}`;
|
|
1138
|
+
}
|
|
1139
|
+
//#endregion
|
|
839
1140
|
//#region src/kit-ops.ts
|
|
840
1141
|
/**
|
|
841
1142
|
* kit-ops.ts — dashboard 纯操作层(acc_kit → dashboard,v1.30;零 DSH 依赖)
|
|
@@ -933,7 +1234,7 @@ function checkRegistryHealth(root) {
|
|
|
933
1234
|
issues
|
|
934
1235
|
};
|
|
935
1236
|
}
|
|
936
|
-
async function runKit(root, args) {
|
|
1237
|
+
async function runKit(root, args, hostCtx) {
|
|
937
1238
|
switch (args.action) {
|
|
938
1239
|
case "health": {
|
|
939
1240
|
const cccName = readCccName$1(root);
|
|
@@ -973,6 +1274,11 @@ async function runKit(root, args) {
|
|
|
973
1274
|
if (root) report.config = loadSerenityConfig(root);
|
|
974
1275
|
report.accVersion = ACC_VERSION;
|
|
975
1276
|
report.dshVersion = readDshVersion();
|
|
1277
|
+
if (hostCtx !== void 0) {
|
|
1278
|
+
const contract = probeHostContract(hostCtx, report.dshVersion);
|
|
1279
|
+
report.hostContract = contract;
|
|
1280
|
+
if (!contract.ok) report.status = "degraded";
|
|
1281
|
+
}
|
|
976
1282
|
return report;
|
|
977
1283
|
}
|
|
978
1284
|
case "time": {
|
|
@@ -1006,29 +1312,35 @@ function renderText$11(value) {
|
|
|
1006
1312
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
1007
1313
|
}];
|
|
1008
1314
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1315
|
+
/**
|
|
1316
|
+
* dashboard 工具(v1.30.7:闭包捕获 ctx → health 可附宿主契约探针 F-05)。
|
|
1317
|
+
* ctx 仅用于**只读探针**(`ctx.get` / 属性读取),无副作用。
|
|
1318
|
+
*/
|
|
1319
|
+
function createKitTool(ctx) {
|
|
1320
|
+
return defineTool({
|
|
1321
|
+
name: "dashboard",
|
|
1322
|
+
description: "Dashboard — always-on container instruments (works any time, no side effects): health (CCC three-principle check P1/P2/config + MSM registry integrity + host-contract probe, healthy/degraded) / time (now_iso/now_local/epoch_ms) / wait (wait N seconds, default 1). Routine self-check before entering a CCC.",
|
|
1323
|
+
parameters: {
|
|
1324
|
+
action: {
|
|
1325
|
+
type: "string",
|
|
1326
|
+
enum: [...KIT_ACTIONS],
|
|
1327
|
+
required: true,
|
|
1328
|
+
description: "Subcommand"
|
|
1329
|
+
},
|
|
1330
|
+
seconds: {
|
|
1331
|
+
type: "integer",
|
|
1332
|
+
description: "Seconds to wait (positive integer, default 1)"
|
|
1333
|
+
}
|
|
1018
1334
|
},
|
|
1019
|
-
|
|
1020
|
-
type: "
|
|
1021
|
-
|
|
1335
|
+
output: {
|
|
1336
|
+
schema: { type: "json" },
|
|
1337
|
+
render: (args, value) => renderText$11(value)
|
|
1338
|
+
},
|
|
1339
|
+
async execute(args, exec) {
|
|
1340
|
+
return await runKit(findSerenityRoot(agentCwd$8(exec)), args, ctx);
|
|
1022
1341
|
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
schema: { type: "json" },
|
|
1026
|
-
render: (args, value) => renderText$11(value)
|
|
1027
|
-
},
|
|
1028
|
-
async execute(args, exec) {
|
|
1029
|
-
return await runKit(findSerenityRoot(agentCwd$8(exec)), args);
|
|
1030
|
-
}
|
|
1031
|
-
});
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1032
1344
|
//#endregion
|
|
1033
1345
|
//#region src/git-ops.ts
|
|
1034
1346
|
/**
|
|
@@ -1265,6 +1577,28 @@ const gitTool = defineTool({
|
|
|
1265
1577
|
}
|
|
1266
1578
|
});
|
|
1267
1579
|
//#endregion
|
|
1580
|
+
//#region src/weixin-send-endpoint.ts
|
|
1581
|
+
/**
|
|
1582
|
+
* weixin-send-endpoint.ts — 主动发送入口地址的**零依赖叶模块**(v1.30.9,S142)
|
|
1583
|
+
*
|
|
1584
|
+
* 为什么单独一个模块(R↓):地址的读者是 `msm-ops.buildMsmEnv`(把地址注入 MSM 子进程 env),
|
|
1585
|
+
* 写者才是 `weixin-send-api`(监听器启停)。若 msm-ops 直接 import weixin-send-api,
|
|
1586
|
+
* 静态依赖链会拖进整条微信桥栈(weixin-bridge → skiff-core → `@deepseek-ai/dsh-llm`)——
|
|
1587
|
+
* 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
|
|
1588
|
+
* (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
|
|
1589
|
+
*
|
|
1590
|
+
* 同款先例:`host/effect.ts`(拆卸登记)、`agent-idle.ts`(等待结算)。
|
|
1591
|
+
*/
|
|
1592
|
+
let endpoint = null;
|
|
1593
|
+
/** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
|
|
1594
|
+
function setWeixinSendEndpoint(value) {
|
|
1595
|
+
endpoint = value;
|
|
1596
|
+
}
|
|
1597
|
+
/** 当前入口地址(形如 http://127.0.0.1:3082;未启动 → null) */
|
|
1598
|
+
function weixinSendEndpoint() {
|
|
1599
|
+
return endpoint;
|
|
1600
|
+
}
|
|
1601
|
+
//#endregion
|
|
1268
1602
|
//#region src/msm-ops.ts
|
|
1269
1603
|
/**
|
|
1270
1604
|
* msm-ops.ts — msm 纯操作层(acc_msm → msm 执行 + container_admin 管理面,v1.30;零 DSH 依赖)
|
|
@@ -1313,14 +1647,21 @@ function assertPathInsideRoot(root, value, flagName) {
|
|
|
1313
1647
|
if (e instanceof Error && e.message.includes("symlink")) throw e;
|
|
1314
1648
|
}
|
|
1315
1649
|
}
|
|
1316
|
-
/**
|
|
1650
|
+
/**
|
|
1651
|
+
* 业务子进程 env:注入 SERENITY_ROOT / SERENITY_CCC / SERENITY_VERSION(对齐 osp)
|
|
1652
|
+
* + SERENITY_WEIXIN_API(v1.30.9:微信主动发送入口地址——MSM 脚本零硬编码端口;
|
|
1653
|
+
* 未启动则不注入,脚本回退默认值)。
|
|
1654
|
+
*/
|
|
1317
1655
|
function buildMsmEnv(root) {
|
|
1318
|
-
|
|
1656
|
+
const env = {
|
|
1319
1657
|
...process.env,
|
|
1320
1658
|
SERENITY_ROOT: root,
|
|
1321
1659
|
SERENITY_CCC: readCccName(root) ?? "",
|
|
1322
1660
|
SERENITY_VERSION: ACC_VERSION
|
|
1323
1661
|
};
|
|
1662
|
+
const weixinApi = weixinSendEndpoint();
|
|
1663
|
+
if (weixinApi) env.SERENITY_WEIXIN_API = weixinApi;
|
|
1664
|
+
return env;
|
|
1324
1665
|
}
|
|
1325
1666
|
/**
|
|
1326
1667
|
* Windows 兼容(审计观察点 A):`.cmd` 不能直接被 CreateProcess 解析——
|
|
@@ -1489,7 +1830,11 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1489
1830
|
账号/路由/开关在此文件;**bot_token 凭据在 CCC localstore credential scope**
|
|
1490
1831
|
(扫码绑定后自动写入,永不进 git 明文面)。
|
|
1491
1832
|
路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC skiff.roles。
|
|
1492
|
-
面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"])
|
|
1833
|
+
面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"|"guide"]) 排查 + 大而全指南。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
|
|
1834
|
+
▸ 主动发送(v1.30.9):CCC 用 msm("weixin-send", ["send", "--ccc", "<CCC>", "--user", "<别名|id>", "<文本>"])
|
|
1835
|
+
给用户发消息——经 dsp 主动发送入口(**只绑 127.0.0.1**,plugin 全局配置 ~/.dsh/serenity-hooks.json
|
|
1836
|
+
的 weixinApi.port,默认 3082;公网不可达故无密钥)→ weixin-bridge.sendProactiveText →
|
|
1837
|
+
成功后触发 outgoing hook(source="proactive")。send-file 仍由 MSM 直连 iLink 上传并自行补记。
|
|
1493
1838
|
|
|
1494
1839
|
Config:
|
|
1495
1840
|
{ "weixin": {
|
|
@@ -1505,7 +1850,10 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1505
1850
|
|
|
1506
1851
|
触发(双向):
|
|
1507
1852
|
incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
|
|
1508
|
-
outgoing = bot →
|
|
1853
|
+
outgoing = bot → 用户:发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
|
|
1854
|
+
· source="reply"(缺省)= 回复用户消息;source="proactive"(v1.30.9)= bot 主动发起
|
|
1855
|
+
(CCC 经 weixin-send MSM → dsp 主动发送入口 → sendProactiveText)
|
|
1856
|
+
· file(仅 send-file 补记):{ "name": "报告.pdf", "size": 12345, "caption": "可选" }
|
|
1509
1857
|
|
|
1510
1858
|
脚本约定(事件 JSON 单行经 stdin 传入;bun 优先 node 兜底):
|
|
1511
1859
|
#!/usr/bin/env bun 或 node script.js —— 读 process.stdin 整段 JSON.parse
|
|
@@ -1519,7 +1867,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1519
1867
|
"message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
|
|
1520
1868
|
{ "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
|
|
1521
1869
|
"userId": ..., "sessionId": ..., "role": ...,
|
|
1522
|
-
"reply": "已记录(纯文本)" }
|
|
1870
|
+
"reply": "已记录(纯文本)", "source": "reply" | "proactive" }
|
|
1523
1871
|
|
|
1524
1872
|
示例脚本(追加到按日文件——持久化归 CCC 自选:文件/DB/远端均可):
|
|
1525
1873
|
const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';
|
|
@@ -2332,7 +2680,7 @@ function handymanPresetInheritance(parentCtx) {
|
|
|
2332
2680
|
*
|
|
2333
2681
|
* 机制:ctx.agents.create()(带 setup 钩子)创建专用 agent(进程内),
|
|
2334
2682
|
* 每轮 followup → agent/status idle → 读 session.events 响应 → 写进度 → stop token 检查 →
|
|
2335
|
-
* 未完成继续下一轮;followup
|
|
2683
|
+
* 未完成继续下一轮;followup/等待空闲抛错(非正常停止)→ dispose 并重新 create agent
|
|
2336
2684
|
* (重启计数,≤100),同一轮重试。工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
|
|
2337
2685
|
*
|
|
2338
2686
|
* preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
|
|
@@ -2347,21 +2695,6 @@ function renderText$4(value) {
|
|
|
2347
2695
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
2348
2696
|
}];
|
|
2349
2697
|
}
|
|
2350
|
-
/** 等待 agent 空闲(agent/status → idle);无超时(handyman 可永续,agent 工作多久等多久) */
|
|
2351
|
-
function waitIdle(ctx, agent) {
|
|
2352
|
-
return new Promise((resolve) => {
|
|
2353
|
-
let settled = false;
|
|
2354
|
-
const finish = () => {
|
|
2355
|
-
if (settled) return;
|
|
2356
|
-
settled = true;
|
|
2357
|
-
dispose();
|
|
2358
|
-
resolve();
|
|
2359
|
-
};
|
|
2360
|
-
const dispose = ctx.on("agent/status", (payload) => {
|
|
2361
|
-
if (payload.agent === agent && payload.status === "idle") finish();
|
|
2362
|
-
});
|
|
2363
|
-
});
|
|
2364
|
-
}
|
|
2365
2698
|
/** 读取会话最后一个 assistant/message 文本 */
|
|
2366
2699
|
function lastAssistantText$1(agent) {
|
|
2367
2700
|
const s = agent.session;
|
|
@@ -2461,7 +2794,7 @@ async function runHandymanJob(ctx, opts) {
|
|
|
2461
2794
|
plugin: "dsh-serenity-hooks"
|
|
2462
2795
|
}
|
|
2463
2796
|
}));
|
|
2464
|
-
await
|
|
2797
|
+
await waitAgentIdle(ctx, workerAgent);
|
|
2465
2798
|
lastResponse = lastAssistantText$1(workerAgent);
|
|
2466
2799
|
} catch {
|
|
2467
2800
|
restarts++;
|
|
@@ -2727,7 +3060,7 @@ function renameDshSessionOnUse(deps, session, titles, active, summary) {
|
|
|
2727
3060
|
*/
|
|
2728
3061
|
function renameDshSessionForActive(ctx, exec, info, summary) {
|
|
2729
3062
|
try {
|
|
2730
|
-
const titles = ctx
|
|
3063
|
+
const titles = hostService(ctx, "sessionTitle");
|
|
2731
3064
|
const dshSession = exec.agent?.session;
|
|
2732
3065
|
if (!dshSession) {
|
|
2733
3066
|
console.warn(`[serenity-hooks] dsh 会话重命名未执行: 缺少 agent session(info: ${info.sessionId})`);
|
|
@@ -3328,6 +3661,40 @@ const containerAdminTool = defineTool({
|
|
|
3328
3661
|
}
|
|
3329
3662
|
});
|
|
3330
3663
|
//#endregion
|
|
3664
|
+
//#region src/host/effect.ts
|
|
3665
|
+
/** 已告警的标签(进程级去重:重复装配/多测试替身不刷屏,但绝不静默) */
|
|
3666
|
+
const warnedLabels = /* @__PURE__ */ new Set();
|
|
3667
|
+
/**
|
|
3668
|
+
* 登记一个卸载时执行的清理。
|
|
3669
|
+
* @param ctx 宿主插件上下文
|
|
3670
|
+
* @param label 日志标签(定位是哪个资源未装配)
|
|
3671
|
+
* @param cleanup 卸载时执行;抛错被吞掉(不阻断其它清理)
|
|
3672
|
+
* @returns 是否成功装配(false = 宿主无 `ctx.effect`,已告警)
|
|
3673
|
+
*/
|
|
3674
|
+
function registerDisposer(ctx, label, cleanup) {
|
|
3675
|
+
const effect = ctx?.effect;
|
|
3676
|
+
if (typeof effect !== "function") {
|
|
3677
|
+
if (!warnedLabels.has(label)) {
|
|
3678
|
+
warnedLabels.add(label);
|
|
3679
|
+
console.warn(`[serenity-hooks] ✗ 资源拆卸未装配(${label}):ctx.effect 不可用(宿主契约缺失)`);
|
|
3680
|
+
}
|
|
3681
|
+
return false;
|
|
3682
|
+
}
|
|
3683
|
+
try {
|
|
3684
|
+
effect.call(ctx, () => () => {
|
|
3685
|
+
try {
|
|
3686
|
+
cleanup();
|
|
3687
|
+
} catch (err) {
|
|
3688
|
+
console.warn(`[serenity-hooks] ✗ 资源清理失败(${label}): ${String(err?.message ?? err)}`);
|
|
3689
|
+
}
|
|
3690
|
+
}, `dsh-serenity-hooks: ${label}`);
|
|
3691
|
+
return true;
|
|
3692
|
+
} catch (err) {
|
|
3693
|
+
console.warn(`[serenity-hooks] ✗ 资源拆卸注册失败(${label}): ${String(err?.message ?? err)}`);
|
|
3694
|
+
return false;
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
//#endregion
|
|
3331
3698
|
//#region src/autopilot-trajectory.ts
|
|
3332
3699
|
var autopilot_trajectory_exports = /* @__PURE__ */ __exportAll({
|
|
3333
3700
|
AUDIT_HISTORY_MAX: () => 50,
|
|
@@ -3678,6 +4045,12 @@ function registerAutopilot(ctx) {
|
|
|
3678
4045
|
try {
|
|
3679
4046
|
ctx.on("serenity/settings-changed", () => startTimer());
|
|
3680
4047
|
} catch {}
|
|
4048
|
+
registerDisposer(ctx, "autopilot 时钟", () => {
|
|
4049
|
+
if (timer) {
|
|
4050
|
+
clearInterval(timer);
|
|
4051
|
+
timer = null;
|
|
4052
|
+
}
|
|
4053
|
+
});
|
|
3681
4054
|
}
|
|
3682
4055
|
/**
|
|
3683
4056
|
* 目标 agent:从 SESSION.md 反向定位 dsh 会话(cwd 归属校验 + **bound 精确匹配优先**,
|
|
@@ -3697,8 +4070,8 @@ function resolveTargetAgent(ctx, mdPath) {
|
|
|
3697
4070
|
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
3698
4071
|
const sid = idMatch ? `S${idMatch[1]}` : null;
|
|
3699
4072
|
const targetRoot = findSerenityRoot(mdPath);
|
|
3700
|
-
const sessions = ctx
|
|
3701
|
-
const agents = ctx
|
|
4073
|
+
const sessions = hostSessions(ctx);
|
|
4074
|
+
const agents = hostAgents(ctx);
|
|
3702
4075
|
const candidates = [];
|
|
3703
4076
|
try {
|
|
3704
4077
|
for (const s of sessions?.list?.() ?? []) {
|
|
@@ -3764,7 +4137,7 @@ function diagnoseTargetUnavailable(ctx, mdPath) {
|
|
|
3764
4137
|
const sameCccTitles = [];
|
|
3765
4138
|
const agentMissing = { matched: false };
|
|
3766
4139
|
try {
|
|
3767
|
-
const sessions = ctx
|
|
4140
|
+
const sessions = hostSessions(ctx);
|
|
3768
4141
|
for (const s of sessions?.list?.() ?? []) {
|
|
3769
4142
|
const sess = s;
|
|
3770
4143
|
const cwd = sess?.header?.cwd ?? "";
|
|
@@ -3777,7 +4150,7 @@ function diagnoseTargetUnavailable(ctx, mdPath) {
|
|
|
3777
4150
|
const boundMatched = bound !== null && bound.dirName === dirName;
|
|
3778
4151
|
const titleMatched = sid !== null && title !== null && (title === sid || title.startsWith(`${sid}-`));
|
|
3779
4152
|
if (boundMatched || titleMatched) {
|
|
3780
|
-
if (!ctx
|
|
4153
|
+
if (!hostAgents(ctx)?.get?.(sess.id ?? "")) agentMissing.matched = true;
|
|
3781
4154
|
}
|
|
3782
4155
|
}
|
|
3783
4156
|
} catch {}
|
|
@@ -3790,7 +4163,7 @@ function resolveAutopilotRoot(ctx) {
|
|
|
3790
4163
|
const fromCwd = findSerenityRoot(process.cwd());
|
|
3791
4164
|
if (fromCwd) return fromCwd;
|
|
3792
4165
|
try {
|
|
3793
|
-
const sessions = ctx
|
|
4166
|
+
const sessions = hostSessions(ctx);
|
|
3794
4167
|
for (const s of sessions?.list?.() ?? []) {
|
|
3795
4168
|
const r = findSerenityRoot(s?.header?.cwd ?? "");
|
|
3796
4169
|
if (r) return r;
|
|
@@ -3843,7 +4216,7 @@ function getAutopilotStatus(root) {
|
|
|
3843
4216
|
function listLiveSessions(ctx) {
|
|
3844
4217
|
const out = [];
|
|
3845
4218
|
try {
|
|
3846
|
-
const sessions = ctx
|
|
4219
|
+
const sessions = hostSessions(ctx);
|
|
3847
4220
|
for (const s of sessions?.list?.() ?? []) {
|
|
3848
4221
|
const cwd = s?.header?.cwd ?? null;
|
|
3849
4222
|
out.push({
|
|
@@ -4457,7 +4830,7 @@ function rebuildReminderText(tokensK, thresholdK, escalated = false) {
|
|
|
4457
4830
|
/** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
|
|
4458
4831
|
function readContextPressure(ctx, session) {
|
|
4459
4832
|
try {
|
|
4460
|
-
const projections = ctx
|
|
4833
|
+
const projections = hostService(ctx, "sessionProjections");
|
|
4461
4834
|
if (!projections) return null;
|
|
4462
4835
|
const pressure = (projections.snapshot?.(session))?.values?.contextPressure;
|
|
4463
4836
|
if (!pressure || typeof pressure.projectedTokens !== "number") return null;
|
|
@@ -4711,6 +5084,10 @@ function defaultAdvancedSettings() {
|
|
|
4711
5084
|
publicAsk: {
|
|
4712
5085
|
key: "",
|
|
4713
5086
|
allowed: []
|
|
5087
|
+
},
|
|
5088
|
+
weixinApi: {
|
|
5089
|
+
enabled: true,
|
|
5090
|
+
port: 3082
|
|
4714
5091
|
}
|
|
4715
5092
|
};
|
|
4716
5093
|
}
|
|
@@ -4761,6 +5138,7 @@ function mergeWithDefaults(raw) {
|
|
|
4761
5138
|
const gateway = o.gateway ?? {};
|
|
4762
5139
|
const persona = o.persona ?? {};
|
|
4763
5140
|
const publicAsk = o.publicAsk ?? {};
|
|
5141
|
+
const weixinApi = o.weixinApi ?? {};
|
|
4764
5142
|
return {
|
|
4765
5143
|
gateway: {
|
|
4766
5144
|
enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
|
|
@@ -4779,6 +5157,10 @@ function mergeWithDefaults(raw) {
|
|
|
4779
5157
|
publicAsk: {
|
|
4780
5158
|
key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
|
|
4781
5159
|
allowed: Array.isArray(publicAsk.allowed) ? publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : def.publicAsk.allowed
|
|
5160
|
+
},
|
|
5161
|
+
weixinApi: {
|
|
5162
|
+
enabled: typeof weixinApi.enabled === "boolean" ? weixinApi.enabled : def.weixinApi.enabled,
|
|
5163
|
+
port: typeof weixinApi.port === "number" && Number.isInteger(weixinApi.port) && weixinApi.port >= 0 ? weixinApi.port : def.weixinApi.port
|
|
4782
5164
|
}
|
|
4783
5165
|
};
|
|
4784
5166
|
}
|
|
@@ -4816,7 +5198,11 @@ function updateAdvancedSettings(patch) {
|
|
|
4816
5198
|
publicAsk: patch.publicAsk !== void 0 ? {
|
|
4817
5199
|
key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
|
|
4818
5200
|
allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
|
|
4819
|
-
} : current.publicAsk
|
|
5201
|
+
} : current.publicAsk,
|
|
5202
|
+
weixinApi: patch.weixinApi !== void 0 ? {
|
|
5203
|
+
enabled: typeof patch.weixinApi.enabled === "boolean" ? patch.weixinApi.enabled : current.weixinApi.enabled,
|
|
5204
|
+
port: typeof patch.weixinApi.port === "number" && Number.isInteger(patch.weixinApi.port) && patch.weixinApi.port >= 0 ? patch.weixinApi.port : current.weixinApi.port
|
|
5205
|
+
} : current.weixinApi
|
|
4820
5206
|
};
|
|
4821
5207
|
writeAdvancedSettings(next);
|
|
4822
5208
|
return next;
|
|
@@ -5838,7 +6224,7 @@ function saveFileToTmp(root, fileName, data) {
|
|
|
5838
6224
|
writeFileSync(join(dir, filename), bytes);
|
|
5839
6225
|
return `${FILE_UPLOAD_DIR}/${filename}`;
|
|
5840
6226
|
}
|
|
5841
|
-
function readBody$
|
|
6227
|
+
function readBody$3(req, maxBytes) {
|
|
5842
6228
|
return new Promise((resolve, reject) => {
|
|
5843
6229
|
let data = "";
|
|
5844
6230
|
req.on("data", (chunk) => {
|
|
@@ -5852,7 +6238,7 @@ function readBody$2(req, maxBytes) {
|
|
|
5852
6238
|
req.on("error", reject);
|
|
5853
6239
|
});
|
|
5854
6240
|
}
|
|
5855
|
-
function sendJson$
|
|
6241
|
+
function sendJson$2(res, code, body) {
|
|
5856
6242
|
const payload = JSON.stringify(body);
|
|
5857
6243
|
res.writeHead(code, {
|
|
5858
6244
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -5874,10 +6260,10 @@ function resolveWorkspaceCore(sessionCwd, workspaceParam, listCwds, fallback) {
|
|
|
5874
6260
|
}
|
|
5875
6261
|
function resolveWorkspace(ctx, params) {
|
|
5876
6262
|
let sessionCwd;
|
|
5877
|
-
if (params.sessionId) sessionCwd = (ctx
|
|
6263
|
+
if (params.sessionId) sessionCwd = (hostSessions(ctx)?.get?.(params.sessionId))?.header?.cwd;
|
|
5878
6264
|
let listCwds = () => [];
|
|
5879
6265
|
try {
|
|
5880
|
-
const sessions = ctx
|
|
6266
|
+
const sessions = hostSessions(ctx);
|
|
5881
6267
|
if (typeof sessions?.list === "function") listCwds = () => sessions.list().map((s) => s.header?.cwd ?? "").filter(Boolean);
|
|
5882
6268
|
} catch {}
|
|
5883
6269
|
return resolveWorkspaceCore(sessionCwd, params.workspace, listCwds, process.cwd());
|
|
@@ -5890,7 +6276,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
5890
6276
|
handler: async (req, res) => {
|
|
5891
6277
|
try {
|
|
5892
6278
|
if (req.method !== "GET") {
|
|
5893
|
-
sendJson$
|
|
6279
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
5894
6280
|
return;
|
|
5895
6281
|
}
|
|
5896
6282
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -5900,12 +6286,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
5900
6286
|
});
|
|
5901
6287
|
const root = findSerenityRoot(workspace);
|
|
5902
6288
|
if (!root) {
|
|
5903
|
-
sendJson$
|
|
6289
|
+
sendJson$2(res, 200, { handymen: [] });
|
|
5904
6290
|
return;
|
|
5905
6291
|
}
|
|
5906
|
-
sendJson$
|
|
6292
|
+
sendJson$2(res, 200, { handymen: listActiveHandymen(root) });
|
|
5907
6293
|
} catch (err) {
|
|
5908
|
-
sendJson$
|
|
6294
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
5909
6295
|
}
|
|
5910
6296
|
}
|
|
5911
6297
|
});
|
|
@@ -5915,14 +6301,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
5915
6301
|
handler: async (req, res) => {
|
|
5916
6302
|
try {
|
|
5917
6303
|
if (req.method !== "POST") {
|
|
5918
|
-
sendJson$
|
|
6304
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
5919
6305
|
return;
|
|
5920
6306
|
}
|
|
5921
6307
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
5922
|
-
sendJson$
|
|
6308
|
+
sendJson$2(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
|
|
5923
6309
|
return;
|
|
5924
6310
|
}
|
|
5925
|
-
const raw = await readBody$
|
|
6311
|
+
const raw = await readBody$3(req, 20971520);
|
|
5926
6312
|
const body = JSON.parse(raw);
|
|
5927
6313
|
const workspace = resolveWorkspace(ctx, {
|
|
5928
6314
|
sessionId: body.sessionId,
|
|
@@ -5930,12 +6316,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
5930
6316
|
});
|
|
5931
6317
|
const root = findSerenityRoot(workspace);
|
|
5932
6318
|
if (!root) {
|
|
5933
|
-
sendJson$
|
|
6319
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
5934
6320
|
return;
|
|
5935
6321
|
}
|
|
5936
|
-
sendJson$
|
|
6322
|
+
sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
5937
6323
|
} catch (err) {
|
|
5938
|
-
sendJson$
|
|
6324
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
5939
6325
|
}
|
|
5940
6326
|
}
|
|
5941
6327
|
});
|
|
@@ -5945,14 +6331,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
5945
6331
|
handler: async (req, res) => {
|
|
5946
6332
|
try {
|
|
5947
6333
|
if (req.method !== "POST") {
|
|
5948
|
-
sendJson$
|
|
6334
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
5949
6335
|
return;
|
|
5950
6336
|
}
|
|
5951
6337
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
5952
|
-
sendJson$
|
|
6338
|
+
sendJson$2(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
|
|
5953
6339
|
return;
|
|
5954
6340
|
}
|
|
5955
|
-
const raw = await readBody$
|
|
6341
|
+
const raw = await readBody$3(req, 20971520);
|
|
5956
6342
|
const body = JSON.parse(raw);
|
|
5957
6343
|
const workspace = resolveWorkspace(ctx, {
|
|
5958
6344
|
sessionId: body.sessionId,
|
|
@@ -5960,12 +6346,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
5960
6346
|
});
|
|
5961
6347
|
const root = findSerenityRoot(workspace);
|
|
5962
6348
|
if (!root) {
|
|
5963
|
-
sendJson$
|
|
6349
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
5964
6350
|
return;
|
|
5965
6351
|
}
|
|
5966
|
-
sendJson$
|
|
6352
|
+
sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
5967
6353
|
} catch (err) {
|
|
5968
|
-
sendJson$
|
|
6354
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
5969
6355
|
}
|
|
5970
6356
|
}
|
|
5971
6357
|
});
|
|
@@ -5981,7 +6367,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
5981
6367
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
5982
6368
|
}), configPaths);
|
|
5983
6369
|
const runtime = ctx.get("codeRuntime");
|
|
5984
|
-
sendJson$
|
|
6370
|
+
sendJson$2(res, 200, {
|
|
5985
6371
|
...status,
|
|
5986
6372
|
...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
|
|
5987
6373
|
language: runtime.language,
|
|
@@ -5992,10 +6378,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
5992
6378
|
}
|
|
5993
6379
|
if (req.method === "POST") {
|
|
5994
6380
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
5995
|
-
sendJson$
|
|
6381
|
+
sendJson$2(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
|
|
5996
6382
|
return;
|
|
5997
6383
|
}
|
|
5998
|
-
const raw = await readBody$
|
|
6384
|
+
const raw = await readBody$3(req, 65536);
|
|
5999
6385
|
const body = JSON.parse(raw);
|
|
6000
6386
|
const workspace = resolveWorkspace(ctx, {
|
|
6001
6387
|
sessionId: body.sessionId,
|
|
@@ -6003,19 +6389,19 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6003
6389
|
});
|
|
6004
6390
|
const root = findSerenityRoot(workspace);
|
|
6005
6391
|
if (!root) {
|
|
6006
|
-
sendJson$
|
|
6392
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
6007
6393
|
return;
|
|
6008
6394
|
}
|
|
6009
6395
|
const result = setSafeMode(root, body.on === true);
|
|
6010
|
-
sendJson$
|
|
6396
|
+
sendJson$2(res, 200, {
|
|
6011
6397
|
...getStatus(workspace, configPaths),
|
|
6012
6398
|
...result
|
|
6013
6399
|
});
|
|
6014
6400
|
return;
|
|
6015
6401
|
}
|
|
6016
|
-
sendJson$
|
|
6402
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6017
6403
|
} catch (err) {
|
|
6018
|
-
sendJson$
|
|
6404
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6019
6405
|
}
|
|
6020
6406
|
}
|
|
6021
6407
|
});
|
|
@@ -6025,34 +6411,33 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6025
6411
|
handler: async (req, res) => {
|
|
6026
6412
|
try {
|
|
6027
6413
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6028
|
-
sendJson$
|
|
6414
|
+
sendJson$2(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
|
|
6029
6415
|
return;
|
|
6030
6416
|
}
|
|
6031
6417
|
if (req.method === "GET") {
|
|
6032
6418
|
const settings = readAdvancedSettings();
|
|
6033
6419
|
let known = [];
|
|
6034
6420
|
try {
|
|
6035
|
-
|
|
6036
|
-
known = projectKnownWorkspaces(registry?.list?.() ?? [], settings.gateway.workspaces);
|
|
6421
|
+
known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
|
|
6037
6422
|
} catch {}
|
|
6038
|
-
sendJson$
|
|
6423
|
+
sendJson$2(res, 200, {
|
|
6039
6424
|
config: toWire(settings),
|
|
6040
6425
|
knownWorkspaces: known
|
|
6041
6426
|
});
|
|
6042
6427
|
return;
|
|
6043
6428
|
}
|
|
6044
6429
|
if (req.method === "PUT") {
|
|
6045
|
-
const raw = await readBody$
|
|
6430
|
+
const raw = await readBody$3(req, 131072);
|
|
6046
6431
|
const saved = applyWirePatch(JSON.parse(raw).config ?? {});
|
|
6047
6432
|
try {
|
|
6048
6433
|
ctx.emit?.("serenity/config-updated");
|
|
6049
6434
|
} catch {}
|
|
6050
|
-
sendJson$
|
|
6435
|
+
sendJson$2(res, 200, { config: toWire(saved) });
|
|
6051
6436
|
return;
|
|
6052
6437
|
}
|
|
6053
|
-
sendJson$
|
|
6438
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6054
6439
|
} catch (err) {
|
|
6055
|
-
sendJson$
|
|
6440
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6056
6441
|
}
|
|
6057
6442
|
}
|
|
6058
6443
|
});
|
|
@@ -6062,7 +6447,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6062
6447
|
handler: async (req, res) => {
|
|
6063
6448
|
try {
|
|
6064
6449
|
if (req.method !== "GET") {
|
|
6065
|
-
sendJson$
|
|
6450
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6066
6451
|
return;
|
|
6067
6452
|
}
|
|
6068
6453
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -6071,10 +6456,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6071
6456
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
6072
6457
|
});
|
|
6073
6458
|
const root = findSerenityRoot(workspace) ?? "";
|
|
6074
|
-
const { discoverCccs } = await import("./skiff-debug-
|
|
6075
|
-
sendJson$
|
|
6459
|
+
const { discoverCccs } = await import("./skiff-debug-TONJlpgF.js").then((n) => n.i);
|
|
6460
|
+
sendJson$2(res, 200, { cccs: await discoverCccs(ctx, root) });
|
|
6076
6461
|
} catch (err) {
|
|
6077
|
-
sendJson$
|
|
6462
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6078
6463
|
}
|
|
6079
6464
|
}
|
|
6080
6465
|
});
|
|
@@ -6084,24 +6469,24 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6084
6469
|
handler: async (req, res) => {
|
|
6085
6470
|
try {
|
|
6086
6471
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6087
|
-
sendJson$
|
|
6472
|
+
sendJson$2(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
|
|
6088
6473
|
return;
|
|
6089
6474
|
}
|
|
6090
6475
|
if (req.method === "PUT") {
|
|
6091
|
-
const raw = await readBody$
|
|
6476
|
+
const raw = await readBody$3(req, 16384);
|
|
6092
6477
|
if (JSON.parse(raw).action !== "rotate") {
|
|
6093
|
-
sendJson$
|
|
6478
|
+
sendJson$2(res, 400, { error: "unsupported action (expected \"rotate\")" });
|
|
6094
6479
|
return;
|
|
6095
6480
|
}
|
|
6096
|
-
sendJson$
|
|
6481
|
+
sendJson$2(res, 200, { key: rotatePublicAskKey() });
|
|
6097
6482
|
return;
|
|
6098
6483
|
}
|
|
6099
6484
|
if (req.method !== "GET") {
|
|
6100
|
-
sendJson$
|
|
6485
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6101
6486
|
return;
|
|
6102
6487
|
}
|
|
6103
6488
|
const settings = readAdvancedSettings();
|
|
6104
|
-
const { readSimpleSettings } = await import("./settings-section-
|
|
6489
|
+
const { readSimpleSettings } = await import("./settings-section-BPk0e1du.js").then((n) => n.r);
|
|
6105
6490
|
const simple = readSimpleSettings();
|
|
6106
6491
|
const allowed = settings.publicAsk.allowed;
|
|
6107
6492
|
const port = simple.acpHttpPort ?? 3100;
|
|
@@ -6110,7 +6495,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6110
6495
|
name,
|
|
6111
6496
|
url: `${base}/c/${encodeURIComponent(name)}`
|
|
6112
6497
|
}));
|
|
6113
|
-
sendJson$
|
|
6498
|
+
sendJson$2(res, 200, {
|
|
6114
6499
|
enabled: simple.publicAskEnabled ?? false,
|
|
6115
6500
|
port,
|
|
6116
6501
|
key: ensurePublicAskKey(),
|
|
@@ -6119,7 +6504,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6119
6504
|
listUrl: `${base}/`
|
|
6120
6505
|
});
|
|
6121
6506
|
} catch (err) {
|
|
6122
|
-
sendJson$
|
|
6507
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6123
6508
|
}
|
|
6124
6509
|
}
|
|
6125
6510
|
});
|
|
@@ -6130,13 +6515,13 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6130
6515
|
try {
|
|
6131
6516
|
if (req.method === "POST") {
|
|
6132
6517
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6133
|
-
sendJson$
|
|
6518
|
+
sendJson$2(res, 403, { error: "立即唤起仅限 WebUI(agent 不可自行唤起)" });
|
|
6134
6519
|
return;
|
|
6135
6520
|
}
|
|
6136
|
-
const raw = await readBody$
|
|
6521
|
+
const raw = await readBody$3(req, 16384);
|
|
6137
6522
|
const body = JSON.parse(raw);
|
|
6138
6523
|
if (body.action !== "wake") {
|
|
6139
|
-
sendJson$
|
|
6524
|
+
sendJson$2(res, 400, { error: "unsupported action (expected \"wake\")" });
|
|
6140
6525
|
return;
|
|
6141
6526
|
}
|
|
6142
6527
|
const workspace = resolveWorkspace(ctx, {
|
|
@@ -6146,21 +6531,21 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6146
6531
|
const root = findSerenityRoot(workspace);
|
|
6147
6532
|
const effectiveRoot = body.ccc?.trim() || !body.workspace && !body.sessionId ? body.ccc?.trim() ?? (await Promise.resolve().then(() => autopilot_trajectory_exports)).collectAutopilotCccs(ctx)[0] ?? root : root;
|
|
6148
6533
|
if (!effectiveRoot) {
|
|
6149
|
-
sendJson$
|
|
6534
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
6150
6535
|
return;
|
|
6151
6536
|
}
|
|
6152
6537
|
const { performAutopilotWake, readAutopilotSettings } = await Promise.resolve().then(() => autopilot_trajectory_exports);
|
|
6153
6538
|
const settings = readAutopilotSettings(effectiveRoot);
|
|
6154
6539
|
if (!settings) {
|
|
6155
|
-
sendJson$
|
|
6540
|
+
sendJson$2(res, 400, { error: "Autopilot Trajectory 未配置(.opencode/serenity.json 缺段)" });
|
|
6156
6541
|
return;
|
|
6157
6542
|
}
|
|
6158
6543
|
const result = await performAutopilotWake(ctx, effectiveRoot, settings, { force: true });
|
|
6159
|
-
sendJson$
|
|
6544
|
+
sendJson$2(res, result.ok ? 200 : 400, result);
|
|
6160
6545
|
return;
|
|
6161
6546
|
}
|
|
6162
6547
|
if (req.method !== "GET") {
|
|
6163
|
-
sendJson$
|
|
6548
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6164
6549
|
return;
|
|
6165
6550
|
}
|
|
6166
6551
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -6172,12 +6557,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6172
6557
|
const { getAutopilotStatus, collectAutopilotCccs } = await Promise.resolve().then(() => autopilot_trajectory_exports);
|
|
6173
6558
|
const effectiveRoot = url.searchParams.get("ccc")?.trim() ?? (!url.searchParams.get("workspace") && !url.searchParams.get("sessionId") ? collectAutopilotCccs(ctx)[0] : null) ?? root;
|
|
6174
6559
|
if (!effectiveRoot) {
|
|
6175
|
-
sendJson$
|
|
6560
|
+
sendJson$2(res, 200, { status: null });
|
|
6176
6561
|
return;
|
|
6177
6562
|
}
|
|
6178
|
-
sendJson$
|
|
6563
|
+
sendJson$2(res, 200, { status: getAutopilotStatus(effectiveRoot) });
|
|
6179
6564
|
} catch (err) {
|
|
6180
|
-
sendJson$
|
|
6565
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6181
6566
|
}
|
|
6182
6567
|
}
|
|
6183
6568
|
});
|
|
@@ -6187,28 +6572,28 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6187
6572
|
handler: async (req, res) => {
|
|
6188
6573
|
try {
|
|
6189
6574
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6190
|
-
sendJson$
|
|
6575
|
+
sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
|
|
6191
6576
|
return;
|
|
6192
6577
|
}
|
|
6193
6578
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
6194
6579
|
if (req.method === "GET") {
|
|
6195
6580
|
const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
|
|
6196
6581
|
if (!chk.ok) {
|
|
6197
|
-
sendJson$
|
|
6582
|
+
sendJson$2(res, 400, { error: chk.error });
|
|
6198
6583
|
return;
|
|
6199
6584
|
}
|
|
6200
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6585
|
+
const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6201
6586
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6202
6587
|
const settings = readWeixinSettings(chk.root);
|
|
6203
6588
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
6204
|
-
const { readWeixinCredential } = await import("./weixin-route-
|
|
6589
|
+
const { readWeixinCredential } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6205
6590
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
6206
6591
|
accountId: a.accountId,
|
|
6207
6592
|
name: a.name ?? void 0,
|
|
6208
6593
|
enabled: a.enabled !== false,
|
|
6209
6594
|
bound: readWeixinCredential(chk.root, a.accountId) !== null
|
|
6210
6595
|
}));
|
|
6211
|
-
sendJson$
|
|
6596
|
+
sendJson$2(res, 200, {
|
|
6212
6597
|
enabled: settings.enabled === true,
|
|
6213
6598
|
botType: settings.botType ?? void 0,
|
|
6214
6599
|
accounts,
|
|
@@ -6218,22 +6603,22 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6218
6603
|
return;
|
|
6219
6604
|
}
|
|
6220
6605
|
if (req.method !== "POST") {
|
|
6221
|
-
sendJson$
|
|
6606
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6222
6607
|
return;
|
|
6223
6608
|
}
|
|
6224
|
-
const raw = await readBody$
|
|
6609
|
+
const raw = await readBody$3(req, 131072);
|
|
6225
6610
|
const body = JSON.parse(raw);
|
|
6226
6611
|
const chk = requireCcc(body.ccc);
|
|
6227
6612
|
if (!chk.ok) {
|
|
6228
|
-
sendJson$
|
|
6613
|
+
sendJson$2(res, 400, { error: chk.error });
|
|
6229
6614
|
return;
|
|
6230
6615
|
}
|
|
6231
6616
|
const root = chk.root;
|
|
6232
6617
|
if (body.action === "login-start") {
|
|
6233
|
-
const { fetchQRCode } = await import("./weixin-api-
|
|
6618
|
+
const { fetchQRCode } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
|
|
6234
6619
|
const { randomUUID } = await import("node:crypto");
|
|
6235
6620
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
6236
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6621
|
+
const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6237
6622
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
6238
6623
|
const loginKey = randomUUID();
|
|
6239
6624
|
weixinLogins.set(loginKey, {
|
|
@@ -6242,7 +6627,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6242
6627
|
startedAt: Date.now(),
|
|
6243
6628
|
polling: false
|
|
6244
6629
|
});
|
|
6245
|
-
sendJson$
|
|
6630
|
+
sendJson$2(res, 200, {
|
|
6246
6631
|
qrcode: qr.qrcode,
|
|
6247
6632
|
qrcode_img_content: qr.qrcode_img_content,
|
|
6248
6633
|
loginKey
|
|
@@ -6251,40 +6636,40 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6251
6636
|
}
|
|
6252
6637
|
if (body.action === "remove-account") {
|
|
6253
6638
|
if (!body.accountId) {
|
|
6254
|
-
sendJson$
|
|
6639
|
+
sendJson$2(res, 400, { error: "missing accountId" });
|
|
6255
6640
|
return;
|
|
6256
6641
|
}
|
|
6257
|
-
const { removeWeixinAccount } = await import("./weixin-route-
|
|
6642
|
+
const { removeWeixinAccount } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6258
6643
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6259
6644
|
removeWeixinAccount(root, body.accountId);
|
|
6260
6645
|
syncCccBridge(ctx, root);
|
|
6261
|
-
sendJson$
|
|
6646
|
+
sendJson$2(res, 200, { removed: body.accountId });
|
|
6262
6647
|
return;
|
|
6263
6648
|
}
|
|
6264
6649
|
if (body.action === "save-routes") {
|
|
6265
6650
|
const routes = body.routes ?? [];
|
|
6266
6651
|
if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
|
|
6267
|
-
sendJson$
|
|
6652
|
+
sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
6268
6653
|
return;
|
|
6269
6654
|
}
|
|
6270
|
-
const { saveWeixinRoutes } = await import("./weixin-route-
|
|
6271
|
-
const { readSkiffRoles } = await import("./skiff-role-
|
|
6655
|
+
const { saveWeixinRoutes } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6656
|
+
const { readSkiffRoles } = await import("./skiff-role-DlrbHPLD.js").then((n) => n.c);
|
|
6272
6657
|
const roles = readSkiffRoles(root);
|
|
6273
6658
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
6274
|
-
sendJson$
|
|
6659
|
+
sendJson$2(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
|
|
6275
6660
|
return;
|
|
6276
6661
|
}
|
|
6277
6662
|
saveWeixinRoutes(root, routes);
|
|
6278
|
-
sendJson$
|
|
6663
|
+
sendJson$2(res, 200, { saved: routes.length });
|
|
6279
6664
|
return;
|
|
6280
6665
|
}
|
|
6281
6666
|
if (body.action === "set-enabled") {
|
|
6282
6667
|
const enabled = body.enabled === true;
|
|
6283
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-
|
|
6668
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6284
6669
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6285
6670
|
const settings = readWeixinSettings(root);
|
|
6286
6671
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
6287
|
-
sendJson$
|
|
6672
|
+
sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
|
|
6288
6673
|
return;
|
|
6289
6674
|
}
|
|
6290
6675
|
setWeixinEnabled(root, enabled);
|
|
@@ -6293,12 +6678,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6293
6678
|
const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6294
6679
|
stopCccBridge(root);
|
|
6295
6680
|
}
|
|
6296
|
-
sendJson$
|
|
6681
|
+
sendJson$2(res, 200, { enabled });
|
|
6297
6682
|
return;
|
|
6298
6683
|
}
|
|
6299
|
-
sendJson$
|
|
6684
|
+
sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
|
|
6300
6685
|
} catch (err) {
|
|
6301
|
-
sendJson$
|
|
6686
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6302
6687
|
}
|
|
6303
6688
|
}
|
|
6304
6689
|
});
|
|
@@ -6308,26 +6693,26 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6308
6693
|
handler: async (req, res) => {
|
|
6309
6694
|
try {
|
|
6310
6695
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6311
|
-
sendJson$
|
|
6696
|
+
sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
|
|
6312
6697
|
return;
|
|
6313
6698
|
}
|
|
6314
6699
|
if (req.method !== "GET") {
|
|
6315
|
-
sendJson$
|
|
6700
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6316
6701
|
return;
|
|
6317
6702
|
}
|
|
6318
6703
|
const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
|
|
6319
6704
|
const login = weixinLogins.get(key);
|
|
6320
6705
|
if (!login) {
|
|
6321
|
-
sendJson$
|
|
6706
|
+
sendJson$2(res, 404, { error: "login not found or expired" });
|
|
6322
6707
|
return;
|
|
6323
6708
|
}
|
|
6324
6709
|
if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
|
|
6325
6710
|
weixinLogins.delete(key);
|
|
6326
|
-
sendJson$
|
|
6711
|
+
sendJson$2(res, 200, { status: "expired" });
|
|
6327
6712
|
return;
|
|
6328
6713
|
}
|
|
6329
|
-
const { pollQRStatus } = await import("./weixin-api-
|
|
6330
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6714
|
+
const { pollQRStatus } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
|
|
6715
|
+
const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6331
6716
|
const settings = readWeixinSettings(login.root);
|
|
6332
6717
|
const status = await pollQRStatus({
|
|
6333
6718
|
baseUrl: void 0,
|
|
@@ -6336,13 +6721,13 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6336
6721
|
switch (status.status) {
|
|
6337
6722
|
case "confirmed": {
|
|
6338
6723
|
if (!status.bot_token) {
|
|
6339
|
-
sendJson$
|
|
6724
|
+
sendJson$2(res, 200, {
|
|
6340
6725
|
status: "error",
|
|
6341
6726
|
error: "confirmed but no bot_token"
|
|
6342
6727
|
});
|
|
6343
6728
|
return;
|
|
6344
6729
|
}
|
|
6345
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-
|
|
6730
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6346
6731
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6347
6732
|
const accountId = nextWeixinAccountId(settings);
|
|
6348
6733
|
upsertWeixinAccount(login.root, {
|
|
@@ -6357,7 +6742,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6357
6742
|
});
|
|
6358
6743
|
syncCccBridge(ctx, login.root);
|
|
6359
6744
|
weixinLogins.delete(key);
|
|
6360
|
-
sendJson$
|
|
6745
|
+
sendJson$2(res, 200, {
|
|
6361
6746
|
status: "confirmed",
|
|
6362
6747
|
accountId,
|
|
6363
6748
|
tokenSaved: true
|
|
@@ -6366,14 +6751,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6366
6751
|
}
|
|
6367
6752
|
case "expired":
|
|
6368
6753
|
weixinLogins.delete(key);
|
|
6369
|
-
sendJson$
|
|
6754
|
+
sendJson$2(res, 200, { status: "expired" });
|
|
6370
6755
|
return;
|
|
6371
6756
|
default:
|
|
6372
|
-
sendJson$
|
|
6757
|
+
sendJson$2(res, 200, { status: status.status ?? "wait" });
|
|
6373
6758
|
return;
|
|
6374
6759
|
}
|
|
6375
6760
|
} catch (err) {
|
|
6376
|
-
sendJson$
|
|
6761
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6377
6762
|
}
|
|
6378
6763
|
}
|
|
6379
6764
|
});
|
|
@@ -6395,7 +6780,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6395
6780
|
const root = sessionsRootDir();
|
|
6396
6781
|
if (req.method === "GET") {
|
|
6397
6782
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
6398
|
-
sendJson$
|
|
6783
|
+
sendJson$2(res, 200, {
|
|
6399
6784
|
root,
|
|
6400
6785
|
olderThanDays: days,
|
|
6401
6786
|
dryRun: true,
|
|
@@ -6409,15 +6794,15 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6409
6794
|
return;
|
|
6410
6795
|
}
|
|
6411
6796
|
if (req.method !== "POST") {
|
|
6412
|
-
sendJson$
|
|
6797
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6413
6798
|
return;
|
|
6414
6799
|
}
|
|
6415
6800
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6416
|
-
sendJson$
|
|
6801
|
+
sendJson$2(res, 403, { error: "会话清理仅限 WebUI(client 专用)" });
|
|
6417
6802
|
return;
|
|
6418
6803
|
}
|
|
6419
6804
|
const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
|
|
6420
|
-
sendJson$
|
|
6805
|
+
sendJson$2(res, 200, {
|
|
6421
6806
|
root,
|
|
6422
6807
|
olderThanDays: days,
|
|
6423
6808
|
dryRun: false,
|
|
@@ -6425,7 +6810,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6425
6810
|
errors: result.errors
|
|
6426
6811
|
});
|
|
6427
6812
|
} catch (err) {
|
|
6428
|
-
sendJson$
|
|
6813
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6429
6814
|
}
|
|
6430
6815
|
}
|
|
6431
6816
|
});
|
|
@@ -6859,6 +7244,38 @@ function workspaceDenyResponse(rpcId) {
|
|
|
6859
7244
|
});
|
|
6860
7245
|
}
|
|
6861
7246
|
/**
|
|
7247
|
+
* 判定 workspace 创建请求的端点(v1.30.6,review F-03)。
|
|
7248
|
+
*
|
|
7249
|
+
* 宿主 rc.1 的 Remote 端点由 `typertEndpoint({namespace,method})` 派生为
|
|
7250
|
+
* `workspace/create`,因此浏览器实际请求 `POST /api/workspace/create`
|
|
7251
|
+
* (packages/typert/registry/src/service.ts:63-69;client/connection/src/client/rpc.ts:44)。
|
|
7252
|
+
* 旧实现只匹配 `/api/workspace.create` → 该分支从未命中,外部白名单与禁建校验失效。
|
|
7253
|
+
* 保留旧端点以兼容旧宿主。
|
|
7254
|
+
*/
|
|
7255
|
+
function isWorkspaceCreatePath(pathname) {
|
|
7256
|
+
return pathname === "/api/workspace/create" || pathname === "/api/workspace.create";
|
|
7257
|
+
}
|
|
7258
|
+
/**
|
|
7259
|
+
* 从 workspace 创建请求体解析 `{ rpcId, path }`(v1.30.6,review F-03)。
|
|
7260
|
+
*
|
|
7261
|
+
* rc.1 wire 信封:`{ type:'client-request', rpcId, method:'workspace/create',
|
|
7262
|
+
* payload:{ args:{ path } } }`(packages/api/gateway/src/index.ts:955 `args: payload.args`);
|
|
7263
|
+
* 旧宿主为 `payload.path`。解析失败返回 rpcId='unknown' + path=undefined。
|
|
7264
|
+
*/
|
|
7265
|
+
function parseWorkspaceCreateBody(body) {
|
|
7266
|
+
try {
|
|
7267
|
+
const parsed = JSON.parse(body);
|
|
7268
|
+
const rpcId = typeof parsed.rpcId === "string" ? parsed.rpcId : "unknown";
|
|
7269
|
+
const raw = parsed.payload?.args?.path ?? parsed.payload?.path;
|
|
7270
|
+
return typeof raw === "string" ? {
|
|
7271
|
+
rpcId,
|
|
7272
|
+
path: raw
|
|
7273
|
+
} : { rpcId };
|
|
7274
|
+
} catch {
|
|
7275
|
+
return { rpcId: "unknown" };
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
/**
|
|
6862
7279
|
* 在 HTML 的 </head> 前注入 polyfill(幂等:含 marker 则跳过)
|
|
6863
7280
|
*/
|
|
6864
7281
|
function injectPolyfillHtml(html) {
|
|
@@ -6978,7 +7395,7 @@ function createDshCookieProvider(connection, authority) {
|
|
|
6978
7395
|
//#endregion
|
|
6979
7396
|
//#region src/gateway.ts
|
|
6980
7397
|
/** 读取请求体(≤ maxBytes;超限 reject) */
|
|
6981
|
-
function readBody$
|
|
7398
|
+
function readBody$2(req, maxBytes) {
|
|
6982
7399
|
return new Promise((resolve, reject) => {
|
|
6983
7400
|
let data = "";
|
|
6984
7401
|
req.on("data", (chunk) => {
|
|
@@ -7102,14 +7519,10 @@ function startGateway(config, getAccounts, getDshCookie) {
|
|
|
7102
7519
|
}
|
|
7103
7520
|
const session = authed(req);
|
|
7104
7521
|
if (session !== void 0) {
|
|
7105
|
-
if (req.method === "POST" && url.pathname
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
try {
|
|
7110
|
-
const parsed = JSON.parse(body);
|
|
7111
|
-
if (typeof parsed.rpcId === "string") rpcId = parsed.rpcId;
|
|
7112
|
-
} catch {}
|
|
7522
|
+
if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
|
|
7523
|
+
readBody$2(req, 131072).then((body) => {
|
|
7524
|
+
const { rpcId, path } = parseWorkspaceCreateBody(body);
|
|
7525
|
+
if (!allowWorkspaceCreate) {
|
|
7113
7526
|
res.writeHead(403, { "content-type": "application/json" });
|
|
7114
7527
|
res.end(JSON.stringify({
|
|
7115
7528
|
type: "server-response",
|
|
@@ -7123,31 +7536,8 @@ function startGateway(config, getAccounts, getDshCookie) {
|
|
|
7123
7536
|
}
|
|
7124
7537
|
}
|
|
7125
7538
|
}));
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
res.end(JSON.stringify({
|
|
7129
|
-
type: "server-response",
|
|
7130
|
-
rpcId: "unknown",
|
|
7131
|
-
result: {
|
|
7132
|
-
ok: false,
|
|
7133
|
-
error: {
|
|
7134
|
-
code: "bad-request",
|
|
7135
|
-
message: "body too large",
|
|
7136
|
-
details: {}
|
|
7137
|
-
}
|
|
7138
|
-
}
|
|
7139
|
-
}));
|
|
7140
|
-
});
|
|
7141
|
-
return;
|
|
7142
|
-
}
|
|
7143
|
-
readBody$1(req, 131072).then((body) => {
|
|
7144
|
-
let rpcId = "unknown";
|
|
7145
|
-
let path;
|
|
7146
|
-
try {
|
|
7147
|
-
const parsed = JSON.parse(body);
|
|
7148
|
-
if (typeof parsed.rpcId === "string") rpcId = parsed.rpcId;
|
|
7149
|
-
if (typeof parsed.payload?.path === "string") path = parsed.payload.path;
|
|
7150
|
-
} catch {}
|
|
7539
|
+
return;
|
|
7540
|
+
}
|
|
7151
7541
|
if (!workspaceAllowed(allowWorkspaces, path)) {
|
|
7152
7542
|
res.writeHead(403, { "content-type": "application/json" });
|
|
7153
7543
|
res.end(workspaceDenyResponse(rpcId));
|
|
@@ -7372,7 +7762,7 @@ function buildDshCookieProvider(ctx, mainPort) {
|
|
|
7372
7762
|
let diagnosed = false;
|
|
7373
7763
|
return () => {
|
|
7374
7764
|
try {
|
|
7375
|
-
const connection = ctx
|
|
7765
|
+
const connection = hostService(ctx, "connection");
|
|
7376
7766
|
if (!connection || typeof connection.authenticatedUrl !== "function" || typeof connection.authorizeIndex !== "function") {
|
|
7377
7767
|
if (!diagnosed) {
|
|
7378
7768
|
diagnosed = true;
|
|
@@ -7404,7 +7794,7 @@ function registerGateway(ctx) {
|
|
|
7404
7794
|
let lastSig = null;
|
|
7405
7795
|
const sync = () => {
|
|
7406
7796
|
try {
|
|
7407
|
-
const webServer = ctx
|
|
7797
|
+
const webServer = hostWebServer(ctx);
|
|
7408
7798
|
if (!webServer?.port) return;
|
|
7409
7799
|
const enabled = readSimpleSettings().gatewayEnabled;
|
|
7410
7800
|
const settings = readAdvancedSettings();
|
|
@@ -7455,6 +7845,11 @@ function registerGateway(ctx) {
|
|
|
7455
7845
|
ctx.on("serenity/settings-changed", () => {
|
|
7456
7846
|
sync();
|
|
7457
7847
|
});
|
|
7848
|
+
registerDisposer(ctx, "gateway 第二监听器", () => {
|
|
7849
|
+
current?.dispose();
|
|
7850
|
+
current = null;
|
|
7851
|
+
lastSig = null;
|
|
7852
|
+
});
|
|
7458
7853
|
}
|
|
7459
7854
|
//#endregion
|
|
7460
7855
|
//#region src/rebuild.ts
|
|
@@ -7702,7 +8097,7 @@ function renameAfterRebuild(ctx, agent, pending) {
|
|
|
7702
8097
|
dirName,
|
|
7703
8098
|
mdPath
|
|
7704
8099
|
}, pending.summary);
|
|
7705
|
-
const titles = ctx
|
|
8100
|
+
const titles = hostService(ctx, "sessionTitle");
|
|
7706
8101
|
if (!titles || typeof titles.rename !== "function") {
|
|
7707
8102
|
console.warn("[serenity-hooks] rebuild 后重命名跳过: sessionTitle 服务不可用");
|
|
7708
8103
|
return;
|
|
@@ -7738,7 +8133,7 @@ function registerRebuildTurnHook(ctx) {
|
|
|
7738
8133
|
}
|
|
7739
8134
|
pendingRebuilds.delete(id);
|
|
7740
8135
|
try {
|
|
7741
|
-
const tokenMeter = ctx
|
|
8136
|
+
const tokenMeter = hostService(ctx, "tokenMeter");
|
|
7742
8137
|
const meter = tokenMeter && typeof tokenMeter.estimateMessage === "function" ? { estimateMessage: (m) => tokenMeter.estimateMessage(m) } : void 0;
|
|
7743
8138
|
if (performRebuild(agent.session, pending, meter)) {
|
|
7744
8139
|
renameAfterRebuild(ctx, agent, pending);
|
|
@@ -8096,14 +8491,14 @@ var AcpServer = class {
|
|
|
8096
8491
|
sessionId: result.sessionId
|
|
8097
8492
|
};
|
|
8098
8493
|
}
|
|
8099
|
-
/** session/cancel:{ sessionId } →
|
|
8494
|
+
/** session/cancel:{ sessionId } → 取消该会话的当前活动(Agent.cancel,宿主 rc.1 契约) */
|
|
8100
8495
|
cancel(params) {
|
|
8101
8496
|
const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
|
|
8102
8497
|
if (!sessionId) return { cancelled: false };
|
|
8103
8498
|
const agent = getSkiffAgent(sessionId);
|
|
8104
8499
|
if (!agent) return { cancelled: false };
|
|
8105
8500
|
try {
|
|
8106
|
-
agent.
|
|
8501
|
+
agent.cancel({ kind: "user" });
|
|
8107
8502
|
} catch {}
|
|
8108
8503
|
return { cancelled: true };
|
|
8109
8504
|
}
|
|
@@ -8192,8 +8587,8 @@ function rpcError(id, code, message) {
|
|
|
8192
8587
|
* 实验性质:未开启时零资源占用(无监听、无 agent 创建)。
|
|
8193
8588
|
*/
|
|
8194
8589
|
/** 运行中的 ACP HTTP 服务(单实例;进程级) */
|
|
8195
|
-
let active = null;
|
|
8196
|
-
function readBody(req) {
|
|
8590
|
+
let active$1 = null;
|
|
8591
|
+
function readBody$1(req) {
|
|
8197
8592
|
return new Promise((resolve, reject) => {
|
|
8198
8593
|
const chunks = [];
|
|
8199
8594
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -8201,7 +8596,7 @@ function readBody(req) {
|
|
|
8201
8596
|
req.on("error", reject);
|
|
8202
8597
|
});
|
|
8203
8598
|
}
|
|
8204
|
-
function sendJson(res, status, payload) {
|
|
8599
|
+
function sendJson$1(res, status, payload) {
|
|
8205
8600
|
const body = jscSafeJsonText(JSON.stringify(payload));
|
|
8206
8601
|
res.writeHead(status, {
|
|
8207
8602
|
"Content-Type": "application/json; charset=utf-8",
|
|
@@ -8223,7 +8618,7 @@ function sendHtml(res, html) {
|
|
|
8223
8618
|
* @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
|
|
8224
8619
|
*/
|
|
8225
8620
|
async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
8226
|
-
if (active) return;
|
|
8621
|
+
if (active$1) return;
|
|
8227
8622
|
const server = createServer((req, res) => {
|
|
8228
8623
|
handle(ctx, defaultRoot ?? "", req, res);
|
|
8229
8624
|
});
|
|
@@ -8231,7 +8626,7 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
|
8231
8626
|
server.once("error", reject);
|
|
8232
8627
|
server.listen(port, "127.0.0.1", () => resolve());
|
|
8233
8628
|
});
|
|
8234
|
-
active = {
|
|
8629
|
+
active$1 = {
|
|
8235
8630
|
server,
|
|
8236
8631
|
port
|
|
8237
8632
|
};
|
|
@@ -8242,11 +8637,11 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
|
8242
8637
|
console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
|
|
8243
8638
|
}
|
|
8244
8639
|
function stopAcpHttpServer() {
|
|
8245
|
-
if (!active) return;
|
|
8640
|
+
if (!active$1) return;
|
|
8246
8641
|
try {
|
|
8247
|
-
active.server.close();
|
|
8642
|
+
active$1.server.close();
|
|
8248
8643
|
} catch {}
|
|
8249
|
-
active = null;
|
|
8644
|
+
active$1 = null;
|
|
8250
8645
|
}
|
|
8251
8646
|
async function handle(ctx, defaultRoot, req, res) {
|
|
8252
8647
|
try {
|
|
@@ -8255,14 +8650,14 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8255
8650
|
const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
|
|
8256
8651
|
if (req.method === "POST" && url === "/") {
|
|
8257
8652
|
if (!s.acpEnabled) {
|
|
8258
|
-
sendJson(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
|
|
8653
|
+
sendJson$1(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
|
|
8259
8654
|
return;
|
|
8260
8655
|
}
|
|
8261
8656
|
let body;
|
|
8262
8657
|
try {
|
|
8263
|
-
body = JSON.parse(await readBody(req));
|
|
8658
|
+
body = JSON.parse(await readBody$1(req));
|
|
8264
8659
|
} catch {
|
|
8265
|
-
sendJson(res, 400, [{
|
|
8660
|
+
sendJson$1(res, 400, [{
|
|
8266
8661
|
jsonrpc: "2.0",
|
|
8267
8662
|
id: null,
|
|
8268
8663
|
error: {
|
|
@@ -8272,7 +8667,7 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8272
8667
|
}]);
|
|
8273
8668
|
return;
|
|
8274
8669
|
}
|
|
8275
|
-
sendJson(res, 200, await dispatchRpc(new AcpServer(ctx), body));
|
|
8670
|
+
sendJson$1(res, 200, await dispatchRpc(new AcpServer(ctx), body));
|
|
8276
8671
|
return;
|
|
8277
8672
|
}
|
|
8278
8673
|
if (!s.publicAskEnabled) {
|
|
@@ -8281,10 +8676,10 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8281
8676
|
return;
|
|
8282
8677
|
}
|
|
8283
8678
|
if (req.method === "POST" && (url === "/ask" || url.startsWith("/c/"))) {
|
|
8284
|
-
sendJson(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
|
|
8679
|
+
sendJson$1(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
|
|
8285
8680
|
return;
|
|
8286
8681
|
}
|
|
8287
|
-
sendJson(res, 404, { error: "not found" });
|
|
8682
|
+
sendJson$1(res, 404, { error: "not found" });
|
|
8288
8683
|
return;
|
|
8289
8684
|
}
|
|
8290
8685
|
const containerAllowed = (name) => {
|
|
@@ -8315,16 +8710,16 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8315
8710
|
const name = decodeURIComponent(aMatch[1] ?? "");
|
|
8316
8711
|
const ccc = (await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).find((c) => c.name === name);
|
|
8317
8712
|
if (!ccc || !containerAllowed(name)) {
|
|
8318
|
-
sendJson(res, 403, { error: `container "${name}" is not open for public ask` });
|
|
8713
|
+
sendJson$1(res, 403, { error: `container "${name}" is not open for public ask` });
|
|
8319
8714
|
return;
|
|
8320
8715
|
}
|
|
8321
8716
|
let body;
|
|
8322
8717
|
try {
|
|
8323
|
-
const parsed = JSON.parse(await readBody(req));
|
|
8718
|
+
const parsed = JSON.parse(await readBody$1(req));
|
|
8324
8719
|
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
8325
8720
|
body = parsed;
|
|
8326
8721
|
} catch {
|
|
8327
|
-
sendJson(res, 400, { error: "invalid JSON body" });
|
|
8722
|
+
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
8328
8723
|
return;
|
|
8329
8724
|
}
|
|
8330
8725
|
await handleAskParsed(ctx, ccc, body, res, ip);
|
|
@@ -8333,11 +8728,11 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8333
8728
|
if (req.method === "POST" && url === "/ask") {
|
|
8334
8729
|
let body;
|
|
8335
8730
|
try {
|
|
8336
|
-
const parsed = JSON.parse(await readBody(req));
|
|
8731
|
+
const parsed = JSON.parse(await readBody$1(req));
|
|
8337
8732
|
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
8338
8733
|
body = parsed;
|
|
8339
8734
|
} catch {
|
|
8340
|
-
sendJson(res, 400, { error: "invalid JSON body" });
|
|
8735
|
+
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
8341
8736
|
return;
|
|
8342
8737
|
}
|
|
8343
8738
|
const cccValue = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : void 0;
|
|
@@ -8353,15 +8748,15 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8353
8748
|
};
|
|
8354
8749
|
}
|
|
8355
8750
|
if (!ccc || !containerAllowed(ccc.name)) {
|
|
8356
|
-
sendJson(res, 403, { error: "container is not open for public ask" });
|
|
8751
|
+
sendJson$1(res, 403, { error: "container is not open for public ask" });
|
|
8357
8752
|
return;
|
|
8358
8753
|
}
|
|
8359
8754
|
await handleAskParsed(ctx, ccc, body, res, ip);
|
|
8360
8755
|
return;
|
|
8361
8756
|
}
|
|
8362
|
-
sendJson(res, 404, { error: "not found" });
|
|
8757
|
+
sendJson$1(res, 404, { error: "not found" });
|
|
8363
8758
|
} catch (err) {
|
|
8364
|
-
sendJson(res, 500, { error: err?.message ?? String(err) });
|
|
8759
|
+
sendJson$1(res, 500, { error: err?.message ?? String(err) });
|
|
8365
8760
|
}
|
|
8366
8761
|
}
|
|
8367
8762
|
/**
|
|
@@ -8373,12 +8768,12 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8373
8768
|
async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
8374
8769
|
const key = typeof body.key === "string" ? body.key : void 0;
|
|
8375
8770
|
if (isPublicAskIpLocked(ip)) {
|
|
8376
|
-
sendJson(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
|
|
8771
|
+
sendJson$1(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
|
|
8377
8772
|
return;
|
|
8378
8773
|
}
|
|
8379
8774
|
if (!verifyPublicAskKey(key)) {
|
|
8380
8775
|
const remaining = recordPublicAskFail(ip);
|
|
8381
|
-
sendJson(res, remaining > 0 ? 429 : 401, { error: remaining > 0 ? `too many failed attempts — locked for ${Math.ceil(remaining / 6e4)} min` : "invalid or missing key" });
|
|
8776
|
+
sendJson$1(res, remaining > 0 ? 429 : 401, { error: remaining > 0 ? `too many failed attempts — locked for ${Math.ceil(remaining / 6e4)} min` : "invalid or missing key" });
|
|
8382
8777
|
return;
|
|
8383
8778
|
}
|
|
8384
8779
|
resetPublicAskIpFail(ip);
|
|
@@ -8387,7 +8782,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
|
8387
8782
|
const question = typeof body.question === "string" ? body.question : "";
|
|
8388
8783
|
const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
|
|
8389
8784
|
if (!question.trim()) {
|
|
8390
|
-
sendJson(res, 400, { error: "empty question" });
|
|
8785
|
+
sendJson$1(res, 400, { error: "empty question" });
|
|
8391
8786
|
return;
|
|
8392
8787
|
}
|
|
8393
8788
|
const roles = readSkiffRoles(root);
|
|
@@ -8395,7 +8790,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
|
8395
8790
|
const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
|
|
8396
8791
|
const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
|
|
8397
8792
|
if (!effectiveRole || !effectiveCfg) {
|
|
8398
|
-
sendJson(res, 400, { error: `no skiff role available in ccc: ${root}` });
|
|
8793
|
+
sendJson$1(res, 400, { error: `no skiff role available in ccc: ${root}` });
|
|
8399
8794
|
return;
|
|
8400
8795
|
}
|
|
8401
8796
|
let agent;
|
|
@@ -8413,7 +8808,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
|
8413
8808
|
agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, hc?.defaultModel)).agent;
|
|
8414
8809
|
}
|
|
8415
8810
|
const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
|
|
8416
|
-
sendJson(res, 200, {
|
|
8811
|
+
sendJson$1(res, 200, {
|
|
8417
8812
|
answer: result.answer,
|
|
8418
8813
|
answer_html: renderSkiffMarkdown(result.answer, true),
|
|
8419
8814
|
sessionId: result.sessionId,
|
|
@@ -8794,7 +9189,8 @@ function buildOutgoingHookEvent(input) {
|
|
|
8794
9189
|
userId: input.userId,
|
|
8795
9190
|
sessionId: input.sessionId,
|
|
8796
9191
|
role: input.role,
|
|
8797
|
-
reply: input.reply
|
|
9192
|
+
reply: input.reply,
|
|
9193
|
+
...input.source === "proactive" ? { source: "proactive" } : {}
|
|
8798
9194
|
};
|
|
8799
9195
|
}
|
|
8800
9196
|
/**
|
|
@@ -8909,6 +9305,8 @@ function invokeWeixinHook(root, hookRel, event) {
|
|
|
8909
9305
|
var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
|
|
8910
9306
|
handleIncoming: () => handleIncoming,
|
|
8911
9307
|
registerWeixinBridge: () => registerWeixinBridge,
|
|
9308
|
+
sendProactiveText: () => sendProactiveText,
|
|
9309
|
+
stopAllBridges: () => stopAllBridges,
|
|
8912
9310
|
stopCccBridge: () => stopCccBridge,
|
|
8913
9311
|
syncCccBridge: () => syncCccBridge,
|
|
8914
9312
|
weixinBridgeStatus: () => weixinBridgeStatus
|
|
@@ -9037,7 +9435,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
9037
9435
|
const existing = getSkiffAgent(sessionId);
|
|
9038
9436
|
const hc = readHandymanConfig(root);
|
|
9039
9437
|
if (existing) try {
|
|
9040
|
-
ensureSkiffSession(root, existing, roleName, role);
|
|
9438
|
+
ensureSkiffSession(root, existing, roleName, role, true);
|
|
9041
9439
|
} catch (err) {
|
|
9042
9440
|
console.warn(`[serenity-hooks] weixin-bridge ensure 失败(不影响处理): ${String(err?.message ?? err)}`);
|
|
9043
9441
|
}
|
|
@@ -9188,6 +9586,93 @@ function stopCccBridge(root) {
|
|
|
9188
9586
|
for (const loop of bridge.loops.values()) loop.stopped = true;
|
|
9189
9587
|
bridges.delete(root);
|
|
9190
9588
|
}
|
|
9589
|
+
/**
|
|
9590
|
+
* 停止全部桥(插件 dispose)
|
|
9591
|
+
*/
|
|
9592
|
+
function stopAllBridges() {
|
|
9593
|
+
for (const bridge of bridges.values()) for (const loop of bridge.loops.values()) loop.stopped = true;
|
|
9594
|
+
bridges.clear();
|
|
9595
|
+
}
|
|
9596
|
+
/**
|
|
9597
|
+
* 以 bot 身份主动给指定用户发文本(**不经用户消息触发**)。
|
|
9598
|
+
*
|
|
9599
|
+
* 为什么放在桥里而不是让 CCC 自己直连 iLink(用户拍板 A 方案):
|
|
9600
|
+
* ① **记录归桥**——发送成功后触发既有 outgoing hook,`_weixin-logs/*.jsonl` 与对话回复
|
|
9601
|
+
* 同源同格式(事件多一个 `source: 'proactive'` 标记);② 协议/账号/凭据解析单一真相源,
|
|
9602
|
+
* CCC 侧不必重复实现 sendmessage;③ 多账号可选。
|
|
9603
|
+
*
|
|
9604
|
+
* 记录一致性:`sessionId` 取 `weixinSessionIdFor(toUserId)`——与该用户平时对话**同一条轨迹**,
|
|
9605
|
+
* 记录里能直接按会话串联;`role` 取该 CCC 路由命中角色(未命中 → 空串)。
|
|
9606
|
+
*
|
|
9607
|
+
* 主动消息不带 context_token(iLink 接受 bot 主动发起;带 token 的路径见 handleIncoming 回复)。
|
|
9608
|
+
* @param input CCC 根 + 目标用户 + 文本 +(可选)账号
|
|
9609
|
+
* @returns 成功含 accountId/userId/sessionId/role;失败含稳定 code 与可行动提示(不抛错)
|
|
9610
|
+
*/
|
|
9611
|
+
async function sendProactiveText(input) {
|
|
9612
|
+
const { root, toUserId } = input;
|
|
9613
|
+
const settings = readWeixinSettings(root);
|
|
9614
|
+
if (!settings.enabled) return {
|
|
9615
|
+
ok: false,
|
|
9616
|
+
code: "BRIDGE_DISABLED",
|
|
9617
|
+
error: `微信桥未启用(ccc=${root})`,
|
|
9618
|
+
remediation: "在 CCC 的 .opencode/serenity.json weixin.enabled 打开"
|
|
9619
|
+
};
|
|
9620
|
+
const accounts = (settings.accounts ?? []).filter((a) => a.enabled !== false);
|
|
9621
|
+
if (accounts.length === 0) return {
|
|
9622
|
+
ok: false,
|
|
9623
|
+
code: "NO_ACCOUNT",
|
|
9624
|
+
error: "微信桥未配置任何启用账号",
|
|
9625
|
+
remediation: "在设置面板「微信桥」扫码绑定账号"
|
|
9626
|
+
};
|
|
9627
|
+
const accountId = input.accountId ?? accounts[0].accountId;
|
|
9628
|
+
if (!accounts.some((a) => a.accountId === accountId)) return {
|
|
9629
|
+
ok: false,
|
|
9630
|
+
code: "ACCOUNT_NOT_FOUND",
|
|
9631
|
+
error: `账号不存在或未启用: ${accountId}`,
|
|
9632
|
+
remediation: `可用账号: ${accounts.map((a) => a.accountId).join(", ")}`
|
|
9633
|
+
};
|
|
9634
|
+
const cred = readWeixinCredential(root, accountId);
|
|
9635
|
+
if (!cred) return {
|
|
9636
|
+
ok: false,
|
|
9637
|
+
code: "ACCOUNT_NOT_BOUND",
|
|
9638
|
+
error: `账号未绑定(无凭据): ${accountId}`,
|
|
9639
|
+
remediation: "在设置面板重新扫码绑定"
|
|
9640
|
+
};
|
|
9641
|
+
const sessionId = weixinSessionIdFor(toUserId);
|
|
9642
|
+
const role = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
|
|
9643
|
+
try {
|
|
9644
|
+
await sendTextMessage({
|
|
9645
|
+
baseUrl: cred.baseUrl,
|
|
9646
|
+
token: cred.token,
|
|
9647
|
+
toUserId,
|
|
9648
|
+
text: input.text
|
|
9649
|
+
});
|
|
9650
|
+
} catch (err) {
|
|
9651
|
+
return {
|
|
9652
|
+
ok: false,
|
|
9653
|
+
code: "SEND_FAILED",
|
|
9654
|
+
error: `发送失败: ${err instanceof Error ? err.message : String(err)}`
|
|
9655
|
+
};
|
|
9656
|
+
}
|
|
9657
|
+
if (settings.hook) invokeWeixinHook(root, settings.hook, buildOutgoingHookEvent({
|
|
9658
|
+
cccRoot: root,
|
|
9659
|
+
accountId,
|
|
9660
|
+
userId: toUserId,
|
|
9661
|
+
sessionId,
|
|
9662
|
+
role,
|
|
9663
|
+
reply: markdownToPlainText(input.text),
|
|
9664
|
+
source: "proactive"
|
|
9665
|
+
})).catch((err) => {
|
|
9666
|
+
console.log(`[serenity-hooks] weixin hook outgoing(proactive) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
9667
|
+
});
|
|
9668
|
+
return {
|
|
9669
|
+
ok: true,
|
|
9670
|
+
accountId,
|
|
9671
|
+
userId: toUserId,
|
|
9672
|
+
sessionId,
|
|
9673
|
+
role
|
|
9674
|
+
};
|
|
9675
|
+
}
|
|
9191
9676
|
/** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
|
|
9192
9677
|
function weixinBridgeStatus() {
|
|
9193
9678
|
const out = [];
|
|
@@ -9208,7 +9693,7 @@ function weixinBridgeStatus() {
|
|
|
9208
9693
|
function registerWeixinBridge(ctx) {
|
|
9209
9694
|
const syncFromLive = () => {
|
|
9210
9695
|
try {
|
|
9211
|
-
const cwds = (ctx
|
|
9696
|
+
const cwds = (hostSessions(ctx)?.list?.() ?? []).map((s) => s.header?.cwd ?? "").filter(Boolean);
|
|
9212
9697
|
const roots = [...new Set(cwds.map((c) => findSerenityRoot(c))).values()].filter((r) => r !== null);
|
|
9213
9698
|
for (const root of roots) syncCccBridge(ctx, root);
|
|
9214
9699
|
} catch {}
|
|
@@ -9219,6 +9704,410 @@ function registerWeixinBridge(ctx) {
|
|
|
9219
9704
|
} catch {}
|
|
9220
9705
|
}
|
|
9221
9706
|
//#endregion
|
|
9707
|
+
//#region src/weixin-send-api.ts
|
|
9708
|
+
/**
|
|
9709
|
+
* weixin-send-api.ts — 主动发送入口(S142 v1.30.9,用户需求"微信桥支持被调用发消息给指定用户")
|
|
9710
|
+
*
|
|
9711
|
+
* 形态(用户拍板 A2):**独立监听器,只绑 127.0.0.1**(默认 3082,plugin 全局配置 weixinApi)。
|
|
9712
|
+
* 为什么不挂在主 webServer(3080) 上:3080 会被 3081 公网网关原样反代(含全部请求头),
|
|
9713
|
+
* 而 3081 的登录态属于"家庭账号"——一旦挂上去,任何已登录的外部用户都能冒充 bot 发消息。
|
|
9714
|
+
* 独立 loopback 监听器则**不经网关、公网不可达**,因此不做共享密钥;仍校验来源地址。
|
|
9715
|
+
*
|
|
9716
|
+
* 调用者 = CCC 自己的 MSM(`msm weixin-send ...`):ACC **不新增工具**,只多一个本机 HTTP 面。
|
|
9717
|
+
* 发送与记录都经 `weixin-bridge.sendProactiveText` → 复用既有 outgoing hook(`source: 'proactive'`)。
|
|
9718
|
+
*
|
|
9719
|
+
* 契约:
|
|
9720
|
+
* POST /send { ccc: "<名称|绝对路径>", user: "<from_user_id>", text: "...", accountId?: "wechat-1" }
|
|
9721
|
+
* → 200 { ok: true, accountId, userId, sessionId, role }
|
|
9722
|
+
* → 4xx/5xx { ok: false, code, error, remediation? }
|
|
9723
|
+
* GET /health → 200 { ok: true, port }
|
|
9724
|
+
*
|
|
9725
|
+
* 职责切分(E↑):**别名 → id 的解析归 CCC**(MSM 读该 CCC localstore 的 WEIXIN_USER_*);
|
|
9726
|
+
* 本入口只接受 iLink 形式的 id。协议、账号选择、记录归桥。
|
|
9727
|
+
*/
|
|
9728
|
+
/** 请求体上限(文本消息远小于此;防滥用) */
|
|
9729
|
+
const MAX_BODY_BYTES = 65536;
|
|
9730
|
+
/** 文本长度上限(微信侧单条消息实践上限;超限拒绝而非静默截断) */
|
|
9731
|
+
const WEIXIN_SEND_MAX_TEXT = 4e3;
|
|
9732
|
+
/** 运行中的入口(null = 未启动) */
|
|
9733
|
+
let active = null;
|
|
9734
|
+
/** 当前入口地址(MSM 环境变量注入用;未启动 → null)——状态在叶模块(msm-ops 零依赖读取) */
|
|
9735
|
+
/** 测试辅助:读取当前监听端口(生产零调用) */
|
|
9736
|
+
function weixinSendPort() {
|
|
9737
|
+
return active?.port ?? null;
|
|
9738
|
+
}
|
|
9739
|
+
/** 来源必须是 loopback(127.0.0.1 / ::1 / IPv4-mapped) */
|
|
9740
|
+
function isLoopbackAddress(address) {
|
|
9741
|
+
if (typeof address !== "string" || address === "") return false;
|
|
9742
|
+
const a = address.startsWith("::ffff:") ? address.slice(7) : address;
|
|
9743
|
+
return a === "127.0.0.1" || a === "::1";
|
|
9744
|
+
}
|
|
9745
|
+
/**
|
|
9746
|
+
* 校验并归一 /send 请求体(纯函数,可测)。
|
|
9747
|
+
* 只做**结构**校验:ccc 必填(无隐式当前 CCC——用户拍板)、user 必须是 iLink id 形式、text 非空且不超限。
|
|
9748
|
+
*/
|
|
9749
|
+
function parseSendRequest(body) {
|
|
9750
|
+
if (body === null || typeof body !== "object") return {
|
|
9751
|
+
ok: false,
|
|
9752
|
+
error: "body must be a JSON object"
|
|
9753
|
+
};
|
|
9754
|
+
const o = body;
|
|
9755
|
+
const ccc = typeof o.ccc === "string" ? o.ccc.trim() : "";
|
|
9756
|
+
if (ccc === "") return {
|
|
9757
|
+
ok: false,
|
|
9758
|
+
error: "missing \"ccc\"",
|
|
9759
|
+
remediation: "ccc = CCC 名称或绝对路径(必填,无默认值)"
|
|
9760
|
+
};
|
|
9761
|
+
const user = typeof o.user === "string" ? o.user.trim() : "";
|
|
9762
|
+
if (user === "") return {
|
|
9763
|
+
ok: false,
|
|
9764
|
+
error: "missing \"user\"",
|
|
9765
|
+
remediation: "user = iLink from_user_id(形如 xxx@im.wechat);别名解析归调用方"
|
|
9766
|
+
};
|
|
9767
|
+
if (!user.includes("@")) return {
|
|
9768
|
+
ok: false,
|
|
9769
|
+
error: `invalid "user": ${user}`,
|
|
9770
|
+
remediation: "需为 iLink from_user_id(含 @);别名(yh/danica)请在调用方解析"
|
|
9771
|
+
};
|
|
9772
|
+
const text = typeof o.text === "string" ? o.text.trim() : "";
|
|
9773
|
+
if (text === "") return {
|
|
9774
|
+
ok: false,
|
|
9775
|
+
error: "missing \"text\"",
|
|
9776
|
+
remediation: "text = 要发送的文本(非空)"
|
|
9777
|
+
};
|
|
9778
|
+
if (text.length > 4e3) return {
|
|
9779
|
+
ok: false,
|
|
9780
|
+
error: `text too long: ${text.length} > ${WEIXIN_SEND_MAX_TEXT}`
|
|
9781
|
+
};
|
|
9782
|
+
const accountId = typeof o.accountId === "string" && o.accountId.trim() !== "" ? o.accountId.trim() : void 0;
|
|
9783
|
+
return {
|
|
9784
|
+
ok: true,
|
|
9785
|
+
ccc,
|
|
9786
|
+
user,
|
|
9787
|
+
text,
|
|
9788
|
+
...accountId ? { accountId } : {}
|
|
9789
|
+
};
|
|
9790
|
+
}
|
|
9791
|
+
/**
|
|
9792
|
+
* 按名称/路径解析 CCC 根(纯函数,可测)。
|
|
9793
|
+
* 匹配顺序:① 绝对路径精确/上溯 ② 目录名 ③ `.serenity` 首行名(大小写不敏感)。
|
|
9794
|
+
* 歧义(多个同名)→ 报错并列出候选(不猜——用户拍板"ccc 必须指定")。
|
|
9795
|
+
*/
|
|
9796
|
+
function matchCcc(input, candidates) {
|
|
9797
|
+
const trimmed = input.trim();
|
|
9798
|
+
if (trimmed === "") return {
|
|
9799
|
+
ok: false,
|
|
9800
|
+
error: "empty ccc"
|
|
9801
|
+
};
|
|
9802
|
+
if (trimmed.startsWith("/")) {
|
|
9803
|
+
const root = findSerenityRoot(trimmed);
|
|
9804
|
+
if (!root) return {
|
|
9805
|
+
ok: false,
|
|
9806
|
+
error: `no CCC found from path: ${trimmed}`,
|
|
9807
|
+
remediation: "确认该路径在某个 CCC 内(含 .serenity 标记)"
|
|
9808
|
+
};
|
|
9809
|
+
return {
|
|
9810
|
+
ok: true,
|
|
9811
|
+
root
|
|
9812
|
+
};
|
|
9813
|
+
}
|
|
9814
|
+
const lower = trimmed.toLowerCase();
|
|
9815
|
+
const exactPath = candidates.find((c) => c.root.toLowerCase() === lower);
|
|
9816
|
+
if (exactPath) return {
|
|
9817
|
+
ok: true,
|
|
9818
|
+
root: exactPath.root
|
|
9819
|
+
};
|
|
9820
|
+
const byName = candidates.filter((c) => c.dirName.toLowerCase() === lower || c.cccName !== null && c.cccName.toLowerCase() === lower);
|
|
9821
|
+
if (byName.length === 1) return {
|
|
9822
|
+
ok: true,
|
|
9823
|
+
root: byName[0].root
|
|
9824
|
+
};
|
|
9825
|
+
if (byName.length > 1) return {
|
|
9826
|
+
ok: false,
|
|
9827
|
+
error: `ambiguous ccc name: ${trimmed}`,
|
|
9828
|
+
remediation: `同名多个,请用绝对路径:${byName.map((c) => c.root).join(" | ")}`
|
|
9829
|
+
};
|
|
9830
|
+
return {
|
|
9831
|
+
ok: false,
|
|
9832
|
+
error: `unknown ccc: ${trimmed}`,
|
|
9833
|
+
remediation: candidates.length === 0 ? "当前进程未发现任何 CCC(打开一个 CCC 会话后重试)" : `已知 CCC: ${candidates.map((c) => c.dirName).join(", ")}`
|
|
9834
|
+
};
|
|
9835
|
+
}
|
|
9836
|
+
/** 组装候选列表(discoverCccs 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
9837
|
+
async function collectCandidates(ctx) {
|
|
9838
|
+
const { discoverCccs } = await import("./skiff-debug-TONJlpgF.js").then((n) => n.i);
|
|
9839
|
+
const entries = await discoverCccs(ctx, process.cwd());
|
|
9840
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9841
|
+
const out = [];
|
|
9842
|
+
for (const e of entries) {
|
|
9843
|
+
if (!e?.root || seen.has(e.root)) continue;
|
|
9844
|
+
seen.add(e.root);
|
|
9845
|
+
out.push({
|
|
9846
|
+
root: e.root,
|
|
9847
|
+
dirName: e.name || basename(e.root),
|
|
9848
|
+
cccName: readCccName$2(e.root)
|
|
9849
|
+
});
|
|
9850
|
+
}
|
|
9851
|
+
return out;
|
|
9852
|
+
}
|
|
9853
|
+
function sendJson(res, status, payload) {
|
|
9854
|
+
const body = JSON.stringify(payload);
|
|
9855
|
+
res.writeHead(status, {
|
|
9856
|
+
"content-type": "application/json; charset=utf-8",
|
|
9857
|
+
"content-length": Buffer.byteLength(body)
|
|
9858
|
+
});
|
|
9859
|
+
res.end(body);
|
|
9860
|
+
}
|
|
9861
|
+
async function readBody(req) {
|
|
9862
|
+
const chunks = [];
|
|
9863
|
+
let total = 0;
|
|
9864
|
+
for await (const chunk of req) {
|
|
9865
|
+
const buf = chunk;
|
|
9866
|
+
total += buf.length;
|
|
9867
|
+
if (total > MAX_BODY_BYTES) throw new Error(`body too large (> ${MAX_BODY_BYTES} bytes)`);
|
|
9868
|
+
chunks.push(buf);
|
|
9869
|
+
}
|
|
9870
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
9871
|
+
}
|
|
9872
|
+
/** HTTP 状态映射(失败 code → 语义化状态;调用方按 code 判定,不看文案) */
|
|
9873
|
+
function statusForCode(code) {
|
|
9874
|
+
switch (code) {
|
|
9875
|
+
case "ACCOUNT_NOT_BOUND":
|
|
9876
|
+
case "BRIDGE_DISABLED":
|
|
9877
|
+
case "NO_ACCOUNT": return 409;
|
|
9878
|
+
case "ACCOUNT_NOT_FOUND": return 400;
|
|
9879
|
+
case "SEND_FAILED": return 502;
|
|
9880
|
+
default: return 400;
|
|
9881
|
+
}
|
|
9882
|
+
}
|
|
9883
|
+
/** 单请求处理(导出供测试直接驱动,不经真实端口) */
|
|
9884
|
+
async function handleWeixinSendRequest(ctx, req, res) {
|
|
9885
|
+
if (!isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
9886
|
+
sendJson(res, 403, {
|
|
9887
|
+
ok: false,
|
|
9888
|
+
code: "FORBIDDEN",
|
|
9889
|
+
error: "only loopback clients allowed"
|
|
9890
|
+
});
|
|
9891
|
+
return;
|
|
9892
|
+
}
|
|
9893
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
9894
|
+
if (req.method === "GET" && path === "/health") {
|
|
9895
|
+
sendJson(res, 200, {
|
|
9896
|
+
ok: true,
|
|
9897
|
+
port: weixinSendPort()
|
|
9898
|
+
});
|
|
9899
|
+
return;
|
|
9900
|
+
}
|
|
9901
|
+
if (path !== "/send") {
|
|
9902
|
+
sendJson(res, 404, {
|
|
9903
|
+
ok: false,
|
|
9904
|
+
code: "NOT_FOUND",
|
|
9905
|
+
error: `unknown path: ${path}`
|
|
9906
|
+
});
|
|
9907
|
+
return;
|
|
9908
|
+
}
|
|
9909
|
+
if (req.method !== "POST") {
|
|
9910
|
+
sendJson(res, 405, {
|
|
9911
|
+
ok: false,
|
|
9912
|
+
code: "METHOD_NOT_ALLOWED",
|
|
9913
|
+
error: "use POST /send"
|
|
9914
|
+
});
|
|
9915
|
+
return;
|
|
9916
|
+
}
|
|
9917
|
+
let raw;
|
|
9918
|
+
try {
|
|
9919
|
+
raw = await readBody(req);
|
|
9920
|
+
} catch (err) {
|
|
9921
|
+
sendJson(res, 413, {
|
|
9922
|
+
ok: false,
|
|
9923
|
+
code: "BODY_TOO_LARGE",
|
|
9924
|
+
error: String(err?.message ?? err)
|
|
9925
|
+
});
|
|
9926
|
+
return;
|
|
9927
|
+
}
|
|
9928
|
+
let body;
|
|
9929
|
+
try {
|
|
9930
|
+
body = JSON.parse(raw === "" ? "{}" : raw);
|
|
9931
|
+
} catch (err) {
|
|
9932
|
+
sendJson(res, 400, {
|
|
9933
|
+
ok: false,
|
|
9934
|
+
code: "BAD_JSON",
|
|
9935
|
+
error: `invalid JSON: ${String(err?.message ?? err)}`
|
|
9936
|
+
});
|
|
9937
|
+
return;
|
|
9938
|
+
}
|
|
9939
|
+
const parsed = parseSendRequest(body);
|
|
9940
|
+
if (!parsed.ok) {
|
|
9941
|
+
sendJson(res, 400, {
|
|
9942
|
+
ok: false,
|
|
9943
|
+
code: "BAD_REQUEST",
|
|
9944
|
+
error: parsed.error,
|
|
9945
|
+
remediation: parsed.remediation
|
|
9946
|
+
});
|
|
9947
|
+
return;
|
|
9948
|
+
}
|
|
9949
|
+
let root;
|
|
9950
|
+
try {
|
|
9951
|
+
const matched = matchCcc(parsed.ccc, await collectCandidates(ctx));
|
|
9952
|
+
if (!matched.ok) {
|
|
9953
|
+
sendJson(res, 404, {
|
|
9954
|
+
ok: false,
|
|
9955
|
+
code: "UNKNOWN_CCC",
|
|
9956
|
+
error: matched.error,
|
|
9957
|
+
remediation: matched.remediation
|
|
9958
|
+
});
|
|
9959
|
+
return;
|
|
9960
|
+
}
|
|
9961
|
+
root = matched.root;
|
|
9962
|
+
} catch (err) {
|
|
9963
|
+
sendJson(res, 500, {
|
|
9964
|
+
ok: false,
|
|
9965
|
+
code: "CCC_DISCOVERY_FAILED",
|
|
9966
|
+
error: String(err?.message ?? err)
|
|
9967
|
+
});
|
|
9968
|
+
return;
|
|
9969
|
+
}
|
|
9970
|
+
const result = await sendProactiveText({
|
|
9971
|
+
root,
|
|
9972
|
+
toUserId: parsed.user,
|
|
9973
|
+
text: parsed.text,
|
|
9974
|
+
...parsed.accountId ? { accountId: parsed.accountId } : {}
|
|
9975
|
+
});
|
|
9976
|
+
if (result.ok) {
|
|
9977
|
+
sendJson(res, 200, {
|
|
9978
|
+
ok: true,
|
|
9979
|
+
accountId: result.accountId,
|
|
9980
|
+
userId: result.userId,
|
|
9981
|
+
sessionId: result.sessionId,
|
|
9982
|
+
role: result.role
|
|
9983
|
+
});
|
|
9984
|
+
return;
|
|
9985
|
+
}
|
|
9986
|
+
sendJson(res, statusForCode(result.code), {
|
|
9987
|
+
ok: false,
|
|
9988
|
+
code: result.code,
|
|
9989
|
+
error: result.error,
|
|
9990
|
+
remediation: result.remediation
|
|
9991
|
+
});
|
|
9992
|
+
}
|
|
9993
|
+
/** 启动监听器(仅 127.0.0.1);已启动 → 幂等返回 */
|
|
9994
|
+
async function startWeixinSendApi(ctx, port) {
|
|
9995
|
+
if (active) return active.port;
|
|
9996
|
+
const server = createServer((req, res) => {
|
|
9997
|
+
handleWeixinSendRequest(ctx, req, res).catch((err) => {
|
|
9998
|
+
console.warn(`[serenity-hooks] ✗ weixin send api 处理失败: ${String(err?.message ?? err)}`);
|
|
9999
|
+
try {
|
|
10000
|
+
sendJson(res, 500, {
|
|
10001
|
+
ok: false,
|
|
10002
|
+
code: "INTERNAL",
|
|
10003
|
+
error: "internal error"
|
|
10004
|
+
});
|
|
10005
|
+
} catch {}
|
|
10006
|
+
});
|
|
10007
|
+
});
|
|
10008
|
+
await new Promise((resolve, reject) => {
|
|
10009
|
+
server.once("error", reject);
|
|
10010
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
10011
|
+
});
|
|
10012
|
+
server.unref();
|
|
10013
|
+
const addr = server.address();
|
|
10014
|
+
const actualPort = typeof addr === "object" && addr !== null ? addr.port : port;
|
|
10015
|
+
active = {
|
|
10016
|
+
server,
|
|
10017
|
+
port: actualPort
|
|
10018
|
+
};
|
|
10019
|
+
setWeixinSendEndpoint(`http://127.0.0.1:${actualPort}`);
|
|
10020
|
+
console.log(`[serenity-hooks] ✓ 微信主动发送入口: http://127.0.0.1:${actualPort}/send(仅 loopback;CCC MSM 调用)`);
|
|
10021
|
+
return actualPort;
|
|
10022
|
+
}
|
|
10023
|
+
/** 停止监听器(幂等) */
|
|
10024
|
+
function stopWeixinSendApi() {
|
|
10025
|
+
if (!active) return;
|
|
10026
|
+
try {
|
|
10027
|
+
active.server.close();
|
|
10028
|
+
} catch {}
|
|
10029
|
+
active = null;
|
|
10030
|
+
setWeixinSendEndpoint(null);
|
|
10031
|
+
}
|
|
10032
|
+
/**
|
|
10033
|
+
* 装配(index.ts apply 调用):按 plugin 全局配置 weixinApi 启停,配置变化热同步。
|
|
10034
|
+
* 默认启用(仅 loopback,零公网暴露);`enabled:false` 或 `port:0` 关闭。
|
|
10035
|
+
*/
|
|
10036
|
+
function registerWeixinSendApi(ctx) {
|
|
10037
|
+
const sync = () => {
|
|
10038
|
+
let cfg;
|
|
10039
|
+
try {
|
|
10040
|
+
cfg = readAdvancedSettings().weixinApi;
|
|
10041
|
+
} catch (err) {
|
|
10042
|
+
console.warn(`[serenity-hooks] ✗ weixin send api 配置读取失败,跳过同步: ${String(err?.message ?? err)}`);
|
|
10043
|
+
return;
|
|
10044
|
+
}
|
|
10045
|
+
const want = cfg.enabled && cfg.port > 0;
|
|
10046
|
+
if (want && !active) {
|
|
10047
|
+
startWeixinSendApi(ctx, cfg.port).catch((err) => {
|
|
10048
|
+
console.warn(`[serenity-hooks] ✗ 微信主动发送入口启动失败(port=${cfg.port}): ${String(err?.message ?? err)}`);
|
|
10049
|
+
});
|
|
10050
|
+
return;
|
|
10051
|
+
}
|
|
10052
|
+
if (!want && active) stopWeixinSendApi();
|
|
10053
|
+
};
|
|
10054
|
+
try {
|
|
10055
|
+
ctx.on("serenity/config-updated", sync);
|
|
10056
|
+
} catch {}
|
|
10057
|
+
try {
|
|
10058
|
+
ctx.on("serenity/settings-changed", sync);
|
|
10059
|
+
} catch {}
|
|
10060
|
+
sync();
|
|
10061
|
+
registerDisposer(ctx, "weixin send api", () => stopWeixinSendApi());
|
|
10062
|
+
}
|
|
10063
|
+
//#endregion
|
|
10064
|
+
//#region src/seams/lifecycle.ts
|
|
10065
|
+
/** 从 Agent / Session / 事件负载中取会话 id(形状宽容:未知结构返回 null) */
|
|
10066
|
+
function sessionIdOf(value) {
|
|
10067
|
+
const v = value;
|
|
10068
|
+
const id = v?.session?.id ?? v?.id ?? v?.header?.id;
|
|
10069
|
+
return typeof id === "string" && id !== "" ? id : null;
|
|
10070
|
+
}
|
|
10071
|
+
/** 单会话内存态清理(幂等;无对应状态即 no-op) */
|
|
10072
|
+
function cleanupSessionState(sessionId) {
|
|
10073
|
+
try {
|
|
10074
|
+
unregisterSkiffSession(sessionId);
|
|
10075
|
+
} catch {}
|
|
10076
|
+
try {
|
|
10077
|
+
clearActiveSessionInfo(sessionId);
|
|
10078
|
+
} catch {}
|
|
10079
|
+
}
|
|
10080
|
+
/**
|
|
10081
|
+
* 装配生命周期订阅与资源拆卸。
|
|
10082
|
+
* @param ctx 宿主插件上下文
|
|
10083
|
+
*/
|
|
10084
|
+
function registerLifecycle(ctx) {
|
|
10085
|
+
try {
|
|
10086
|
+
ctx.on("agent/disposed", (payload) => {
|
|
10087
|
+
const id = sessionIdOf(payload?.agent);
|
|
10088
|
+
if (id) cleanupSessionState(id);
|
|
10089
|
+
});
|
|
10090
|
+
} catch {}
|
|
10091
|
+
try {
|
|
10092
|
+
ctx.on("session/disposed", (session) => {
|
|
10093
|
+
const id = sessionIdOf(session);
|
|
10094
|
+
if (id) cleanupSessionState(id);
|
|
10095
|
+
});
|
|
10096
|
+
} catch {}
|
|
10097
|
+
const disposeAll = () => {
|
|
10098
|
+
try {
|
|
10099
|
+
stopSkiffDebugServer();
|
|
10100
|
+
} catch {}
|
|
10101
|
+
try {
|
|
10102
|
+
stopAcpHttpServer();
|
|
10103
|
+
} catch {}
|
|
10104
|
+
try {
|
|
10105
|
+
stopAllBridges();
|
|
10106
|
+
} catch {}
|
|
10107
|
+
};
|
|
10108
|
+
registerDisposer(ctx, "self-started resources (skiff-debug/acp/weixin)", disposeAll);
|
|
10109
|
+
}
|
|
10110
|
+
//#endregion
|
|
9222
10111
|
//#region src/index.ts
|
|
9223
10112
|
const name = "dsh-serenity-hooks";
|
|
9224
10113
|
/** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
|
|
@@ -9262,10 +10151,14 @@ const Config = z.object({
|
|
|
9262
10151
|
})
|
|
9263
10152
|
});
|
|
9264
10153
|
function apply(ctx, config) {
|
|
10154
|
+
try {
|
|
10155
|
+
const report = probeHostContract(ctx, readDshVersion());
|
|
10156
|
+
if (report.issues.length > 0) console.warn(`[serenity-hooks] ${summarizeHostContract(report)}`);
|
|
10157
|
+
} catch {}
|
|
9265
10158
|
if (config.tools) {
|
|
9266
10159
|
ctx.tools.register(ccFsTool);
|
|
9267
10160
|
ctx.tools.register(createSessionTool(ctx));
|
|
9268
|
-
ctx.tools.register(
|
|
10161
|
+
ctx.tools.register(createKitTool(ctx));
|
|
9269
10162
|
ctx.tools.register(gitTool);
|
|
9270
10163
|
ctx.tools.register(msmTool);
|
|
9271
10164
|
ctx.tools.register(praxisTool);
|
|
@@ -9300,6 +10193,8 @@ function apply(ctx, config) {
|
|
|
9300
10193
|
registerAcp(ctx);
|
|
9301
10194
|
registerAutopilot(ctx);
|
|
9302
10195
|
registerWeixinBridge(ctx);
|
|
10196
|
+
registerWeixinSendApi(ctx);
|
|
10197
|
+
registerLifecycle(ctx);
|
|
9303
10198
|
}
|
|
9304
10199
|
/**
|
|
9305
10200
|
* F4 Skiff 调试服务装配:启停 = 人工(设置面板 Skiff 区块开关,settings 持久化)。
|
|
@@ -9341,7 +10236,7 @@ function registerSkiff(ctx) {
|
|
|
9341
10236
|
function resolveSkiffRoot(ctx) {
|
|
9342
10237
|
const liveRoots = [];
|
|
9343
10238
|
try {
|
|
9344
|
-
const sessions = ctx
|
|
10239
|
+
const sessions = hostSessions(ctx);
|
|
9345
10240
|
for (const s of sessions?.list?.() ?? []) {
|
|
9346
10241
|
const cwd = s?.header?.cwd;
|
|
9347
10242
|
if (typeof cwd === "string") {
|
|
@@ -9358,7 +10253,7 @@ function resolveSkiffRoot(ctx) {
|
|
|
9358
10253
|
/** 主 WebUI 端口(WebUI 链接;webServer 未装配回退 3080) */
|
|
9359
10254
|
function readWebPort(ctx) {
|
|
9360
10255
|
try {
|
|
9361
|
-
const ws = ctx
|
|
10256
|
+
const ws = hostWebServer(ctx);
|
|
9362
10257
|
return typeof ws?.port === "number" ? ws.port : 3080;
|
|
9363
10258
|
} catch {
|
|
9364
10259
|
return 3080;
|