dsh-proxy-routing 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/LICENSE +21 -0
- package/README.i18n.yaml +4 -0
- package/README.md +193 -0
- package/README.zh.md +193 -0
- package/cordis.patch.yml +6 -0
- package/lib/actions.js +61 -0
- package/lib/als.js +8 -0
- package/lib/client.js +480 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +194 -0
- package/lib/control.js +199 -0
- package/lib/discovery.js +89 -0
- package/lib/fetch-router.js +77 -0
- package/lib/index.js +296 -0
- package/lib/llm-router.js +63 -0
- package/lib/probe.js +68 -0
- package/lib/proxy/connect.js +166 -0
- package/lib/proxy/decode.js +60 -0
- package/lib/proxy/errors.js +26 -0
- package/lib/proxy/http11.js +133 -0
- package/lib/proxy/http2.js +96 -0
- package/lib/proxy/noproxy.js +92 -0
- package/lib/proxy/parse.js +22 -0
- package/lib/proxy/request.js +206 -0
- package/lib/proxy/stream.js +179 -0
- package/lib/proxy-env.js +173 -0
- package/lib/routes.js +75 -0
- package/lib/rpc.js +110 -0
- package/lib/settings.js +60 -0
- package/lib/shell-router.js +201 -0
- package/lib/status.js +61 -0
- package/lib/tools.js +453 -0
- package/package.json +114 -0
- package/tools/gen-cert.sh +13 -0
- package/tools/proxy-probe.mjs +49 -0
package/lib/tools.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
// lib/tools.js — agent 工具层:v2 路由控制面
|
|
2
|
+
//
|
|
3
|
+
// 写操作(enable/disable)走 tools/pre-execute 审批门禁:full access 权限等级
|
|
4
|
+
// (会话生效 sandbox mode 为 danger-full-access)免询问直接放行,更低等级需
|
|
5
|
+
// 经人类批准;enable 只改 Agent binding(可顺带更新 default profile 端点),
|
|
6
|
+
// disable 只置 Agent 直连,均不动显式 provider 覆盖。未配置端点时给出固定
|
|
7
|
+
// 指引,绝不静默采用默认代理。
|
|
8
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
9
|
+
import { UNCONFIGURED_GUIDANCE } from "./config.js";
|
|
10
|
+
import { enableProfile, agentDirect } from "./actions.js";
|
|
11
|
+
import { discoverProxies } from "./discovery.js";
|
|
12
|
+
|
|
13
|
+
function statusProbe(result) {
|
|
14
|
+
return {
|
|
15
|
+
ok: result.ok,
|
|
16
|
+
connectMs: result.connectMs,
|
|
17
|
+
totalMs: result.totalMs,
|
|
18
|
+
httpStatus: result.httpStatus,
|
|
19
|
+
...(typeof result.error === "string" ? { error: result.error } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Register five net_proxy_* tools.
|
|
25
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
26
|
+
* @param {object} control
|
|
27
|
+
* @param {()=>object} control.getStatus 公开状态(buildStatus 产物,已脱敏)
|
|
28
|
+
* @param {(opts:{apply:(c:object)=>object, persist?:boolean})=>Promise<object>} control.mutate
|
|
29
|
+
* @param {(proxy:object, opts?:object)=>Promise<object>} control.probe 探测(显式路由,不改状态)
|
|
30
|
+
*/
|
|
31
|
+
export function registerProxyTools(ctx, control) {
|
|
32
|
+
ctx.tools.register(defineTool({
|
|
33
|
+
name: "net_proxy_status",
|
|
34
|
+
description: "查询网络出口路由插件的当前状态:配置修订号、default profile 是否已配置端点、Agent 生效路由(直连/代理,脱敏端点)、provider 覆盖、fetch 路由归属、子进程适配与持久 shell 代际。可选 verify=true 顺带探测一次连通性(会花几秒)。",
|
|
35
|
+
parameters: {
|
|
36
|
+
verify: { type: "boolean", default: false, description: "是否顺带探测代理连通性(默认 false)" },
|
|
37
|
+
},
|
|
38
|
+
output: {
|
|
39
|
+
schema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
revision: { type: "number" },
|
|
43
|
+
migration: {
|
|
44
|
+
oneOf: [
|
|
45
|
+
{ type: "object", properties: { from: { type: "string" }, configured: { type: "boolean" } }, additionalProperties: false },
|
|
46
|
+
{ type: "null" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
profiles: {
|
|
50
|
+
type: "array",
|
|
51
|
+
items: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
id: { type: "string" },
|
|
55
|
+
configured: { type: "boolean" },
|
|
56
|
+
protocol: { type: "string" },
|
|
57
|
+
host: { type: "string" },
|
|
58
|
+
port: { type: "number" },
|
|
59
|
+
hasUsername: { type: "boolean" },
|
|
60
|
+
hasPassword: { type: "boolean" },
|
|
61
|
+
noProxy: { type: "array", items: { type: "string" } },
|
|
62
|
+
timeout: { type: "number" },
|
|
63
|
+
},
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
agent: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
route: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
kind: { type: "string" },
|
|
74
|
+
profileId: { type: "string" },
|
|
75
|
+
endpoint: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
protocol: { type: "string" },
|
|
79
|
+
host: { type: "string" },
|
|
80
|
+
port: { type: "number" },
|
|
81
|
+
hasUsername: { type: "boolean" },
|
|
82
|
+
hasPassword: { type: "boolean" },
|
|
83
|
+
},
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
generation: { type: "number" },
|
|
90
|
+
},
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
providers: {
|
|
94
|
+
type: "array",
|
|
95
|
+
items: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
provider: { type: "string" },
|
|
99
|
+
route: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
kind: { type: "string" },
|
|
103
|
+
profileId: { type: "string" },
|
|
104
|
+
},
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
additionalProperties: false,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
gateway: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
enabled: { type: "boolean" },
|
|
115
|
+
port: { type: "number" },
|
|
116
|
+
dedicatedPurposePorts: { type: "boolean" },
|
|
117
|
+
purposes: { type: "array", items: { type: "string" } },
|
|
118
|
+
},
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
121
|
+
fetchRouter: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: { owned: { type: "boolean" }, owners: { type: "number" } },
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
},
|
|
126
|
+
shell: {
|
|
127
|
+
oneOf: [
|
|
128
|
+
{
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
generation: { type: "number" },
|
|
132
|
+
persistentShells: {
|
|
133
|
+
type: "array",
|
|
134
|
+
items: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
agentId: { oneOf: [{ type: "string" }, { type: "null" }] },
|
|
138
|
+
stale: { type: "boolean" },
|
|
139
|
+
current: { type: "boolean" },
|
|
140
|
+
},
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
additionalProperties: false,
|
|
146
|
+
},
|
|
147
|
+
{ type: "null" },
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
probe: {
|
|
151
|
+
oneOf: [
|
|
152
|
+
{
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
ok: { type: "boolean" },
|
|
156
|
+
connectMs: { type: "number" },
|
|
157
|
+
totalMs: { type: "number" },
|
|
158
|
+
httpStatus: { type: "number" },
|
|
159
|
+
error: { type: "string" },
|
|
160
|
+
},
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
},
|
|
163
|
+
{ type: "null" },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
additionalProperties: false,
|
|
168
|
+
},
|
|
169
|
+
render: (_args, value) => [{ type: "text", text: describeStatus(value) }],
|
|
170
|
+
},
|
|
171
|
+
async execute(args) {
|
|
172
|
+
const out = control.getStatus();
|
|
173
|
+
if (args.verify) {
|
|
174
|
+
const route = out.agent.route;
|
|
175
|
+
if (route.kind === "profile" && route.endpoint) {
|
|
176
|
+
const { protocol, host, port } = route.endpoint;
|
|
177
|
+
const pr = await control.probe({ protocol, host, port }, {});
|
|
178
|
+
out.probe = statusProbe(pr);
|
|
179
|
+
} else {
|
|
180
|
+
out.probe = { ok: false, error: "当前 Agent 路由为直连或未配置,无需/无法探测代理" };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
},
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
ctx.tools.register(defineTool({
|
|
188
|
+
name: "net_proxy_discover",
|
|
189
|
+
description: "仅在当前会话 Full Access(danger-full-access)下,扫描代理环境变量和有限的本机常见代理端口并探测连通性;只报告候选,不写入配置、不启用代理。受限权限不会扫描,会返回请用户提供 protocol/host/port 的引导。",
|
|
190
|
+
parameters: {
|
|
191
|
+
url: { type: "string", description: "探测目标 URL(默认 gstatic generate_204)" },
|
|
192
|
+
timeout: { type: "number", description: "每个候选的超时毫秒(默认 1500,最大 5000)" },
|
|
193
|
+
},
|
|
194
|
+
output: {
|
|
195
|
+
schema: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
allowed: { type: "boolean" },
|
|
199
|
+
message: { type: "string" },
|
|
200
|
+
scanned: { type: "number" },
|
|
201
|
+
candidates: {
|
|
202
|
+
type: "array",
|
|
203
|
+
items: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
source: { type: "string" },
|
|
207
|
+
protocol: { type: "string" },
|
|
208
|
+
host: { type: "string" },
|
|
209
|
+
port: { type: "number" },
|
|
210
|
+
ok: { type: "boolean" },
|
|
211
|
+
connectMs: { type: "number" },
|
|
212
|
+
totalMs: { type: "number" },
|
|
213
|
+
httpStatus: { type: "number" },
|
|
214
|
+
error: { type: "string" },
|
|
215
|
+
},
|
|
216
|
+
additionalProperties: false,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
additionalProperties: false,
|
|
221
|
+
},
|
|
222
|
+
render: (_args, value) => [{ type: "text", text: value.message }],
|
|
223
|
+
},
|
|
224
|
+
async execute(args, exec) {
|
|
225
|
+
if (!isFullAccess(exec, ctx)) {
|
|
226
|
+
return {
|
|
227
|
+
allowed: false,
|
|
228
|
+
message: "当前会话不是 Full Access,出于权限边界不会主动扫描本机代理;请向用户询问已运行代理的 protocol/host/port,再使用 net_proxy_probe 检测。",
|
|
229
|
+
scanned: 0,
|
|
230
|
+
candidates: [],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const timeout = Math.min(Math.max(Number.isFinite(args.timeout) ? args.timeout : 1500, 250), 5000);
|
|
234
|
+
const found = await discoverProxies({ target: args.url, timeout });
|
|
235
|
+
return {
|
|
236
|
+
allowed: true,
|
|
237
|
+
message: found.candidates.length > 0
|
|
238
|
+
? `发现 ${found.candidates.length} 个可用代理候选;请先确认候选用途,再请求 net_proxy_enable。`
|
|
239
|
+
: `未发现可用代理候选(已扫描 ${found.scanned} 个候选);请向用户询问 protocol/host/port。`,
|
|
240
|
+
scanned: found.scanned,
|
|
241
|
+
candidates: found.candidates,
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
}));
|
|
245
|
+
ctx.tools.register(defineTool({
|
|
246
|
+
name: "net_proxy_probe",
|
|
247
|
+
description: "用当前 default profile(或临时指定的 protocol/host/port)做一次连通与延迟探测,不改动任何配置。未配置端点时返回固定指引。",
|
|
248
|
+
parameters: {
|
|
249
|
+
url: { type: "string", description: "探测目标 URL(默认 gstatic generate_204)" },
|
|
250
|
+
protocol: { type: "string", enum: ["http", "socks5"], description: "临时覆盖协议(不持久化)" },
|
|
251
|
+
host: { type: "string", description: "临时覆盖代理地址(不持久化)" },
|
|
252
|
+
port: { type: "number", description: "临时覆盖代理端口(不持久化)" },
|
|
253
|
+
username: { type: "string", description: "临时覆盖认证用户名(不持久化)" },
|
|
254
|
+
password: { type: "string", description: "临时覆盖认证密码(不持久化)" },
|
|
255
|
+
timeout: { type: "number", description: "超时毫秒(默认 15000)" },
|
|
256
|
+
},
|
|
257
|
+
output: {
|
|
258
|
+
schema: {
|
|
259
|
+
type: "object",
|
|
260
|
+
properties: {
|
|
261
|
+
ok: { type: "boolean" },
|
|
262
|
+
connectMs: { type: "number" },
|
|
263
|
+
totalMs: { type: "number" },
|
|
264
|
+
httpStatus: { type: "number" },
|
|
265
|
+
target: { type: "string" },
|
|
266
|
+
error: { type: "string" },
|
|
267
|
+
},
|
|
268
|
+
additionalProperties: false,
|
|
269
|
+
},
|
|
270
|
+
render: (_args, value) => [{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: value.ok
|
|
273
|
+
? `代理链路正常:${value.target} → HTTP ${value.httpStatus}(TCP ${value.connectMs}ms / 总 ${value.totalMs}ms)`
|
|
274
|
+
: `代理链路不可用:${value.error || "unknown"}(TCP ${value.connectMs}ms / 总 ${value.totalMs}ms)`,
|
|
275
|
+
}],
|
|
276
|
+
},
|
|
277
|
+
async execute(args) {
|
|
278
|
+
const status = control.getStatus();
|
|
279
|
+
const profile = status.profiles.find((p) => p.id === "default");
|
|
280
|
+
const hasEndpoint = args.host !== undefined || args.port !== undefined;
|
|
281
|
+
const usable = profile?.configured || hasEndpoint;
|
|
282
|
+
if (!usable) {
|
|
283
|
+
return { ok: false, error: UNCONFIGURED_GUIDANCE };
|
|
284
|
+
}
|
|
285
|
+
const probeCfg = {
|
|
286
|
+
protocol: args.protocol ?? profile?.protocol ?? "http",
|
|
287
|
+
host: args.host ?? profile?.host,
|
|
288
|
+
port: args.port ?? profile?.port,
|
|
289
|
+
username: args.username,
|
|
290
|
+
password: args.password,
|
|
291
|
+
noProxy: profile?.noProxy ?? [],
|
|
292
|
+
timeout: args.timeout ?? profile?.timeout ?? 60000,
|
|
293
|
+
};
|
|
294
|
+
return control.probe(probeCfg, { target: args.url, timeout: args.timeout });
|
|
295
|
+
},
|
|
296
|
+
}));
|
|
297
|
+
|
|
298
|
+
ctx.tools.register(defineTool({
|
|
299
|
+
name: "net_proxy_enable",
|
|
300
|
+
description: "启用网络代理:把 Agent 路由指向 default profile;可顺带更新该 profile 的 protocol/host/port/noProxy 等端点并持久化。未配置端点且未提供 host/port 时给出配置指引。此操作需要人类批准。",
|
|
301
|
+
parameters: {
|
|
302
|
+
protocol: { type: "string", enum: ["http", "socks5"], description: "代理协议:http(含 CONNECT 隧道,Clash/v2rayN 等默认)或 socks5" },
|
|
303
|
+
host: { type: "string", description: "代理地址(如 127.0.0.1)" },
|
|
304
|
+
port: { type: "number", description: "代理端口(如 Clash Verge 为 7897,1-65535)" },
|
|
305
|
+
username: { type: "string", description: "可选认证用户名" },
|
|
306
|
+
password: { type: "string", description: "可选认证密码" },
|
|
307
|
+
noProxy: { type: "array", items: { type: "string" }, description: "直连白名单(host / .domain / host:port / CIDR / <local> / *)" },
|
|
308
|
+
},
|
|
309
|
+
output: {
|
|
310
|
+
schema: {
|
|
311
|
+
type: "object",
|
|
312
|
+
properties: {
|
|
313
|
+
ok: { type: "boolean" },
|
|
314
|
+
message: { type: "string" },
|
|
315
|
+
config: {
|
|
316
|
+
type: "object",
|
|
317
|
+
properties: {
|
|
318
|
+
id: { type: "string" },
|
|
319
|
+
configured: { type: "boolean" },
|
|
320
|
+
protocol: { type: "string" },
|
|
321
|
+
host: { type: "string" },
|
|
322
|
+
port: { type: "number" },
|
|
323
|
+
hasUsername: { type: "boolean" },
|
|
324
|
+
hasPassword: { type: "boolean" },
|
|
325
|
+
noProxy: { type: "array", items: { type: "string" } },
|
|
326
|
+
timeout: { type: "number" },
|
|
327
|
+
},
|
|
328
|
+
additionalProperties: false,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
additionalProperties: false,
|
|
332
|
+
},
|
|
333
|
+
render: (_args, value) => [{ type: "text", text: value.message }],
|
|
334
|
+
},
|
|
335
|
+
async execute(args) {
|
|
336
|
+
if (args.port !== undefined && (!Number.isInteger(args.port) || args.port < 1 || args.port > 65535)) {
|
|
337
|
+
throw new Error(`net_proxy_enable: port 必须是 1-65535 的整数,收到 ${args.port}`);
|
|
338
|
+
}
|
|
339
|
+
const endpoint = {
|
|
340
|
+
...(args.protocol !== undefined ? { protocol: args.protocol } : {}),
|
|
341
|
+
...(args.host !== undefined ? { host: args.host } : {}),
|
|
342
|
+
...(args.port !== undefined ? { port: args.port } : {}),
|
|
343
|
+
...(args.username !== undefined ? { username: args.username } : {}),
|
|
344
|
+
...(args.password !== undefined ? { password: args.password } : {}),
|
|
345
|
+
...(args.noProxy !== undefined ? { noProxy: args.noProxy } : {}),
|
|
346
|
+
};
|
|
347
|
+
let result;
|
|
348
|
+
try {
|
|
349
|
+
result = await control.mutate({ apply: (c) => enableProfile(c, endpoint), persist: true });
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (error && error.code === "UNCONFIGURED_PROFILE") throw new Error(UNCONFIGURED_GUIDANCE);
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
const status = control.getStatus();
|
|
355
|
+
const profile = status.profiles.find((p) => p.id === "default");
|
|
356
|
+
return {
|
|
357
|
+
ok: true,
|
|
358
|
+
message: profile?.configured
|
|
359
|
+
? `代理已启用:${profile.protocol}://${profile.host}:${profile.port}(修订 #${result.revision})`
|
|
360
|
+
: `代理已启用(修订 #${result.revision})`,
|
|
361
|
+
config: profile,
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
ctx.tools.register(defineTool({
|
|
367
|
+
name: "net_proxy_disable",
|
|
368
|
+
description: "停用网络代理:把 Agent 路由置为直连。显式 provider 覆盖保持独立,不受影响。此操作需要人类批准。",
|
|
369
|
+
parameters: {},
|
|
370
|
+
output: {
|
|
371
|
+
schema: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
ok: { type: "boolean" },
|
|
375
|
+
message: { type: "string" },
|
|
376
|
+
},
|
|
377
|
+
additionalProperties: false,
|
|
378
|
+
},
|
|
379
|
+
render: (_args, value) => [{ type: "text", text: value.message }],
|
|
380
|
+
},
|
|
381
|
+
async execute() {
|
|
382
|
+
const result = await control.mutate({ apply: agentDirect, persist: true });
|
|
383
|
+
return { ok: true, message: `代理已停用,Agent 路由恢复直连(修订 #${result.revision})` };
|
|
384
|
+
},
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* 会话生效的权限等级是否为 full access(sandbox mode = danger-full-access)。
|
|
390
|
+
* 折叠会话日志中最后一个 `sandbox/mode` 事件(与 dsh-sandbox-policy 的
|
|
391
|
+
* effectiveSandboxMode 同语义);无会话级覆盖时回退到 shell 服务的部署默认
|
|
392
|
+
* 模式。无法判定时 fail-closed:视为受限等级(需要询问)。
|
|
393
|
+
* @param {object} exec tools/pre-execute 的 exec 载荷
|
|
394
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
395
|
+
*/
|
|
396
|
+
function isFullAccess(exec, ctx) {
|
|
397
|
+
const events = exec?.agent?.session?.events;
|
|
398
|
+
if (Array.isArray(events)) {
|
|
399
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
400
|
+
const event = events[index];
|
|
401
|
+
if (event?.type === "sandbox/mode") return event?.data?.mode === "danger-full-access";
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return ctx?.shell?.sandboxMode === "danger-full-access";
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* 审批门禁:net_proxy_enable / net_proxy_disable 调用需经 dsh 审批 seam。
|
|
409
|
+
* 权限分级:full access(danger-full-access)等级免询问直接放行;
|
|
410
|
+
* 其他低等级(read-only / workspace-write 等)返回 ask,经人类批准后生效。
|
|
411
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
412
|
+
*/
|
|
413
|
+
export function installApprovalGate(ctx) {
|
|
414
|
+
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
415
|
+
if (exec.name === "net_proxy_enable" || exec.name === "net_proxy_disable") {
|
|
416
|
+
if (isFullAccess(exec, ctx)) return next();
|
|
417
|
+
return {
|
|
418
|
+
kind: "ask",
|
|
419
|
+
reason: exec.name === "net_proxy_enable"
|
|
420
|
+
? "启用网络代理:Agent 的 shell 执行与未覆盖的 LLM provider 流量将改经本机代理发出"
|
|
421
|
+
: "停用网络代理:Agent 路由恢复直连",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return next();
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** 状态文本描述(render 用)。 */
|
|
429
|
+
function describeStatus(value) {
|
|
430
|
+
const parts = [`修订 #${value.revision}`];
|
|
431
|
+
const profile = value.profiles?.find((p) => p.id === "default");
|
|
432
|
+
parts.push(profile?.configured
|
|
433
|
+
? `default 已配置:${profile.protocol}://${profile.host}:${profile.port}${profile.hasUsername ? "(含认证)" : ""}`
|
|
434
|
+
: "default 未配置端点");
|
|
435
|
+
const route = value.agent?.route;
|
|
436
|
+
parts.push(route?.kind === "profile"
|
|
437
|
+
? `Agent 路由:代理(${route.endpoint?.protocol}://${route.endpoint?.host}:${route.endpoint?.port})`
|
|
438
|
+
: "Agent 路由:直连");
|
|
439
|
+
if (value.agent?.generation) parts.push(`策略代际 #${value.agent.generation}`);
|
|
440
|
+
if (value.providers?.length) parts.push(`provider 覆盖:${value.providers.map((p) => p.provider).join(", ")}`);
|
|
441
|
+
if (value.fetchRouter) parts.push(`fetch 路由:${value.fetchRouter.owned ? `已接管(${value.fetchRouter.owners} owner)` : "未接管"}`);
|
|
442
|
+
if (value.shell) {
|
|
443
|
+
const stale = value.shell.persistentShells.filter((s) => s.stale).length;
|
|
444
|
+
parts.push(`持久 shell:${value.shell.persistentShells.length} 个,其中 ${stale} 个为旧策略(需重置)`);
|
|
445
|
+
}
|
|
446
|
+
if (value.migration) parts.push(`配置迁移:${value.migration.from}(configured=${value.migration.configured})`);
|
|
447
|
+
if (value.probe) {
|
|
448
|
+
parts.push(value.probe.ok
|
|
449
|
+
? `探测:正常(HTTP ${value.probe.httpStatus})`
|
|
450
|
+
: `探测:不可用(${value.probe.error || "unknown"})`);
|
|
451
|
+
}
|
|
452
|
+
return parts.join(";");
|
|
453
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-proxy-routing",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "DSH 网络出口路由插件:通过官方 proxy-routing settings namespace 接入用户已运行的 HTTP/SOCKS5 代理,按每次执行路由 shell 与 LLM provider 流量。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib",
|
|
10
|
+
"tools",
|
|
11
|
+
"README.md",
|
|
12
|
+
"README.zh.md",
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"cordis.patch.yml"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./lib/index.js",
|
|
19
|
+
"./client": "./lib/client.js",
|
|
20
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"deepseek",
|
|
25
|
+
"harness",
|
|
26
|
+
"dsh",
|
|
27
|
+
"dsh-plugin",
|
|
28
|
+
"proxy",
|
|
29
|
+
"http",
|
|
30
|
+
"socks5",
|
|
31
|
+
"network"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/chenjiyan2001/dsh-proxy-routing.git"
|
|
36
|
+
},
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
},
|
|
41
|
+
"client": {
|
|
42
|
+
"platform": "web",
|
|
43
|
+
"inject": [
|
|
44
|
+
"@deepseek-ai/dsh-client-connection",
|
|
45
|
+
"@deepseek-ai/dsh-client-locale",
|
|
46
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
49
|
+
"@deepseek-ai/dsh-client-web-react"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@deepseek-ai/cordis": ">=4.0.0",
|
|
55
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
|
56
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
60
|
+
"@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.6",
|
|
61
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.6",
|
|
63
|
+
"@deepseek-ai/schemastery": ">=3.0.0",
|
|
64
|
+
"react": "^18.2.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"@deepseek-ai/cordis": { "optional": true },
|
|
68
|
+
"@deepseek-ai/dsh-client-connection": { "optional": true },
|
|
69
|
+
"@deepseek-ai/dsh-client-locale": { "optional": true },
|
|
70
|
+
"@deepseek-ai/dsh-client-runtime": { "optional": true },
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-settings": { "optional": true },
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-slots": { "optional": true },
|
|
73
|
+
"@deepseek-ai/dsh-client-web-react": { "optional": true },
|
|
74
|
+
"@deepseek-ai/dsh-settings": { "optional": true },
|
|
75
|
+
"@deepseek-ai/dsh-tools": { "optional": true },
|
|
76
|
+
"@deepseek-ai/schemastery": { "optional": true },
|
|
77
|
+
"react": { "optional": true }
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"yaml": "^2.9.0"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@deepseek-ai/cordis": ">=4.0.0",
|
|
84
|
+
"@deepseek-ai/dsh-agent": "0.1.0-rc.6",
|
|
85
|
+
"@deepseek-ai/dsh-client-connection": "0.1.0-rc.6",
|
|
86
|
+
"@deepseek-ai/dsh-client-locale": "0.1.0-rc.6",
|
|
87
|
+
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6",
|
|
88
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.6",
|
|
89
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
|
|
90
|
+
"@deepseek-ai/dsh-client-web-react": "0.1.0-rc.6",
|
|
91
|
+
"@deepseek-ai/dsh-code-runtime": "0.1.0-rc.6",
|
|
92
|
+
"@deepseek-ai/dsh-invariants": "0.1.0-rc.6",
|
|
93
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
94
|
+
"@deepseek-ai/dsh-scope": "0.1.0-rc.6",
|
|
95
|
+
"@deepseek-ai/dsh-session": "0.1.0-rc.6",
|
|
96
|
+
"@deepseek-ai/dsh-settings": "0.1.1-rc.1",
|
|
97
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6",
|
|
98
|
+
"@deepseek-ai/dsh-timeout": "0.1.0-rc.6",
|
|
99
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.6",
|
|
100
|
+
"@deepseek-ai/dsh-user-approval": "0.1.0-rc.6",
|
|
101
|
+
"@deepseek-ai/schemastery": ">=3.0.0",
|
|
102
|
+
"@types/node": "^22.20.0",
|
|
103
|
+
"@types/react": "~18.3.1",
|
|
104
|
+
"lightningcss": "^1.32.0",
|
|
105
|
+
"react": "^18.3.1",
|
|
106
|
+
"tsdown": "^0.22.2",
|
|
107
|
+
"typescript": "^6.0.3"
|
|
108
|
+
},
|
|
109
|
+
"scripts": {
|
|
110
|
+
"test": "bash tools/gen-cert.sh && NODE_EXTRA_CA_CERTS=$PWD/tests/.certs/cert.pem node --test \"tests/*.test.mjs\"",
|
|
111
|
+
"build:client": "tsdown --config tsdown.client.config.ts",
|
|
112
|
+
"probe": "node tools/proxy-probe.mjs"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tools/gen-cert.sh — 生成集成测试用的自签证书(localhost SAN)
|
|
3
|
+
# 输出到 tests/.certs/{cert,key}.pem;测试以 NODE_EXTRA_CA_CERTS 信任之。
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
cd "$(dirname "$0")/.."
|
|
6
|
+
mkdir -p tests/.certs
|
|
7
|
+
if [[ ! -f tests/.certs/cert.pem || ! -f tests/.certs/key.pem ]]; then
|
|
8
|
+
openssl req -x509 -newkey rsa:2048 -nodes \
|
|
9
|
+
-keyout tests/.certs/key.pem -out tests/.certs/cert.pem -days 30 \
|
|
10
|
+
-subj "/CN=localhost" \
|
|
11
|
+
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2>/dev/null
|
|
12
|
+
fi
|
|
13
|
+
echo "cert ready: tests/.certs/cert.pem"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// tools/proxy-probe.mjs — 命令行探测:测试本机代理链路(不改任何配置)
|
|
2
|
+
//
|
|
3
|
+
// 用法:
|
|
4
|
+
// node tools/proxy-probe.mjs [--proxy http://127.0.0.1:7897] [--target https://www.gstatic.com/generate_204]
|
|
5
|
+
// 缺省 --proxy 时读取官方 `$DSH_HOME/settings.yaml` 的 `proxy-routing` section。
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
import { probeProxy } from "../lib/probe.js";
|
|
11
|
+
import { canonicalDefaults, toProxy, validateCanonical } from "../lib/config.js";
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const flag = (name, dflt) => {
|
|
15
|
+
const i = args.indexOf(name);
|
|
16
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : dflt;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let proxy;
|
|
20
|
+
const rawProxy = flag("--proxy", "");
|
|
21
|
+
if (rawProxy) {
|
|
22
|
+
try {
|
|
23
|
+
const u = new URL(rawProxy);
|
|
24
|
+
proxy = { protocol: u.protocol === "socks5:" ? "socks5" : "http", host: u.hostname, port: Number(u.port || 7890), noProxy: [] };
|
|
25
|
+
} catch {
|
|
26
|
+
console.error(`无法解析 --proxy: ${rawProxy}`);
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
const home = process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
31
|
+
const settingsFile = path.join(home, "settings.yaml");
|
|
32
|
+
let document = {};
|
|
33
|
+
if (fs.existsSync(settingsFile)) {
|
|
34
|
+
document = parseYaml(fs.readFileSync(settingsFile, "utf8")) ?? {};
|
|
35
|
+
}
|
|
36
|
+
const canonical = validateCanonical(document["proxy-routing"] ?? canonicalDefaults());
|
|
37
|
+
proxy = toProxy(canonical);
|
|
38
|
+
if (!proxy.protocol || !proxy.host || !proxy.port) {
|
|
39
|
+
console.error("proxy-routing settings 中未配置可探测的代理端点");
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
console.log(`使用已保存配置: ${proxy.protocol}://${proxy.host}:${proxy.port}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const target = flag("--target", "https://www.gstatic.com/generate_204");
|
|
46
|
+
console.log(`探测目标: ${target}`);
|
|
47
|
+
const res = await probeProxy(proxy, { target });
|
|
48
|
+
console.log(JSON.stringify(res, null, 2));
|
|
49
|
+
process.exit(res.ok ? 0 : 1);
|