@shgroup/dsh-serenity-hooks 1.30.11 → 1.30.12
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/cordis.patch.yml +20 -0
- package/dsh.plugin.json +1 -1
- package/lib/{access-CdL6BAYj.js → access-dU_vG1q8.js} +5 -1
- package/lib/host/access.d.ts +7 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +215 -8
- package/lib/{settings-section-BPk0e1du.js → settings-section-CP4uMJf_.js} +1 -1
- package/lib/{skiff-debug-TONJlpgF.js → skiff-debug-CizJBLeZ.js} +1 -1
- package/lib/web-fetch-provider.d.ts +73 -0
- package/package.json +6 -1
package/cordis.patch.yml
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
# dsh-serenity-hooks bundle 层(经 package.json `dsh.bundle.patch` 声明)
|
|
2
2
|
# 装入 profile(`dsh plugin --profile <name> add @shgroup/dsh-serenity-hooks`)
|
|
3
3
|
# 后由 profile-boot 组合:行 name 即包名,Loader 从 profile node_modules 解析。
|
|
4
|
+
#
|
|
5
|
+
# patch 语义(宿主 app-boot `applyEntryPatches` 实证,0.1.2-rc.1):
|
|
6
|
+
# - `insert:` 追加条目;无 id 追加到根,有 id 追加到该 group 的 config
|
|
7
|
+
# - 无 insert 的行按 id 定向覆盖字段(`config` 为**整体替换**,非深合并)
|
|
8
|
+
# - 未命中的 patch 只告警跳过(宿主换版本时不会炸)
|
|
9
|
+
# - 各 bundle 的 patch 与本 profile 的 cordis.patch.yml 被**展平成一个列表**按序应用
|
|
10
|
+
# (本包在 bundles 末尾 → 其 patch 在所有宿主 bundle 之后生效)
|
|
4
11
|
- insert:
|
|
5
12
|
- id: serenity-hooks
|
|
6
13
|
name: '@shgroup/dsh-serenity-hooks'
|
|
7
14
|
config:
|
|
8
15
|
serenityConfigPaths: ['.dsh/serenity.json', '.opencode/serenity.json']
|
|
16
|
+
|
|
17
|
+
# ── v1.30.12(S142 用户拍板 L3):web_fetch 在 fake-ip/TUN 网络下恒失败 ────────────
|
|
18
|
+
# 宿主内置 provider `@deepseek-ai/dsh-web-fetch-http` 用 ipaddr.js 判「公网单播」,
|
|
19
|
+
# 而本网络 DNS 走 Clash fake-ip(域名→198.18.0.0/15,实证 cdn.jsdelivr.net→198.18.1.85)
|
|
20
|
+
# → 恒抛 WEB_BLOCKED_URL,且该包 Config 无开关(只有 5 个配额字段)。
|
|
21
|
+
# 屏蔽:禁用宿主内置 provider,改由本插件注册的 provider 接管**同一 id**(`http`,
|
|
22
|
+
# HttpFetchProvider 的实例字段 LOCAL_FETCH_PROVIDER_ID)——宿主 `web` 的既有配置
|
|
23
|
+
# `fetchProvider: http` 因此无需改动(也就不会覆盖 searchProvider)。
|
|
24
|
+
# 需要恢复宿主原行为:把下面两行删除或改 `disabled: false`,并同时关掉插件配置
|
|
25
|
+
# `serenity-hooks.webFetch.enabled`(否则会 WEB_DUPLICATE_PROVIDER)。
|
|
26
|
+
- id: web-fetch-http
|
|
27
|
+
name: '@deepseek-ai/dsh-web-fetch-http'
|
|
28
|
+
disabled: true
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.30.
|
|
3
|
+
"version": "1.30.12",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——10 个工具(container_fs/logbook/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/autopilot-trajectory)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/自主巡航。适配 DSH 0.1.2-rc.1(deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
|
@@ -45,5 +45,9 @@ function hostWebServer(ctx) {
|
|
|
45
45
|
function hostSettings(ctx) {
|
|
46
46
|
return hostInjected(ctx, "settings");
|
|
47
47
|
}
|
|
48
|
+
/** `ctx.web`(injected;v1.30.12 web_fetch provider 注册通道) */
|
|
49
|
+
function hostWeb(ctx) {
|
|
50
|
+
return hostInjected(ctx, "web");
|
|
51
|
+
}
|
|
48
52
|
//#endregion
|
|
49
|
-
export { hostSettings as a, hostSessions as i, hostInjected as n,
|
|
53
|
+
export { hostSettings as a, hostSessions as i, hostInjected as n, hostWeb as o, hostService as r, hostWebServer as s, hostAgents as t };
|
package/lib/host/access.d.ts
CHANGED
|
@@ -41,6 +41,11 @@ export interface HostWebServer {
|
|
|
41
41
|
export interface HostSettings {
|
|
42
42
|
installSection?: (...args: unknown[]) => unknown;
|
|
43
43
|
}
|
|
44
|
+
/** `ctx.web`(injected;v1.30.12:fetch provider 注册通道) */
|
|
45
|
+
export interface HostWeb {
|
|
46
|
+
registerFetchProvider?: (provider: unknown) => unknown;
|
|
47
|
+
registerSearchProvider?: (provider: unknown) => unknown;
|
|
48
|
+
}
|
|
44
49
|
/** `ctx.sessions`(injected) */
|
|
45
50
|
export declare function hostSessions(ctx: unknown): HostSessions | undefined;
|
|
46
51
|
/** `ctx.agents`(injected) */
|
|
@@ -49,5 +54,7 @@ export declare function hostAgents(ctx: unknown): HostAgents | undefined;
|
|
|
49
54
|
export declare function hostWebServer(ctx: unknown): HostWebServer | undefined;
|
|
50
55
|
/** `ctx.settings`(injected;提供 settings 面板装配通道) */
|
|
51
56
|
export declare function hostSettings(ctx: unknown): HostSettings | undefined;
|
|
57
|
+
/** `ctx.web`(injected;v1.30.12 web_fetch provider 注册通道) */
|
|
58
|
+
export declare function hostWeb(ctx: unknown): HostWeb | undefined;
|
|
52
59
|
/** 会话 cwd 列表(live 会话;形状不符时返回空数组而非抛错) */
|
|
53
60
|
export declare function hostSessionCwds(ctx: unknown): string[];
|
package/lib/index.d.ts
CHANGED
|
@@ -60,6 +60,10 @@ export interface Config {
|
|
|
60
60
|
enabled?: boolean;
|
|
61
61
|
httpPort?: number;
|
|
62
62
|
};
|
|
63
|
+
/** v1.30.12 web_fetch provider 接管(fake-ip / TUN 网络:宿主内置 provider 判「非公网」恒拒) */
|
|
64
|
+
webFetch?: {
|
|
65
|
+
enabled?: boolean;
|
|
66
|
+
};
|
|
63
67
|
}
|
|
64
68
|
export declare const Config: z<Config>;
|
|
65
69
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
2
|
import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-DAsSHsub.js";
|
|
3
3
|
import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-DlrbHPLD.js";
|
|
4
|
-
import { C as newStopToken, D as writeFailedStatus, E as splitModel, O as writeProgress, S as listActiveHandymen, T as requireWhitelistedModel, _ as workspaceTrajectoryLine, a as startSkiffDebugServer, b as buildRoundPrompt, c as askSkiff, d as getSkiffAgent, f as skiffMsmGate, g as unregisterSkiffSession, h as skiffTrajectoryEnabled, k as skiffRoleFor, l as createSkiffAgent, m as skiffSessionSnapshot, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionInfo, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as waitAgentIdle, w as readProgress, x as handymanProgressPaths, y as HANDYMAN_GUIDE } from "./skiff-debug-
|
|
5
|
-
import { i as hostSessions, n as hostInjected, o as
|
|
4
|
+
import { C as newStopToken, D as writeFailedStatus, E as splitModel, O as writeProgress, S as listActiveHandymen, T as requireWhitelistedModel, _ as workspaceTrajectoryLine, a as startSkiffDebugServer, b as buildRoundPrompt, c as askSkiff, d as getSkiffAgent, f as skiffMsmGate, g as unregisterSkiffSession, h as skiffTrajectoryEnabled, k as skiffRoleFor, l as createSkiffAgent, m as skiffSessionSnapshot, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionInfo, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as waitAgentIdle, w as readProgress, x as handymanProgressPaths, y as HANDYMAN_GUIDE } from "./skiff-debug-CizJBLeZ.js";
|
|
5
|
+
import { i as hostSessions, n as hostInjected, o as hostWeb, r as hostService, s as hostWebServer, t as hostAgents } from "./access-dU_vG1q8.js";
|
|
6
6
|
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-BD_yYAZz.js";
|
|
7
7
|
import { C as summarize, S as showSession, _ as readActiveSessionMd, a as archiveSessions, b as sessionsRoot, c as createSession, d as findSession, f as getActiveSessionInfo, g as qaCheck, h as parseSessionContextFromEvents, i as SESSION_ACTIONS, l as extractSessionMdPathFromText, m as listSessions, n as readLastBound, o as clearActiveSessionInfo, p as healthCheck, r as DEFAULT_SESSION_SCOPE, s as closeSession, t as appendBound, u as findLatestActiveSessionMd, v as resolveSessionByTitle, w as useSession, x as setActiveSessionInfo, y as sessionEvents } from "./session-bound-D2ANqVn-.js";
|
|
8
|
-
import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-
|
|
8
|
+
import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-CP4uMJf_.js";
|
|
9
9
|
import { a as markdownToPlainText, c as sniffImageExt, i as getUpdates, n as downloadMedia, o as sendTextMessage, r as getConfig, s as sendTyping, t as TypingStatus } from "./weixin-api-BMpljodH.js";
|
|
10
10
|
import z from "@deepseek-ai/schemastery";
|
|
11
11
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -21,6 +21,8 @@ import { createHmac, randomBytes, randomUUID, scryptSync, timingSafeEqual } from
|
|
|
21
21
|
import { createServer, request } from "node:http";
|
|
22
22
|
import * as zlib from "node:zlib";
|
|
23
23
|
import { deriveEventMessage } from "@deepseek-ai/dsh-session";
|
|
24
|
+
import { lookup } from "node:dns/promises";
|
|
25
|
+
import { isIP } from "node:net";
|
|
24
26
|
//#region src/fs-ops.ts
|
|
25
27
|
/**
|
|
26
28
|
* fs-ops.ts — container_fs 纯操作层(cc_fs → container_fs,v1.30;零 DSH 依赖,可独立单测)
|
|
@@ -914,6 +916,17 @@ const HOST_SERVICES = [
|
|
|
914
916
|
impact: "设置面板不安装 → 所有开关静默 no-op",
|
|
915
917
|
required: true
|
|
916
918
|
},
|
|
919
|
+
{
|
|
920
|
+
id: "web",
|
|
921
|
+
name: "web",
|
|
922
|
+
access: "injected",
|
|
923
|
+
members: [{
|
|
924
|
+
name: "registerFetchProvider",
|
|
925
|
+
kind: "function"
|
|
926
|
+
}],
|
|
927
|
+
impact: "web_fetch provider 无法注册 → fake-ip 网络下 web_fetch 不可用",
|
|
928
|
+
required: false
|
|
929
|
+
},
|
|
917
930
|
{
|
|
918
931
|
id: "systemPrompt",
|
|
919
932
|
name: "systemPrompt",
|
|
@@ -6465,7 +6478,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6465
6478
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
6466
6479
|
});
|
|
6467
6480
|
const root = findSerenityRoot(workspace) ?? "";
|
|
6468
|
-
const { discoverCccs } = await import("./skiff-debug-
|
|
6481
|
+
const { discoverCccs } = await import("./skiff-debug-CizJBLeZ.js").then((n) => n.i);
|
|
6469
6482
|
sendJson$2(res, 200, { cccs: await discoverCccs(ctx, root) });
|
|
6470
6483
|
} catch (err) {
|
|
6471
6484
|
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
@@ -6495,7 +6508,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6495
6508
|
return;
|
|
6496
6509
|
}
|
|
6497
6510
|
const settings = readAdvancedSettings();
|
|
6498
|
-
const { readSimpleSettings } = await import("./settings-section-
|
|
6511
|
+
const { readSimpleSettings } = await import("./settings-section-CP4uMJf_.js").then((n) => n.r);
|
|
6499
6512
|
const simple = readSimpleSettings();
|
|
6500
6513
|
const allowed = settings.publicAsk.allowed;
|
|
6501
6514
|
const port = simple.acpHttpPort ?? 3100;
|
|
@@ -9872,7 +9885,7 @@ function matchCcc(input, candidates) {
|
|
|
9872
9885
|
}
|
|
9873
9886
|
/** 组装候选列表(discoverCccs 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
9874
9887
|
async function collectCandidates(ctx) {
|
|
9875
|
-
const { discoverCccs } = await import("./skiff-debug-
|
|
9888
|
+
const { discoverCccs } = await import("./skiff-debug-CizJBLeZ.js").then((n) => n.i);
|
|
9876
9889
|
const entries = await discoverCccs(ctx, process.cwd());
|
|
9877
9890
|
const seen = /* @__PURE__ */ new Set();
|
|
9878
9891
|
const out = [];
|
|
@@ -10145,6 +10158,197 @@ function registerLifecycle(ctx) {
|
|
|
10145
10158
|
registerDisposer(ctx, "self-started resources (skiff-debug/acp/weixin)", disposeAll);
|
|
10146
10159
|
}
|
|
10147
10160
|
//#endregion
|
|
10161
|
+
//#region src/web-fetch-provider.ts
|
|
10162
|
+
/**
|
|
10163
|
+
* web-fetch-provider.ts — fake-ip / TUN 网络下的 web_fetch provider(v1.30.12,S142)
|
|
10164
|
+
*
|
|
10165
|
+
* 问题(用户报告 + 实证):`web_fetch` 在本机恒失败,报
|
|
10166
|
+
* `URL hostname "X" resolves to a non-public IP address`(`WEB_BLOCKED_URL`)。
|
|
10167
|
+
* 根因不是 DSH 误判,而是**本地 DNS 在说谎**:Clash/mihomo 的 fake-ip 让所有域名解析到
|
|
10168
|
+
* 198.18.0.0/15(实证 `cdn.jsdelivr.net → 198.18.1.85`),而宿主的
|
|
10169
|
+
* `@deepseek-ai/dsh-web-fetch-http` 用 `ipaddr.js` 判定 `range() === "unicast"`,
|
|
10170
|
+
* 该段属 reserved → 无条件 throw(其 Config 只有 5 个配额字段,**没有开关**)。
|
|
10171
|
+
*
|
|
10172
|
+
* 方案(用户拍板 L3):ACC 自己注册一个 fetch provider——**复用宿主的 HttpFetchProvider**
|
|
10173
|
+
* (重定向策略/字节与字符配额/字符集解码/连接固定全部继承),只替换 `resolveAddresses`:
|
|
10174
|
+
* 「必须公网单播」→「公网单播 **或** fake-ip 段」。其余私网段(loopback / link-local /
|
|
10175
|
+
* RFC1918 / CGNAT / 云元数据 169.254.169.254 / ULA / 组播)**依旧拒绝**。
|
|
10176
|
+
*
|
|
10177
|
+
* 屏蔽(用户要求「屏蔽掉 dsh 自身注册的」):宿主内置的 `web-fetch-http` 插件由本包的
|
|
10178
|
+
* `cordis.patch.yml`(bundle patch 层)`disabled: true` 关闭——两者都注册 id `http`
|
|
10179
|
+
* (`LOCAL_FETCH_PROVIDER_ID`,HttpFetchProvider 的实例字段),同时存在会
|
|
10180
|
+
* `WEB_DUPLICATE_PROVIDER`;禁用后由本模块的实例接管同一 id,宿主 `web` 的既有
|
|
10181
|
+
* 配置 `fetchProvider: http` 无需改动(**不改宿主的 web 配置对象**,避免覆盖 searchProvider)。
|
|
10182
|
+
*
|
|
10183
|
+
* 边界:本模块只放宽「地址可达性」一条判据;URL 校验(协议/凭据/长度)、同源重定向、
|
|
10184
|
+
* 配额、二进制拒绝全部仍由宿主实现执行。
|
|
10185
|
+
*/
|
|
10186
|
+
/**
|
|
10187
|
+
* 与宿主 `WebError` 同形的最小错误(带机器可路由的 `code`)。
|
|
10188
|
+
*
|
|
10189
|
+
* 为什么不 import 宿主的 WebError:宿主 peer 包在测试环境不可解析(现有测试全部
|
|
10190
|
+
* `vi.mock('@deepseek-ai/dsh-tools')` 同因),而 `dsh-tool-web` 只渲染 `error.message`、
|
|
10191
|
+
* 不判 `instanceof`——因此本地错误对象足以满足契约,且让本模块零运行时宿主依赖。
|
|
10192
|
+
*/
|
|
10193
|
+
function fetchError(message, code) {
|
|
10194
|
+
const error = new Error(message);
|
|
10195
|
+
error.code = code;
|
|
10196
|
+
return error;
|
|
10197
|
+
}
|
|
10198
|
+
/**
|
|
10199
|
+
* 传输与配额上限。
|
|
10200
|
+
*
|
|
10201
|
+
* 值镜像 `@deepseek-ai/dsh-web-fetch-http` 的 Config 默认值(该包 README 的字段表;
|
|
10202
|
+
* 它不导出解析后的默认值,`Config` 是 schemastery schema 而非结果)。`userAgent`
|
|
10203
|
+
* 运行时取该包导出的 `DEFAULT_USER_AGENT`(单一真相源,不复制字符串)。
|
|
10204
|
+
*/
|
|
10205
|
+
const FETCH_LIMIT_DEFAULTS = {
|
|
10206
|
+
maxResponseBytes: 5e6,
|
|
10207
|
+
maxBodyChars: 1e5,
|
|
10208
|
+
timeoutMs: 3e4,
|
|
10209
|
+
maxRedirects: 5
|
|
10210
|
+
};
|
|
10211
|
+
/** Clash/mihomo fake-ip 默认段 198.18.0.0/15(可配 fake-ip-range;改配置需同步此常量) */
|
|
10212
|
+
const FAKE_IP_OCTET_A = 198;
|
|
10213
|
+
const FAKE_IP_OCTET_B_MIN = 18;
|
|
10214
|
+
const FAKE_IP_OCTET_B_MAX = 19;
|
|
10215
|
+
/** 去掉 IPv6 字面量的方括号(`[::1]` → `::1`) */
|
|
10216
|
+
function stripBrackets(hostname) {
|
|
10217
|
+
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
10218
|
+
}
|
|
10219
|
+
function octets(address) {
|
|
10220
|
+
const parts = address.split(".");
|
|
10221
|
+
if (parts.length !== 4) return null;
|
|
10222
|
+
const nums = parts.map((p) => /^\d{1,3}$/.test(p) ? Number(p) : NaN);
|
|
10223
|
+
const [a, b, c, d] = nums;
|
|
10224
|
+
if (a === void 0 || b === void 0 || c === void 0 || d === void 0) return null;
|
|
10225
|
+
if (nums.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
|
|
10226
|
+
return [
|
|
10227
|
+
a,
|
|
10228
|
+
b,
|
|
10229
|
+
c,
|
|
10230
|
+
d
|
|
10231
|
+
];
|
|
10232
|
+
}
|
|
10233
|
+
/** 公网单播 IPv4(拒绝全部保留/私网段——与宿主判定口径一致,只额外放行 fake-ip) */
|
|
10234
|
+
function isPublicV4(address) {
|
|
10235
|
+
const parsed = octets(address);
|
|
10236
|
+
if (parsed === null) return false;
|
|
10237
|
+
const [a, b] = parsed;
|
|
10238
|
+
if (a === 0 || a === 10 || a === 127) return false;
|
|
10239
|
+
if (a === 100 && b >= 64 && b <= 127) return false;
|
|
10240
|
+
if (a === 169 && b === 254) return false;
|
|
10241
|
+
if (a === 172 && b >= 16 && b <= 31) return false;
|
|
10242
|
+
if (a === 192 && b === 168) return false;
|
|
10243
|
+
if (a === 192 && b === 0) return false;
|
|
10244
|
+
if (a === 198 && (b === 18 || b === 19)) return false;
|
|
10245
|
+
if (a === 198 && b === 51) return false;
|
|
10246
|
+
if (a === 203 && b === 0) return false;
|
|
10247
|
+
if (a >= 224) return false;
|
|
10248
|
+
return true;
|
|
10249
|
+
}
|
|
10250
|
+
/** fake-ip 段判定(198.18.0.0/15) */
|
|
10251
|
+
function isFakeIpV4(address) {
|
|
10252
|
+
const parsed = octets(address);
|
|
10253
|
+
if (parsed === null) return false;
|
|
10254
|
+
const [a, b] = parsed;
|
|
10255
|
+
return a === FAKE_IP_OCTET_A && b >= FAKE_IP_OCTET_B_MIN && b <= FAKE_IP_OCTET_B_MAX;
|
|
10256
|
+
}
|
|
10257
|
+
/**
|
|
10258
|
+
* 公网单播 IPv6:只接受全局单播 2000::/3;IPv4-mapped(`::ffff:a.b.c.d`)按内嵌 IPv4 判定。
|
|
10259
|
+
* 其余(`::1` / `::` / ULA fc00::/7 / link-local fe80::/10 / 组播 ff00::/8 / v4-translated)一律拒绝。
|
|
10260
|
+
*/
|
|
10261
|
+
function isPublicV6(address) {
|
|
10262
|
+
const lower = address.toLowerCase();
|
|
10263
|
+
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(lower);
|
|
10264
|
+
if (mapped?.[1] !== void 0) return isPublicV4(mapped[1]);
|
|
10265
|
+
const first = lower.split(":")[0] ?? "";
|
|
10266
|
+
if (first === "") return false;
|
|
10267
|
+
const value = Number.parseInt(first, 16);
|
|
10268
|
+
if (!Number.isInteger(value)) return false;
|
|
10269
|
+
return value >= 8192 && value <= 16383;
|
|
10270
|
+
}
|
|
10271
|
+
/** 该地址是否允许被抓取:公网单播 **或** fake-ip 段 */
|
|
10272
|
+
function isAllowedFetchAddress(address) {
|
|
10273
|
+
const bare = stripBrackets(address);
|
|
10274
|
+
const family = isIP(bare);
|
|
10275
|
+
if (family === 4) return isPublicV4(bare) || isFakeIpV4(bare);
|
|
10276
|
+
if (family === 6) return isPublicV6(bare);
|
|
10277
|
+
return false;
|
|
10278
|
+
}
|
|
10279
|
+
/**
|
|
10280
|
+
* 解析并校验地址集合(宿主 `HttpFetchResolver` 契约)。
|
|
10281
|
+
*
|
|
10282
|
+
* 语义与宿主 `resolvePublicAddresses` 一致——**任一地址不合法即整体拒绝**(防 DNS 重绑定),
|
|
10283
|
+
* 仅判据从「公网单播」放宽为 `isAllowedFetchAddress`。
|
|
10284
|
+
*/
|
|
10285
|
+
const resolveAllowedAddresses = async (hostname, signal) => {
|
|
10286
|
+
const bare = stripBrackets(hostname);
|
|
10287
|
+
const literalFamily = isIP(bare);
|
|
10288
|
+
let resolved;
|
|
10289
|
+
if (literalFamily === 0) resolved = await lookupAll(bare, signal);
|
|
10290
|
+
else resolved = [{
|
|
10291
|
+
address: bare,
|
|
10292
|
+
family: literalFamily
|
|
10293
|
+
}];
|
|
10294
|
+
if (resolved.length === 0) throw fetchError(`hostname "${hostname}" resolved to no addresses`, "WEB_PROVIDER_ERROR");
|
|
10295
|
+
const addresses = [];
|
|
10296
|
+
for (const entry of resolved) {
|
|
10297
|
+
if (entry.family !== 4 && entry.family !== 6) throw fetchError(`hostname "${hostname}" resolved to an invalid IP address`, "WEB_PROVIDER_ERROR");
|
|
10298
|
+
if (isIP(entry.address) !== entry.family) throw fetchError(`hostname "${hostname}" resolved to an invalid IP address`, "WEB_PROVIDER_ERROR");
|
|
10299
|
+
if (!isAllowedFetchAddress(entry.address)) throw fetchError(`URL hostname "${hostname}" resolves to a non-public IP address`, "WEB_BLOCKED_URL");
|
|
10300
|
+
addresses.push({
|
|
10301
|
+
address: entry.address,
|
|
10302
|
+
family: entry.family
|
|
10303
|
+
});
|
|
10304
|
+
}
|
|
10305
|
+
return addresses;
|
|
10306
|
+
};
|
|
10307
|
+
/** `dns.lookup` 的 `all` 形态 + 信号中断(宿主同款语义:OS 查询可能无谓完成) */
|
|
10308
|
+
async function lookupAll(hostname, signal) {
|
|
10309
|
+
if (signal.aborted) throw fetchError("web fetch aborted", "WEB_ABORTED");
|
|
10310
|
+
const lookupPromise = lookup(hostname, {
|
|
10311
|
+
all: true,
|
|
10312
|
+
order: "verbatim"
|
|
10313
|
+
});
|
|
10314
|
+
const aborted = new Promise((_, reject) => {
|
|
10315
|
+
signal.addEventListener("abort", () => reject(fetchError("web fetch aborted", "WEB_ABORTED")), { once: true });
|
|
10316
|
+
});
|
|
10317
|
+
try {
|
|
10318
|
+
return (await Promise.race([lookupPromise, aborted])).map((e) => ({
|
|
10319
|
+
address: e.address,
|
|
10320
|
+
family: e.family
|
|
10321
|
+
}));
|
|
10322
|
+
} finally {
|
|
10323
|
+
lookupPromise.catch(() => void 0);
|
|
10324
|
+
}
|
|
10325
|
+
}
|
|
10326
|
+
/**
|
|
10327
|
+
* 注册接管 `http` id 的 fetch provider(宿主内置 provider 由本包 bundle patch 禁用)。
|
|
10328
|
+
*
|
|
10329
|
+
* 后端包 `@deepseek-ai/dsh-web-fetch-http` 用**动态 import**:宿主版本里若没有该包,
|
|
10330
|
+
* 只降级为一条告警(绝不因缺一个可选后端而让整机启动失败——apply 抛错 = dsh 启动失败)。
|
|
10331
|
+
* 失败一律响亮但不抛错。
|
|
10332
|
+
*/
|
|
10333
|
+
async function registerWebFetchProvider(ctx) {
|
|
10334
|
+
const web = hostWeb(ctx);
|
|
10335
|
+
if (typeof web?.registerFetchProvider !== "function") {
|
|
10336
|
+
console.warn("[serenity-hooks] ✗ web fetch provider 未注册:宿主 web 服务不可用(web_fetch 退回宿主默认实现)");
|
|
10337
|
+
return;
|
|
10338
|
+
}
|
|
10339
|
+
try {
|
|
10340
|
+
const backend = await import("@deepseek-ai/dsh-web-fetch-http");
|
|
10341
|
+
const limits = {
|
|
10342
|
+
...FETCH_LIMIT_DEFAULTS,
|
|
10343
|
+
userAgent: backend.DEFAULT_USER_AGENT
|
|
10344
|
+
};
|
|
10345
|
+
web.registerFetchProvider(new backend.HttpFetchProvider(limits, resolveAllowedAddresses));
|
|
10346
|
+
console.log(`[serenity-hooks] ✓ web fetch provider 已接管(id=${backend.LOCAL_FETCH_PROVIDER_ID},额外放行 fake-ip 段 198.18.0.0/15)`);
|
|
10347
|
+
} catch (error) {
|
|
10348
|
+
console.error(`[serenity-hooks] ✗ web fetch provider 注册失败(web_fetch 将不可用): ${String(error?.message ?? error)}`);
|
|
10349
|
+
}
|
|
10350
|
+
}
|
|
10351
|
+
//#endregion
|
|
10148
10352
|
//#region src/index.ts
|
|
10149
10353
|
const name = "dsh-serenity-hooks";
|
|
10150
10354
|
/** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
|
|
@@ -10159,7 +10363,8 @@ const inject = [
|
|
|
10159
10363
|
"agents",
|
|
10160
10364
|
"systemPrompt",
|
|
10161
10365
|
"sessionProjections",
|
|
10162
|
-
"settings"
|
|
10366
|
+
"settings",
|
|
10367
|
+
"web"
|
|
10163
10368
|
];
|
|
10164
10369
|
const Config = z.object({
|
|
10165
10370
|
serenityConfigPaths: z.array(z.string()).default([...DEFAULT_SERENITY_CONFIG_PATHS]),
|
|
@@ -10185,7 +10390,8 @@ const Config = z.object({
|
|
|
10185
10390
|
acp: z.object({
|
|
10186
10391
|
enabled: z.boolean().default(false),
|
|
10187
10392
|
httpPort: z.number().min(1024).max(65535).default(3100)
|
|
10188
|
-
})
|
|
10393
|
+
}),
|
|
10394
|
+
webFetch: z.object({ enabled: z.boolean().default(true) })
|
|
10189
10395
|
});
|
|
10190
10396
|
function apply(ctx, config) {
|
|
10191
10397
|
try {
|
|
@@ -10232,6 +10438,7 @@ function apply(ctx, config) {
|
|
|
10232
10438
|
registerWeixinBridge(ctx);
|
|
10233
10439
|
registerWeixinSendApi(ctx);
|
|
10234
10440
|
registerLifecycle(ctx);
|
|
10441
|
+
if (config.webFetch?.enabled !== false) registerWebFetchProvider(ctx);
|
|
10235
10442
|
}
|
|
10236
10443
|
/**
|
|
10237
10444
|
* F4 Skiff 调试服务装配:启停 = 人工(设置面板 Skiff 区块开关,settings 持久化)。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { a as hostSettings } from "./access-
|
|
2
|
+
import { a as hostSettings } from "./access-dU_vG1q8.js";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
//#region src/settings-section.ts
|
|
5
5
|
var settings_section_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
2
|
import { a as findSerenityRoot, f as readHandymanConfig } from "./ccc-DAsSHsub.js";
|
|
3
3
|
import { a as resolveRoleSystemPrompt, i as readSkiffRoles, n as buildSkiffBasePrompt, o as roleMsmWhitelist, r as isSkiffSessionId, u as trajectorySubset } from "./skiff-role-DlrbHPLD.js";
|
|
4
|
-
import { i as hostSessions, r as hostService, t as hostAgents } from "./access-
|
|
4
|
+
import { i as hostSessions, r as hostService, t as hostAgents } from "./access-dU_vG1q8.js";
|
|
5
5
|
import { c as createSession, f as getActiveSessionInfo, n as readLastBound, t as appendBound, x as setActiveSessionInfo } from "./session-bound-D2ANqVn-.js";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { basename, join } from "node:path";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* web-fetch-provider.ts — fake-ip / TUN 网络下的 web_fetch provider(v1.30.12,S142)
|
|
3
|
+
*
|
|
4
|
+
* 问题(用户报告 + 实证):`web_fetch` 在本机恒失败,报
|
|
5
|
+
* `URL hostname "X" resolves to a non-public IP address`(`WEB_BLOCKED_URL`)。
|
|
6
|
+
* 根因不是 DSH 误判,而是**本地 DNS 在说谎**:Clash/mihomo 的 fake-ip 让所有域名解析到
|
|
7
|
+
* 198.18.0.0/15(实证 `cdn.jsdelivr.net → 198.18.1.85`),而宿主的
|
|
8
|
+
* `@deepseek-ai/dsh-web-fetch-http` 用 `ipaddr.js` 判定 `range() === "unicast"`,
|
|
9
|
+
* 该段属 reserved → 无条件 throw(其 Config 只有 5 个配额字段,**没有开关**)。
|
|
10
|
+
*
|
|
11
|
+
* 方案(用户拍板 L3):ACC 自己注册一个 fetch provider——**复用宿主的 HttpFetchProvider**
|
|
12
|
+
* (重定向策略/字节与字符配额/字符集解码/连接固定全部继承),只替换 `resolveAddresses`:
|
|
13
|
+
* 「必须公网单播」→「公网单播 **或** fake-ip 段」。其余私网段(loopback / link-local /
|
|
14
|
+
* RFC1918 / CGNAT / 云元数据 169.254.169.254 / ULA / 组播)**依旧拒绝**。
|
|
15
|
+
*
|
|
16
|
+
* 屏蔽(用户要求「屏蔽掉 dsh 自身注册的」):宿主内置的 `web-fetch-http` 插件由本包的
|
|
17
|
+
* `cordis.patch.yml`(bundle patch 层)`disabled: true` 关闭——两者都注册 id `http`
|
|
18
|
+
* (`LOCAL_FETCH_PROVIDER_ID`,HttpFetchProvider 的实例字段),同时存在会
|
|
19
|
+
* `WEB_DUPLICATE_PROVIDER`;禁用后由本模块的实例接管同一 id,宿主 `web` 的既有
|
|
20
|
+
* 配置 `fetchProvider: http` 无需改动(**不改宿主的 web 配置对象**,避免覆盖 searchProvider)。
|
|
21
|
+
*
|
|
22
|
+
* 边界:本模块只放宽「地址可达性」一条判据;URL 校验(协议/凭据/长度)、同源重定向、
|
|
23
|
+
* 配额、二进制拒绝全部仍由宿主实现执行。
|
|
24
|
+
*/
|
|
25
|
+
import type { Context } from 'cordis';
|
|
26
|
+
import type { HttpFetchResolver } from '@deepseek-ai/dsh-web-fetch-http';
|
|
27
|
+
/**
|
|
28
|
+
* 与宿主 `WebError` 同形的最小错误(带机器可路由的 `code`)。
|
|
29
|
+
*
|
|
30
|
+
* 为什么不 import 宿主的 WebError:宿主 peer 包在测试环境不可解析(现有测试全部
|
|
31
|
+
* `vi.mock('@deepseek-ai/dsh-tools')` 同因),而 `dsh-tool-web` 只渲染 `error.message`、
|
|
32
|
+
* 不判 `instanceof`——因此本地错误对象足以满足契约,且让本模块零运行时宿主依赖。
|
|
33
|
+
*/
|
|
34
|
+
export declare function fetchError(message: string, code: string): Error;
|
|
35
|
+
/**
|
|
36
|
+
* 传输与配额上限。
|
|
37
|
+
*
|
|
38
|
+
* 值镜像 `@deepseek-ai/dsh-web-fetch-http` 的 Config 默认值(该包 README 的字段表;
|
|
39
|
+
* 它不导出解析后的默认值,`Config` 是 schemastery schema 而非结果)。`userAgent`
|
|
40
|
+
* 运行时取该包导出的 `DEFAULT_USER_AGENT`(单一真相源,不复制字符串)。
|
|
41
|
+
*/
|
|
42
|
+
export declare const FETCH_LIMIT_DEFAULTS: {
|
|
43
|
+
readonly maxResponseBytes: 5000000;
|
|
44
|
+
readonly maxBodyChars: 100000;
|
|
45
|
+
readonly timeoutMs: 30000;
|
|
46
|
+
readonly maxRedirects: 5;
|
|
47
|
+
};
|
|
48
|
+
/** 公网单播 IPv4(拒绝全部保留/私网段——与宿主判定口径一致,只额外放行 fake-ip) */
|
|
49
|
+
export declare function isPublicV4(address: string): boolean;
|
|
50
|
+
/** fake-ip 段判定(198.18.0.0/15) */
|
|
51
|
+
export declare function isFakeIpV4(address: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* 公网单播 IPv6:只接受全局单播 2000::/3;IPv4-mapped(`::ffff:a.b.c.d`)按内嵌 IPv4 判定。
|
|
54
|
+
* 其余(`::1` / `::` / ULA fc00::/7 / link-local fe80::/10 / 组播 ff00::/8 / v4-translated)一律拒绝。
|
|
55
|
+
*/
|
|
56
|
+
export declare function isPublicV6(address: string): boolean;
|
|
57
|
+
/** 该地址是否允许被抓取:公网单播 **或** fake-ip 段 */
|
|
58
|
+
export declare function isAllowedFetchAddress(address: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* 解析并校验地址集合(宿主 `HttpFetchResolver` 契约)。
|
|
61
|
+
*
|
|
62
|
+
* 语义与宿主 `resolvePublicAddresses` 一致——**任一地址不合法即整体拒绝**(防 DNS 重绑定),
|
|
63
|
+
* 仅判据从「公网单播」放宽为 `isAllowedFetchAddress`。
|
|
64
|
+
*/
|
|
65
|
+
export declare const resolveAllowedAddresses: HttpFetchResolver;
|
|
66
|
+
/**
|
|
67
|
+
* 注册接管 `http` id 的 fetch provider(宿主内置 provider 由本包 bundle patch 禁用)。
|
|
68
|
+
*
|
|
69
|
+
* 后端包 `@deepseek-ai/dsh-web-fetch-http` 用**动态 import**:宿主版本里若没有该包,
|
|
70
|
+
* 只降级为一条告警(绝不因缺一个可选后端而让整机启动失败——apply 抛错 = dsh 启动失败)。
|
|
71
|
+
* 失败一律响亮但不抛错。
|
|
72
|
+
*/
|
|
73
|
+
export declare function registerWebFetchProvider(ctx: Context): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.30.
|
|
3
|
+
"version": "1.30.12",
|
|
4
4
|
"description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:10 个工具(container_fs/logbook/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/autopilot-trajectory)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/自主巡航。适配 DSH 0.1.2-rc.1。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -65,6 +65,8 @@
|
|
|
65
65
|
"@deepseek-ai/dsh-skill": "^0.1.2-rc.1",
|
|
66
66
|
"@deepseek-ai/dsh-system-prompt": "^0.1.2-rc.1",
|
|
67
67
|
"@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-web": "^0.1.2-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-web-fetch-http": "^0.1.2-rc.1",
|
|
68
70
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
69
71
|
"cordis": "^4.0.0-rc.7",
|
|
70
72
|
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
|
@@ -87,6 +89,9 @@
|
|
|
87
89
|
},
|
|
88
90
|
"@deepseek-ai/dsh-compaction": {
|
|
89
91
|
"optional": true
|
|
92
|
+
},
|
|
93
|
+
"@deepseek-ai/dsh-web-fetch-http": {
|
|
94
|
+
"optional": true
|
|
90
95
|
}
|
|
91
96
|
},
|
|
92
97
|
"devDependencies": {
|