@shgroup/dsh-serenity-hooks 1.57.0 → 1.57.2
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/assets/eap-paper-LICENSE.txt +21 -0
- package/assets/eap-paper.md +492 -0
- 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 +197 -1079
- 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/eap.d.ts +33 -3
- package/lib/tools/praxis.d.ts +4 -2
- 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 +3 -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;
|
|
@@ -4411,36 +4302,69 @@ const msmTool = defineTool({
|
|
|
4411
4302
|
//#endregion
|
|
4412
4303
|
//#region src/tools/eap.ts
|
|
4413
4304
|
/**
|
|
4414
|
-
* eap.ts — EAP
|
|
4305
|
+
* eap.ts — EAP 内容源(**完整论文全文**,v1.30 三工具合并为 praxis 后只保留内容源)
|
|
4306
|
+
*
|
|
4307
|
+
* 🔴 本文件的形态由一条 owner 令决定(2026-10-15,S142 续 125):
|
|
4308
|
+
* 原实现内嵌一份约 1.5 KB 的「简易说明」(三变量表 + 自检清单),而 `praxis eap`
|
|
4309
|
+
* 的职责是**注入 EAP 论文全文**。owner 原话:「praxis · eap 放的不是 eap 完整论文全文
|
|
4310
|
+
* 而是简易说明,这是不行的,要求改成全文」。
|
|
4311
|
+
*
|
|
4312
|
+
* ⇒ 正文**不再内嵌**(58 KB 塞进 TS 模板字符串还要转义论文里 4 处 ```mermaid 围栏——
|
|
4313
|
+
* 本容器为此踩过四次),改为**随包的静态资产 + 运行时读取**:
|
|
4314
|
+
* 资产 = `assets/eap-paper.md`(**逐字**来自上游论文,未经改写)
|
|
4315
|
+
* 读取 = `loadEapPaper()`(进程内缓存一次;同 `voyage-page.ts` 的既有先例)
|
|
4316
|
+
*
|
|
4317
|
+
* 🔴 **单一真相源**:正文只在 `assets/eap-paper.md` 一个地方。本文件负责**定位与读取**,
|
|
4318
|
+
* `praxis.ts` 只负责**路由**。二者都不得内嵌任何论文片段——否则又是"第二份会漂的正文"。
|
|
4415
4319
|
*
|
|
4416
|
-
*
|
|
4320
|
+
* 上游:https://github.com/tellmewhattodo/theory-eap(MIT,许可证随包见
|
|
4321
|
+
* `assets/eap-paper-LICENSE.txt`;同 `assets/diagram/*.ttf` 的既有做法)。
|
|
4417
4322
|
*/
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4323
|
+
/**
|
|
4324
|
+
* 资产相对包根的路径。
|
|
4325
|
+
* 🔒 它**必须**同时出现在两处,否则发布包会缺文件、`praxis eap` 在**运行态**才报错:
|
|
4326
|
+
* ① `package.json` 的 `files` 白名单;
|
|
4327
|
+
* ② `scripts/dsh-develop.ts` 的 `verifyTarball` / `deploy` 的 `required` 数组。
|
|
4328
|
+
* 同 v1.26.15 事故形态(`files` 白名单漏运行时必需文件)。
|
|
4329
|
+
*/
|
|
4330
|
+
const EAP_PAPER_ASSET = "assets/eap-paper.md";
|
|
4331
|
+
/**
|
|
4332
|
+
* 包根 = **向上找到含 `package.json` 的那一级**。
|
|
4333
|
+
*
|
|
4334
|
+
* 🔴 为什么不写 `join(__dirname, '..')`:本模块在 `src/tools/` 里(发布态 `lib/tools/`),
|
|
4335
|
+
* 一次 `..` 只到 `src/`,于是会去找 `src/assets/eap-paper.md` —— **实测 ENOENT**,
|
|
4336
|
+
* 而症状("论文不在")与真因(路径层级算错)长得完全不同。
|
|
4337
|
+
* 按 `package.json` 上溯对**开发态(vitest 直跑 TS)与发布态(lib/)都成立**。
|
|
4338
|
+
* 与 `src/diagram/rasterize.ts` 的 `packageRootPath()` 同一形态(该处已由实测固化)。
|
|
4339
|
+
*
|
|
4340
|
+
* @returns 包根绝对路径;四层内找不到 `package.json` 时回落"上一级"(不抛——定位失败由调用方响亮报错)
|
|
4341
|
+
*/
|
|
4342
|
+
function packageRootPath$2() {
|
|
4343
|
+
const start = dirname(fileURLToPath(import.meta.url));
|
|
4344
|
+
let dir = start;
|
|
4345
|
+
for (let i = 0; i < 4; i++) {
|
|
4346
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
4347
|
+
dir = dirname(dir);
|
|
4348
|
+
}
|
|
4349
|
+
return join(start, "..");
|
|
4350
|
+
}
|
|
4351
|
+
/** 论文资产的绝对路径(导出以便测试与 pack-check 共用同一判据来源) */
|
|
4352
|
+
function eapPaperPath() {
|
|
4353
|
+
return join(packageRootPath$2(), EAP_PAPER_ASSET);
|
|
4354
|
+
}
|
|
4355
|
+
/** 缓存(58 KB;`praxis eap` 可能被反复调用——进程内只读一次) */
|
|
4356
|
+
let cachedPaper = null;
|
|
4357
|
+
/**
|
|
4358
|
+
* 读取论文全文。**读不到即抛错** —— 绝不静默返回空串或退回一份"简易说明":
|
|
4359
|
+
* 那种降级会把"资产缺失"伪装成"调用成功",而本工具的全部价值就是那份正文。
|
|
4360
|
+
*/
|
|
4361
|
+
function loadEapPaper(assetPath = eapPaperPath()) {
|
|
4362
|
+
if (assetPath === eapPaperPath()) {
|
|
4363
|
+
if (cachedPaper === null) cachedPaper = readFileSync(assetPath, "utf-8");
|
|
4364
|
+
return cachedPaper;
|
|
4365
|
+
}
|
|
4366
|
+
return readFileSync(assetPath, "utf-8");
|
|
4367
|
+
}
|
|
4444
4368
|
//#endregion
|
|
4445
4369
|
//#region src/tools/neat.ts
|
|
4446
4370
|
/**
|
|
@@ -4843,12 +4767,29 @@ Both OEPE review stages run in an isolated context. Three rules make them work:
|
|
|
4843
4767
|
* Metaphor 映射:Engineering Drawings(图纸 = 可实践理论的编码,第 3 条隐喻)。
|
|
4844
4768
|
*
|
|
4845
4769
|
* section 参数渐进披露:不传 = 返回精简目录;传 = 返回对应框架全文/章节。
|
|
4846
|
-
*
|
|
4770
|
+
* 每个框架一个内容源(单一真相源):neat.ts / cce.ts / ontology.ts;
|
|
4771
|
+
* 🔴 **eap 的内容源是随包资产 `assets/eap-paper.md`**(论文全文,58 KB)——
|
|
4772
|
+
* 读它在 `tools/eap.ts`(本文件只路由,不内嵌任何正文;见该文件的成因注释)。
|
|
4847
4773
|
*/
|
|
4848
|
-
/** 知识源:与各原工具内容保持一致(单一真相源——
|
|
4774
|
+
/** 知识源:与各原工具内容保持一致(单一真相源——neat/cce/ontology 各自源文件已并入此处) */
|
|
4775
|
+
/**
|
|
4776
|
+
* 读 EAP 论文全文。
|
|
4777
|
+
*
|
|
4778
|
+
* 🔴 **绝不降级**:缺资产时抛错(带解析到的绝对路径),不返回空串、不退回"简易说明"——
|
|
4779
|
+
* 那种降级会把"资产缺失"伪装成"调用成功",而本工具的全部价值就是那份正文
|
|
4780
|
+
* (这正是 2026-10-15 owner 令要修的缺陷形态)。
|
|
4781
|
+
*/
|
|
4782
|
+
function eapPaper() {
|
|
4783
|
+
try {
|
|
4784
|
+
return loadEapPaper();
|
|
4785
|
+
} catch (err) {
|
|
4786
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4787
|
+
throw new Error(`EAP paper asset unavailable (${EAP_PAPER_ASSET}): ${msg} — resolved: ${eapPaperPath()}`);
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4849
4790
|
/** 框架清单(无参调用时返回的精简目录) */
|
|
4850
4791
|
const PRAXIS_INDEX = `praxis — 可实践理论注入(认知框架,按需加载)
|
|
4851
|
-
选择框架:praxis eap(认知质量——
|
|
4792
|
+
选择框架:praxis eap(认知质量——EAP 论文全文)
|
|
4852
4793
|
praxis neat(Neat 协作协议——小步对齐/显式决策/文档驱动)
|
|
4853
4794
|
praxis cce(认知连续性工程——容器 5 行为约束 + H_op 操作熵)
|
|
4854
4795
|
praxis ontology(本体论工程方法群——触发/编码/四组判据 M·L·D·V/OEPE 循环/公理/反模式)
|
|
@@ -4862,7 +4803,7 @@ function renderText$7(value) {
|
|
|
4862
4803
|
}
|
|
4863
4804
|
const praxisTool = defineTool({
|
|
4864
4805
|
name: "praxis",
|
|
4865
|
-
description: "Praxis — actionable theory injection (cognitive frameworks, loaded on demand): eap (
|
|
4806
|
+
description: "Praxis — actionable theory injection (cognitive frameworks, loaded on demand): eap (the EXPLICIT ABSTRACTION PRINCIPLE — the full paper: motivation, definitions, the three variables E↑/R↓/S↑, language-as-interface, empirical cases, theoretical corollaries, formal proof) / neat (Neat design collaboration protocol: small-step alignment, explicit decisions, document-driven) / cce (Cognitive Continuity Engineering: container identity/accessibility/evolution under bounded resources) / ontology (ontology engineering method family: when to model, Turtle encoding, four judgement sets M1-M10 structural / L1-L5 semantic hints / D1-D8 semantic review incl. OntoClean / V1-V4 runtime compliance, the OEPE audit loop, workflows, universal axioms, anti-patterns). No section returns the index; pass a section to load that framework.",
|
|
4866
4807
|
parameters: { section: {
|
|
4867
4808
|
type: "string",
|
|
4868
4809
|
enum: [
|
|
@@ -4878,7 +4819,7 @@ const praxisTool = defineTool({
|
|
|
4878
4819
|
render: (_args, value) => renderText$7(value)
|
|
4879
4820
|
},
|
|
4880
4821
|
async execute(args) {
|
|
4881
|
-
if (args.section === "eap") return
|
|
4822
|
+
if (args.section === "eap") return eapPaper();
|
|
4882
4823
|
if (args.section === "neat") return NEAT_CONTENT;
|
|
4883
4824
|
if (args.section === "cce") return CCE_CONTENT;
|
|
4884
4825
|
if (args.section === "ontology") return ONTOLOGY_CONTENT;
|
|
@@ -12275,11 +12216,10 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
|
|
|
12275
12216
|
|
|
12276
12217
|
── 运行 ──
|
|
12277
12218
|
调试:设置面板「Serenity」页 Skiff 区块开启 → http://127.0.0.1:<debugPort> 问答页实测
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
session/cancel / session/close / session/list / request_permission(恒 allow)
|
|
12219
|
+
🔴 外部程序化接入(2026-10-15 起改走官方):本插件自造的 ACP 面(HTTP JSON-RPC 3100 /
|
|
12220
|
+
建议问答页)**已整体退役**(owner 裁「砍掉外部问答」)。现在要程序化/远程接入,走宿主
|
|
12221
|
+
自带的官方 ACP server:dsh --profile acp(纯 stdio、零认证)+ 任一 ACP 客户端
|
|
12222
|
+
(手机端如 Agmente / ACP UI)经隧道接入。详见 S142 的接入指南文档。
|
|
12283
12223
|
启停 = 人工(设置面板开关,不随插件加载自动启动);未开启零资源占用
|
|
12284
12224
|
生效机制:角色配置**实时读取**(改 .opencode/serenity.json → 刷新调试页即生效);
|
|
12285
12225
|
改配置后用 container_admin role apply 做显式校验 + 应用确认(绑定 CCC + 角色清单)
|
|
@@ -12660,7 +12600,9 @@ function claimsUserNotification(text) {
|
|
|
12660
12600
|
/**
|
|
12661
12601
|
* **不许开**的会话(设计 §3 的"建议写死",本版**确实写死**):
|
|
12662
12602
|
* `skiff-`(F4 问答 / **微信桥会话也是这个前缀** —— 桥那侧的"用户"是**家人**,
|
|
12663
|
-
* 代理替家人说话 = **冒充**)| `acp
|
|
12603
|
+
* 代理替家人说话 = **冒充**)| `acp-`(对外程序化面 —— ⚠️ **2026-10-15 起已成死判据**:
|
|
12604
|
+
* 该前缀从无生产者且面 E 已退役;理由与"为何保留"见 `output-guard-seam.ts` 文件头)|
|
|
12605
|
+
* `cheap-worker-`(自主 worker,已有自己的完成码循环)。
|
|
12664
12606
|
*
|
|
12665
12607
|
* ⚠️ **`rebuild-` 不在排除面**(与 `output-guard-seam.ts` 的取向**刻意不同**):重建会话是
|
|
12666
12608
|
* **同一轨迹的载体**(内部维护面),不是对外面;把它排除会让"重建后就没人推了"。
|
|
@@ -14733,7 +14675,6 @@ const UPLOAD_PATH = "/serenity/image-upload";
|
|
|
14733
14675
|
const FILE_UPLOAD_PATH = "/serenity/file-upload";
|
|
14734
14676
|
const CONFIG_PATH = "/serenity/config";
|
|
14735
14677
|
const CCCS_PATH = "/serenity/cccs";
|
|
14736
|
-
const PUBLIC_ASK_PATH = "/serenity/public-ask";
|
|
14737
14678
|
const WEIXIN_PATH = "/serenity/weixin";
|
|
14738
14679
|
/** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
|
|
14739
14680
|
const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
|
|
@@ -14817,7 +14758,7 @@ function saveFileToTmp(root, fileName, data) {
|
|
|
14817
14758
|
writeFileSync(join(dir, filename), bytes);
|
|
14818
14759
|
return `${FILE_UPLOAD_DIR}/${filename}`;
|
|
14819
14760
|
}
|
|
14820
|
-
function readBody$
|
|
14761
|
+
function readBody$3(req, maxBytes) {
|
|
14821
14762
|
return new Promise((resolve, reject) => {
|
|
14822
14763
|
let data = "";
|
|
14823
14764
|
req.on("data", (chunk) => {
|
|
@@ -14831,7 +14772,7 @@ function readBody$4(req, maxBytes) {
|
|
|
14831
14772
|
req.on("error", reject);
|
|
14832
14773
|
});
|
|
14833
14774
|
}
|
|
14834
|
-
function sendJson$
|
|
14775
|
+
function sendJson$3(res, code, body) {
|
|
14835
14776
|
const payload = JSON.stringify(body);
|
|
14836
14777
|
res.writeHead(code, {
|
|
14837
14778
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -14856,7 +14797,7 @@ function senderIsWebUi(req) {
|
|
|
14856
14797
|
*/
|
|
14857
14798
|
function requireWebUi(req, res, message) {
|
|
14858
14799
|
if (senderIsWebUi(req)) return true;
|
|
14859
|
-
sendJson$
|
|
14800
|
+
sendJson$3(res, 403, { error: message });
|
|
14860
14801
|
return false;
|
|
14861
14802
|
}
|
|
14862
14803
|
/**
|
|
@@ -14889,11 +14830,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14889
14830
|
handler: async (req, res) => {
|
|
14890
14831
|
try {
|
|
14891
14832
|
if (req.method !== "POST") {
|
|
14892
|
-
sendJson$
|
|
14833
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
14893
14834
|
return;
|
|
14894
14835
|
}
|
|
14895
14836
|
if (!requireWebUi(req, res, "图片落盘仅限 WebUI(client 专用)")) return;
|
|
14896
|
-
const raw = await readBody$
|
|
14837
|
+
const raw = await readBody$3(req, 20971520);
|
|
14897
14838
|
const body = JSON.parse(raw);
|
|
14898
14839
|
const workspace = resolveWorkspace(ctx, {
|
|
14899
14840
|
sessionId: body.sessionId,
|
|
@@ -14901,12 +14842,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14901
14842
|
});
|
|
14902
14843
|
const root = cccRootForCwd(workspace);
|
|
14903
14844
|
if (!root) {
|
|
14904
|
-
sendJson$
|
|
14845
|
+
sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
14905
14846
|
return;
|
|
14906
14847
|
}
|
|
14907
|
-
sendJson$
|
|
14848
|
+
sendJson$3(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
14908
14849
|
} catch (err) {
|
|
14909
|
-
sendJson$
|
|
14850
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
14910
14851
|
}
|
|
14911
14852
|
}
|
|
14912
14853
|
});
|
|
@@ -14916,11 +14857,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14916
14857
|
handler: async (req, res) => {
|
|
14917
14858
|
try {
|
|
14918
14859
|
if (req.method !== "POST") {
|
|
14919
|
-
sendJson$
|
|
14860
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
14920
14861
|
return;
|
|
14921
14862
|
}
|
|
14922
14863
|
if (!requireWebUi(req, res, "文件落盘仅限 WebUI(client 专用)")) return;
|
|
14923
|
-
const raw = await readBody$
|
|
14864
|
+
const raw = await readBody$3(req, 20971520);
|
|
14924
14865
|
const body = JSON.parse(raw);
|
|
14925
14866
|
const workspace = resolveWorkspace(ctx, {
|
|
14926
14867
|
sessionId: body.sessionId,
|
|
@@ -14928,12 +14869,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14928
14869
|
});
|
|
14929
14870
|
const root = cccRootForCwd(workspace);
|
|
14930
14871
|
if (!root) {
|
|
14931
|
-
sendJson$
|
|
14872
|
+
sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
14932
14873
|
return;
|
|
14933
14874
|
}
|
|
14934
|
-
sendJson$
|
|
14875
|
+
sendJson$3(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
14935
14876
|
} catch (err) {
|
|
14936
|
-
sendJson$
|
|
14877
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
14937
14878
|
}
|
|
14938
14879
|
}
|
|
14939
14880
|
});
|
|
@@ -14949,7 +14890,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14949
14890
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
14950
14891
|
}), configPaths);
|
|
14951
14892
|
const runtime = ctx.get("codeRuntime");
|
|
14952
|
-
sendJson$
|
|
14893
|
+
sendJson$3(res, 200, {
|
|
14953
14894
|
...status,
|
|
14954
14895
|
...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
|
|
14955
14896
|
language: runtime.language,
|
|
@@ -14960,7 +14901,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14960
14901
|
}
|
|
14961
14902
|
if (req.method === "POST") {
|
|
14962
14903
|
if (!requireWebUi(req, res, "safe-mode 切换仅限 WebUI(agent 不可自行开关)")) return;
|
|
14963
|
-
const raw = await readBody$
|
|
14904
|
+
const raw = await readBody$3(req, 65536);
|
|
14964
14905
|
const body = JSON.parse(raw);
|
|
14965
14906
|
const workspace = resolveWorkspace(ctx, {
|
|
14966
14907
|
sessionId: body.sessionId,
|
|
@@ -14968,19 +14909,19 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14968
14909
|
});
|
|
14969
14910
|
const root = cccRootForCwd(workspace);
|
|
14970
14911
|
if (!root) {
|
|
14971
|
-
sendJson$
|
|
14912
|
+
sendJson$3(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
14972
14913
|
return;
|
|
14973
14914
|
}
|
|
14974
14915
|
const result = setSafeMode(root, body.on === true);
|
|
14975
|
-
sendJson$
|
|
14916
|
+
sendJson$3(res, 200, {
|
|
14976
14917
|
...getStatus(workspace, configPaths),
|
|
14977
14918
|
...result
|
|
14978
14919
|
});
|
|
14979
14920
|
return;
|
|
14980
14921
|
}
|
|
14981
|
-
sendJson$
|
|
14922
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
14982
14923
|
} catch (err) {
|
|
14983
|
-
sendJson$
|
|
14924
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
14984
14925
|
}
|
|
14985
14926
|
}
|
|
14986
14927
|
});
|
|
@@ -14996,24 +14937,24 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14996
14937
|
try {
|
|
14997
14938
|
known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
|
|
14998
14939
|
} catch {}
|
|
14999
|
-
sendJson$
|
|
14940
|
+
sendJson$3(res, 200, {
|
|
15000
14941
|
config: toWire(settings),
|
|
15001
14942
|
knownWorkspaces: known
|
|
15002
14943
|
});
|
|
15003
14944
|
return;
|
|
15004
14945
|
}
|
|
15005
14946
|
if (req.method === "PUT") {
|
|
15006
|
-
const raw = await readBody$
|
|
14947
|
+
const raw = await readBody$3(req, 131072);
|
|
15007
14948
|
const saved = applyWirePatch(JSON.parse(raw).config ?? {});
|
|
15008
14949
|
try {
|
|
15009
14950
|
ctx.emit?.("serenity/config-updated");
|
|
15010
14951
|
} catch {}
|
|
15011
|
-
sendJson$
|
|
14952
|
+
sendJson$3(res, 200, { config: toWire(saved) });
|
|
15012
14953
|
return;
|
|
15013
14954
|
}
|
|
15014
|
-
sendJson$
|
|
14955
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15015
14956
|
} catch (err) {
|
|
15016
|
-
sendJson$
|
|
14957
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
15017
14958
|
}
|
|
15018
14959
|
}
|
|
15019
14960
|
});
|
|
@@ -15023,7 +14964,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15023
14964
|
handler: async (req, res) => {
|
|
15024
14965
|
try {
|
|
15025
14966
|
if (req.method !== "GET") {
|
|
15026
|
-
sendJson$
|
|
14967
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15027
14968
|
return;
|
|
15028
14969
|
}
|
|
15029
14970
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -15032,54 +14973,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15032
14973
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
15033
14974
|
});
|
|
15034
14975
|
const root = cccRootForCwd(workspace) ?? "";
|
|
15035
|
-
sendJson$
|
|
14976
|
+
sendJson$3(res, 200, { cccs: await listCccs(ctx, {
|
|
15036
14977
|
defaultRoot: root,
|
|
15037
14978
|
withRoles: true
|
|
15038
14979
|
}) });
|
|
15039
14980
|
} 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) });
|
|
14981
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
15083
14982
|
}
|
|
15084
14983
|
}
|
|
15085
14984
|
});
|
|
@@ -15093,7 +14992,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15093
14992
|
if (req.method === "GET") {
|
|
15094
14993
|
const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
|
|
15095
14994
|
if (!chk.ok) {
|
|
15096
|
-
sendJson$
|
|
14995
|
+
sendJson$3(res, 400, { error: chk.error });
|
|
15097
14996
|
return;
|
|
15098
14997
|
}
|
|
15099
14998
|
const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
@@ -15107,7 +15006,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15107
15006
|
enabled: a.enabled !== false,
|
|
15108
15007
|
bound: readWeixinCredential(chk.root, a.accountId) !== null
|
|
15109
15008
|
}));
|
|
15110
|
-
sendJson$
|
|
15009
|
+
sendJson$3(res, 200, {
|
|
15111
15010
|
enabled: settings.enabled === true,
|
|
15112
15011
|
botType: settings.botType ?? void 0,
|
|
15113
15012
|
accounts,
|
|
@@ -15117,14 +15016,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15117
15016
|
return;
|
|
15118
15017
|
}
|
|
15119
15018
|
if (req.method !== "POST") {
|
|
15120
|
-
sendJson$
|
|
15019
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15121
15020
|
return;
|
|
15122
15021
|
}
|
|
15123
|
-
const raw = await readBody$
|
|
15022
|
+
const raw = await readBody$3(req, 131072);
|
|
15124
15023
|
const body = JSON.parse(raw);
|
|
15125
15024
|
const chk = requireCcc(body.ccc);
|
|
15126
15025
|
if (!chk.ok) {
|
|
15127
|
-
sendJson$
|
|
15026
|
+
sendJson$3(res, 400, { error: chk.error });
|
|
15128
15027
|
return;
|
|
15129
15028
|
}
|
|
15130
15029
|
const root = chk.root;
|
|
@@ -15141,7 +15040,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15141
15040
|
startedAt: Date.now(),
|
|
15142
15041
|
polling: false
|
|
15143
15042
|
});
|
|
15144
|
-
sendJson$
|
|
15043
|
+
sendJson$3(res, 200, {
|
|
15145
15044
|
qrcode: qr.qrcode,
|
|
15146
15045
|
qrcode_img_content: qr.qrcode_img_content,
|
|
15147
15046
|
loginKey
|
|
@@ -15150,31 +15049,31 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15150
15049
|
}
|
|
15151
15050
|
if (body.action === "remove-account") {
|
|
15152
15051
|
if (!body.accountId) {
|
|
15153
|
-
sendJson$
|
|
15052
|
+
sendJson$3(res, 400, { error: "missing accountId" });
|
|
15154
15053
|
return;
|
|
15155
15054
|
}
|
|
15156
15055
|
const { removeWeixinAccount } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15157
15056
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15158
15057
|
removeWeixinAccount(root, body.accountId);
|
|
15159
15058
|
syncCccBridge(ctx, root);
|
|
15160
|
-
sendJson$
|
|
15059
|
+
sendJson$3(res, 200, { removed: body.accountId });
|
|
15161
15060
|
return;
|
|
15162
15061
|
}
|
|
15163
15062
|
if (body.action === "save-routes") {
|
|
15164
15063
|
const routes = body.routes ?? [];
|
|
15165
15064
|
if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
|
|
15166
|
-
sendJson$
|
|
15065
|
+
sendJson$3(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
15167
15066
|
return;
|
|
15168
15067
|
}
|
|
15169
15068
|
const { saveWeixinRoutes } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15170
15069
|
const { readSkiffRoles } = await import("./skiff-role-B615L9MV.js").then((n) => n.u);
|
|
15171
15070
|
const roles = readSkiffRoles(root);
|
|
15172
15071
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
15173
|
-
sendJson$
|
|
15072
|
+
sendJson$3(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
|
|
15174
15073
|
return;
|
|
15175
15074
|
}
|
|
15176
15075
|
saveWeixinRoutes(root, routes);
|
|
15177
|
-
sendJson$
|
|
15076
|
+
sendJson$3(res, 200, { saved: routes.length });
|
|
15178
15077
|
return;
|
|
15179
15078
|
}
|
|
15180
15079
|
if (body.action === "set-enabled") {
|
|
@@ -15183,7 +15082,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15183
15082
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15184
15083
|
const settings = readWeixinSettings(root);
|
|
15185
15084
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
15186
|
-
sendJson$
|
|
15085
|
+
sendJson$3(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
|
|
15187
15086
|
return;
|
|
15188
15087
|
}
|
|
15189
15088
|
setWeixinEnabled(root, enabled);
|
|
@@ -15192,12 +15091,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15192
15091
|
const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15193
15092
|
stopCccBridge(root);
|
|
15194
15093
|
}
|
|
15195
|
-
sendJson$
|
|
15094
|
+
sendJson$3(res, 200, { enabled });
|
|
15196
15095
|
return;
|
|
15197
15096
|
}
|
|
15198
|
-
sendJson$
|
|
15097
|
+
sendJson$3(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
|
|
15199
15098
|
} catch (err) {
|
|
15200
|
-
sendJson$
|
|
15099
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
15201
15100
|
}
|
|
15202
15101
|
}
|
|
15203
15102
|
});
|
|
@@ -15208,18 +15107,18 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15208
15107
|
try {
|
|
15209
15108
|
if (!requireWebUi(req, res, "微信桥配置仅限 WebUI(client 专用)")) return;
|
|
15210
15109
|
if (req.method !== "GET") {
|
|
15211
|
-
sendJson$
|
|
15110
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15212
15111
|
return;
|
|
15213
15112
|
}
|
|
15214
15113
|
const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
|
|
15215
15114
|
const login = weixinLogins.get(key);
|
|
15216
15115
|
if (!login) {
|
|
15217
|
-
sendJson$
|
|
15116
|
+
sendJson$3(res, 404, { error: "login not found or expired" });
|
|
15218
15117
|
return;
|
|
15219
15118
|
}
|
|
15220
15119
|
if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
|
|
15221
15120
|
weixinLogins.delete(key);
|
|
15222
|
-
sendJson$
|
|
15121
|
+
sendJson$3(res, 200, { status: "expired" });
|
|
15223
15122
|
return;
|
|
15224
15123
|
}
|
|
15225
15124
|
const { pollQRStatus } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
|
|
@@ -15232,7 +15131,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15232
15131
|
switch (status.status) {
|
|
15233
15132
|
case "confirmed": {
|
|
15234
15133
|
if (!status.bot_token) {
|
|
15235
|
-
sendJson$
|
|
15134
|
+
sendJson$3(res, 200, {
|
|
15236
15135
|
status: "error",
|
|
15237
15136
|
error: "confirmed but no bot_token"
|
|
15238
15137
|
});
|
|
@@ -15253,7 +15152,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15253
15152
|
});
|
|
15254
15153
|
syncCccBridge(ctx, login.root);
|
|
15255
15154
|
weixinLogins.delete(key);
|
|
15256
|
-
sendJson$
|
|
15155
|
+
sendJson$3(res, 200, {
|
|
15257
15156
|
status: "confirmed",
|
|
15258
15157
|
accountId,
|
|
15259
15158
|
tokenSaved: true
|
|
@@ -15262,14 +15161,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15262
15161
|
}
|
|
15263
15162
|
case "expired":
|
|
15264
15163
|
weixinLogins.delete(key);
|
|
15265
|
-
sendJson$
|
|
15164
|
+
sendJson$3(res, 200, { status: "expired" });
|
|
15266
15165
|
return;
|
|
15267
15166
|
default:
|
|
15268
|
-
sendJson$
|
|
15167
|
+
sendJson$3(res, 200, { status: status.status ?? "wait" });
|
|
15269
15168
|
return;
|
|
15270
15169
|
}
|
|
15271
15170
|
} catch (err) {
|
|
15272
|
-
sendJson$
|
|
15171
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
15273
15172
|
}
|
|
15274
15173
|
}
|
|
15275
15174
|
});
|
|
@@ -15291,7 +15190,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15291
15190
|
const root = sessionsRootDir();
|
|
15292
15191
|
if (req.method === "GET") {
|
|
15293
15192
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
15294
|
-
sendJson$
|
|
15193
|
+
sendJson$3(res, 200, {
|
|
15295
15194
|
root,
|
|
15296
15195
|
olderThanDays: days,
|
|
15297
15196
|
dryRun: true,
|
|
@@ -15305,12 +15204,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15305
15204
|
return;
|
|
15306
15205
|
}
|
|
15307
15206
|
if (req.method !== "POST") {
|
|
15308
|
-
sendJson$
|
|
15207
|
+
sendJson$3(res, 405, { error: "method not allowed" });
|
|
15309
15208
|
return;
|
|
15310
15209
|
}
|
|
15311
15210
|
if (!requireWebUi(req, res, "会话清理仅限 WebUI(client 专用)")) return;
|
|
15312
15211
|
const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
|
|
15313
|
-
sendJson$
|
|
15212
|
+
sendJson$3(res, 200, {
|
|
15314
15213
|
root,
|
|
15315
15214
|
olderThanDays: days,
|
|
15316
15215
|
dryRun: false,
|
|
@@ -15318,7 +15217,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15318
15217
|
errors: result.errors
|
|
15319
15218
|
});
|
|
15320
15219
|
} catch (err) {
|
|
15321
|
-
sendJson$
|
|
15220
|
+
sendJson$3(res, 400, { error: err.message ?? String(err) });
|
|
15322
15221
|
}
|
|
15323
15222
|
}
|
|
15324
15223
|
});
|
|
@@ -15371,7 +15270,7 @@ function loadVoyageHtml(assetPath = voyageAssetPath()) {
|
|
|
15371
15270
|
return readFileSync(assetPath, "utf-8");
|
|
15372
15271
|
}
|
|
15373
15272
|
/** 发 JSON(错误分支用;与 api.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
|
|
15374
|
-
function sendJson$
|
|
15273
|
+
function sendJson$2(res, status, body) {
|
|
15375
15274
|
res.statusCode = status;
|
|
15376
15275
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
15377
15276
|
res.end(JSON.stringify(body));
|
|
@@ -15386,14 +15285,14 @@ function registerVoyageApi(ctx, opts = {}) {
|
|
|
15386
15285
|
path: VOYAGE_PATH,
|
|
15387
15286
|
handler: (req, res) => {
|
|
15388
15287
|
if (req.method !== "GET") {
|
|
15389
|
-
sendJson$
|
|
15288
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
15390
15289
|
return;
|
|
15391
15290
|
}
|
|
15392
15291
|
let html;
|
|
15393
15292
|
try {
|
|
15394
15293
|
html = loadVoyageHtml(opts.assetPath ?? voyageAssetPath());
|
|
15395
15294
|
} catch (err) {
|
|
15396
|
-
sendJson$
|
|
15295
|
+
sendJson$2(res, 500, { error: `voyage asset unavailable: ${err?.message ?? String(err)}` });
|
|
15397
15296
|
return;
|
|
15398
15297
|
}
|
|
15399
15298
|
res.statusCode = 200;
|
|
@@ -15988,14 +15887,16 @@ function summarizeHostContract(report) {
|
|
|
15988
15887
|
*
|
|
15989
15888
|
* ## 现状 → 本模块要收掉什么(取证:`…/acc-c4c5-current-state.md` §①/§③ 3.1)
|
|
15990
15889
|
*
|
|
15991
|
-
* 插件自己 spawn
|
|
15992
|
-
*
|
|
15890
|
+
* 插件自己 spawn 了**三个** node:http 监听器(B 3081 网关 / C 3082 微信发送 / D 3099 Skiff 调试)。
|
|
15891
|
+
* ⚠️ **2026-10-15 变更**:原为四个(含 **E 3100 ACP+问答**),E 已随 owner「砍掉外部问答」令**整面退役**
|
|
15892
|
+
* ⇒ 仍保有面 B/C/D 三个;本段保留"当年是四个"的记述,因为下方"两套拆卸"的病史由它解释。
|
|
15893
|
+
* 此前四处各写一份同款样板:`createServer` + `server.once('error')` +
|
|
15993
15894
|
* `listen(port, host)` + 模块级 `active` 变量 + `close()`;拆卸也分裂成**两套**——
|
|
15994
15895
|
* `seams/lifecycle.ts` 有一个聚合 disposer,而 `gateway.ts` 与 `weixin-send-api.ts`
|
|
15995
15896
|
* **各自另注册**一个(D/E 干脆没有自己的 disposer)。
|
|
15996
15897
|
*
|
|
15997
15898
|
* 本模块把"listener 生命周期"收成一处:**active 表 + 统一 listen/close + 统一错误与端口占用
|
|
15998
|
-
*
|
|
15899
|
+
* 处理**。各面各自只提供 `{ name, port, host, enabled, handler }`。
|
|
15999
15900
|
*
|
|
16000
15901
|
* ## 🔴 共享的是「生命周期与样板」,**绝不是**「路由 / 监听器 / 鉴权」
|
|
16001
15902
|
*
|
|
@@ -16003,12 +15904,12 @@ function summarizeHostContract(report) {
|
|
|
16003
15904
|
* · **A ↔ C 解耦**(`weixin-send-api.ts:4-7`):3080 会被 3081 原样反代(连请求头),
|
|
16004
15905
|
* 把 3082 的端点挂到 3080 上 = 已登录的外部家庭账号即可冒充 bot 发消息
|
|
16005
15906
|
* ⇒ 本模块**不提供**"把面挂到别的面"的能力,也不共享路由表;
|
|
16006
|
-
* · **E ↔ D**
|
|
15907
|
+
* · (原 **E ↔ D** 共用会话核心但各自独立 listener 一条,**随 E 退役删除**);
|
|
16007
15908
|
* · **B 是 A 的代理宿主**(把请求打到 `127.0.0.1:<主端口>`),不是另一个路由面。
|
|
16008
15909
|
*
|
|
16009
15910
|
* ## 面 A(3080)不在本模块之列
|
|
16010
15911
|
* 它是**宿主 DSH 主 WebUI**,插件只往上挂 `/serenity/*` 路由(`ctx.webServer.register`),
|
|
16011
|
-
* **插件不起这个 listener** ⇒ 面宿主只持有 B/C/D
|
|
15912
|
+
* **插件不起这个 listener** ⇒ 面宿主只持有 B/C/D 三个自起面。
|
|
16012
15913
|
*
|
|
16013
15914
|
* ## 两个入口的分工(这是本模块唯一的"抽象决策",别混)
|
|
16014
15915
|
* · {@link startFace} —— **机械启动**:把一个 handler 绑到 `host:port` 上。同名面若已在监听,
|
|
@@ -16633,7 +16534,7 @@ function createDshCookieProvider(connection, authority) {
|
|
|
16633
16534
|
/** 面名(C4 块 A:listener 生命周期归 `face-host`) */
|
|
16634
16535
|
const FACE_GATEWAY = "gateway";
|
|
16635
16536
|
/** 读取请求体(≤ maxBytes;超限 reject) */
|
|
16636
|
-
function readBody$
|
|
16537
|
+
function readBody$2(req, maxBytes) {
|
|
16637
16538
|
return new Promise((resolve, reject) => {
|
|
16638
16539
|
let data = "";
|
|
16639
16540
|
req.on("data", (chunk) => {
|
|
@@ -16760,7 +16661,7 @@ async function startGateway(config, getAccounts, getDshCookie) {
|
|
|
16760
16661
|
const session = authed(req);
|
|
16761
16662
|
if (session !== void 0) {
|
|
16762
16663
|
if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
|
|
16763
|
-
readBody$
|
|
16664
|
+
readBody$2(req, 131072).then((body) => {
|
|
16764
16665
|
const { rpcId, path } = parseWorkspaceCreateBody(body);
|
|
16765
16666
|
if (!allowWorkspaceCreate) {
|
|
16766
16667
|
res.writeHead(403, { "content-type": "application/json" });
|
|
@@ -19090,7 +18991,7 @@ const SKIFF_DEBUG_HOST = FACE_PORTS.skiffDebug.host ?? "127.0.0.1";
|
|
|
19090
18991
|
function skiffDebugActive() {
|
|
19091
18992
|
return faceActive(FACE_SKIFF_DEBUG);
|
|
19092
18993
|
}
|
|
19093
|
-
function readBody$
|
|
18994
|
+
function readBody$1(req) {
|
|
19094
18995
|
return new Promise((resolve, reject) => {
|
|
19095
18996
|
const chunks = [];
|
|
19096
18997
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -19098,7 +18999,7 @@ function readBody$2(req) {
|
|
|
19098
18999
|
req.on("error", reject);
|
|
19099
19000
|
});
|
|
19100
19001
|
}
|
|
19101
|
-
function sendJson$
|
|
19002
|
+
function sendJson$1(res, status, payload) {
|
|
19102
19003
|
const body = JSON.stringify(payload);
|
|
19103
19004
|
res.writeHead(status, {
|
|
19104
19005
|
"Content-Type": "application/json; charset=utf-8",
|
|
@@ -19106,7 +19007,7 @@ function sendJson$2(res, status, payload) {
|
|
|
19106
19007
|
});
|
|
19107
19008
|
res.end(body);
|
|
19108
19009
|
}
|
|
19109
|
-
function sendHtml
|
|
19010
|
+
function sendHtml(res, html) {
|
|
19110
19011
|
res.writeHead(200, {
|
|
19111
19012
|
"Content-Type": "text/html; charset=utf-8",
|
|
19112
19013
|
"Cache-Control": "no-store"
|
|
@@ -19303,8 +19204,8 @@ function escapeHtml(s) {
|
|
|
19303
19204
|
* 在 JSON **文本层**把它们替换为 `\uXXXX` 转义序列:JSON.parse 后语义完全一致(还原原字符),
|
|
19304
19205
|
* 且 JSC 正则看到的是常规 ASCII 转义(与 JSON.stringify 对控制字符的输出同形态,安全)。
|
|
19305
19206
|
*
|
|
19306
|
-
*
|
|
19307
|
-
*
|
|
19207
|
+
* 3099 调试页客户端 `await res.json()` 与页面内嵌 JSON 均需此兼容层
|
|
19208
|
+
* ——iOS Safari 用户实测"复杂回答"触发。(原同批还列了 3100 问答页,该面已退役,2026-10-15)
|
|
19308
19209
|
*
|
|
19309
19210
|
* @param jsonText JSON.stringify 的输出文本;原地等价替换,返回 JSC 安全文本
|
|
19310
19211
|
*/
|
|
@@ -19432,7 +19333,7 @@ function skiffDebugSpec(ctx, root, port, webPort) {
|
|
|
19432
19333
|
port,
|
|
19433
19334
|
host: SKIFF_DEBUG_HOST,
|
|
19434
19335
|
enabled: () => readSimpleSettings().skiffEnabled,
|
|
19435
|
-
handler: (req, res) => handle
|
|
19336
|
+
handler: (req, res) => handle(ctx, root, webPort, req, res)
|
|
19436
19337
|
};
|
|
19437
19338
|
}
|
|
19438
19339
|
/**
|
|
@@ -19456,11 +19357,11 @@ async function startSkiffDebugServer(ctx, root, port, webPort) {
|
|
|
19456
19357
|
function stopSkiffDebugServer() {
|
|
19457
19358
|
stopFace(FACE_SKIFF_DEBUG);
|
|
19458
19359
|
}
|
|
19459
|
-
async function handle
|
|
19360
|
+
async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
19460
19361
|
try {
|
|
19461
19362
|
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
19462
19363
|
if (req.method === "GET" && url === "/") {
|
|
19463
|
-
sendHtml
|
|
19364
|
+
sendHtml(res, skiffDebugPage(await listCccs(ctx, {
|
|
19464
19365
|
defaultRoot,
|
|
19465
19366
|
withRoles: true
|
|
19466
19367
|
}), defaultRoot, webPort));
|
|
@@ -19469,11 +19370,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19469
19370
|
if (req.method === "POST" && url === "/ask") {
|
|
19470
19371
|
let body;
|
|
19471
19372
|
try {
|
|
19472
|
-
const parsed = JSON.parse(await readBody$
|
|
19373
|
+
const parsed = JSON.parse(await readBody$1(req));
|
|
19473
19374
|
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
19474
19375
|
body = parsed;
|
|
19475
19376
|
} catch {
|
|
19476
|
-
sendJson$
|
|
19377
|
+
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
19477
19378
|
return;
|
|
19478
19379
|
}
|
|
19479
19380
|
const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot;
|
|
@@ -19482,11 +19383,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19482
19383
|
const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
|
|
19483
19384
|
const role = readSkiffRoles(ccc).get(roleName);
|
|
19484
19385
|
if (!roleName || !role) {
|
|
19485
|
-
sendJson$
|
|
19386
|
+
sendJson$1(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
|
|
19486
19387
|
return;
|
|
19487
19388
|
}
|
|
19488
19389
|
if (!question.trim()) {
|
|
19489
|
-
sendJson$
|
|
19390
|
+
sendJson$1(res, 400, { error: "empty question" });
|
|
19490
19391
|
return;
|
|
19491
19392
|
}
|
|
19492
19393
|
let agent;
|
|
@@ -19495,11 +19396,11 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19495
19396
|
const info = skiffSessionInfo(sessionId);
|
|
19496
19397
|
const live = getSkiffAgent(sessionId);
|
|
19497
19398
|
if (!info || !live) {
|
|
19498
|
-
sendJson$
|
|
19399
|
+
sendJson$1(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
|
|
19499
19400
|
return;
|
|
19500
19401
|
}
|
|
19501
19402
|
if (info.role !== roleName || info.ccc !== ccc) {
|
|
19502
|
-
sendJson$
|
|
19403
|
+
sendJson$1(res, 400, { error: `session "${sessionId}" belongs to role "${info.role}" in another CCC — start a new conversation` });
|
|
19503
19404
|
return;
|
|
19504
19405
|
}
|
|
19505
19406
|
agent = live;
|
|
@@ -19508,7 +19409,7 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19508
19409
|
const hc = readCheapWorkerConfig(ccc);
|
|
19509
19410
|
if (!agent) agent = (await createSkiffAgent(ctx, ccc, roleName, role, hc?.defaultModel)).agent;
|
|
19510
19411
|
const result = await askSkiff(ctx, agent, question, 0);
|
|
19511
|
-
sendJson$
|
|
19412
|
+
sendJson$1(res, 200, {
|
|
19512
19413
|
answer: result.answer,
|
|
19513
19414
|
answer_html: renderSkiffMarkdown(result.answer),
|
|
19514
19415
|
sessionId: result.sessionId,
|
|
@@ -19517,9 +19418,9 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19517
19418
|
});
|
|
19518
19419
|
return;
|
|
19519
19420
|
}
|
|
19520
|
-
sendJson$
|
|
19421
|
+
sendJson$1(res, 404, { error: "not found" });
|
|
19521
19422
|
} catch (err) {
|
|
19522
|
-
sendJson$
|
|
19423
|
+
sendJson$1(res, 500, { error: err?.message ?? String(err) });
|
|
19523
19424
|
}
|
|
19524
19425
|
}
|
|
19525
19426
|
//#endregion
|
|
@@ -19663,7 +19564,9 @@ const rebukeStates = /* @__PURE__ */ new Map();
|
|
|
19663
19564
|
//#endregion
|
|
19664
19565
|
//#region src/output-guard-seam.ts
|
|
19665
19566
|
/** 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免):
|
|
19666
|
-
* skiff-(F4 问答)/
|
|
19567
|
+
* skiff-(F4 问答)/ rebuild-(v1.22.4 重建会话)前缀。
|
|
19568
|
+
* 🔴 `acp-` 分支保留但**已是死判据**(无生产者;面 E 已于 2026-10-15 退役)——
|
|
19569
|
+
* 理由与"为何不擅自删"见文件头注释。 */
|
|
19667
19570
|
function isExternalFaceSession(sessionId) {
|
|
19668
19571
|
if (!sessionId) return false;
|
|
19669
19572
|
return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
|
|
@@ -19958,760 +19861,6 @@ function registerUnattendedSeam(ctx) {
|
|
|
19958
19861
|
});
|
|
19959
19862
|
}
|
|
19960
19863
|
//#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
19864
|
//#region src/human-channel.ts
|
|
20716
19865
|
/**
|
|
20717
19866
|
* human-channel.ts — 人机信道的**机制层**(机器级;2026-09-26 owner 定案 ⇒ 方案 §2.6)
|
|
@@ -21033,7 +20182,7 @@ function buildManualOutputRebuke(session) {
|
|
|
21033
20182
|
}
|
|
21034
20183
|
/**
|
|
21035
20184
|
* 装配闸门(index.ts apply 调用)。
|
|
21036
|
-
* 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff
|
|
20185
|
+
* 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff 会话)零干预。
|
|
21037
20186
|
*/
|
|
21038
20187
|
function registerWeixinOutputGuard(ctx) {
|
|
21039
20188
|
try {
|
|
@@ -21394,7 +20543,7 @@ function manualOutputFallbackNeeded(input) {
|
|
|
21394
20543
|
*/
|
|
21395
20544
|
async function resolveCccRootByAlias(ctx, alias) {
|
|
21396
20545
|
try {
|
|
21397
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20546
|
+
const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
|
|
21398
20547
|
const entries = await listCccs(ctx);
|
|
21399
20548
|
const byName = entries.find((e) => e.name === alias);
|
|
21400
20549
|
if (byName) return byName.root;
|
|
@@ -21893,7 +21042,7 @@ function syncHumanChannel(ctx) {
|
|
|
21893
21042
|
(async () => {
|
|
21894
21043
|
try {
|
|
21895
21044
|
stopMachineLoops();
|
|
21896
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21045
|
+
const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
|
|
21897
21046
|
for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
|
|
21898
21047
|
} catch {}
|
|
21899
21048
|
})();
|
|
@@ -22129,7 +21278,7 @@ function matchCcc(input, candidates) {
|
|
|
22129
21278
|
}
|
|
22130
21279
|
/** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
22131
21280
|
async function collectCandidates(ctx) {
|
|
22132
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21281
|
+
const { listCccs } = await import("./ccc-roots-GIJ3A_tw.js").then((n) => n.a);
|
|
22133
21282
|
const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
|
|
22134
21283
|
const seen = /* @__PURE__ */ new Set();
|
|
22135
21284
|
const out = [];
|
|
@@ -22395,7 +21544,7 @@ function registerLifecycle(ctx) {
|
|
|
22395
21544
|
stopAllBridges();
|
|
22396
21545
|
} catch {}
|
|
22397
21546
|
};
|
|
22398
|
-
registerDisposer(ctx, "self-started resources (gateway/weixin/skiff-debug
|
|
21547
|
+
registerDisposer(ctx, "self-started resources (gateway/weixin/skiff-debug faces + weixin bridges)", disposeAll);
|
|
22399
21548
|
}
|
|
22400
21549
|
//#endregion
|
|
22401
21550
|
//#region src/web-fetch-provider.ts
|
|
@@ -24597,7 +23746,7 @@ function renderDsl(model) {
|
|
|
24597
23746
|
* 🔴 **为什么 PNG 是必需的**:宿主的对话内图片通路**白名单只有光栅格式**
|
|
24598
23747
|
* (`image/png|jpeg|webp|gif`,见 `dsh-attachment/types.d.ts`)⇒ **SVG 进不去**。
|
|
24599
23748
|
*
|
|
24600
|
-
* 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 skiff
|
|
23749
|
+
* 🔴 **开关**:由 `index.ts` 门控(**关时不注册** ⇒ 工具根本不进模型工具清单),同 skiff 口径。
|
|
24601
23750
|
*
|
|
24602
23751
|
* ## 🔴 两条**宿主契约**(2026-09-29 现场取证;第 ① 条是本文件第一版写错的地方)
|
|
24603
23752
|
*
|
|
@@ -24971,10 +24120,6 @@ const Config = z.object({
|
|
|
24971
24120
|
enabled: z.boolean().default(false),
|
|
24972
24121
|
debugPort: z.number().min(1024).max(65535).default(SKIFF_DEBUG_PORT)
|
|
24973
24122
|
}),
|
|
24974
|
-
acp: z.object({
|
|
24975
|
-
enabled: z.boolean().default(false),
|
|
24976
|
-
httpPort: z.number().min(1024).max(65535).default(ACP_HTTP_PORT)
|
|
24977
|
-
}),
|
|
24978
24123
|
webFetch: z.object({ enabled: z.boolean().default(true) }),
|
|
24979
24124
|
opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
|
|
24980
24125
|
visionPatch: z.object({ enabled: z.boolean().default(true) }),
|
|
@@ -24989,9 +24134,6 @@ const Config = z.object({
|
|
|
24989
24134
|
rebuildThresholdK: z.number().min(50).max(4e3).volatile(),
|
|
24990
24135
|
skiffEnabled: z.boolean().volatile(),
|
|
24991
24136
|
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
24137
|
croEnabled: z.boolean().volatile(),
|
|
24996
24138
|
unattendedEnabled: z.boolean().volatile(),
|
|
24997
24139
|
diagramEnabled: z.boolean().volatile()
|
|
@@ -25054,7 +24196,6 @@ function apply(ctx, config) {
|
|
|
25054
24196
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
25055
24197
|
registerBootstrap(ctx);
|
|
25056
24198
|
registerSkiff(ctx);
|
|
25057
|
-
registerAcp(ctx);
|
|
25058
24199
|
registerCroTurnTracking(ctx);
|
|
25059
24200
|
registerWakeScheduler(ctx);
|
|
25060
24201
|
registerWeixinBridge(ctx);
|
|
@@ -25078,7 +24219,7 @@ function apply(ctx, config) {
|
|
|
25078
24219
|
* ① 听宿主的**真事件** `settings/document-updated`(ns = 我们那张设置页)——顺带说明为什么不用
|
|
25079
24220
|
* 我们自己的 `serenity/settings-changed`:那个事件**全仓没有任何 `emit` 点**(只有 `ctx.on` 侧
|
|
25080
24221
|
* 在用)=**死通道**,照着它接线等于接了个永远不响的铃;
|
|
25081
|
-
* ② 兜底再挂 `agent/created` / `session/created`(与 skiff
|
|
24222
|
+
* ② 兜底再挂 `agent/created` / `session/created`(与 skiff 同款做法,那两条在实测里确实会 fire)
|
|
25082
24223
|
* —— 宿主事件面若换名,至少还有这一条能自愈;
|
|
25083
24224
|
* ③ 启动时同步一次(apply 时按当前配置定初始态)。
|
|
25084
24225
|
* 🔴 **关的时候要注销**(不是"留着但不响应"):与"关时不注册"同一口径 —— 工具不出现在模型清单里。
|
|
@@ -25125,7 +24266,8 @@ function registerDiagramTool(ctx) {
|
|
|
25125
24266
|
* 同步一次——此刻通常还没有 live 会话,进程 cwd(服务启动目录)也不在 CCC 内 →
|
|
25126
24267
|
* `resolveSkiffRoot` 返回 null → 打印一行警告后**永不重试**(实证:重启日志
|
|
25127
24268
|
* `✗ Skiff 调试服务未启动:无法定位 CCC root`,`ss -ltn` 无 3099;同批 ACP 因容忍
|
|
25128
|
-
* `root ?? undefined`
|
|
24269
|
+
* `root ?? undefined` 而正常启动 —— ⚠️ **该对照已随面 E 退役而失效**(2026-10-15),
|
|
24270
|
+
* 但本条记录的**病因与修法不受影响**(Skiff 侧仍是"首次未定位 → 曾永不重试")。
|
|
25129
24271
|
*
|
|
25130
24272
|
* v1.30.14(重启日志实证后的收敛):**并发启动守卫** + 首次未定位降级为信息级日志。
|
|
25131
24273
|
* - 实证(v1.30.13 重启日志):定时触发与"会话就绪"事件几乎同时到达 → 两次 sync 都
|
|
@@ -25249,29 +24391,5 @@ function readWebPort(ctx) {
|
|
|
25249
24391
|
return MAIN_WEB_PORT;
|
|
25250
24392
|
}
|
|
25251
24393
|
}
|
|
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
24394
|
//#endregion
|
|
25277
24395
|
export { Config, apply, humanChannelAccountSchema, humanChannelSubscriptionSchema, inject, name };
|