@zhushanwen/pi-network-proxy 0.3.2 → 0.3.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-network-proxy",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "LLM network proxy: rule-based per-hostname traffic splitting via a globalThis.fetch wrapper (http/https/socks5 proxy), with request counters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -41,14 +41,15 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"undici": "^7.28.0",
|
|
44
|
-
"@zhushanwen/pi-
|
|
45
|
-
"@zhushanwen/pi-ext-guards": "0.5.0",
|
|
44
|
+
"@zhushanwen/pi-ext-guards": "0.5.1",
|
|
46
45
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
47
|
-
"@zhushanwen/pi-file-lock": "0.4.1"
|
|
46
|
+
"@zhushanwen/pi-file-lock": "0.4.1",
|
|
47
|
+
"@zhushanwen/pi-llm-shared": "0.12.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@vitest/coverage-v8": "^4.1.9",
|
|
51
|
-
"vitest": "^4.1.8"
|
|
51
|
+
"vitest": "^4.1.8",
|
|
52
|
+
"@taiji/shared": "0.10.0"
|
|
52
53
|
},
|
|
53
54
|
"files": [
|
|
54
55
|
"index.ts",
|
|
@@ -65,6 +66,6 @@
|
|
|
65
66
|
}
|
|
66
67
|
},
|
|
67
68
|
"scripts": {
|
|
68
|
-
"test": "vitest run"
|
|
69
|
+
"test": "vitest run --pool=threads"
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* network-proxy 校验/匹配语义跨包对拍测试(架构腐化审计 network-proxy.md 候选 1 的机器锁)。
|
|
3
|
+
*
|
|
4
|
+
* 语义 SSOT = packages/shared/src/network-proxy.ts;本扩展 src/config.ts、src/match.ts
|
|
5
|
+
* 是 bundle 自包含复制体。此前两侧测试各自独立维护、互不 import(config.test.ts /
|
|
6
|
+
* match.test.ts 与 shared 侧 network-proxy.test.ts 各自锁本侧),注释声称的「镜像锁定
|
|
7
|
+
* 测试锁两侧行为一致」实际不存在——shared 侧语义变更不会使本包测试变红。
|
|
8
|
+
*
|
|
9
|
+
* 本测试照 retry-rules-validation-parity.test.ts 先例:import @taiji/shared 真实实现,
|
|
10
|
+
* 同一输入矩阵分别喂两侧,断言双侧同判——校验含错误清单逐字一致(文案面向用户且
|
|
11
|
+
* 含修复动作,两侧分叉 = 一侧的用户指引失效)。shared 侧调整语义 → 本测试红 →
|
|
12
|
+
* 按提示同步本包复制体(以 shared 为准)。
|
|
13
|
+
*
|
|
14
|
+
* 签名说明:shared validateNetworkProxyConfig 参数已统一为 unknown(与扩展侧同款,
|
|
15
|
+
* 运行时守卫本就按任意输入设计,类型级收窄统一、零运行时行为变化)。
|
|
16
|
+
*
|
|
17
|
+
* @taiji/shared 仅测试面消费(devDependencies + workspace 源直引,bundle 自包含
|
|
18
|
+
* 不受影响)。单包环境未安装该 devDependency 时本文件 import 即失败(不 skip)——
|
|
19
|
+
* 先补装 `@taiji/shared: workspace:*` 再跑。
|
|
20
|
+
*/
|
|
21
|
+
import { describe, expect, it } from "vitest";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_NETWORK_PROXY_CONFIG as SHARED_DEFAULT,
|
|
25
|
+
matchesHostPattern as sharedMatchesHostPattern,
|
|
26
|
+
normalizeHostname as sharedNormalizeHostname,
|
|
27
|
+
validateNetworkProxyConfig as sharedValidateNetworkProxyConfig,
|
|
28
|
+
} from "@taiji/shared";
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_NETWORK_PROXY_CONFIG,
|
|
32
|
+
validateNetworkProxyConfig,
|
|
33
|
+
} from "../config.ts";
|
|
34
|
+
import { matchesHostPattern, normalizeHostname } from "../match.ts";
|
|
35
|
+
|
|
36
|
+
// ============================================================
|
|
37
|
+
// normalizeHostname:同输入双侧同出
|
|
38
|
+
// ============================================================
|
|
39
|
+
|
|
40
|
+
const HOSTNAME_MATRIX: string[] = [
|
|
41
|
+
"api.anthropic.com",
|
|
42
|
+
"API.Anthropic.COM",
|
|
43
|
+
"api.anthropic.com.",
|
|
44
|
+
"api.anthropic.com..",
|
|
45
|
+
"LocalHost",
|
|
46
|
+
"localhost",
|
|
47
|
+
"xapi.openai.com",
|
|
48
|
+
"127.0.0.1",
|
|
49
|
+
"",
|
|
50
|
+
".",
|
|
51
|
+
"..",
|
|
52
|
+
" api.anthropic.com",
|
|
53
|
+
"api.anthropic.com ",
|
|
54
|
+
" ",
|
|
55
|
+
"api.xn--wse.academy",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
describe("normalizeHostname 双侧同判(归一化语义跨包对拍)", () => {
|
|
59
|
+
it("矩阵逐条:本侧输出 === shared 侧输出", () => {
|
|
60
|
+
for (const host of HOSTNAME_MATRIX) {
|
|
61
|
+
expect(normalizeHostname(host), `host=${JSON.stringify(host)}`).toBe(sharedNormalizeHostname(host));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("锚定值抽查(防两侧同漂时对拍空转——shared 侧自身测试另有行为锁)", () => {
|
|
66
|
+
expect(normalizeHostname("API.Anthropic.COM.")).toBe("api.anthropic.com");
|
|
67
|
+
expect(normalizeHostname(" api.anthropic.com")).toBe(" api.anthropic.com");
|
|
68
|
+
expect(normalizeHostname("")).toBe("");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ============================================================
|
|
73
|
+
// matchesHostPattern:真值表双侧同判
|
|
74
|
+
// ============================================================
|
|
75
|
+
|
|
76
|
+
const MATCH_MATRIX: { host: string; pattern: string }[] = [
|
|
77
|
+
// 等于 / suffix 命中
|
|
78
|
+
{ host: "api.anthropic.com", pattern: "api.anthropic.com" },
|
|
79
|
+
{ host: "x.api.anthropic.com", pattern: "api.anthropic.com" },
|
|
80
|
+
{ host: "api.anthropic.com", pattern: "anthropic.com" },
|
|
81
|
+
// 点边界:无点前缀拼接不命中
|
|
82
|
+
{ host: "xapi.anthropic.com", pattern: "api.anthropic.com" },
|
|
83
|
+
// 恶意后缀不命中
|
|
84
|
+
{ host: "api-anthropic.com.evil.com", pattern: "api-anthropic.com" },
|
|
85
|
+
// 大小写 / 末尾点归一化后命中
|
|
86
|
+
{ host: "X.API.Anthropic.COM.", pattern: "api.anthropic.com" },
|
|
87
|
+
{ host: "api.anthropic.com", pattern: "API.Anthropic.COM." },
|
|
88
|
+
// 空主机名 / 空 pattern / 纯点 pattern
|
|
89
|
+
{ host: "", pattern: "api.anthropic.com" },
|
|
90
|
+
{ host: "api.anthropic.com", pattern: "" },
|
|
91
|
+
{ host: "api.anthropic.com", pattern: "." },
|
|
92
|
+
{ host: "api.anthropic.com", pattern: ".." },
|
|
93
|
+
// 带空白条目恒不命中(归一化不 trim,校验层显式拒绝)
|
|
94
|
+
{ host: "api.anthropic.com", pattern: " api.anthropic.com" },
|
|
95
|
+
{ host: " api.anthropic.com", pattern: "api.anthropic.com" },
|
|
96
|
+
// pattern 比主机名长(suffix 反向不命中)
|
|
97
|
+
{ host: "anthropic.com", pattern: "api.anthropic.com" },
|
|
98
|
+
// IP 条目(noProxy 默认成员形态)
|
|
99
|
+
{ host: "127.0.0.1", pattern: "127.0.0.1" },
|
|
100
|
+
{ host: "localhost", pattern: "localhost" },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
describe("matchesHostPattern 双侧同判(suffix 点边界真值表跨包对拍)", () => {
|
|
104
|
+
it("矩阵逐条:本侧输出 === shared 侧输出", () => {
|
|
105
|
+
for (const { host, pattern } of MATCH_MATRIX) {
|
|
106
|
+
expect(
|
|
107
|
+
matchesHostPattern(host, pattern),
|
|
108
|
+
`host=${JSON.stringify(host)}, pattern=${JSON.stringify(pattern)}`,
|
|
109
|
+
).toBe(sharedMatchesHostPattern(host, pattern));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("锚定值抽查(点边界与恶意后缀是安全语义,两侧同漂也不许双双失守)", () => {
|
|
114
|
+
expect(matchesHostPattern("xapi.anthropic.com", "api.anthropic.com")).toBe(false);
|
|
115
|
+
expect(matchesHostPattern("api-anthropic.com.evil.com", "api-anthropic.com")).toBe(false);
|
|
116
|
+
expect(matchesHostPattern("x.api.anthropic.com", "api.anthropic.com")).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ============================================================
|
|
121
|
+
// validateNetworkProxyConfig:同输入双侧同判(含错误清单逐字一致)
|
|
122
|
+
// ============================================================
|
|
123
|
+
|
|
124
|
+
/** 合法基座(每次浅拷贝,矩阵条目间无共享可变状态)。 */
|
|
125
|
+
function validConfig(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
126
|
+
return {
|
|
127
|
+
enabled: true,
|
|
128
|
+
proxy: { protocol: "http", host: "127.0.0.1", port: 7890, username: "", password: "" },
|
|
129
|
+
noProxy: ["localhost"],
|
|
130
|
+
rules: ["api.anthropic.com"],
|
|
131
|
+
ipv4Only: [],
|
|
132
|
+
...overrides,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 单输入双侧对拍:同一配置喂两侧 validateNetworkProxyConfig,断言
|
|
138
|
+
* ① ok 同判;② 都拒绝时错误清单逐字一致(含条目顺序——顺序也是用户可见形态)。
|
|
139
|
+
*/
|
|
140
|
+
function expectSameValidation(label: string, config: unknown): void {
|
|
141
|
+
const sharedResult = sharedValidateNetworkProxyConfig(config);
|
|
142
|
+
const extResult = validateNetworkProxyConfig(config);
|
|
143
|
+
expect(extResult.ok, `[${label}] ok 双侧同判`).toBe(sharedResult.ok);
|
|
144
|
+
if (!sharedResult.ok && !extResult.ok) {
|
|
145
|
+
expect(extResult.errors, `[${label}] 错误清单逐字一致`).toEqual(sharedResult.errors);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const VALIDATION_MATRIX: { label: string; config: unknown }[] = [
|
|
150
|
+
// ── 合法形态 ──
|
|
151
|
+
{ label: "全字段合法", config: validConfig() },
|
|
152
|
+
{ label: "socks5 协议 + 成对凭据", config: validConfig({ proxy: { protocol: "socks5", host: "p.example.com", port: 1080, username: "u", password: "p" } }) },
|
|
153
|
+
{ label: "ipv4Only 缺省(旧配置文件向后兼容形态)", config: (() => { const c = validConfig(); delete c.ipv4Only; return c; })() },
|
|
154
|
+
{ label: "空清单 + 禁用态(DEFAULT 形态)", config: { enabled: false, proxy: { protocol: "http", host: "127.0.0.1", port: 7890, username: "", password: "" }, noProxy: ["localhost", "127.0.0.1"], rules: [], ipv4Only: [] } },
|
|
155
|
+
{ label: "重复条目不报错(匹配天然幂等)", config: validConfig({ rules: ["a.com", "a.com"] }) },
|
|
156
|
+
// ── 顶层对象性 ──
|
|
157
|
+
{ label: "null", config: null },
|
|
158
|
+
{ label: "数组", config: [validConfig()] },
|
|
159
|
+
{ label: "字符串", config: "{}" },
|
|
160
|
+
{ label: "数字", config: 42 },
|
|
161
|
+
// ── enabled ──
|
|
162
|
+
{ label: "enabled 缺省", config: (() => { const c = validConfig(); delete c.enabled; return c; })() },
|
|
163
|
+
{ label: "enabled 非布尔", config: validConfig({ enabled: "yes" }) },
|
|
164
|
+
// ── proxy ──
|
|
165
|
+
{ label: "proxy 缺省", config: (() => { const c = validConfig(); delete c.proxy; return c; })() },
|
|
166
|
+
{ label: "proxy null", config: validConfig({ proxy: null }) },
|
|
167
|
+
{ label: "proxy 数组", config: validConfig({ proxy: ["http"] }) },
|
|
168
|
+
{ label: "协议词表外", config: validConfig({ proxy: { protocol: "socks4", host: "127.0.0.1", port: 7890, username: "", password: "" } }) },
|
|
169
|
+
{ label: "协议非字符串", config: validConfig({ proxy: { protocol: 1, host: "127.0.0.1", port: 7890, username: "", password: "" } }) },
|
|
170
|
+
{ label: "host 空串", config: validConfig({ proxy: { protocol: "http", host: "", port: 7890, username: "", password: "" } }) },
|
|
171
|
+
{ label: "host 纯空白", config: validConfig({ proxy: { protocol: "http", host: " ", port: 7890, username: "", password: "" } }) },
|
|
172
|
+
{ label: "host 非字符串", config: validConfig({ proxy: { protocol: "http", host: 80, port: 7890, username: "", password: "" } }) },
|
|
173
|
+
{ label: "端口 0", config: validConfig({ proxy: { protocol: "http", host: "127.0.0.1", port: 0, username: "", password: "" } }) },
|
|
174
|
+
{ label: "端口 65536", config: validConfig({ proxy: { protocol: "http", host: "127.0.0.1", port: 65536, username: "", password: "" } }) },
|
|
175
|
+
{ label: "端口小数", config: validConfig({ proxy: { protocol: "http", host: "127.0.0.1", port: 7890.5, username: "", password: "" } }) },
|
|
176
|
+
{ label: "端口字符串", config: validConfig({ proxy: { protocol: "http", host: "127.0.0.1", port: "7890", username: "", password: "" } }) },
|
|
177
|
+
{ label: "凭据非文本", config: validConfig({ proxy: { protocol: "http", host: "127.0.0.1", port: 7890, username: 1, password: "" } }) },
|
|
178
|
+
{ label: "凭据单边(有用户名无密码)", config: validConfig({ proxy: { protocol: "http", host: "127.0.0.1", port: 7890, username: "u", password: "" } }) },
|
|
179
|
+
{ label: "凭据单边(无用户名有密码)", config: validConfig({ proxy: { protocol: "http", host: "127.0.0.1", port: 7890, username: "", password: "p" } }) },
|
|
180
|
+
// ── noProxy / rules 条目 ──
|
|
181
|
+
{ label: "noProxy 非数组", config: validConfig({ noProxy: "localhost" }) },
|
|
182
|
+
{ label: "noProxy 空串条目", config: validConfig({ noProxy: [""] }) },
|
|
183
|
+
{ label: "noProxy 纯空白条目", config: validConfig({ noProxy: [" "] }) },
|
|
184
|
+
{ label: "noProxy 非字符串条目", config: validConfig({ noProxy: [123] }) },
|
|
185
|
+
{ label: "rules 含空白字符条目", config: validConfig({ rules: ["api.anthropic.com "] }) },
|
|
186
|
+
{ label: "rules 非数组", config: validConfig({ rules: {} }) },
|
|
187
|
+
// ── ipv4Only ──
|
|
188
|
+
{ label: "ipv4Only 非数组(显式声明非法形态才报错)", config: validConfig({ ipv4Only: "example.com" }) },
|
|
189
|
+
{ label: "ipv4Only 空串条目", config: validConfig({ ipv4Only: [""] }) },
|
|
190
|
+
{ label: "ipv4Only 含空白条目", config: validConfig({ ipv4Only: [" example.com"] }) },
|
|
191
|
+
// ── 多错误并存(锁收集顺序)──
|
|
192
|
+
{ label: "多字段同时非法(enabled + proxy + noProxy + rules + ipv4Only)", config: { enabled: 1, proxy: null, noProxy: "x", rules: [""], ipv4Only: 3 } },
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
describe("validateNetworkProxyConfig 双侧同判(同输入矩阵跨包对拍)", () => {
|
|
196
|
+
it("矩阵逐条:ok 同判 + 拒绝时错误清单逐字一致", () => {
|
|
197
|
+
for (const { label, config } of VALIDATION_MATRIX) {
|
|
198
|
+
expectSameValidation(label, config);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("锚定值抽查:合法最小形态两侧都 ok、顶层非对象两侧都拒", () => {
|
|
203
|
+
expect(validateNetworkProxyConfig(validConfig())).toEqual({ ok: true });
|
|
204
|
+
expect(sharedValidateNetworkProxyConfig(validConfig())).toEqual({ ok: true });
|
|
205
|
+
expect(validateNetworkProxyConfig(null).ok).toBe(false);
|
|
206
|
+
expect(sharedValidateNetworkProxyConfig(null).ok).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ============================================================
|
|
211
|
+
// DEFAULT_NETWORK_PROXY_CONFIG:两侧深相等
|
|
212
|
+
// ============================================================
|
|
213
|
+
|
|
214
|
+
describe("DEFAULT_NETWORK_PROXY_CONFIG 双侧深相等(默认面起点一致)", () => {
|
|
215
|
+
it("本侧 DEFAULT === shared 侧 DEFAULT", () => {
|
|
216
|
+
expect(DEFAULT_NETWORK_PROXY_CONFIG).toEqual(SHARED_DEFAULT);
|
|
217
|
+
});
|
|
218
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* 类型与校验语义 SSOT = packages/shared/src/network-proxy.ts(u-foundation 契约)——
|
|
5
5
|
* extension bundle 自包含不 import shared 运行时代码(extensions 对 shared 的既有惯例:
|
|
6
6
|
* 结构类型本地声明 + 行为镜像锁定测试),本文件是复制体;漂移时以 shared 为准,
|
|
7
|
-
* __tests__/config.test.ts
|
|
7
|
+
* __tests__/config.test.ts 锁本侧行为,__tests__/network-proxy-shared-parity.test.ts
|
|
8
|
+
* 跨包对拍(同输入矩阵 import @taiji/shared 双侧同判)锁两侧一致。
|
|
8
9
|
*
|
|
9
10
|
* 配置文件 = <agentDir>/config/network-proxy-ext-config.json(llm-shared getConfigPath
|
|
10
11
|
* 推导;runtime 写侧与 settings 页经 RPC 打通,设计 .tmp/tech-design/llm-network-proxy.md
|
package/src/match.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* 语义 SSOT = packages/shared/src/network-proxy.ts 的同名函数——extension bundle 自包含
|
|
5
5
|
* 不 import shared 运行时代码(extensions 对 shared 的既有惯例:结构类型本地声明 + 行为
|
|
6
6
|
* 真值表镜像锁定,见 rename-session RENAME_INSTRUCTION_MAX_LENGTH 先例),本文件是复制体;
|
|
7
|
-
* 漂移时以 shared 为准,__tests__/match.test.ts
|
|
7
|
+
* 漂移时以 shared 为准,__tests__/match.test.ts 锁本侧真值表,
|
|
8
|
+
* __tests__/network-proxy-shared-parity.test.ts 跨包对拍锁两侧一致。
|
|
8
9
|
*
|
|
9
10
|
* 匹配语义(设计 .tmp/tech-design/llm-network-proxy.md §3.1 配置 schema):
|
|
10
11
|
* suffix = 主机名等于或以 `.pattern` 结尾;点边界必须对齐——`xapi.anthropic.com` 不命中
|