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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// lib/shell-router.js — 每次执行的 shell 路由:
|
|
2
|
+
// tools/execute 处建立独立 ALS 上下文,symbol registry 包装 ctx.subprocess 的
|
|
3
|
+
// spawn/spawnTerminal,仅对"处于 shell 上下文且 Agent 路由为 profile"的调用
|
|
4
|
+
// 克隆 spec 并追加显式代理环境;不写 process.env、不改调用方对象。
|
|
5
|
+
// 持久 shell 创建时记录策略代际,状态据此报告 stale/current。
|
|
6
|
+
import { shellStore } from "./als.js";
|
|
7
|
+
import { proxyUrl, mergeNoProxyEnv } from "./proxy-env.js";
|
|
8
|
+
import { resolveAgentRoute } from "./routes.js";
|
|
9
|
+
|
|
10
|
+
const SPAWN_REGISTRY = Symbol.for("dsh-proxy-routing.subprocess-registry");
|
|
11
|
+
|
|
12
|
+
export const PROXY_OVERLAY_KEYS = Object.freeze([
|
|
13
|
+
"HTTP_PROXY",
|
|
14
|
+
"HTTPS_PROXY",
|
|
15
|
+
"ALL_PROXY",
|
|
16
|
+
"http_proxy",
|
|
17
|
+
"https_proxy",
|
|
18
|
+
"all_proxy",
|
|
19
|
+
"NO_PROXY",
|
|
20
|
+
"no_proxy",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 计算一次执行的代理环境覆盖:
|
|
25
|
+
* - profile 路由 → 全部别名 + 合并 ambient/configured/loopback 的 NO_PROXY
|
|
26
|
+
* - direct/缺省 → null(不加任何覆盖,保留子进程提供方的环境基线)
|
|
27
|
+
* ambient 只读采样 host process.env,绝不写入。
|
|
28
|
+
*/
|
|
29
|
+
export function shellOverlay(route, { ambient = process.env } = {}) {
|
|
30
|
+
if (!route || route.kind !== "profile") return null;
|
|
31
|
+
const url = proxyUrl(route.profile);
|
|
32
|
+
const noProxy = mergeNoProxyEnv(ambient, route.profile.noProxy ?? []);
|
|
33
|
+
const value = noProxy.join(",");
|
|
34
|
+
return {
|
|
35
|
+
HTTP_PROXY: url,
|
|
36
|
+
HTTPS_PROXY: url,
|
|
37
|
+
ALL_PROXY: url,
|
|
38
|
+
http_proxy: url,
|
|
39
|
+
https_proxy: url,
|
|
40
|
+
all_proxy: url,
|
|
41
|
+
NO_PROXY: value,
|
|
42
|
+
no_proxy: value,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function registry() {
|
|
47
|
+
let reg = globalThis[SPAWN_REGISTRY];
|
|
48
|
+
if (!reg) {
|
|
49
|
+
reg = { owners: new Map(), wrapped: false, wrapper: undefined, original: undefined };
|
|
50
|
+
Object.defineProperty(globalThis, SPAWN_REGISTRY, { value: reg, configurable: true, writable: true });
|
|
51
|
+
}
|
|
52
|
+
return reg;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class ShellRouter {
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} deps
|
|
58
|
+
* @param {()=>object} deps.getAgentRoute 当前 Agent 路由快照
|
|
59
|
+
* @param {()=>number} deps.getRevision 当前配置修订号
|
|
60
|
+
*/
|
|
61
|
+
constructor({ getAgentRoute, getRevision }) {
|
|
62
|
+
this.name = "shell-router";
|
|
63
|
+
this.token = Symbol("shell-router-owner");
|
|
64
|
+
this.getAgentRoute = getAgentRoute;
|
|
65
|
+
this.getRevision = getRevision;
|
|
66
|
+
this.generation = 0; // 策略代际:Agent 路由变化时自增
|
|
67
|
+
this.lastAgentRoute = null;
|
|
68
|
+
this.persistentShells = new Set(); // {handle, agentId, generation, revision}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ------------------------------------------------------------- ControlPlane 参与者
|
|
72
|
+
|
|
73
|
+
prepare({ next, prev }) {
|
|
74
|
+
return { next, prev };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
commit({ next, prev }) {
|
|
78
|
+
const nextRoute = safeRoute(next);
|
|
79
|
+
const prevRoute = safeRoute(prev);
|
|
80
|
+
if (JSON.stringify(nextRoute) !== JSON.stringify(prevRoute)) {
|
|
81
|
+
this.generation += 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
finalize() {}
|
|
86
|
+
rollback() {}
|
|
87
|
+
dispose() {
|
|
88
|
+
this.unwrap();
|
|
89
|
+
this.disposeExecute?.();
|
|
90
|
+
this.disposeExecute = undefined;
|
|
91
|
+
this.persistentShells.clear();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ------------------------------------------------------------- 注册
|
|
95
|
+
|
|
96
|
+
register(ctx) {
|
|
97
|
+
this.ctx = ctx;
|
|
98
|
+
this.disposeExecute = ctx.on?.("tools/execute", (exec, next) => {
|
|
99
|
+
const store = {
|
|
100
|
+
route: this.getAgentRoute(),
|
|
101
|
+
revision: this.getRevision(),
|
|
102
|
+
generation: this.generation,
|
|
103
|
+
agent: exec?.agent?.id ?? null,
|
|
104
|
+
};
|
|
105
|
+
return shellStore.run(store, () => next());
|
|
106
|
+
}, { prepend: true });
|
|
107
|
+
this.wrapSubprocess();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
wrapSubprocess() {
|
|
111
|
+
const svc = this.ctx?.subprocess;
|
|
112
|
+
if (!svc) return;
|
|
113
|
+
const reg = registry();
|
|
114
|
+
if (reg.owners.has(this.token)) return;
|
|
115
|
+
reg.owners.set(this.token, {});
|
|
116
|
+
if (!reg.wrapped) {
|
|
117
|
+
reg.original = { spawn: svc.spawn, spawnTerminal: svc.spawnTerminal };
|
|
118
|
+
const wrapper = {
|
|
119
|
+
spawn: (spec) => this.dispatchSpawn(svc, spec),
|
|
120
|
+
spawnTerminal: (spec) => this.dispatchTerminal(svc, spec),
|
|
121
|
+
};
|
|
122
|
+
svc.spawn = wrapper.spawn;
|
|
123
|
+
svc.spawnTerminal = wrapper.spawnTerminal;
|
|
124
|
+
reg.wrapper = wrapper;
|
|
125
|
+
reg.wrapped = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
unwrap() {
|
|
130
|
+
const reg = registry();
|
|
131
|
+
reg.owners.delete(this.token);
|
|
132
|
+
const svc = this.ctx?.subprocess;
|
|
133
|
+
if (reg.owners.size === 0 && reg.wrapped && svc) {
|
|
134
|
+
if (svc.spawn === reg.wrapper?.spawn) svc.spawn = reg.original.spawn;
|
|
135
|
+
if (svc.spawnTerminal === reg.wrapper?.spawnTerminal) svc.spawnTerminal = reg.original.spawnTerminal;
|
|
136
|
+
reg.wrapped = false;
|
|
137
|
+
reg.wrapper = undefined;
|
|
138
|
+
reg.original = undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ------------------------------------------------------------- 派发
|
|
143
|
+
|
|
144
|
+
/** 带覆盖的 spec:克隆 + 追加 overlay(overlay 优先于现有 env 项)。 */
|
|
145
|
+
applyOverlay(spec, overlay) {
|
|
146
|
+
if (!overlay) return spec;
|
|
147
|
+
return { ...spec, env: { ...(spec.env || {}), ...overlay } };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
dispatchSpawn(svc, spec) {
|
|
151
|
+
const store = shellStore.getStore();
|
|
152
|
+
const overlay = store ? shellOverlay(store.route) : null;
|
|
153
|
+
return regOriginal(svc, "spawn").call(svc, this.applyOverlay(spec, overlay));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async dispatchTerminal(svc, spec) {
|
|
157
|
+
const store = shellStore.getStore();
|
|
158
|
+
const overlay = store ? shellOverlay(store.route) : null;
|
|
159
|
+
const handle = await regOriginal(svc, "spawnTerminal").call(svc, this.applyOverlay(spec, overlay));
|
|
160
|
+
if (store) {
|
|
161
|
+
const entry = {
|
|
162
|
+
handle,
|
|
163
|
+
agentId: store.agent,
|
|
164
|
+
generation: store.generation,
|
|
165
|
+
revision: store.revision,
|
|
166
|
+
};
|
|
167
|
+
this.persistentShells.add(entry);
|
|
168
|
+
handle.done?.then(
|
|
169
|
+
() => this.persistentShells.delete(entry),
|
|
170
|
+
() => this.persistentShells.delete(entry),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return handle;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 当前 Agent 的持久 shell 状态(status 用):stale / current / unknown。 */
|
|
177
|
+
shellStatus() {
|
|
178
|
+
const current = this.generation;
|
|
179
|
+
const shells = [...this.persistentShells].map((s) => ({
|
|
180
|
+
agentId: s.agentId,
|
|
181
|
+
generation: s.generation,
|
|
182
|
+
current: s.generation === current,
|
|
183
|
+
stale: s.generation < current,
|
|
184
|
+
revision: s.revision,
|
|
185
|
+
}));
|
|
186
|
+
return { generation: current, shells };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function regOriginal(svc, method) {
|
|
191
|
+
const reg = registry();
|
|
192
|
+
return reg.original?.[method] ?? svc[method];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function safeRoute(canonical) {
|
|
196
|
+
try {
|
|
197
|
+
return resolveAgentRoute(canonical);
|
|
198
|
+
} catch {
|
|
199
|
+
return Object.freeze({ kind: "direct" });
|
|
200
|
+
}
|
|
201
|
+
}
|
package/lib/status.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// lib/status.js — 公开状态投影(脱敏;凭据值永不出现)
|
|
2
|
+
import { publicSnapshot } from "./config.js";
|
|
3
|
+
|
|
4
|
+
/** 渲染一条路由:direct 或 profile(仅脱敏端点,hasUsername/hasPassword)。 */
|
|
5
|
+
export function renderRoute(route, canonical) {
|
|
6
|
+
if (!route || route.kind === "direct") return { kind: "direct" };
|
|
7
|
+
const profile = canonical.profiles.find((p) => p.id === route.profileId);
|
|
8
|
+
return {
|
|
9
|
+
kind: "profile",
|
|
10
|
+
profileId: route.profileId,
|
|
11
|
+
...(profile
|
|
12
|
+
? {
|
|
13
|
+
endpoint: {
|
|
14
|
+
protocol: profile.protocol,
|
|
15
|
+
host: profile.host,
|
|
16
|
+
port: profile.port,
|
|
17
|
+
hasUsername: Boolean(profile.username),
|
|
18
|
+
hasPassword: Boolean(profile.password),
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
: {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 组装 net_proxy_status 使用的公开状态。
|
|
27
|
+
* @param {object} deps
|
|
28
|
+
* @param {object} deps.canonical 当前 canonical
|
|
29
|
+
* @param {number} deps.revision 公开修订号
|
|
30
|
+
* @param {object} [deps.migration] 迁移元数据 {from, configured}
|
|
31
|
+
* @param {object} [deps.fetchRouter] FetchRouter 实例(暴露 ownerCount/owned)
|
|
32
|
+
* @param {object} [deps.shellRouter] ShellRouter 实例
|
|
33
|
+
*/
|
|
34
|
+
export function buildStatus({ canonical, revision, migration, fetchRouter, shellRouter }) {
|
|
35
|
+
const pub = publicSnapshot(canonical);
|
|
36
|
+
return {
|
|
37
|
+
revision,
|
|
38
|
+
migration: migration ? { from: migration.from, configured: Boolean(migration.configured) } : null,
|
|
39
|
+
profiles: pub.profiles,
|
|
40
|
+
agent: {
|
|
41
|
+
route: renderRoute(canonical.bindings.agent, canonical),
|
|
42
|
+
generation: shellRouter?.generation ?? 0,
|
|
43
|
+
},
|
|
44
|
+
providers: pub.bindings.providers,
|
|
45
|
+
gateway: pub.gateway,
|
|
46
|
+
fetchRouter: {
|
|
47
|
+
owned: Boolean(fetchRouter?.owned()),
|
|
48
|
+
owners: fetchRouter?.ownerCount() ?? 0,
|
|
49
|
+
},
|
|
50
|
+
shell: shellRouter
|
|
51
|
+
? {
|
|
52
|
+
generation: shellRouter.generation,
|
|
53
|
+
persistentShells: shellRouter.shellStatus().shells.map((s) => ({
|
|
54
|
+
agentId: s.agentId,
|
|
55
|
+
stale: s.stale,
|
|
56
|
+
current: s.current,
|
|
57
|
+
})),
|
|
58
|
+
}
|
|
59
|
+
: null,
|
|
60
|
+
};
|
|
61
|
+
}
|