@shgroup/dsh-serenity-hooks 1.57.0 → 1.57.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -9
- package/dsh.plugin.json +1 -1
- package/lib/{ccc-roots-BC8mn8iM.js → ccc-roots-GIJ3A_tw.js} +79 -2
- package/lib/ccc-roots.d.ts +2 -1
- package/lib/client.js +43 -379
- package/lib/config-ops.d.ts +3 -34
- package/lib/face-host.d.ts +7 -5
- package/lib/index.d.ts +0 -31
- package/lib/index.js +114 -1046
- package/lib/{message-source-BcCRsEOh.js → message-source-B4P3Uw6D.js} +1 -1
- package/lib/output-guard-seam.d.ts +9 -2
- package/lib/ports.d.ts +6 -6
- package/lib/seams/lifecycle.d.ts +3 -3
- package/lib/{settings-section-xHABl8nk.js → settings-section-DQIVsTqE.js} +45 -30
- package/lib/settings-section.d.ts +3 -16
- package/lib/skiff-core.d.ts +6 -5
- package/lib/skiff-debug.d.ts +5 -4
- package/lib/skiff-role.d.ts +1 -1
- package/lib/tools/skiff-admin.d.ts +1 -1
- package/lib/unattended-ops.d.ts +3 -1
- package/lib/weixin-bridge.d.ts +9 -2
- package/lib/weixin-output-guard.d.ts +1 -1
- package/package.json +1 -1
- package/lib/access-CNccNtgf.js +0 -79
- package/lib/acp-core.d.ts +0 -80
- package/lib/acp-http.d.ts +0 -37
- package/lib/skiff-registry-BjIX_KD1.js +0 -43
package/lib/index.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
|
-
import {
|
|
2
|
+
import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-GIJ3A_tw.js";
|
|
3
3
|
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readTrajectorySkills, i as findGitRoot, l as pathInside, m as readSafeModeHiddenTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readExclusiveTools, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-B0ZGL9lx.js";
|
|
4
|
-
import { i as
|
|
5
|
-
import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-BcCRsEOh.js";
|
|
4
|
+
import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-B4P3Uw6D.js";
|
|
6
5
|
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-B9gU1YXz.js";
|
|
7
|
-
import { a as
|
|
6
|
+
import { a as GATEWAY_PORT, c as SKIFF_DEBUG_PORT, d as skiffRoleFor$1, f as skiffSessionInfo$1, i as FACE_PORTS, l as WEIXIN_SEND_PORT, m as ACC_VERSION, n as readSimpleSettings, o as MAIN_WEB_PORT, p as unregisterSkiffSession$1, r as registerSettingsSection, s as MECHANISM_PORTS, u as registerSkiffSession$1 } from "./settings-section-DQIVsTqE.js";
|
|
8
7
|
import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-B615L9MV.js";
|
|
9
|
-
import { a as ACP_HTTP_PORT, c as MAIN_WEB_PORT, d as WEIXIN_SEND_PORT, l as MECHANISM_PORTS, n as readSimpleSettings, o as FACE_PORTS, r as registerSettingsSection, s as GATEWAY_PORT, u as SKIFF_DEBUG_PORT } from "./settings-section-xHABl8nk.js";
|
|
10
8
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
11
9
|
import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-BrRcPiQs.js";
|
|
12
10
|
import { createRequire } from "node:module";
|
|
@@ -1086,7 +1084,8 @@ Directory of ACC capabilities — each area lists where to go for details (guide
|
|
|
1086
1084
|
cheap-worker(杂工 agent 编排——guide 子命令:cheap-worker guide)
|
|
1087
1085
|
专属工具(**默认对所有 CCC 隐藏**,只有在自己 exclusiveTools 里点名的 CCC 可见)→ acc-diag(运行态诊断)
|
|
1088
1086
|
④ 角色与对外面 → container_admin role(Skiff 认知子集角色:guide/validate/apply/list——skiff_admin 已并入)
|
|
1089
|
-
|
|
1087
|
+
3099 Skiff 调试问答页(人工调试面)。🔴 2026-10-15:自造 ACP 面已退役
|
|
1088
|
+
⇒ 外部程序化接入改走官方 dsh --profile acp + 现成 ACP 客户端
|
|
1090
1089
|
⑤ 自主与接入 → container_trajectory send-now(跨轨迹的**即时**递送,**不排队**:live 目标在跑就当场注入当前轮、空闲则立即起轮;非 live 冷载入——等效于直接 wake;**有同步回执**)
|
|
1091
1090
|
container_trajectory send-later(跨轨迹的"未来时刻 + 一条 message";fire-and-forget,无回执无回收)
|
|
1092
1091
|
weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:msm("weixin-doctor", ["guide"]))
|
|
@@ -2943,10 +2942,6 @@ function defaultAdvancedSettings() {
|
|
|
2943
2942
|
mode: "",
|
|
2944
2943
|
overrideText: ""
|
|
2945
2944
|
},
|
|
2946
|
-
publicAsk: {
|
|
2947
|
-
key: "",
|
|
2948
|
-
allowed: []
|
|
2949
|
-
},
|
|
2950
2945
|
weixinApi: {
|
|
2951
2946
|
enabled: true,
|
|
2952
2947
|
port: WEIXIN_SEND_PORT
|
|
@@ -2999,7 +2994,6 @@ function mergeWithDefaults(raw) {
|
|
|
2999
2994
|
const o = raw;
|
|
3000
2995
|
const gateway = o.gateway ?? {};
|
|
3001
2996
|
const persona = o.persona ?? {};
|
|
3002
|
-
const publicAsk = o.publicAsk ?? {};
|
|
3003
2997
|
const weixinApi = o.weixinApi ?? {};
|
|
3004
2998
|
return {
|
|
3005
2999
|
gateway: {
|
|
@@ -3016,10 +3010,6 @@ function mergeWithDefaults(raw) {
|
|
|
3016
3010
|
mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
|
|
3017
3011
|
overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
|
|
3018
3012
|
},
|
|
3019
|
-
publicAsk: {
|
|
3020
|
-
key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
|
|
3021
|
-
allowed: Array.isArray(publicAsk.allowed) ? publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : def.publicAsk.allowed
|
|
3022
|
-
},
|
|
3023
3013
|
weixinApi: {
|
|
3024
3014
|
enabled: typeof weixinApi.enabled === "boolean" ? weixinApi.enabled : def.weixinApi.enabled,
|
|
3025
3015
|
port: typeof weixinApi.port === "number" && Number.isInteger(weixinApi.port) && weixinApi.port >= 0 ? weixinApi.port : def.weixinApi.port
|
|
@@ -3057,10 +3047,6 @@ function updateAdvancedSettings(patch) {
|
|
|
3057
3047
|
mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
|
|
3058
3048
|
overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
|
|
3059
3049
|
} : current.persona,
|
|
3060
|
-
publicAsk: patch.publicAsk !== void 0 ? {
|
|
3061
|
-
key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
|
|
3062
|
-
allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
|
|
3063
|
-
} : current.publicAsk,
|
|
3064
3050
|
weixinApi: patch.weixinApi !== void 0 ? {
|
|
3065
3051
|
enabled: typeof patch.weixinApi.enabled === "boolean" ? patch.weixinApi.enabled : current.weixinApi.enabled,
|
|
3066
3052
|
port: typeof patch.weixinApi.port === "number" && Number.isInteger(patch.weixinApi.port) && patch.weixinApi.port >= 0 ? patch.weixinApi.port : current.weixinApi.port
|
|
@@ -3070,87 +3056,6 @@ function updateAdvancedSettings(patch) {
|
|
|
3070
3056
|
return next;
|
|
3071
3057
|
}
|
|
3072
3058
|
/**
|
|
3073
|
-
* 确保问答页访问 key 存在(首次启用自动生成 + 写回固定,用户拍板"配置时生成"):
|
|
3074
|
-
* 读取全局配置 publicAsk.key;为空 → 生成 32 字节随机 hex(64 字符)→ 写回持久化。
|
|
3075
|
-
* @returns 当前固定 key(已生成则返回既有值——**幂等**,不会覆盖用户手改的 key)
|
|
3076
|
-
*/
|
|
3077
|
-
function ensurePublicAskKey() {
|
|
3078
|
-
const current = readAdvancedSettings();
|
|
3079
|
-
if (current.publicAsk.key !== "") return current.publicAsk.key;
|
|
3080
|
-
const key = randomBytes(32).toString("hex");
|
|
3081
|
-
updateAdvancedSettings({ publicAsk: {
|
|
3082
|
-
key,
|
|
3083
|
-
allowed: current.publicAsk.allowed
|
|
3084
|
-
} });
|
|
3085
|
-
return key;
|
|
3086
|
-
}
|
|
3087
|
-
/** 校验问答页访问 key(timing-safe;key 未生成 → 恒 false——没有 key 不工作) */
|
|
3088
|
-
function verifyPublicAskKey(provided) {
|
|
3089
|
-
const expected = readAdvancedSettings().publicAsk.key;
|
|
3090
|
-
if (!expected || typeof provided !== "string" || provided === "") return false;
|
|
3091
|
-
const a = Buffer.from(provided);
|
|
3092
|
-
const b = Buffer.from(expected);
|
|
3093
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
3094
|
-
}
|
|
3095
|
-
/**
|
|
3096
|
-
* 轮换问答页访问 key(v1.26.5,S142 用户:公网开放 + 不好说要换 key 呢):
|
|
3097
|
-
* 生成新 32 字节随机 hex(64 字符)→ 覆盖写回(**强制替换**——旧 key 立即失效)。
|
|
3098
|
-
* @returns 新 key(面板展示给管理员,重新分享给使用者)
|
|
3099
|
-
*/
|
|
3100
|
-
function rotatePublicAskKey() {
|
|
3101
|
-
const current = readAdvancedSettings();
|
|
3102
|
-
const key = randomBytes(32).toString("hex");
|
|
3103
|
-
updateAdvancedSettings({ publicAsk: {
|
|
3104
|
-
key,
|
|
3105
|
-
allowed: current.publicAsk.allowed
|
|
3106
|
-
} });
|
|
3107
|
-
return key;
|
|
3108
|
-
}
|
|
3109
|
-
/** 失败锁定阈值(连续失败次数) */
|
|
3110
|
-
const PUBLIC_ASK_FAIL_THRESHOLD = 5;
|
|
3111
|
-
/** 首次锁定基础时长(指数退避底数) */
|
|
3112
|
-
const PUBLIC_ASK_LOCK_BASE_MS = 9e5;
|
|
3113
|
-
/** 锁定上限 */
|
|
3114
|
-
const PUBLIC_ASK_LOCK_MAX_MS = 144e5;
|
|
3115
|
-
const ipFailStates = /* @__PURE__ */ new Map();
|
|
3116
|
-
/** 重置 IP 失败状态(测试/管理员解封) */
|
|
3117
|
-
function resetPublicAskIpFail(ip) {
|
|
3118
|
-
ipFailStates.delete(ip);
|
|
3119
|
-
}
|
|
3120
|
-
/** IP 当前是否锁定(到期自动解锁) */
|
|
3121
|
-
function isPublicAskIpLocked(ip) {
|
|
3122
|
-
const st = ipFailStates.get(ip);
|
|
3123
|
-
if (!st || st.lockedUntil === 0) return false;
|
|
3124
|
-
if (Date.now() >= st.lockedUntil) {
|
|
3125
|
-
st.lockedUntil = 0;
|
|
3126
|
-
st.count = 0;
|
|
3127
|
-
return false;
|
|
3128
|
-
}
|
|
3129
|
-
return true;
|
|
3130
|
-
}
|
|
3131
|
-
/** 记录一次 key 校验失败;达到阈值 → 锁定(指数退避)。返回锁定剩余毫秒(0 = 未锁定) */
|
|
3132
|
-
function recordPublicAskFail(ip) {
|
|
3133
|
-
let st = ipFailStates.get(ip);
|
|
3134
|
-
if (!st) {
|
|
3135
|
-
st = {
|
|
3136
|
-
count: 0,
|
|
3137
|
-
lockedUntil: 0,
|
|
3138
|
-
lockRound: 0
|
|
3139
|
-
};
|
|
3140
|
-
ipFailStates.set(ip, st);
|
|
3141
|
-
}
|
|
3142
|
-
if (st.lockedUntil > 0 && Date.now() < st.lockedUntil) return st.lockedUntil - Date.now();
|
|
3143
|
-
st.count += 1;
|
|
3144
|
-
if (st.count >= PUBLIC_ASK_FAIL_THRESHOLD) {
|
|
3145
|
-
const base = PUBLIC_ASK_LOCK_BASE_MS * 2 ** st.lockRound;
|
|
3146
|
-
st.lockedUntil = Date.now() + Math.min(base, PUBLIC_ASK_LOCK_MAX_MS);
|
|
3147
|
-
st.lockRound += 1;
|
|
3148
|
-
st.count = 0;
|
|
3149
|
-
return st.lockedUntil - Date.now();
|
|
3150
|
-
}
|
|
3151
|
-
return 0;
|
|
3152
|
-
}
|
|
3153
|
-
/**
|
|
3154
3059
|
* 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
|
|
3155
3060
|
* 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
|
|
3156
3061
|
* @param root - CCC 根(localstore.json 所在目录)
|
|
@@ -3183,8 +3088,7 @@ function toWire(settings) {
|
|
|
3183
3088
|
allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
|
|
3184
3089
|
totpEnabled: settings.gateway.totpEnabled
|
|
3185
3090
|
},
|
|
3186
|
-
persona: settings.persona
|
|
3187
|
-
publicAsk: { allowed: [...settings.publicAsk.allowed] }
|
|
3091
|
+
persona: settings.persona
|
|
3188
3092
|
};
|
|
3189
3093
|
}
|
|
3190
3094
|
/**
|
|
@@ -3253,11 +3157,6 @@ function applyWirePatch(wire) {
|
|
|
3253
3157
|
if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
|
|
3254
3158
|
patch.persona = psPatch;
|
|
3255
3159
|
}
|
|
3256
|
-
if (wire.publicAsk !== void 0 && Array.isArray(wire.publicAsk.allowed)) {
|
|
3257
|
-
const paPatch = { ...current.publicAsk };
|
|
3258
|
-
paPatch.allowed = wire.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "");
|
|
3259
|
-
patch.publicAsk = paPatch;
|
|
3260
|
-
}
|
|
3261
3160
|
return updateAdvancedSettings(patch);
|
|
3262
3161
|
}
|
|
3263
3162
|
//#endregion
|
|
@@ -3877,15 +3776,6 @@ function unregisterSkiffSession(sessionId) {
|
|
|
3877
3776
|
skiffAgents.delete(sessionId);
|
|
3878
3777
|
unregisterSkiffSession$1(sessionId);
|
|
3879
3778
|
}
|
|
3880
|
-
/** 测试/调试:注册表快照(sessionId → {role} 兼容展示) */
|
|
3881
|
-
function skiffSessionSnapshot() {
|
|
3882
|
-
const out = /* @__PURE__ */ new Map();
|
|
3883
|
-
for (const [id, b] of skiffSessionSnapshot$1()) out.set(id, {
|
|
3884
|
-
role: b.role,
|
|
3885
|
-
ccc: b.ccc
|
|
3886
|
-
});
|
|
3887
|
-
return out;
|
|
3888
|
-
}
|
|
3889
3779
|
/** 自动创建的 bound 标记(readLastBound 恢复判定——只恢复本机制建的;手工 use 的旧 bound 不认) */
|
|
3890
3780
|
const AUTO_BOUND_NOTE_PREFIX = "auto-created for skiff role";
|
|
3891
3781
|
/**
|
|
@@ -3974,7 +3864,7 @@ function ensureWorkspacePromptSection(agent, scope) {
|
|
|
3974
3864
|
*
|
|
3975
3865
|
* 判定链:
|
|
3976
3866
|
* ⓪ **临时身份不建工作台**(v1.30.9,S142 用户报"SESSION 爆炸"根治):`persistent=false`
|
|
3977
|
-
*
|
|
3867
|
+
* (调试页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
|
|
3978
3868
|
* 调用都是新 id、无身份延续,为其建 SESSION = 每次问答留一个永久空目录(无界熵增);
|
|
3979
3869
|
* 用户模型「skiff 本质是临时会话机制」——工作台属于**持久身份**,不属于单次调用。
|
|
3980
3870
|
* ① 角色未启用 session 能力(trajectory.session !== true)→ null(现状零变化)
|
|
@@ -4043,10 +3933,10 @@ const SKIFF_PRESET = "standard";
|
|
|
4043
3933
|
* 指定 sessionId 且磁盘已有持久化 log → `ctx.agents.resume`(DSH 持久化语义:
|
|
4044
3934
|
* sessionId 即身份,已持久化会话必须 resume——历史 + turn 续号延续,同用户长期记忆保留);
|
|
4045
3935
|
* 无 log(首次)/持久化未配置 → `ctx.agents.create`(新建)。不指定 sessionId →
|
|
4046
|
-
* 随机 id 恒 create
|
|
3936
|
+
* 随机 id 恒 create(调试页默认路径)。
|
|
4047
3937
|
*
|
|
4048
3938
|
* @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现用户↔会话长期映射——
|
|
4049
|
-
* 不传则随机生成 skiff-<role>-<uuid
|
|
3939
|
+
* 不传则随机生成 skiff-<role>-<uuid>,调试页默认路径)
|
|
4050
3940
|
*/
|
|
4051
3941
|
async function createSkiffAgent(ctx, root, roleName, role, defaultModel, sessionId) {
|
|
4052
3942
|
if (!ctx.agents) throw new Error("skiff: ctx.agents unavailable");
|
|
@@ -4257,8 +4147,9 @@ function truncate(s, n) {
|
|
|
4257
4147
|
* 提问一轮:followup → 等 idle → 读答案 + 轨迹。
|
|
4258
4148
|
* @param eventsStart 轨迹起点:显式传 0 = 全量轨迹(会话追问时页面重绘完整时间线,
|
|
4259
4149
|
* v1.25.10 用户拍板);不传(undefined)= 本轮增量(followup 前 events 之后)
|
|
4260
|
-
* @param options.includeTrajectory 是否计算轨迹(v1.26.10
|
|
4261
|
-
* 公开问答页 / ACP JSON-RPC
|
|
4150
|
+
* @param options.includeTrajectory 是否计算轨迹(v1.26.10 起供**对外面**传 false 跳过计算——
|
|
4151
|
+
* 原消费方 = 3100 公开问答页 / ACP JSON-RPC,**该面已于 2026-10-15 退役** ⇒ 现无生产调用方传 false;
|
|
4152
|
+
* 3099 调试页默认 true 保留)
|
|
4262
4153
|
*/
|
|
4263
4154
|
async function askSkiff(ctx, agent, question, eventsStart, options) {
|
|
4264
4155
|
const before = eventsStart === void 0 ? sessionEvents(agent.session).length : eventsStart;
|
|
@@ -12275,11 +12166,10 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
|
|
|
12275
12166
|
|
|
12276
12167
|
── 运行 ──
|
|
12277
12168
|
调试:设置面板「Serenity」页 Skiff 区块开启 → http://127.0.0.1:<debugPort> 问答页实测
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
session/cancel / session/close / session/list / request_permission(恒 allow)
|
|
12169
|
+
🔴 外部程序化接入(2026-10-15 起改走官方):本插件自造的 ACP 面(HTTP JSON-RPC 3100 /
|
|
12170
|
+
建议问答页)**已整体退役**(owner 裁「砍掉外部问答」)。现在要程序化/远程接入,走宿主
|
|
12171
|
+
自带的官方 ACP server:dsh --profile acp(纯 stdio、零认证)+ 任一 ACP 客户端
|
|
12172
|
+
(手机端如 Agmente / ACP UI)经隧道接入。详见 S142 的接入指南文档。
|
|
12283
12173
|
启停 = 人工(设置面板开关,不随插件加载自动启动);未开启零资源占用
|
|
12284
12174
|
生效机制:角色配置**实时读取**(改 .opencode/serenity.json → 刷新调试页即生效);
|
|
12285
12175
|
改配置后用 container_admin role apply 做显式校验 + 应用确认(绑定 CCC + 角色清单)
|
|
@@ -12660,7 +12550,9 @@ function claimsUserNotification(text) {
|
|
|
12660
12550
|
/**
|
|
12661
12551
|
* **不许开**的会话(设计 §3 的"建议写死",本版**确实写死**):
|
|
12662
12552
|
* `skiff-`(F4 问答 / **微信桥会话也是这个前缀** —— 桥那侧的"用户"是**家人**,
|
|
12663
|
-
* 代理替家人说话 = **冒充**)| `acp
|
|
12553
|
+
* 代理替家人说话 = **冒充**)| `acp-`(对外程序化面 —— ⚠️ **2026-10-15 起已成死判据**:
|
|
12554
|
+
* 该前缀从无生产者且面 E 已退役;理由与"为何保留"见 `output-guard-seam.ts` 文件头)|
|
|
12555
|
+
* `cheap-worker-`(自主 worker,已有自己的完成码循环)。
|
|
12664
12556
|
*
|
|
12665
12557
|
* ⚠️ **`rebuild-` 不在排除面**(与 `output-guard-seam.ts` 的取向**刻意不同**):重建会话是
|
|
12666
12558
|
* **同一轨迹的载体**(内部维护面),不是对外面;把它排除会让"重建后就没人推了"。
|
|
@@ -14733,7 +14625,6 @@ const UPLOAD_PATH = "/serenity/image-upload";
|
|
|
14733
14625
|
const FILE_UPLOAD_PATH = "/serenity/file-upload";
|
|
14734
14626
|
const CONFIG_PATH = "/serenity/config";
|
|
14735
14627
|
const CCCS_PATH = "/serenity/cccs";
|
|
14736
|
-
const PUBLIC_ASK_PATH = "/serenity/public-ask";
|
|
14737
14628
|
const WEIXIN_PATH = "/serenity/weixin";
|
|
14738
14629
|
/** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
|
|
14739
14630
|
const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
|
|
@@ -14817,7 +14708,7 @@ function saveFileToTmp(root, fileName, data) {
|
|
|
14817
14708
|
writeFileSync(join(dir, filename), bytes);
|
|
14818
14709
|
return `${FILE_UPLOAD_DIR}/${filename}`;
|
|
14819
14710
|
}
|
|
14820
|
-
function readBody$
|
|
14711
|
+
function readBody$3(req, maxBytes) {
|
|
14821
14712
|
return new Promise((resolve, reject) => {
|
|
14822
14713
|
let data = "";
|
|
14823
14714
|
req.on("data", (chunk) => {
|
|
@@ -14831,7 +14722,7 @@ function readBody$4(req, maxBytes) {
|
|
|
14831
14722
|
req.on("error", reject);
|
|
14832
14723
|
});
|
|
14833
14724
|
}
|
|
14834
|
-
function sendJson$
|
|
14725
|
+
function sendJson$3(res, code, body) {
|
|
14835
14726
|
const payload = JSON.stringify(body);
|
|
14836
14727
|
res.writeHead(code, {
|
|
14837
14728
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -14856,7 +14747,7 @@ function senderIsWebUi(req) {
|
|
|
14856
14747
|
*/
|
|
14857
14748
|
function requireWebUi(req, res, message) {
|
|
14858
14749
|
if (senderIsWebUi(req)) return true;
|
|
14859
|
-
sendJson$
|
|
14750
|
+
sendJson$3(res, 403, { error: message });
|
|
14860
14751
|
return false;
|
|
14861
14752
|
}
|
|
14862
14753
|
/**
|
|
@@ -14889,11 +14780,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14889
14780
|
handler: async (req, res) => {
|
|
14890
14781
|
try {
|
|
14891
14782
|
if (req.method !== "POST") {
|
|
14892
|
-
sendJson$
|
|
14783
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
14893
14784
|
return;
|
|
14894
14785
|
}
|
|
14895
14786
|
if (!requireWebUi(req, res, "图片落盘仅限 WebUI(client 专用)")) return;
|
|
14896
|
-
const raw = await readBody$
|
|
14787
|
+
const raw = await readBody$3(req, 20971520);
|
|
14897
14788
|
const body = JSON.parse(raw);
|
|
14898
14789
|
const workspace = resolveWorkspace(ctx, {
|
|
14899
14790
|
sessionId: body.sessionId,
|
|
@@ -14901,12 +14792,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14901
14792
|
});
|
|
14902
14793
|
const root = cccRootForCwd(workspace);
|
|
14903
14794
|
if (!root) {
|
|
14904
|
-
sendJson$
|
|
14795
|
+
sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
14905
14796
|
return;
|
|
14906
14797
|
}
|
|
14907
|
-
sendJson$
|
|
14798
|
+
sendJson$3(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
14908
14799
|
} catch (err) {
|
|
14909
|
-
sendJson$
|
|
14800
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
14910
14801
|
}
|
|
14911
14802
|
}
|
|
14912
14803
|
});
|
|
@@ -14916,11 +14807,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14916
14807
|
handler: async (req, res) => {
|
|
14917
14808
|
try {
|
|
14918
14809
|
if (req.method !== "POST") {
|
|
14919
|
-
sendJson$
|
|
14810
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
14920
14811
|
return;
|
|
14921
14812
|
}
|
|
14922
14813
|
if (!requireWebUi(req, res, "文件落盘仅限 WebUI(client 专用)")) return;
|
|
14923
|
-
const raw = await readBody$
|
|
14814
|
+
const raw = await readBody$3(req, 20971520);
|
|
14924
14815
|
const body = JSON.parse(raw);
|
|
14925
14816
|
const workspace = resolveWorkspace(ctx, {
|
|
14926
14817
|
sessionId: body.sessionId,
|
|
@@ -14928,12 +14819,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14928
14819
|
});
|
|
14929
14820
|
const root = cccRootForCwd(workspace);
|
|
14930
14821
|
if (!root) {
|
|
14931
|
-
sendJson$
|
|
14822
|
+
sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
14932
14823
|
return;
|
|
14933
14824
|
}
|
|
14934
|
-
sendJson$
|
|
14825
|
+
sendJson$3(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
14935
14826
|
} catch (err) {
|
|
14936
|
-
sendJson$
|
|
14827
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
14937
14828
|
}
|
|
14938
14829
|
}
|
|
14939
14830
|
});
|
|
@@ -14949,7 +14840,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14949
14840
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
14950
14841
|
}), configPaths);
|
|
14951
14842
|
const runtime = ctx.get("codeRuntime");
|
|
14952
|
-
sendJson$
|
|
14843
|
+
sendJson$3(res, 200, {
|
|
14953
14844
|
...status,
|
|
14954
14845
|
...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
|
|
14955
14846
|
language: runtime.language,
|
|
@@ -14960,7 +14851,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14960
14851
|
}
|
|
14961
14852
|
if (req.method === "POST") {
|
|
14962
14853
|
if (!requireWebUi(req, res, "safe-mode 切换仅限 WebUI(agent 不可自行开关)")) return;
|
|
14963
|
-
const raw = await readBody$
|
|
14854
|
+
const raw = await readBody$3(req, 65536);
|
|
14964
14855
|
const body = JSON.parse(raw);
|
|
14965
14856
|
const workspace = resolveWorkspace(ctx, {
|
|
14966
14857
|
sessionId: body.sessionId,
|
|
@@ -14968,19 +14859,19 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14968
14859
|
});
|
|
14969
14860
|
const root = cccRootForCwd(workspace);
|
|
14970
14861
|
if (!root) {
|
|
14971
|
-
sendJson$
|
|
14862
|
+
sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
14972
14863
|
return;
|
|
14973
14864
|
}
|
|
14974
14865
|
const result = setSafeMode(root, body.on === true);
|
|
14975
|
-
sendJson$
|
|
14866
|
+
sendJson$3(res, 200, {
|
|
14976
14867
|
...getStatus(workspace, configPaths),
|
|
14977
14868
|
...result
|
|
14978
14869
|
});
|
|
14979
14870
|
return;
|
|
14980
14871
|
}
|
|
14981
|
-
sendJson$
|
|
14872
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
14982
14873
|
} catch (err) {
|
|
14983
|
-
sendJson$
|
|
14874
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
14984
14875
|
}
|
|
14985
14876
|
}
|
|
14986
14877
|
});
|
|
@@ -14996,24 +14887,24 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14996
14887
|
try {
|
|
14997
14888
|
known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
|
|
14998
14889
|
} catch {}
|
|
14999
|
-
sendJson$
|
|
14890
|
+
sendJson$3(res, 200, {
|
|
15000
14891
|
config: toWire(settings),
|
|
15001
14892
|
knownWorkspaces: known
|
|
15002
14893
|
});
|
|
15003
14894
|
return;
|
|
15004
14895
|
}
|
|
15005
14896
|
if (req.method === "PUT") {
|
|
15006
|
-
const raw = await readBody$
|
|
14897
|
+
const raw = await readBody$3(req, 131072);
|
|
15007
14898
|
const saved = applyWirePatch(JSON.parse(raw).config ?? {});
|
|
15008
14899
|
try {
|
|
15009
14900
|
ctx.emit?.("serenity/config-updated");
|
|
15010
14901
|
} catch {}
|
|
15011
|
-
sendJson$
|
|
14902
|
+
sendJson$3(res, 200, { config: toWire(saved) });
|
|
15012
14903
|
return;
|
|
15013
14904
|
}
|
|
15014
|
-
sendJson$
|
|
14905
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15015
14906
|
} catch (err) {
|
|
15016
|
-
sendJson$
|
|
14907
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
15017
14908
|
}
|
|
15018
14909
|
}
|
|
15019
14910
|
});
|
|
@@ -15023,7 +14914,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15023
14914
|
handler: async (req, res) => {
|
|
15024
14915
|
try {
|
|
15025
14916
|
if (req.method !== "GET") {
|
|
15026
|
-
sendJson$
|
|
14917
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15027
14918
|
return;
|
|
15028
14919
|
}
|
|
15029
14920
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -15032,54 +14923,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15032
14923
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
15033
14924
|
});
|
|
15034
14925
|
const root = cccRootForCwd(workspace) ?? "";
|
|
15035
|
-
sendJson$
|
|
14926
|
+
sendJson$3(res, 200, { cccs: await listCccs(ctx, {
|
|
15036
14927
|
defaultRoot: root,
|
|
15037
14928
|
withRoles: true
|
|
15038
14929
|
}) });
|
|
15039
14930
|
} catch (err) {
|
|
15040
|
-
sendJson$
|
|
15041
|
-
}
|
|
15042
|
-
}
|
|
15043
|
-
});
|
|
15044
|
-
ctx.webServer.register({
|
|
15045
|
-
kind: "exact",
|
|
15046
|
-
path: PUBLIC_ASK_PATH,
|
|
15047
|
-
handler: async (req, res) => {
|
|
15048
|
-
try {
|
|
15049
|
-
if (!requireWebUi(req, res, "仅限 WebUI(key 属敏感凭据)")) return;
|
|
15050
|
-
if (req.method === "PUT") {
|
|
15051
|
-
const raw = await readBody$4(req, 16384);
|
|
15052
|
-
if (JSON.parse(raw).action !== "rotate") {
|
|
15053
|
-
sendJson$4(res, 400, { error: "unsupported action (expected \"rotate\")" });
|
|
15054
|
-
return;
|
|
15055
|
-
}
|
|
15056
|
-
sendJson$4(res, 200, { key: rotatePublicAskKey() });
|
|
15057
|
-
return;
|
|
15058
|
-
}
|
|
15059
|
-
if (req.method !== "GET") {
|
|
15060
|
-
sendJson$4(res, 405, { error: "method not allowed" });
|
|
15061
|
-
return;
|
|
15062
|
-
}
|
|
15063
|
-
const settings = readAdvancedSettings();
|
|
15064
|
-
const { readSimpleSettings } = await import("./settings-section-xHABl8nk.js").then((n) => n.i);
|
|
15065
|
-
const simple = readSimpleSettings();
|
|
15066
|
-
const allowed = settings.publicAsk.allowed;
|
|
15067
|
-
const port = simple.acpHttpPort ?? ACP_HTTP_PORT;
|
|
15068
|
-
const base = `http://127.0.0.1:${port}`;
|
|
15069
|
-
const containerUrls = allowed.map((name) => ({
|
|
15070
|
-
name,
|
|
15071
|
-
url: `${base}/c/${encodeURIComponent(name)}`
|
|
15072
|
-
}));
|
|
15073
|
-
sendJson$4(res, 200, {
|
|
15074
|
-
enabled: simple.publicAskEnabled ?? false,
|
|
15075
|
-
port,
|
|
15076
|
-
key: ensurePublicAskKey(),
|
|
15077
|
-
allowed,
|
|
15078
|
-
urls: containerUrls,
|
|
15079
|
-
listUrl: `${base}/`
|
|
15080
|
-
});
|
|
15081
|
-
} catch (err) {
|
|
15082
|
-
sendJson$4(res, 400, { error: err.message ?? String(err) });
|
|
14931
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
15083
14932
|
}
|
|
15084
14933
|
}
|
|
15085
14934
|
});
|
|
@@ -15093,7 +14942,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15093
14942
|
if (req.method === "GET") {
|
|
15094
14943
|
const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
|
|
15095
14944
|
if (!chk.ok) {
|
|
15096
|
-
sendJson$
|
|
14945
|
+
sendJson$3(res, 400, { error: chk.error });
|
|
15097
14946
|
return;
|
|
15098
14947
|
}
|
|
15099
14948
|
const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
@@ -15107,7 +14956,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15107
14956
|
enabled: a.enabled !== false,
|
|
15108
14957
|
bound: readWeixinCredential(chk.root, a.accountId) !== null
|
|
15109
14958
|
}));
|
|
15110
|
-
sendJson$
|
|
14959
|
+
sendJson$3(res, 200, {
|
|
15111
14960
|
enabled: settings.enabled === true,
|
|
15112
14961
|
botType: settings.botType ?? void 0,
|
|
15113
14962
|
accounts,
|
|
@@ -15117,14 +14966,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15117
14966
|
return;
|
|
15118
14967
|
}
|
|
15119
14968
|
if (req.method !== "POST") {
|
|
15120
|
-
sendJson$
|
|
14969
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15121
14970
|
return;
|
|
15122
14971
|
}
|
|
15123
|
-
const raw = await readBody$
|
|
14972
|
+
const raw = await readBody$3(req, 131072);
|
|
15124
14973
|
const body = JSON.parse(raw);
|
|
15125
14974
|
const chk = requireCcc(body.ccc);
|
|
15126
14975
|
if (!chk.ok) {
|
|
15127
|
-
sendJson$
|
|
14976
|
+
sendJson$3(res, 400, { error: chk.error });
|
|
15128
14977
|
return;
|
|
15129
14978
|
}
|
|
15130
14979
|
const root = chk.root;
|
|
@@ -15141,7 +14990,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15141
14990
|
startedAt: Date.now(),
|
|
15142
14991
|
polling: false
|
|
15143
14992
|
});
|
|
15144
|
-
sendJson$
|
|
14993
|
+
sendJson$3(res, 200, {
|
|
15145
14994
|
qrcode: qr.qrcode,
|
|
15146
14995
|
qrcode_img_content: qr.qrcode_img_content,
|
|
15147
14996
|
loginKey
|
|
@@ -15150,31 +14999,31 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15150
14999
|
}
|
|
15151
15000
|
if (body.action === "remove-account") {
|
|
15152
15001
|
if (!body.accountId) {
|
|
15153
|
-
sendJson$
|
|
15002
|
+
sendJson$3(res, 400, { error: "missing accountId" });
|
|
15154
15003
|
return;
|
|
15155
15004
|
}
|
|
15156
15005
|
const { removeWeixinAccount } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15157
15006
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15158
15007
|
removeWeixinAccount(root, body.accountId);
|
|
15159
15008
|
syncCccBridge(ctx, root);
|
|
15160
|
-
sendJson$
|
|
15009
|
+
sendJson$3(res, 200, { removed: body.accountId });
|
|
15161
15010
|
return;
|
|
15162
15011
|
}
|
|
15163
15012
|
if (body.action === "save-routes") {
|
|
15164
15013
|
const routes = body.routes ?? [];
|
|
15165
15014
|
if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
|
|
15166
|
-
sendJson$
|
|
15015
|
+
sendJson$3(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
15167
15016
|
return;
|
|
15168
15017
|
}
|
|
15169
15018
|
const { saveWeixinRoutes } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15170
15019
|
const { readSkiffRoles } = await import("./skiff-role-B615L9MV.js").then((n) => n.u);
|
|
15171
15020
|
const roles = readSkiffRoles(root);
|
|
15172
15021
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
15173
|
-
sendJson$
|
|
15022
|
+
sendJson$3(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
|
|
15174
15023
|
return;
|
|
15175
15024
|
}
|
|
15176
15025
|
saveWeixinRoutes(root, routes);
|
|
15177
|
-
sendJson$
|
|
15026
|
+
sendJson$3(res, 200, { saved: routes.length });
|
|
15178
15027
|
return;
|
|
15179
15028
|
}
|
|
15180
15029
|
if (body.action === "set-enabled") {
|
|
@@ -15183,7 +15032,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15183
15032
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15184
15033
|
const settings = readWeixinSettings(root);
|
|
15185
15034
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
15186
|
-
sendJson$
|
|
15035
|
+
sendJson$3(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
|
|
15187
15036
|
return;
|
|
15188
15037
|
}
|
|
15189
15038
|
setWeixinEnabled(root, enabled);
|
|
@@ -15192,12 +15041,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15192
15041
|
const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15193
15042
|
stopCccBridge(root);
|
|
15194
15043
|
}
|
|
15195
|
-
sendJson$
|
|
15044
|
+
sendJson$3(res, 200, { enabled });
|
|
15196
15045
|
return;
|
|
15197
15046
|
}
|
|
15198
|
-
sendJson$
|
|
15047
|
+
sendJson$3(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
|
|
15199
15048
|
} catch (err) {
|
|
15200
|
-
sendJson$
|
|
15049
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
15201
15050
|
}
|
|
15202
15051
|
}
|
|
15203
15052
|
});
|
|
@@ -15208,18 +15057,18 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15208
15057
|
try {
|
|
15209
15058
|
if (!requireWebUi(req, res, "微信桥配置仅限 WebUI(client 专用)")) return;
|
|
15210
15059
|
if (req.method !== "GET") {
|
|
15211
|
-
sendJson$
|
|
15060
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15212
15061
|
return;
|
|
15213
15062
|
}
|
|
15214
15063
|
const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
|
|
15215
15064
|
const login = weixinLogins.get(key);
|
|
15216
15065
|
if (!login) {
|
|
15217
|
-
sendJson$
|
|
15066
|
+
sendJson$3(res, 404, { error: "login not found or expired" });
|
|
15218
15067
|
return;
|
|
15219
15068
|
}
|
|
15220
15069
|
if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
|
|
15221
15070
|
weixinLogins.delete(key);
|
|
15222
|
-
sendJson$
|
|
15071
|
+
sendJson$3(res, 200, { status: "expired" });
|
|
15223
15072
|
return;
|
|
15224
15073
|
}
|
|
15225
15074
|
const { pollQRStatus } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
|
|
@@ -15232,7 +15081,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15232
15081
|
switch (status.status) {
|
|
15233
15082
|
case "confirmed": {
|
|
15234
15083
|
if (!status.bot_token) {
|
|
15235
|
-
sendJson$
|
|
15084
|
+
sendJson$3(res, 200, {
|
|
15236
15085
|
status: "error",
|
|
15237
15086
|
error: "confirmed but no bot_token"
|
|
15238
15087
|
});
|
|
@@ -15253,7 +15102,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15253
15102
|
});
|
|
15254
15103
|
syncCccBridge(ctx, login.root);
|
|
15255
15104
|
weixinLogins.delete(key);
|
|
15256
|
-
sendJson$
|
|
15105
|
+
sendJson$3(res, 200, {
|
|
15257
15106
|
status: "confirmed",
|
|
15258
15107
|
accountId,
|
|
15259
15108
|
tokenSaved: true
|
|
@@ -15262,14 +15111,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15262
15111
|
}
|
|
15263
15112
|
case "expired":
|
|
15264
15113
|
weixinLogins.delete(key);
|
|
15265
|
-
sendJson$
|
|
15114
|
+
sendJson$3(res, 200, { status: "expired" });
|
|
15266
15115
|
return;
|
|
15267
15116
|
default:
|
|
15268
|
-
sendJson$
|
|
15117
|
+
sendJson$3(res, 200, { status: status.status ?? "wait" });
|
|
15269
15118
|
return;
|
|
15270
15119
|
}
|
|
15271
15120
|
} catch (err) {
|
|
15272
|
-
sendJson$
|
|
15121
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
15273
15122
|
}
|
|
15274
15123
|
}
|
|
15275
15124
|
});
|
|
@@ -15291,7 +15140,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15291
15140
|
const root = sessionsRootDir();
|
|
15292
15141
|
if (req.method === "GET") {
|
|
15293
15142
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
15294
|
-
sendJson$
|
|
15143
|
+
sendJson$3(res, 200, {
|
|
15295
15144
|
root,
|
|
15296
15145
|
olderThanDays: days,
|
|
15297
15146
|
dryRun: true,
|
|
@@ -15305,12 +15154,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15305
15154
|
return;
|
|
15306
15155
|
}
|
|
15307
15156
|
if (req.method !== "POST") {
|
|
15308
|
-
sendJson$
|
|
15157
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15309
15158
|
return;
|
|
15310
15159
|
}
|
|
15311
15160
|
if (!requireWebUi(req, res, "会话清理仅限 WebUI(client 专用)")) return;
|
|
15312
15161
|
const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
|
|
15313
|
-
sendJson$
|
|
15162
|
+
sendJson$3(res, 200, {
|
|
15314
15163
|
root,
|
|
15315
15164
|
olderThanDays: days,
|
|
15316
15165
|
dryRun: false,
|
|
@@ -15318,7 +15167,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15318
15167
|
errors: result.errors
|
|
15319
15168
|
});
|
|
15320
15169
|
} catch (err) {
|
|
15321
|
-
sendJson$
|
|
15170
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
15322
15171
|
}
|
|
15323
15172
|
}
|
|
15324
15173
|
});
|
|
@@ -15371,7 +15220,7 @@ function loadVoyageHtml(assetPath = voyageAssetPath()) {
|
|
|
15371
15220
|
return readFileSync(assetPath, "utf-8");
|
|
15372
15221
|
}
|
|
15373
15222
|
/** 发 JSON(错误分支用;与 api.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
|
|
15374
|
-
function sendJson$
|
|
15223
|
+
function sendJson$2(res, status, body) {
|
|
15375
15224
|
res.statusCode = status;
|
|
15376
15225
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
15377
15226
|
res.end(JSON.stringify(body));
|
|
@@ -15386,14 +15235,14 @@ function registerVoyageApi(ctx, opts = {}) {
|
|
|
15386
15235
|
path: VOYAGE_PATH,
|
|
15387
15236
|
handler: (req, res) => {
|
|
15388
15237
|
if (req.method !== "GET") {
|
|
15389
|
-
sendJson$
|
|
15238
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15390
15239
|
return;
|
|
15391
15240
|
}
|
|
15392
15241
|
let html;
|
|
15393
15242
|
try {
|
|
15394
15243
|
html = loadVoyageHtml(opts.assetPath ?? voyageAssetPath());
|
|
15395
15244
|
} catch (err) {
|
|
15396
|
-
sendJson$
|
|
15245
|
+
sendJson$2(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
|
|
15397
15246
|
return;
|
|
15398
15247
|
}
|
|
15399
15248
|
res.statusCode = 200;
|
|
@@ -15988,14 +15837,16 @@ function summarizeHostContract(report) {
|
|
|
15988
15837
|
*
|
|
15989
15838
|
* ## 现状 → 本模块要收掉什么(取证:`…/acc-c4c5-current-state.md` §①/§③ 3.1)
|
|
15990
15839
|
*
|
|
15991
|
-
* 插件自己 spawn
|
|
15992
|
-
*
|
|
15840
|
+
* 插件自己 spawn 了**三个** node:http 监听器(B 3081 网关 / C 3082 微信发送 / D 3099 Skiff 调试)。
|
|
15841
|
+
* ⚠️ **2026-10-15 变更**:原为四个(含 **E 3100 ACP+问答**),E 已随 owner「砍掉外部问答」令**整面退役**
|
|
15842
|
+
* ⇒ 仍保有面 B/C/D 三个;本段保留"当年是四个"的记述,因为下方"两套拆卸"的病史由它解释。
|
|
15843
|
+
* 此前四处各写一份同款样板:`createServer` + `server.once('error')` +
|
|
15993
15844
|
* `listen(port, host)` + 模块级 `active` 变量 + `close()`;拆卸也分裂成**两套**——
|
|
15994
15845
|
* `seams/lifecycle.ts` 有一个聚合 disposer,而 `gateway.ts` 与 `weixin-send-api.ts`
|
|
15995
15846
|
* **各自另注册**一个(D/E 干脆没有自己的 disposer)。
|
|
15996
15847
|
*
|
|
15997
15848
|
* 本模块把"listener 生命周期"收成一处:**active 表 + 统一 listen/close + 统一错误与端口占用
|
|
15998
|
-
*
|
|
15849
|
+
* 处理**。各面各自只提供 `{ name, port, host, enabled, handler }`。
|
|
15999
15850
|
*
|
|
16000
15851
|
* ## 🔴 共享的是「生命周期与样板」,**绝不是**「路由 / 监听器 / 鉴权」
|
|
16001
15852
|
*
|
|
@@ -16003,12 +15854,12 @@ function summarizeHostContract(report) {
|
|
|
16003
15854
|
* · **A ↔ C 解耦**(`weixin-send-api.ts:4-7`):3080 会被 3081 原样反代(连请求头),
|
|
16004
15855
|
* 把 3082 的端点挂到 3080 上 = 已登录的外部家庭账号即可冒充 bot 发消息
|
|
16005
15856
|
* ⇒ 本模块**不提供**"把面挂到别的面"的能力,也不共享路由表;
|
|
16006
|
-
* · **E ↔ D**
|
|
15857
|
+
* · (原 **E ↔ D** 共用会话核心但各自独立 listener 一条,**随 E 退役删除**);
|
|
16007
15858
|
* · **B 是 A 的代理宿主**(把请求打到 `127.0.0.1:<主端口>`),不是另一个路由面。
|
|
16008
15859
|
*
|
|
16009
15860
|
* ## 面 A(3080)不在本模块之列
|
|
16010
15861
|
* 它是**宿主 DSH 主 WebUI**,插件只往上挂 `/serenity/*` 路由(`ctx.webServer.register`),
|
|
16011
|
-
* **插件不起这个 listener** ⇒ 面宿主只持有 B/C/D
|
|
15862
|
+
* **插件不起这个 listener** ⇒ 面宿主只持有 B/C/D 三个自起面。
|
|
16012
15863
|
*
|
|
16013
15864
|
* ## 两个入口的分工(这是本模块唯一的"抽象决策",别混)
|
|
16014
15865
|
* · {@link startFace} —— **机械启动**:把一个 handler 绑到 `host:port` 上。同名面若已在监听,
|
|
@@ -16633,7 +16484,7 @@ function createDshCookieProvider(connection, authority) {
|
|
|
16633
16484
|
/** 面名(C4 块 A:listener 生命周期归 `face-host`) */
|
|
16634
16485
|
const FACE_GATEWAY = "gateway";
|
|
16635
16486
|
/** 读取请求体(≤ maxBytes;超限 reject) */
|
|
16636
|
-
function readBody$
|
|
16487
|
+
function readBody$2(req, maxBytes) {
|
|
16637
16488
|
return new Promise((resolve, reject) => {
|
|
16638
16489
|
let data = "";
|
|
16639
16490
|
req.on("data", (chunk) => {
|
|
@@ -16760,7 +16611,7 @@ async function startGateway(config, getAccounts, getDshCookie) {
|
|
|
16760
16611
|
const session = authed(req);
|
|
16761
16612
|
if (session !== void 0) {
|
|
16762
16613
|
if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
|
|
16763
|
-
readBody$
|
|
16614
|
+
readBody$2(req, 131072).then((body) => {
|
|
16764
16615
|
const { rpcId, path } = parseWorkspaceCreateBody(body);
|
|
16765
16616
|
if (!allowWorkspaceCreate) {
|
|
16766
16617
|
res.writeHead(403, { "content-type": "application/json" });
|
|
@@ -19090,7 +18941,7 @@ const SKIFF_DEBUG_HOST = FACE_PORTS.skiffDebug.host ?? "127.0.0.1";
|
|
|
19090
18941
|
function skiffDebugActive() {
|
|
19091
18942
|
return faceActive(FACE_SKIFF_DEBUG);
|
|
19092
18943
|
}
|
|
19093
|
-
function readBody$
|
|
18944
|
+
function readBody$1(req) {
|
|
19094
18945
|
return new Promise((resolve, reject) => {
|
|
19095
18946
|
const chunks = [];
|
|
19096
18947
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -19098,7 +18949,7 @@ function readBody$2(req) {
|
|
|
19098
18949
|
req.on("error", reject);
|
|
19099
18950
|
});
|
|
19100
18951
|
}
|
|
19101
|
-
function sendJson$
|
|
18952
|
+
function sendJson$1(res, status, payload) {
|
|
19102
18953
|
const body = JSON.stringify(payload);
|
|
19103
18954
|
res.writeHead(status, {
|
|
19104
18955
|
"Content-Type": "application/json; charset=utf-8",
|
|
@@ -19106,7 +18957,7 @@ function sendJson$2(res, status, payload) {
|
|
|
19106
18957
|
});
|
|
19107
18958
|
res.end(body);
|
|
19108
18959
|
}
|
|
19109
|
-
function sendHtml
|
|
18960
|
+
function sendHtml(res, html) {
|
|
19110
18961
|
res.writeHead(200, {
|
|
19111
18962
|
"Content-Type": "text/html; charset=utf-8",
|
|
19112
18963
|
"Cache-Control": "no-store"
|
|
@@ -19303,8 +19154,8 @@ function escapeHtml(s) {
|
|
|
19303
19154
|
* 在 JSON **文本层**把它们替换为 `\uXXXX` 转义序列:JSON.parse 后语义完全一致(还原原字符),
|
|
19304
19155
|
* 且 JSC 正则看到的是常规 ASCII 转义(与 JSON.stringify 对控制字符的输出同形态,安全)。
|
|
19305
19156
|
*
|
|
19306
|
-
*
|
|
19307
|
-
*
|
|
19157
|
+
* 3099 调试页客户端 `await res.json()` 与页面内嵌 JSON 均需此兼容层
|
|
19158
|
+
* ——iOS Safari 用户实测"复杂回答"触发。(原同批还列了 3100 问答页,该面已退役,2026-10-15)
|
|
19308
19159
|
*
|
|
19309
19160
|
* @param jsonText JSON.stringify 的输出文本;原地等价替换,返回 JSC 安全文本
|
|
19310
19161
|
*/
|
|
@@ -19432,7 +19283,7 @@ function skiffDebugSpec(ctx, root, port, webPort) {
|
|
|
19432
19283
|
port,
|
|
19433
19284
|
host: SKIFF_DEBUG_HOST,
|
|
19434
19285
|
enabled: () => readSimpleSettings().skiffEnabled,
|
|
19435
|
-
handler: (req, res) => handle
|
|
19286
|
+
handler: (req, res) => handle(ctx, root, webPort, req, res)
|
|
19436
19287
|
};
|
|
19437
19288
|
}
|
|
19438
19289
|
/**
|
|
@@ -19456,11 +19307,11 @@ async function startSkiffDebugServer(ctx, root, port, webPort) {
|
|
|
19456
19307
|
function stopSkiffDebugServer() {
|
|
19457
19308
|
stopFace(FACE_SKIFF_DEBUG);
|
|
19458
19309
|
}
|
|
19459
|
-
async function handle
|
|
19310
|
+
async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
19460
19311
|
try {
|
|
19461
19312
|
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
19462
19313
|
if (req.method === "GET" && url === "/") {
|
|
19463
|
-
sendHtml
|
|
19314
|
+
sendHtml(res, skiffDebugPage(await listCccs(ctx, {
|
|
19464
19315
|
defaultRoot,
|
|
19465
19316
|
withRoles: true
|
|
19466
19317
|
}), defaultRoot, webPort));
|
|
@@ -19469,11 +19320,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19469
19320
|
if (req.method === "POST" && url === "/ask") {
|
|
19470
19321
|
let body;
|
|
19471
19322
|
try {
|
|
19472
|
-
const parsed = JSON.parse(await readBody$
|
|
19323
|
+
const parsed = JSON.parse(await readBody$1(req));
|
|
19473
19324
|
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
19474
19325
|
body = parsed;
|
|
19475
19326
|
} catch {
|
|
19476
|
-
sendJson$
|
|
19327
|
+
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
19477
19328
|
return;
|
|
19478
19329
|
}
|
|
19479
19330
|
const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot;
|
|
@@ -19482,11 +19333,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19482
19333
|
const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
|
|
19483
19334
|
const role = readSkiffRoles(ccc).get(roleName);
|
|
19484
19335
|
if (!roleName || !role) {
|
|
19485
|
-
sendJson$
|
|
19336
|
+
sendJson$1(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
|
|
19486
19337
|
return;
|
|
19487
19338
|
}
|
|
19488
19339
|
if (!question.trim()) {
|
|
19489
|
-
sendJson$
|
|
19340
|
+
sendJson$1(res, 400, { error: "empty question" });
|
|
19490
19341
|
return;
|
|
19491
19342
|
}
|
|
19492
19343
|
let agent;
|
|
@@ -19495,11 +19346,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19495
19346
|
const info = skiffSessionInfo(sessionId);
|
|
19496
19347
|
const live = getSkiffAgent(sessionId);
|
|
19497
19348
|
if (!info || !live) {
|
|
19498
|
-
sendJson$
|
|
19349
|
+
sendJson$1(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
|
|
19499
19350
|
return;
|
|
19500
19351
|
}
|
|
19501
19352
|
if (info.role !== roleName || info.ccc !== ccc) {
|
|
19502
|
-
sendJson$
|
|
19353
|
+
sendJson$1(res, 400, { error: `session "${sessionId}" belongs to role "${info.role}" in another CCC — start a new conversation` });
|
|
19503
19354
|
return;
|
|
19504
19355
|
}
|
|
19505
19356
|
agent = live;
|
|
@@ -19508,7 +19359,7 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19508
19359
|
const hc = readCheapWorkerConfig(ccc);
|
|
19509
19360
|
if (!agent) agent = (await createSkiffAgent(ctx, ccc, roleName, role, hc?.defaultModel)).agent;
|
|
19510
19361
|
const result = await askSkiff(ctx, agent, question, 0);
|
|
19511
|
-
sendJson$
|
|
19362
|
+
sendJson$1(res, 200, {
|
|
19512
19363
|
answer: result.answer,
|
|
19513
19364
|
answer_html: renderSkiffMarkdown(result.answer),
|
|
19514
19365
|
sessionId: result.sessionId,
|
|
@@ -19517,9 +19368,9 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19517
19368
|
});
|
|
19518
19369
|
return;
|
|
19519
19370
|
}
|
|
19520
|
-
sendJson$
|
|
19371
|
+
sendJson$1(res, 404, { error: "not found" });
|
|
19521
19372
|
} catch (err) {
|
|
19522
|
-
sendJson$
|
|
19373
|
+
sendJson$1(res, 500, { error: err?.message ?? String(err) });
|
|
19523
19374
|
}
|
|
19524
19375
|
}
|
|
19525
19376
|
//#endregion
|
|
@@ -19663,7 +19514,9 @@ const rebukeStates = /* @__PURE__ */ new Map();
|
|
|
19663
19514
|
//#endregion
|
|
19664
19515
|
//#region src/output-guard-seam.ts
|
|
19665
19516
|
/** 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免):
|
|
19666
|
-
* skiff-(F4 问答)/
|
|
19517
|
+
* skiff-(F4 问答)/ rebuild-(v1.22.4 重建会话)前缀。
|
|
19518
|
+
* 🔴 `acp-` 分支保留但**已是死判据**(无生产者;面 E 已于 2026-10-15 退役)——
|
|
19519
|
+
* 理由与"为何不擅自删"见文件头注释。 */
|
|
19667
19520
|
function isExternalFaceSession(sessionId) {
|
|
19668
19521
|
if (!sessionId) return false;
|
|
19669
19522
|
return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
|
|
@@ -19958,760 +19811,6 @@ function registerUnattendedSeam(ctx) {
|
|
|
19958
19811
|
});
|
|
19959
19812
|
}
|
|
19960
19813
|
//#endregion
|
|
19961
|
-
//#region src/acp-core.ts
|
|
19962
|
-
/** JSON-RPC 错误码(对齐标准) */
|
|
19963
|
-
const RPC_ERROR = {
|
|
19964
|
-
PARSE: -32700,
|
|
19965
|
-
INVALID_REQUEST: -32600,
|
|
19966
|
-
METHOD_NOT_FOUND: -32601,
|
|
19967
|
-
INVALID_PARAMS: -32602,
|
|
19968
|
-
INTERNAL: -32603
|
|
19969
|
-
};
|
|
19970
|
-
/**
|
|
19971
|
-
* ACP 会话管理器:持有 skiff agent 会话(与 skiff-core 注册表同源——
|
|
19972
|
-
* 创建走 createSkiffAgent(自动注册),关闭走 unregisterSkiffSession)。
|
|
19973
|
-
* 传输无关:HTTP 端点/企业微信桥/未来 stdio 都调 handle(method, params)。
|
|
19974
|
-
*/
|
|
19975
|
-
var AcpServer = class {
|
|
19976
|
-
ctx;
|
|
19977
|
-
constructor(ctx) {
|
|
19978
|
-
this.ctx = ctx;
|
|
19979
|
-
}
|
|
19980
|
-
/** 实现级方法调度(返回 result 或抛 JSON-RPC 语义错误) */
|
|
19981
|
-
async handle(method, params) {
|
|
19982
|
-
switch (method) {
|
|
19983
|
-
case "initialize": return this.initialize();
|
|
19984
|
-
case "authenticate": return {};
|
|
19985
|
-
case "session/new": return this.newSession(params);
|
|
19986
|
-
case "session/list": return this.listSessions();
|
|
19987
|
-
case "session/prompt": return this.prompt(params);
|
|
19988
|
-
case "session/cancel": return this.cancel(params);
|
|
19989
|
-
case "session/close": return this.closeSession(params);
|
|
19990
|
-
case "session/request_permission": return { allow: true };
|
|
19991
|
-
default: throw new RpcMethodError(`method not found: ${method}`);
|
|
19992
|
-
}
|
|
19993
|
-
}
|
|
19994
|
-
initialize() {
|
|
19995
|
-
return {
|
|
19996
|
-
protocolVersion: 1,
|
|
19997
|
-
capabilities: {
|
|
19998
|
-
sessionId: true,
|
|
19999
|
-
ccc: true,
|
|
20000
|
-
role: true
|
|
20001
|
-
},
|
|
20002
|
-
serverInfo: {
|
|
20003
|
-
name: "dsh-serenity-hooks-acp",
|
|
20004
|
-
version: "1.26.0"
|
|
20005
|
-
}
|
|
20006
|
-
};
|
|
20007
|
-
}
|
|
20008
|
-
/**
|
|
20009
|
-
* session/new:{ ccc, role, sessionId? } → 创建或延续 skiff 会话。
|
|
20010
|
-
* 无 sessionId → createSkiffAgent(新会话,continued:false);
|
|
20011
|
-
* 有 → 进程内延续(getSkiffAgent 命中 + role/ccc 绑定校验;未命中 → 错误)。
|
|
20012
|
-
*/
|
|
20013
|
-
async newSession(params) {
|
|
20014
|
-
const ccc = typeof params?.ccc === "string" && params.ccc !== "" ? params.ccc : void 0;
|
|
20015
|
-
const roleName = typeof params?.role === "string" ? params.role : void 0;
|
|
20016
|
-
const sessionId = typeof params?.sessionId === "string" && params.sessionId !== "" ? params.sessionId : void 0;
|
|
20017
|
-
if (!ccc) throw new RpcInvalidParams("session/new requires ccc (CCC root path)");
|
|
20018
|
-
if (!roleName) throw new RpcInvalidParams("session/new requires role");
|
|
20019
|
-
const role = readSkiffRoles(ccc).get(roleName);
|
|
20020
|
-
if (!role) throw new RpcInvalidParams(`unknown role: ${roleName} (ccc: ${ccc})`);
|
|
20021
|
-
if (sessionId) {
|
|
20022
|
-
const info = skiffSessionInfo(sessionId);
|
|
20023
|
-
const live = getSkiffAgent(sessionId);
|
|
20024
|
-
if (!info || !live) throw new RpcInvalidParams("session is not recoverable (process restarted or session unknown) — start a new conversation");
|
|
20025
|
-
if (info.role !== roleName || info.ccc !== ccc) throw new RpcInvalidParams(`session "${sessionId}" belongs to role "${info.role}" in another CCC`);
|
|
20026
|
-
return {
|
|
20027
|
-
sessionId,
|
|
20028
|
-
role: roleName,
|
|
20029
|
-
ccc,
|
|
20030
|
-
continued: true
|
|
20031
|
-
};
|
|
20032
|
-
}
|
|
20033
|
-
const hc = readCheapWorkerConfig(ccc);
|
|
20034
|
-
return {
|
|
20035
|
-
sessionId: (await createSkiffAgent(this.ctx, ccc, roleName, role, hc?.defaultModel)).sessionId,
|
|
20036
|
-
role: roleName,
|
|
20037
|
-
ccc,
|
|
20038
|
-
continued: false
|
|
20039
|
-
};
|
|
20040
|
-
}
|
|
20041
|
-
listSessions() {
|
|
20042
|
-
const sessions = [];
|
|
20043
|
-
for (const [id, b] of skiffSessionSnapshot()) sessions.push({
|
|
20044
|
-
sessionId: id,
|
|
20045
|
-
role: b.role,
|
|
20046
|
-
ccc: b.ccc
|
|
20047
|
-
});
|
|
20048
|
-
return { sessions };
|
|
20049
|
-
}
|
|
20050
|
-
/**
|
|
20051
|
-
* session/prompt:{ sessionId, question } → 答案(**v1.26.10:不返回 trajectory**——
|
|
20052
|
-
* 3100 对外只提供问答,轨迹含工具结果等内部信息;3099 调试页另走 /ask 保留)
|
|
20053
|
-
*/
|
|
20054
|
-
async prompt(params) {
|
|
20055
|
-
const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
|
|
20056
|
-
const question = typeof params?.question === "string" ? params.question : void 0;
|
|
20057
|
-
if (!sessionId) throw new RpcInvalidParams("session/prompt requires sessionId");
|
|
20058
|
-
if (!question?.trim()) throw new RpcInvalidParams("session/prompt requires question");
|
|
20059
|
-
const agent = getSkiffAgent(sessionId);
|
|
20060
|
-
if (!agent) throw new RpcInvalidParams(`unknown session: ${sessionId} (not recoverable — process restarted?)`);
|
|
20061
|
-
const result = await askSkiff(this.ctx, agent, question, void 0, { includeTrajectory: false });
|
|
20062
|
-
return {
|
|
20063
|
-
answer: result.answer,
|
|
20064
|
-
sessionId: result.sessionId
|
|
20065
|
-
};
|
|
20066
|
-
}
|
|
20067
|
-
/** session/cancel:{ sessionId } → 取消该会话的当前活动(Agent.cancel,宿主 rc.1 契约) */
|
|
20068
|
-
cancel(params) {
|
|
20069
|
-
const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
|
|
20070
|
-
if (!sessionId) return { cancelled: false };
|
|
20071
|
-
const agent = getSkiffAgent(sessionId);
|
|
20072
|
-
if (!agent) return { cancelled: false };
|
|
20073
|
-
try {
|
|
20074
|
-
agent.cancel({ kind: "user" });
|
|
20075
|
-
} catch {}
|
|
20076
|
-
return { cancelled: true };
|
|
20077
|
-
}
|
|
20078
|
-
/** session/close:{ sessionId } → 释放 agent + 注册表清理(进程内) */
|
|
20079
|
-
closeSession(params) {
|
|
20080
|
-
const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
|
|
20081
|
-
if (!sessionId) return { closed: false };
|
|
20082
|
-
if (!getSkiffAgent(sessionId)) return { closed: false };
|
|
20083
|
-
unregisterSkiffSession(sessionId);
|
|
20084
|
-
return { closed: true };
|
|
20085
|
-
}
|
|
20086
|
-
};
|
|
20087
|
-
/** 方法不存在错误(JSON-RPC -32601) */
|
|
20088
|
-
var RpcMethodError = class extends Error {
|
|
20089
|
-
constructor(message) {
|
|
20090
|
-
super(message);
|
|
20091
|
-
this.name = "RpcMethodError";
|
|
20092
|
-
}
|
|
20093
|
-
};
|
|
20094
|
-
/** 参数错误(JSON-RPC -32602) */
|
|
20095
|
-
var RpcInvalidParams = class extends Error {
|
|
20096
|
-
constructor(message) {
|
|
20097
|
-
super(message);
|
|
20098
|
-
this.name = "RpcInvalidParams";
|
|
20099
|
-
}
|
|
20100
|
-
};
|
|
20101
|
-
/**
|
|
20102
|
-
* JSON-RPC 2.0 批处理(单帧):解析请求/通知 → 调 AcpServer.handle → 响应。
|
|
20103
|
-
* 传输无关:acp-http 端点 / 未来 stdio 共用。
|
|
20104
|
-
* @returns 响应帧数组(请求有响应;通知无;解析失败 → 单错误响应)
|
|
20105
|
-
*/
|
|
20106
|
-
async function dispatchRpc(server, frame) {
|
|
20107
|
-
if (frame === null || typeof frame !== "object") return [rpcError(null, RPC_ERROR.INVALID_REQUEST, "invalid request")];
|
|
20108
|
-
const req = frame;
|
|
20109
|
-
if (req.jsonrpc !== "2.0" || typeof req.method !== "string") return [rpcError(req.id ?? null, RPC_ERROR.INVALID_REQUEST, "invalid request")];
|
|
20110
|
-
if (req.id === void 0) {
|
|
20111
|
-
try {
|
|
20112
|
-
await server.handle(req.method, req.params);
|
|
20113
|
-
} catch {}
|
|
20114
|
-
return [];
|
|
20115
|
-
}
|
|
20116
|
-
try {
|
|
20117
|
-
const result = await server.handle(req.method, req.params);
|
|
20118
|
-
return [{
|
|
20119
|
-
jsonrpc: "2.0",
|
|
20120
|
-
id: req.id,
|
|
20121
|
-
result
|
|
20122
|
-
}];
|
|
20123
|
-
} catch (error) {
|
|
20124
|
-
return [rpcError(req.id, rpcCodeOf(error), error instanceof Error ? error.message : String(error))];
|
|
20125
|
-
}
|
|
20126
|
-
}
|
|
20127
|
-
function rpcCodeOf(error) {
|
|
20128
|
-
if (error instanceof RpcMethodError) return RPC_ERROR.METHOD_NOT_FOUND;
|
|
20129
|
-
if (error instanceof RpcInvalidParams) return RPC_ERROR.INVALID_PARAMS;
|
|
20130
|
-
return RPC_ERROR.INTERNAL;
|
|
20131
|
-
}
|
|
20132
|
-
function rpcError(id, code, message) {
|
|
20133
|
-
return {
|
|
20134
|
-
jsonrpc: "2.0",
|
|
20135
|
-
id,
|
|
20136
|
-
error: {
|
|
20137
|
-
code,
|
|
20138
|
-
message
|
|
20139
|
-
}
|
|
20140
|
-
};
|
|
20141
|
-
}
|
|
20142
|
-
//#endregion
|
|
20143
|
-
//#region src/acp-http.ts
|
|
20144
|
-
/** 面名(C4 块 A) */
|
|
20145
|
-
const FACE_ACP_HTTP = "acp-http";
|
|
20146
|
-
/** 监听地址:**只绑 loopback**(JSON-RPC 子面无认证——仅靠 loopback;问答页另有 key 认证) */
|
|
20147
|
-
const ACP_HTTP_HOST = FACE_PORTS.acpHttp.host ?? "127.0.0.1";
|
|
20148
|
-
function acpHttpActive() {
|
|
20149
|
-
return faceActive(FACE_ACP_HTTP);
|
|
20150
|
-
}
|
|
20151
|
-
function readBody$1(req) {
|
|
20152
|
-
return new Promise((resolve, reject) => {
|
|
20153
|
-
const chunks = [];
|
|
20154
|
-
req.on("data", (c) => chunks.push(c));
|
|
20155
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
20156
|
-
req.on("error", reject);
|
|
20157
|
-
});
|
|
20158
|
-
}
|
|
20159
|
-
function sendJson$1(res, status, payload) {
|
|
20160
|
-
const body = jscSafeJsonText(JSON.stringify(payload));
|
|
20161
|
-
res.writeHead(status, {
|
|
20162
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
20163
|
-
"Cache-Control": "no-store"
|
|
20164
|
-
});
|
|
20165
|
-
res.end(body);
|
|
20166
|
-
}
|
|
20167
|
-
function sendHtml(res, html) {
|
|
20168
|
-
res.writeHead(200, {
|
|
20169
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
20170
|
-
"Cache-Control": "no-store"
|
|
20171
|
-
});
|
|
20172
|
-
res.end(html);
|
|
20173
|
-
}
|
|
20174
|
-
/**
|
|
20175
|
-
* 本面的规格(C4 块 A/④:**只提供** name/port/host/enabled/handler)。
|
|
20176
|
-
* 两个子面(ACP JSON-RPC / 对外问答页)的路由与鉴权仍全部归 {@link handle}——
|
|
20177
|
-
* 面宿主不介入面语义,也**不合并** D/E 两面(两者共用会话核心但各有独立 listener/路由/HTML)。
|
|
20178
|
-
*/
|
|
20179
|
-
function acpHttpSpec(ctx, port, defaultRoot) {
|
|
20180
|
-
return {
|
|
20181
|
-
name: FACE_ACP_HTTP,
|
|
20182
|
-
port,
|
|
20183
|
-
host: ACP_HTTP_HOST,
|
|
20184
|
-
enabled: () => {
|
|
20185
|
-
const s = readSimpleSettings();
|
|
20186
|
-
return s.acpEnabled || s.publicAskEnabled;
|
|
20187
|
-
},
|
|
20188
|
-
handler: (req, res) => handle(ctx, defaultRoot ?? "", req, res)
|
|
20189
|
-
};
|
|
20190
|
-
}
|
|
20191
|
-
/**
|
|
20192
|
-
* 启动 ACP HTTP JSON-RPC + 建议问答页服务(单实例;重复启动幂等返回既有实例)。
|
|
20193
|
-
* C4 块 A:listener 的创建/监听/错误/端口占用/关闭归 `face-host`。
|
|
20194
|
-
* @param ctx 插件上下文(AcpServer 内部经 skiff-core 创建 agent)
|
|
20195
|
-
* @param port 监听端口(仅 127.0.0.1)
|
|
20196
|
-
* @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
|
|
20197
|
-
*/
|
|
20198
|
-
async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
20199
|
-
if (acpHttpActive()) return;
|
|
20200
|
-
const handle = await startFace(acpHttpSpec(ctx, port, defaultRoot));
|
|
20201
|
-
const s = readSimpleSettings();
|
|
20202
|
-
const faces = [];
|
|
20203
|
-
if (s.acpEnabled) faces.push("JSON-RPC");
|
|
20204
|
-
if (s.publicAskEnabled) faces.push("问答页(key 认证)");
|
|
20205
|
-
console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${handle.port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
|
|
20206
|
-
}
|
|
20207
|
-
function stopAcpHttpServer() {
|
|
20208
|
-
stopFace(FACE_ACP_HTTP);
|
|
20209
|
-
}
|
|
20210
|
-
async function handle(ctx, defaultRoot, req, res) {
|
|
20211
|
-
try {
|
|
20212
|
-
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
20213
|
-
const s = readSimpleSettings();
|
|
20214
|
-
const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
|
|
20215
|
-
if (req.method === "POST" && url === "/") {
|
|
20216
|
-
if (!s.acpEnabled) {
|
|
20217
|
-
sendJson$1(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
|
|
20218
|
-
return;
|
|
20219
|
-
}
|
|
20220
|
-
let body;
|
|
20221
|
-
try {
|
|
20222
|
-
body = JSON.parse(await readBody$1(req));
|
|
20223
|
-
} catch {
|
|
20224
|
-
sendJson$1(res, 400, [{
|
|
20225
|
-
jsonrpc: "2.0",
|
|
20226
|
-
id: null,
|
|
20227
|
-
error: {
|
|
20228
|
-
code: -32700,
|
|
20229
|
-
message: "parse error"
|
|
20230
|
-
}
|
|
20231
|
-
}]);
|
|
20232
|
-
return;
|
|
20233
|
-
}
|
|
20234
|
-
sendJson$1(res, 200, await dispatchRpc(new AcpServer(ctx), body));
|
|
20235
|
-
return;
|
|
20236
|
-
}
|
|
20237
|
-
if (!s.publicAskEnabled) {
|
|
20238
|
-
if (req.method === "GET" && (url === "/" || url.startsWith("/c/"))) {
|
|
20239
|
-
sendHtml(res, publicAskDisabledHtml());
|
|
20240
|
-
return;
|
|
20241
|
-
}
|
|
20242
|
-
if (req.method === "POST" && (url === "/ask" || url.startsWith("/c/"))) {
|
|
20243
|
-
sendJson$1(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
|
|
20244
|
-
return;
|
|
20245
|
-
}
|
|
20246
|
-
sendJson$1(res, 404, { error: "not found" });
|
|
20247
|
-
return;
|
|
20248
|
-
}
|
|
20249
|
-
const containerAllowed = (name) => {
|
|
20250
|
-
const allowed = readAdvancedSettings().publicAsk.allowed;
|
|
20251
|
-
return allowed.length === 0 || allowed.includes(name);
|
|
20252
|
-
};
|
|
20253
|
-
let cccsCache = null;
|
|
20254
|
-
const cccsOf = async () => {
|
|
20255
|
-
if (cccsCache === null) cccsCache = await listCccs(ctx, {
|
|
20256
|
-
defaultRoot: defaultRoot || (cccRootForCwd(process.cwd()) ?? ""),
|
|
20257
|
-
withRoles: true
|
|
20258
|
-
});
|
|
20259
|
-
return cccsCache;
|
|
20260
|
-
};
|
|
20261
|
-
const cMatch = /^\/c\/([^/]+)$/.exec(url);
|
|
20262
|
-
if (req.method === "GET" && cMatch) {
|
|
20263
|
-
const name = decodeURIComponent(cMatch[1] ?? "");
|
|
20264
|
-
const ccc = (await cccsOf()).find((c) => c.name === name);
|
|
20265
|
-
if (!ccc) {
|
|
20266
|
-
sendHtml(res, publicAskContainerUnknownHtml(name));
|
|
20267
|
-
return;
|
|
20268
|
-
}
|
|
20269
|
-
if (!containerAllowed(name)) {
|
|
20270
|
-
sendHtml(res, publicAskContainerClosedHtml(name));
|
|
20271
|
-
return;
|
|
20272
|
-
}
|
|
20273
|
-
sendHtml(res, publicAskContainerPage(ccc));
|
|
20274
|
-
return;
|
|
20275
|
-
}
|
|
20276
|
-
if (req.method === "GET" && url === "/") {
|
|
20277
|
-
sendHtml(res, publicAskListPage((await cccsOf()).filter((c) => containerAllowed(c.name))));
|
|
20278
|
-
return;
|
|
20279
|
-
}
|
|
20280
|
-
const aMatch = /^\/c\/([^/]+)\/ask$/.exec(url);
|
|
20281
|
-
if (req.method === "POST" && aMatch) {
|
|
20282
|
-
const name = decodeURIComponent(aMatch[1] ?? "");
|
|
20283
|
-
const ccc = (await cccsOf()).find((c) => c.name === name);
|
|
20284
|
-
if (!ccc || !containerAllowed(name)) {
|
|
20285
|
-
sendJson$1(res, 403, { error: `container "${name}" is not open for public ask` });
|
|
20286
|
-
return;
|
|
20287
|
-
}
|
|
20288
|
-
let body;
|
|
20289
|
-
try {
|
|
20290
|
-
const parsed = JSON.parse(await readBody$1(req));
|
|
20291
|
-
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
20292
|
-
body = parsed;
|
|
20293
|
-
} catch {
|
|
20294
|
-
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
20295
|
-
return;
|
|
20296
|
-
}
|
|
20297
|
-
await handleAskParsed(ctx, ccc, body, res, ip);
|
|
20298
|
-
return;
|
|
20299
|
-
}
|
|
20300
|
-
if (req.method === "POST" && url === "/ask") {
|
|
20301
|
-
let body;
|
|
20302
|
-
try {
|
|
20303
|
-
const parsed = JSON.parse(await readBody$1(req));
|
|
20304
|
-
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
20305
|
-
body = parsed;
|
|
20306
|
-
} catch {
|
|
20307
|
-
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
20308
|
-
return;
|
|
20309
|
-
}
|
|
20310
|
-
const cccValue = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : void 0;
|
|
20311
|
-
const nameValue = typeof body.name === "string" && body.name !== "" ? body.name : void 0;
|
|
20312
|
-
let ccc;
|
|
20313
|
-
if (nameValue) ccc = (await cccsOf()).find((c) => c.name === nameValue);
|
|
20314
|
-
else if (cccValue) {
|
|
20315
|
-
ccc = (await cccsOf()).find((c) => c.root === cccValue);
|
|
20316
|
-
if (!ccc && cccValue !== "") ccc = {
|
|
20317
|
-
root: cccValue,
|
|
20318
|
-
name: cccValue.split("/").filter(Boolean).pop() ?? cccValue,
|
|
20319
|
-
roles: [...readSkiffRoles(cccValue).keys()]
|
|
20320
|
-
};
|
|
20321
|
-
}
|
|
20322
|
-
if (!ccc || !containerAllowed(ccc.name)) {
|
|
20323
|
-
sendJson$1(res, 403, { error: "container is not open for public ask" });
|
|
20324
|
-
return;
|
|
20325
|
-
}
|
|
20326
|
-
await handleAskParsed(ctx, ccc, body, res, ip);
|
|
20327
|
-
return;
|
|
20328
|
-
}
|
|
20329
|
-
sendJson$1(res, 404, { error: "not found" });
|
|
20330
|
-
} catch (err) {
|
|
20331
|
-
sendJson$1(res, 500, { error: err?.message ?? String(err) });
|
|
20332
|
-
}
|
|
20333
|
-
}
|
|
20334
|
-
/**
|
|
20335
|
-
* 单容器问答核心(/c/<name>/ask 与 /ask 共用):
|
|
20336
|
-
* body { key, role?, question, sessionId? } → key 校验(401 + IP 失败锁定)→ 角色缺省取 CCC 第一个
|
|
20337
|
-
* → 会话延续(v1.25.10 语义,静默新建)→ askSkiff(0 全量轨迹) → { answer, answer_html, sessionId, continued, trajectory }
|
|
20338
|
-
* @param ip 请求来源 IP(v1.26.5 公网 key 失败锁定:X-Forwarded-For 优先——公网反代,回退 remoteAddress)
|
|
20339
|
-
*/
|
|
20340
|
-
async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
20341
|
-
const key = typeof body.key === "string" ? body.key : void 0;
|
|
20342
|
-
if (isPublicAskIpLocked(ip)) {
|
|
20343
|
-
sendJson$1(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
|
|
20344
|
-
return;
|
|
20345
|
-
}
|
|
20346
|
-
if (!verifyPublicAskKey(key)) {
|
|
20347
|
-
const remaining = recordPublicAskFail(ip);
|
|
20348
|
-
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" });
|
|
20349
|
-
return;
|
|
20350
|
-
}
|
|
20351
|
-
resetPublicAskIpFail(ip);
|
|
20352
|
-
const root = ccc.root;
|
|
20353
|
-
const roleName = typeof body.role === "string" && body.role !== "" ? body.role : void 0;
|
|
20354
|
-
const question = typeof body.question === "string" ? body.question : "";
|
|
20355
|
-
const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
|
|
20356
|
-
if (!question.trim()) {
|
|
20357
|
-
sendJson$1(res, 400, { error: "empty question" });
|
|
20358
|
-
return;
|
|
20359
|
-
}
|
|
20360
|
-
const roles = readSkiffRoles(root);
|
|
20361
|
-
const explicit = roleName ? roles.get(roleName) : void 0;
|
|
20362
|
-
const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
|
|
20363
|
-
const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
|
|
20364
|
-
if (!effectiveRole || !effectiveCfg) {
|
|
20365
|
-
sendJson$1(res, 400, { error: `no skiff role available in ccc: ${root}` });
|
|
20366
|
-
return;
|
|
20367
|
-
}
|
|
20368
|
-
let agent;
|
|
20369
|
-
let continued = false;
|
|
20370
|
-
if (sessionId) {
|
|
20371
|
-
const info = skiffSessionInfo(sessionId);
|
|
20372
|
-
const live = getSkiffAgent(sessionId);
|
|
20373
|
-
if (info && live && info.role === effectiveRole && info.ccc === root) {
|
|
20374
|
-
agent = live;
|
|
20375
|
-
continued = true;
|
|
20376
|
-
}
|
|
20377
|
-
}
|
|
20378
|
-
if (!agent) agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, readCheapWorkerConfig(root)?.defaultModel)).agent;
|
|
20379
|
-
const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
|
|
20380
|
-
sendJson$1(res, 200, {
|
|
20381
|
-
answer: result.answer,
|
|
20382
|
-
answer_html: renderSkiffMarkdown(result.answer, true),
|
|
20383
|
-
sessionId: result.sessionId,
|
|
20384
|
-
continued
|
|
20385
|
-
});
|
|
20386
|
-
}
|
|
20387
|
-
/** 问答页未启用提示页 */
|
|
20388
|
-
function publicAskDisabledHtml() {
|
|
20389
|
-
return `<!DOCTYPE html>
|
|
20390
|
-
<html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
|
|
20391
|
-
<body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
|
|
20392
|
-
<h2>Serenity Public Ask</h2>
|
|
20393
|
-
<p>问答页未启用(请在 dsh 设置面板「Serenity」页开启「建议问答」)。</p>
|
|
20394
|
-
</body></html>`;
|
|
20395
|
-
}
|
|
20396
|
-
/** 容器不存在提示页(URL 容器名不在已发现 CCC 中) */
|
|
20397
|
-
function publicAskContainerUnknownHtml(name) {
|
|
20398
|
-
return `<!DOCTYPE html>
|
|
20399
|
-
<html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
|
|
20400
|
-
<body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
|
|
20401
|
-
<h2>Serenity Public Ask</h2>
|
|
20402
|
-
<p>认知容器「${name}」不存在。</p>
|
|
20403
|
-
<p style="font-size:13px"><a href="/" style="color:#0ba875">返回容器列表</a></p>
|
|
20404
|
-
</body></html>`;
|
|
20405
|
-
}
|
|
20406
|
-
/** 容器未开放提示页(白名单外) */
|
|
20407
|
-
function publicAskContainerClosedHtml(name) {
|
|
20408
|
-
return `<!DOCTYPE html>
|
|
20409
|
-
<html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
|
|
20410
|
-
<body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
|
|
20411
|
-
<h2>Serenity Public Ask</h2>
|
|
20412
|
-
<p>认知容器「${name}」当前未开放问答(管理员在设置面板「开放容器」中配置)。</p>
|
|
20413
|
-
<p style="font-size:13px"><a href="/" style="color:#0ba875">返回容器列表</a></p>
|
|
20414
|
-
</body></html>`;
|
|
20415
|
-
}
|
|
20416
|
-
/** 页面基础样式(列表页 + 单容器页共用;v1.26.4 聊天 UI) */
|
|
20417
|
-
const ASK_CSS = `
|
|
20418
|
-
:root { color-scheme: light dark; --sp-green: #0ba875; --sp-green-dim: #059669; }
|
|
20419
|
-
* { box-sizing: border-box; }
|
|
20420
|
-
html, body { height: 100%; }
|
|
20421
|
-
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; background: #f6f7f9; color: #1f2328; }
|
|
20422
|
-
@media (prefers-color-scheme: dark) { body { background: #16181c; color: #e6e6e6; } }
|
|
20423
|
-
main { max-width: 720px; margin: 0 auto; }
|
|
20424
|
-
h1 { font-size: 18px; margin: 0 0 4px; }
|
|
20425
|
-
.sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
|
|
20426
|
-
.card { background: #fff; border: 1px solid #d0d7de; border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; display: block; text-decoration: none; color: inherit; }
|
|
20427
|
-
@media (prefers-color-scheme: dark) { .card { background: #26282c; border-color: #3a3d42; } }
|
|
20428
|
-
.card:hover { border-color: var(--sp-green); }
|
|
20429
|
-
.card .cname { font-weight: 600; font-size: 15px; }
|
|
20430
|
-
.card .cmeta { opacity: .6; font-size: 12px; margin-top: 2px; }
|
|
20431
|
-
.empty { text-align: center; opacity: .7; padding: 24px 0; }
|
|
20432
|
-
.ccname { font-weight: 600; color: var(--sp-green); }
|
|
20433
|
-
/* key 门(v1.26.6:选容器前必填) */
|
|
20434
|
-
.keyGate { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; background: rgba(127,127,127,.06); border: 1px solid rgba(127,127,127,.18); border-radius: 10px; padding: 10px 14px; }
|
|
20435
|
-
.keyGate label { font-size: 12px; opacity: .7; white-space: nowrap; }
|
|
20436
|
-
.keyGate input { flex: 1; border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 8px 12px; font-size: 14px; }
|
|
20437
|
-
.keyGate input:focus { outline: none; border-color: var(--sp-green); }
|
|
20438
|
-
|
|
20439
|
-
/* ── 聊天布局(v1.26.4 体验升级:参考 ChatGPT/Claude 会话形态)── */
|
|
20440
|
-
.chat { display: flex; flex-direction: column; height: 100vh; max-width: 720px; margin: 0 auto; }
|
|
20441
|
-
.chatHead { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-bottom: 1px solid rgba(127,127,127,.18); background: rgba(127,127,127,.04); position: sticky; top: 0; z-index: 5; }
|
|
20442
|
-
.chatHead .back { color: var(--sp-green); text-decoration: none; font-size: 13px; white-space: nowrap; }
|
|
20443
|
-
.chatHead .title { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
20444
|
-
.chatHead .spacer { flex: 1; }
|
|
20445
|
-
.chatHead select { border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 4px 8px; font-size: 12px; }
|
|
20446
|
-
.chatHead .newBtn { border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 4px 10px; font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
20447
|
-
.chatHead .newBtn:hover { border-color: var(--sp-green); color: var(--sp-green); }
|
|
20448
|
-
/* 消息流 */
|
|
20449
|
-
.msgList { flex: 1; overflow-y: auto; padding: 18px 16px; display: flex; flex-direction: column; gap: 16px; }
|
|
20450
|
-
.msg { display: flex; flex-direction: column; max-width: 88%; }
|
|
20451
|
-
.msg.user { align-self: flex-end; align-items: flex-end; }
|
|
20452
|
-
.msg.assistant { align-self: flex-start; align-items: flex-start; }
|
|
20453
|
-
.msg .who { font-size: 11px; opacity: .55; margin-bottom: 4px; padding: 0 4px; }
|
|
20454
|
-
.msg .bubble { padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.65; word-break: break-word; white-space: normal; }
|
|
20455
|
-
.msg.user .bubble { background: var(--sp-green); color: #fff; border-bottom-right-radius: 4px; }
|
|
20456
|
-
.msg.assistant .bubble { background: #fff; border: 1px solid #d0d7de; border-bottom-left-radius: 4px; }
|
|
20457
|
-
@media (prefers-color-scheme: dark) { .msg.assistant .bubble { background: #26282c; border-color: #3a3d42; } }
|
|
20458
|
-
.bubble p { margin: 6px 0; }
|
|
20459
|
-
.bubble p:first-child { margin-top: 0; }
|
|
20460
|
-
.bubble p:last-child { margin-bottom: 0; }
|
|
20461
|
-
.bubble h1, .bubble h2, .bubble h3, .bubble h4 { margin: 12px 0 6px; font-size: 15px; line-height: 1.4; }
|
|
20462
|
-
.bubble ul, .bubble ol { margin: 6px 0; padding-left: 22px; }
|
|
20463
|
-
.bubble code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, monospace; }
|
|
20464
|
-
.bubble pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 8px 0; }
|
|
20465
|
-
.bubble pre code { background: none; padding: 0; }
|
|
20466
|
-
.bubble blockquote { margin: 6px 0; padding: 2px 12px; border-left: 3px solid rgba(127,127,127,.3); opacity: .85; }
|
|
20467
|
-
.bubble a { color: var(--sp-green); }
|
|
20468
|
-
.bubble table { border-collapse: collapse; margin: 8px 0; }
|
|
20469
|
-
.bubble th, .bubble td { border: 1px solid rgba(127,127,127,.25); padding: 4px 10px; font-size: 13px; }
|
|
20470
|
-
.bubble img { max-width: 100%; border-radius: 8px; }
|
|
20471
|
-
.bubble hr { border: 0; border-top: 1px solid rgba(127,127,127,.2); margin: 10px 0; }
|
|
20472
|
-
.typing { display: inline-flex; align-items: center; gap: 4px; padding: 4px 2px; }
|
|
20473
|
-
.typing i { width: 6px; height: 6px; border-radius: 50%; background: rgba(127,127,127,.5); animation: blink 1.2s infinite; }
|
|
20474
|
-
.typing i:nth-child(2) { animation-delay: .2s; }
|
|
20475
|
-
.typing i:nth-child(3) { animation-delay: .4s; }
|
|
20476
|
-
@keyframes blink { 0%, 80%, 100% { opacity: .3 } 40% { opacity: 1 } }
|
|
20477
|
-
.err { color: #cf222e; white-space: pre-wrap; font-size: 13px; }
|
|
20478
|
-
.msg .err { padding: 0 4px; }
|
|
20479
|
-
/* 输入区 */
|
|
20480
|
-
.chatInput { border-top: 1px solid rgba(127,127,127,.18); padding: 10px 16px calc(12px + env(safe-area-inset-bottom)); background: rgba(127,127,127,.04); }
|
|
20481
|
-
.inputRow { display: flex; align-items: flex-end; gap: 8px; }
|
|
20482
|
-
.inputRow textarea { flex: 1; border: 1px solid rgba(127,127,127,.25); border-radius: 12px; background: #fff; color: inherit; padding: 10px 12px; font-size: 14px; resize: none; min-height: 44px; max-height: 140px; line-height: 1.5; }
|
|
20483
|
-
@media (prefers-color-scheme: dark) { .inputRow textarea { background: #26282c; border-color: #3a3d42; } }
|
|
20484
|
-
.inputRow textarea:focus { outline: none; border-color: var(--sp-green); }
|
|
20485
|
-
.inputRow .sendBtn { border: 0; border-radius: 12px; background: var(--sp-green); color: #fff; padding: 10px 18px; font-size: 14px; font-weight: 600; cursor: pointer; white-space: nowrap; }
|
|
20486
|
-
.inputRow .sendBtn:disabled { opacity: .5; cursor: wait; }
|
|
20487
|
-
.inputRow .sendBtn:hover:not(:disabled) { background: var(--sp-green-dim); }
|
|
20488
|
-
.hint { text-align: center; font-size: 12px; opacity: .55; padding: 20px 0; }
|
|
20489
|
-
|
|
20490
|
-
/* ── 移动端适配(v1.26.6:手机/平板——iOS 防放大 16px / 触控 ≥44px / 100dvh / safe-area)── */
|
|
20491
|
-
@media (max-width: 640px) {
|
|
20492
|
-
main { padding: 20px 16px calc(28px + env(safe-area-inset-bottom)); }
|
|
20493
|
-
.sub { font-size: 14px; margin-bottom: 14px; }
|
|
20494
|
-
.keyGate { flex-wrap: wrap; gap: 6px; }
|
|
20495
|
-
.keyGate input { flex: 1 1 100%; font-size: 16px; padding: 12px 14px; min-height: 48px; } /* 16px 防 iOS 聚焦放大 + 触控 ≥44px */
|
|
20496
|
-
.card { padding: 16px; border-radius: 12px; }
|
|
20497
|
-
.card .cname { font-size: 16px; }
|
|
20498
|
-
.card .cmeta { font-size: 13px; }
|
|
20499
|
-
.chat { height: 100vh; height: 100dvh; } /* dvh:iOS 地址栏收起/展开不跳动 */
|
|
20500
|
-
.chatHead { gap: 8px; padding: 8px 12px calc(8px + env(safe-area-inset-top)); }
|
|
20501
|
-
.chatHead .back { font-size: 14px; padding: 10px 4px; } /* 触控目标 ≥44px */
|
|
20502
|
-
.chatHead .title { font-size: 14px; }
|
|
20503
|
-
.chatHead select { font-size: 16px; padding: 8px 6px; min-height: 44px; } /* 16px 防放大 + 触控 */
|
|
20504
|
-
.chatHead .newBtn { font-size: 14px; padding: 10px 12px; min-height: 44px; }
|
|
20505
|
-
.msgList { padding: 14px 12px calc(20px + env(safe-area-inset-bottom)); gap: 14px; }
|
|
20506
|
-
.msg { max-width: 94%; }
|
|
20507
|
-
.msg .bubble { font-size: 15px; padding: 12px 14px; }
|
|
20508
|
-
.msg .who { font-size: 12px; }
|
|
20509
|
-
.chatInput { padding: 8px 12px calc(10px + env(safe-area-inset-bottom)); }
|
|
20510
|
-
.inputRow textarea { font-size: 16px; padding: 12px 14px; min-height: 48px; } /* 16px 防放大 + 触控 */
|
|
20511
|
-
.inputRow .sendBtn { font-size: 16px; padding: 12px 20px; min-height: 48px; }
|
|
20512
|
-
}
|
|
20513
|
-
`;
|
|
20514
|
-
/** 容器列表页(v1.26.2 → v1.26.6):key 在选容器前填写(v1.26.6 用户拍板:不填 key 不让选容器) */
|
|
20515
|
-
function publicAskListPage(cccs) {
|
|
20516
|
-
const cards = cccs.map((c) => `
|
|
20517
|
-
<a class="card" data-name="${c.name}" href="/c/${encodeURIComponent(c.name)}">
|
|
20518
|
-
<div class="cname">${c.name}</div>
|
|
20519
|
-
<div class="cmeta">${c.roles.length} 个问答角色 · 点击进入</div>
|
|
20520
|
-
</a>`).join("") || "<div class=\"empty\">暂无可问答的认知容器(管理员尚未开放)</div>";
|
|
20521
|
-
return `<!DOCTYPE html>
|
|
20522
|
-
<html lang="zh">
|
|
20523
|
-
<head>
|
|
20524
|
-
<meta charset="utf-8">
|
|
20525
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
20526
|
-
<title>Serenity Public Ask</title>
|
|
20527
|
-
<style>${ASK_CSS}</style>
|
|
20528
|
-
</head>
|
|
20529
|
-
<body>
|
|
20530
|
-
<main>
|
|
20531
|
-
<h1>Serenity Public Ask</h1>
|
|
20532
|
-
<div class="sub">先填写访问 key,再选择认知容器开始提问</div>
|
|
20533
|
-
<div class="keyGate">
|
|
20534
|
-
<label for="gateKey">访问 Key</label>
|
|
20535
|
-
<input id="gateKey" type="password" placeholder="请输入访问 key(仅本次浏览器记忆)" autocomplete="off" autocapitalize="none" enterkeyhint="done">
|
|
20536
|
-
</div>
|
|
20537
|
-
<div id="cards">${cards}</div>
|
|
20538
|
-
</main>
|
|
20539
|
-
<script>
|
|
20540
|
-
const KEY_STORE = 'serenity-public-ask-key'
|
|
20541
|
-
const gateKey = document.getElementById('gateKey')
|
|
20542
|
-
const cardList = document.getElementById('cards')
|
|
20543
|
-
// 容器卡片(key 未填时禁用——v1.26.6 用户拍板:key 在选容器前填写)
|
|
20544
|
-
function applyGate() {
|
|
20545
|
-
const ok = gateKey.value.trim().length > 0
|
|
20546
|
-
for (const card of cardList.querySelectorAll('a.card')) {
|
|
20547
|
-
card.classList.toggle('locked', !ok)
|
|
20548
|
-
card.setAttribute('aria-disabled', ok ? 'false' : 'true')
|
|
20549
|
-
if (!ok) { card.removeAttribute('href'); card.style.pointerEvents = 'none'; card.style.opacity = '.4' }
|
|
20550
|
-
else { card.setAttribute('href', '/c/' + encodeURIComponent(card.dataset.name)); card.style.pointerEvents = ''; card.style.opacity = '' }
|
|
20551
|
-
}
|
|
20552
|
-
}
|
|
20553
|
-
gateKey.addEventListener('input', () => {
|
|
20554
|
-
// 即存 localStorage(v1.26.7 修复:v1.26.6 漏掉保存——填一次就记录,对话页读取)
|
|
20555
|
-
try { localStorage.setItem(KEY_STORE, gateKey.value.trim()) } catch {}
|
|
20556
|
-
applyGate()
|
|
20557
|
-
})
|
|
20558
|
-
// key 自动从 localStorage 恢复
|
|
20559
|
-
let hasSavedKey = false
|
|
20560
|
-
try { const saved = localStorage.getItem(KEY_STORE); if (saved) { gateKey.value = saved; hasSavedKey = true } } catch {}
|
|
20561
|
-
if (hasSavedKey) gateKey.placeholder = '访问 Key(已记忆,可修改)'
|
|
20562
|
-
applyGate()
|
|
20563
|
-
gateKey.focus()
|
|
20564
|
-
<\/script>
|
|
20565
|
-
</body>
|
|
20566
|
-
</html>`;
|
|
20567
|
-
}
|
|
20568
|
-
/** 单容器问答页(v1.26.2→v1.26.6):URL 锁定容器;聊天 UI——消息流 + 连续对话 + key 从列表页记忆(v1.26.6 用户拍板:对话页不再填 key) */
|
|
20569
|
-
function publicAskContainerPage(ccc) {
|
|
20570
|
-
const data = jscSafeJsonText(JSON.stringify({
|
|
20571
|
-
name: ccc.name,
|
|
20572
|
-
root: ccc.root,
|
|
20573
|
-
roles: ccc.roles
|
|
20574
|
-
}).replace(/</g, "\\u003c"));
|
|
20575
|
-
return `<!DOCTYPE html>
|
|
20576
|
-
<html lang="zh">
|
|
20577
|
-
<head>
|
|
20578
|
-
<meta charset="utf-8">
|
|
20579
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
20580
|
-
<title>Serenity · ${ccc.name}</title>
|
|
20581
|
-
<style>${ASK_CSS}</style>
|
|
20582
|
-
</head>
|
|
20583
|
-
<body>
|
|
20584
|
-
<div class="chat">
|
|
20585
|
-
<div class="chatHead">
|
|
20586
|
-
<a class="back" href="/">← 容器</a>
|
|
20587
|
-
<span class="title">Serenity · <span class="ccname">${ccc.name}</span></span>
|
|
20588
|
-
<span class="spacer"></span>
|
|
20589
|
-
<select id="role" title="问答角色"></select>
|
|
20590
|
-
<button id="newChat" class="newBtn" title="清空当前对话(不存储,刷新即失)">新对话</button>
|
|
20591
|
-
</div>
|
|
20592
|
-
|
|
20593
|
-
<div id="msgs" class="msgList"></div>
|
|
20594
|
-
|
|
20595
|
-
<div class="chatInput">
|
|
20596
|
-
<div class="inputRow">
|
|
20597
|
-
<textarea id="q" placeholder="向 ${ccc.name} 提问…(Enter 发送,Shift+Enter 换行)" rows="1" enterkeyhint="send" autocapitalize="sentences"></textarea>
|
|
20598
|
-
<button id="ask" class="sendBtn">发送</button>
|
|
20599
|
-
</div>
|
|
20600
|
-
</div>
|
|
20601
|
-
</div>
|
|
20602
|
-
<script id="ask-data" type="application/json">${data}<\/script>
|
|
20603
|
-
<script>
|
|
20604
|
-
const CCC = JSON.parse(document.getElementById('ask-data').textContent)
|
|
20605
|
-
const KEY_STORE = 'serenity-public-ask-key'
|
|
20606
|
-
const roleSel = document.getElementById('role')
|
|
20607
|
-
const btn = document.getElementById('ask')
|
|
20608
|
-
const msgs = document.getElementById('msgs')
|
|
20609
|
-
const qInput = document.getElementById('q')
|
|
20610
|
-
let sessionId = ''
|
|
20611
|
-
let busy = false
|
|
20612
|
-
|
|
20613
|
-
// 角色下拉
|
|
20614
|
-
roleSel.innerHTML = (CCC.roles || []).map((r) => '<option value="' + r + '">' + r + '</option>').join('') || '<option value="">(无角色)</option>'
|
|
20615
|
-
// key 从 localStorage 读取(列表页填写时记忆;对话页不填——v1.26.6 用户拍板)
|
|
20616
|
-
function getStoredKey() {
|
|
20617
|
-
try { return (localStorage.getItem(KEY_STORE) || '').trim() } catch { return '' }
|
|
20618
|
-
}
|
|
20619
|
-
|
|
20620
|
-
/** 追加一条消息气泡(role: user|assistant) */
|
|
20621
|
-
function addMsg(role, htmlOrText, isHtml) {
|
|
20622
|
-
const m = document.createElement('div')
|
|
20623
|
-
m.className = 'msg ' + role
|
|
20624
|
-
const who = document.createElement('div')
|
|
20625
|
-
who.className = 'who'
|
|
20626
|
-
who.textContent = role === 'user' ? '你' : CCC.name
|
|
20627
|
-
const bubble = document.createElement('div')
|
|
20628
|
-
bubble.className = 'bubble'
|
|
20629
|
-
if (isHtml) bubble.innerHTML = htmlOrText
|
|
20630
|
-
else bubble.textContent = htmlOrText
|
|
20631
|
-
m.appendChild(who)
|
|
20632
|
-
m.appendChild(bubble)
|
|
20633
|
-
msgs.appendChild(m)
|
|
20634
|
-
msgs.scrollTop = msgs.scrollHeight
|
|
20635
|
-
return bubble
|
|
20636
|
-
}
|
|
20637
|
-
|
|
20638
|
-
/** 打字指示器(等待回答时显示) */
|
|
20639
|
-
function addTyping() {
|
|
20640
|
-
const m = document.createElement('div')
|
|
20641
|
-
m.className = 'msg assistant'
|
|
20642
|
-
const who = document.createElement('div')
|
|
20643
|
-
who.className = 'who'
|
|
20644
|
-
who.textContent = CCC.name
|
|
20645
|
-
const bubble = document.createElement('div')
|
|
20646
|
-
bubble.className = 'bubble'
|
|
20647
|
-
bubble.innerHTML = '<span class="typing"><i></i><i></i><i></i></span>'
|
|
20648
|
-
m.appendChild(who)
|
|
20649
|
-
m.appendChild(bubble)
|
|
20650
|
-
msgs.appendChild(m)
|
|
20651
|
-
msgs.scrollTop = msgs.scrollHeight
|
|
20652
|
-
return bubble
|
|
20653
|
-
}
|
|
20654
|
-
|
|
20655
|
-
async function ask() {
|
|
20656
|
-
const q = qInput.value.trim()
|
|
20657
|
-
const key = getStoredKey()
|
|
20658
|
-
const role = roleSel.value || undefined
|
|
20659
|
-
if (!q) return
|
|
20660
|
-
if (!key) { addMsg('assistant', '⚠️ 未找到访问 key,请先回到 <a href="/">容器列表页</a> 填写 key。', true); return }
|
|
20661
|
-
if (busy) return
|
|
20662
|
-
// 追加用户消息 + 打字指示
|
|
20663
|
-
addMsg('user', q, false)
|
|
20664
|
-
qInput.value = ''
|
|
20665
|
-
autoGrow()
|
|
20666
|
-
const typing = addTyping()
|
|
20667
|
-
busy = true
|
|
20668
|
-
btn.disabled = true
|
|
20669
|
-
try {
|
|
20670
|
-
const res = await fetch('/c/' + encodeURIComponent(CCC.name) + '/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, role, question: q, sessionId: sessionId || undefined }) })
|
|
20671
|
-
const data = await res.json()
|
|
20672
|
-
typing.remove() // 移除打字指示,替换为真实回答
|
|
20673
|
-
if (!res.ok) {
|
|
20674
|
-
sessionId = ''
|
|
20675
|
-
addMsg('assistant', '⚠️ ' + (data.error || ('HTTP ' + res.status)), true)
|
|
20676
|
-
return
|
|
20677
|
-
}
|
|
20678
|
-
addMsg('assistant', data.answer_html || data.answer || '(空回答)', true)
|
|
20679
|
-
sessionId = data.sessionId || ''
|
|
20680
|
-
} catch (err) {
|
|
20681
|
-
typing.remove()
|
|
20682
|
-
addMsg('assistant', '⚠️ ' + String(err.message || err), true)
|
|
20683
|
-
} finally {
|
|
20684
|
-
busy = false
|
|
20685
|
-
btn.disabled = false
|
|
20686
|
-
qInput.focus()
|
|
20687
|
-
}
|
|
20688
|
-
}
|
|
20689
|
-
|
|
20690
|
-
function autoGrow() {
|
|
20691
|
-
qInput.style.height = 'auto'
|
|
20692
|
-
qInput.style.height = Math.min(qInput.scrollHeight, 140) + 'px'
|
|
20693
|
-
}
|
|
20694
|
-
|
|
20695
|
-
btn.addEventListener('click', () => void ask())
|
|
20696
|
-
qInput.addEventListener('keydown', (e) => {
|
|
20697
|
-
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void ask() }
|
|
20698
|
-
})
|
|
20699
|
-
qInput.addEventListener('input', autoGrow)
|
|
20700
|
-
// 新对话:重置 sessionId + 清空消息流(前端内存,不存储——刷新即失)
|
|
20701
|
-
document.getElementById('newChat').addEventListener('click', () => {
|
|
20702
|
-
sessionId = ''
|
|
20703
|
-
msgs.innerHTML = ''
|
|
20704
|
-
qInput.focus()
|
|
20705
|
-
})
|
|
20706
|
-
|
|
20707
|
-
// 初始欢迎消息(外层模板求值注入容器名;textContent 渲染防注入)
|
|
20708
|
-
addMsg('assistant', '你好,我是「${ccc.name}」的认知助手。有什么可以帮你的?(本对话仅当前页面有效,刷新即失)', false)
|
|
20709
|
-
qInput.focus()
|
|
20710
|
-
<\/script>
|
|
20711
|
-
</body>
|
|
20712
|
-
</html>`;
|
|
20713
|
-
}
|
|
20714
|
-
//#endregion
|
|
20715
19814
|
//#region src/human-channel.ts
|
|
20716
19815
|
/**
|
|
20717
19816
|
* human-channel.ts — 人机信道的**机制层**(机器级;2026-09-26 owner 定案 ⇒ 方案 §2.6)
|
|
@@ -21033,7 +20132,7 @@ function buildManualOutputRebuke(session) {
|
|
|
21033
20132
|
}
|
|
21034
20133
|
/**
|
|
21035
20134
|
* 装配闸门(index.ts apply 调用)。
|
|
21036
|
-
* 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff
|
|
20135
|
+
* 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff 会话)零干预。
|
|
21037
20136
|
*/
|
|
21038
20137
|
function registerWeixinOutputGuard(ctx) {
|
|
21039
20138
|
try {
|
|
@@ -21394,7 +20493,7 @@ function manualOutputFallbackNeeded(input) {
|
|
|
21394
20493
|
*/
|
|
21395
20494
|
async function resolveCccRootByAlias(ctx, alias) {
|
|
21396
20495
|
try {
|
|
21397
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20496
|
+
const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
|
|
21398
20497
|
const entries = await listCccs(ctx);
|
|
21399
20498
|
const byName = entries.find((e) => e.name === alias);
|
|
21400
20499
|
if (byName) return byName.root;
|
|
@@ -21893,7 +20992,7 @@ function syncHumanChannel(ctx) {
|
|
|
21893
20992
|
(async () => {
|
|
21894
20993
|
try {
|
|
21895
20994
|
stopMachineLoops();
|
|
21896
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20995
|
+
const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
|
|
21897
20996
|
for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
|
|
21898
20997
|
} catch {}
|
|
21899
20998
|
})();
|
|
@@ -22129,7 +21228,7 @@ function matchCcc(input, candidates) {
|
|
|
22129
21228
|
}
|
|
22130
21229
|
/** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
22131
21230
|
async function collectCandidates(ctx) {
|
|
22132
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21231
|
+
const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
|
|
22133
21232
|
const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
|
|
22134
21233
|
const seen = /* @__PURE__ */ new Set();
|
|
22135
21234
|
const out = [];
|
|
@@ -22395,7 +21494,7 @@ function registerLifecycle(ctx) {
|
|
|
22395
21494
|
stopAllBridges();
|
|
22396
21495
|
} catch {}
|
|
22397
21496
|
};
|
|
22398
|
-
registerDisposer(ctx, "self-started resources (gateway/weixin/skiff-debug
|
|
21497
|
+
registerDisposer(ctx, "self-started resources (gateway/weixin/skiff-debug faces + weixin bridges)", disposeAll);
|
|
22399
21498
|
}
|
|
22400
21499
|
//#endregion
|
|
22401
21500
|
//#region src/web-fetch-provider.ts
|
|
@@ -24597,7 +23696,7 @@ function renderDsl(model) {
|
|
|
24597
23696
|
* 🔴 **为什么 PNG 是必需的**:宿主的对话内图片通路**白名单只有光栅格式**
|
|
24598
23697
|
* (`image/png|jpeg|webp|gif`,见 `dsh-attachment/types.d.ts`)⇒ **SVG 进不去**。
|
|
24599
23698
|
*
|
|
24600
|
-
* 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 skiff
|
|
23699
|
+
* 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 skiff 口径。
|
|
24601
23700
|
*
|
|
24602
23701
|
* ## 🔴 两条**宿主契约**(2026-09-29 现场取证;第 ① 条是本文件第一版写错的地方)
|
|
24603
23702
|
*
|
|
@@ -24971,10 +24070,6 @@ const Config = z.object({
|
|
|
24971
24070
|
enabled: z.boolean().default(false),
|
|
24972
24071
|
debugPort: z.number().min(1024).max(65535).default(SKIFF_DEBUG_PORT)
|
|
24973
24072
|
}),
|
|
24974
|
-
acp: z.object({
|
|
24975
|
-
enabled: z.boolean().default(false),
|
|
24976
|
-
httpPort: z.number().min(1024).max(65535).default(ACP_HTTP_PORT)
|
|
24977
|
-
}),
|
|
24978
24073
|
webFetch: z.object({ enabled: z.boolean().default(true) }),
|
|
24979
24074
|
opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
|
|
24980
24075
|
visionPatch: z.object({ enabled: z.boolean().default(true) }),
|
|
@@ -24989,9 +24084,6 @@ const Config = z.object({
|
|
|
24989
24084
|
rebuildThresholdK: z.number().min(50).max(4e3).volatile(),
|
|
24990
24085
|
skiffEnabled: z.boolean().volatile(),
|
|
24991
24086
|
skiffDebugPort: z.number().min(1024).max(65535).volatile(),
|
|
24992
|
-
acpEnabled: z.boolean().volatile(),
|
|
24993
|
-
acpHttpPort: z.number().min(1024).max(65535).volatile(),
|
|
24994
|
-
publicAskEnabled: z.boolean().volatile(),
|
|
24995
24087
|
croEnabled: z.boolean().volatile(),
|
|
24996
24088
|
unattendedEnabled: z.boolean().volatile(),
|
|
24997
24089
|
diagramEnabled: z.boolean().volatile()
|
|
@@ -25054,7 +24146,6 @@ function apply(ctx, config) {
|
|
|
25054
24146
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
25055
24147
|
registerBootstrap(ctx);
|
|
25056
24148
|
registerSkiff(ctx);
|
|
25057
|
-
registerAcp(ctx);
|
|
25058
24149
|
registerCroTurnTracking(ctx);
|
|
25059
24150
|
registerWakeScheduler(ctx);
|
|
25060
24151
|
registerWeixinBridge(ctx);
|
|
@@ -25078,7 +24169,7 @@ function apply(ctx, config) {
|
|
|
25078
24169
|
* ① 听宿主的**真事件** `settings/document-updated`(ns = 我们那张设置页)——顺带说明为什么不用
|
|
25079
24170
|
* 我们自己的 `serenity/settings-changed`:那个事件**全仓没有任何 `emit` 点**(只有 `ctx.on` 侧
|
|
25080
24171
|
* 在用)=**死通道**,照着它接线等于接了个永远不响的铃;
|
|
25081
|
-
* ② 兜底再挂 `agent/created` / `session/created`(与 skiff
|
|
24172
|
+
* ② 兜底再挂 `agent/created` / `session/created`(与 skiff 同款做法,那两条在实测里确实会 fire)
|
|
25082
24173
|
* —— 宿主事件面若换名,至少还有这一条能自愈;
|
|
25083
24174
|
* ③ 启动时同步一次(apply 时按当前配置定初始态)。
|
|
25084
24175
|
* 🔴 **关的时候要注销**(不是"留着但不响应"):与"关时不注册"同一口径 —— 工具不出现在模型清单里。
|
|
@@ -25125,7 +24216,8 @@ function registerDiagramTool(ctx) {
|
|
|
25125
24216
|
* 同步一次——此刻通常还没有 live 会话,进程 cwd(服务启动目录)也不在 CCC 内 →
|
|
25126
24217
|
* `resolveSkiffRoot` 返回 null → 打印一行警告后**永不重试**(实证:重启日志
|
|
25127
24218
|
* `✗ Skiff 调试服务未启动:无法定位 CCC root`,`ss -ltn` 无 3099;同批 ACP 因容忍
|
|
25128
|
-
* `root ?? undefined`
|
|
24219
|
+
* `root ?? undefined` 而正常启动 —— ⚠️ **该对照已随面 E 退役而失效**(2026-10-15),
|
|
24220
|
+
* 但本条记录的**病因与修法不受影响**(Skiff 侧仍是"首次未定位 → 曾永不重试")。
|
|
25129
24221
|
*
|
|
25130
24222
|
* v1.30.14(重启日志实证后的收敛):**并发启动守卫** + 首次未定位降级为信息级日志。
|
|
25131
24223
|
* - 实证(v1.30.13 重启日志):定时触发与"会话就绪"事件几乎同时到达 → 两次 sync 都
|
|
@@ -25249,29 +24341,5 @@ function readWebPort(ctx) {
|
|
|
25249
24341
|
return MAIN_WEB_PORT;
|
|
25250
24342
|
}
|
|
25251
24343
|
}
|
|
25252
|
-
/**
|
|
25253
|
-
* F4c ACP HTTP + F4d 建议问答页装配(v1.26.x 实验性):启停 = 人工(设置面板
|
|
25254
|
-
* 「Serenity」页 ACP / 问答页开关,settings 持久化)。任一面开启即启动服务
|
|
25255
|
-
* (同一端口:POST / = JSON-RPC 需 acpEnabled;GET / + /ask = 问答页需 publicAskEnabled)。
|
|
25256
|
-
* 会话创建/延续走 acp-core → skiff-core;仅监听 127.0.0.1;未开启零资源占用。
|
|
25257
|
-
*/
|
|
25258
|
-
function registerAcp(ctx) {
|
|
25259
|
-
const sync = async () => {
|
|
25260
|
-
const s = readSimpleSettings();
|
|
25261
|
-
const anyFace = s.acpEnabled || s.publicAskEnabled;
|
|
25262
|
-
if (anyFace && !acpHttpActive()) {
|
|
25263
|
-
const root = await resolveSkiffRoot(ctx);
|
|
25264
|
-
return startAcpHttpServer(ctx, s.acpHttpPort, root ?? void 0).catch((err) => {
|
|
25265
|
-
console.error(`[serenity-hooks] ✗ ACP HTTP 服务启动失败: ${String(err?.message ?? err)}`);
|
|
25266
|
-
});
|
|
25267
|
-
} else if (!anyFace && acpHttpActive()) stopAcpHttpServer();
|
|
25268
|
-
};
|
|
25269
|
-
try {
|
|
25270
|
-
ctx.on("serenity/settings-changed", () => {
|
|
25271
|
-
sync();
|
|
25272
|
-
});
|
|
25273
|
-
} catch {}
|
|
25274
|
-
sync();
|
|
25275
|
-
}
|
|
25276
24344
|
//#endregion
|
|
25277
24345
|
export { Config, apply, humanChannelAccountSchema, humanChannelSubscriptionSchema, inject, name };
|