@zhushanwen/pi-network-proxy 0.2.1 → 0.2.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/package.json +2 -2
- package/src/__tests__/index.test.ts +24 -29
- package/src/fetch-wrapper.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-network-proxy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@zhushanwen/pi-ext-guards": "0.5.0",
|
|
44
44
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
45
45
|
"@zhushanwen/pi-file-lock": "0.4.1",
|
|
46
|
-
"@zhushanwen/pi-llm-shared": "0.
|
|
46
|
+
"@zhushanwen/pi-llm-shared": "0.12.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@vitest/coverage-v8": "^4.1.9",
|
|
@@ -9,12 +9,13 @@ import { tmpdir } from "node:os";
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
11
|
|
|
12
|
+
// mock 只覆盖本包模块图实际消费的导出(getLogger / setPiHandle;createLogger 无消费方,
|
|
13
|
+
// 推测性 mock 会在 logger 包改表面时静默失配——缺导出是显式 undefined, loud fail)
|
|
12
14
|
const { loggerMock } = vi.hoisted(() => ({
|
|
13
15
|
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
14
16
|
}));
|
|
15
17
|
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
16
18
|
getLogger: () => loggerMock,
|
|
17
|
-
createLogger: () => loggerMock,
|
|
18
19
|
setPiHandle: vi.fn(),
|
|
19
20
|
}));
|
|
20
21
|
|
|
@@ -55,6 +56,19 @@ function writeConfigFile(content: string): void {
|
|
|
55
56
|
writeFileSync(join(agentDir, "config", "network-proxy-ext-config.json"), content);
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 标准前导:写配置文件 → 加载 extension(afterEach 的 resetModules 后每次 import
|
|
61
|
+
* 都是新模块图实例)→ 装配 fake pi 并执行工厂。config = 配置文件内容(默认 enabled)。
|
|
62
|
+
* 工厂在 helper 内执行:非法配置用例的「不抛」契约由 await 拒绝等价保证(抛即用例红)。
|
|
63
|
+
*/
|
|
64
|
+
async function loadExtension(content = JSON.stringify(ENABLED_CONFIG)): Promise<{ emit: (event: "turn_start" | "turn_end") => void }> {
|
|
65
|
+
writeConfigFile(content);
|
|
66
|
+
const { default: networkProxyExtension } = await import("../index.ts");
|
|
67
|
+
const { pi, emit } = createFakePi();
|
|
68
|
+
networkProxyExtension(pi);
|
|
69
|
+
return { emit };
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
beforeEach(() => {
|
|
59
73
|
agentDir = mkdtempSync(join(tmpdir(), "network-proxy-index-"));
|
|
60
74
|
vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
|
|
@@ -76,9 +90,7 @@ afterEach(() => {
|
|
|
76
90
|
|
|
77
91
|
describe("networkProxyExtension 工厂加载序列", () => {
|
|
78
92
|
it("enabled 且校验过:包装 globalThis.fetch(未命中规则时委托 fake 原 fetch)", async () => {
|
|
79
|
-
|
|
80
|
-
const { default: networkProxyExtension } = await import("../index.ts");
|
|
81
|
-
networkProxyExtension(createFakePi().pi);
|
|
93
|
+
await loadExtension();
|
|
82
94
|
expect(globalThis.fetch).not.toBe(fakeFetch);
|
|
83
95
|
const response = await globalThis.fetch("https://open.bigmodel.cn/api");
|
|
84
96
|
expect(await response.text()).toBe("fake-original");
|
|
@@ -87,26 +99,21 @@ describe("networkProxyExtension 工厂加载序列", () => {
|
|
|
87
99
|
});
|
|
88
100
|
|
|
89
101
|
it("enabled=false:不包装(globalThis.fetch 保持原实现),零 error", async () => {
|
|
90
|
-
|
|
91
|
-
const { default: networkProxyExtension } = await import("../index.ts");
|
|
92
|
-
networkProxyExtension(createFakePi().pi);
|
|
102
|
+
await loadExtension(JSON.stringify({ ...ENABLED_CONFIG, enabled: false }));
|
|
93
103
|
expect(globalThis.fetch).toBe(fakeFetch);
|
|
94
104
|
expect(loggerMock.error).not.toHaveBeenCalled();
|
|
95
105
|
});
|
|
96
106
|
|
|
97
107
|
it("非法配置(坏 JSON):不抛、不包装(全部直连),error 含恢复动作(配置文件路径)", async () => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
expect(() => networkProxyExtension(createFakePi().pi)).not.toThrow();
|
|
108
|
+
// 不抛契约由 loadExtension 的 await 保证(工厂抛错即本用例红)
|
|
109
|
+
await loadExtension("{ not valid json");
|
|
101
110
|
expect(globalThis.fetch).toBe(fakeFetch);
|
|
102
111
|
const messages = loggerMock.error.mock.calls.map((c) => String(c[0]));
|
|
103
112
|
expect(messages.some((m) => m.includes("network-proxy-ext-config.json"))).toBe(true);
|
|
104
113
|
});
|
|
105
114
|
|
|
106
115
|
it("非法字段(端口越界):loadConfig 回落 defaults(enabled=false → 不包装)+ error 可归因", async () => {
|
|
107
|
-
|
|
108
|
-
const { default: networkProxyExtension } = await import("../index.ts");
|
|
109
|
-
expect(() => networkProxyExtension(createFakePi().pi)).not.toThrow();
|
|
116
|
+
await loadExtension(JSON.stringify({ ...ENABLED_CONFIG, proxy: { ...ENABLED_CONFIG.proxy, port: 99999 } }));
|
|
110
117
|
expect(globalThis.fetch).toBe(fakeFetch);
|
|
111
118
|
expect(loggerMock.error.mock.calls.length).toBeGreaterThan(0);
|
|
112
119
|
});
|
|
@@ -114,10 +121,7 @@ describe("networkProxyExtension 工厂加载序列", () => {
|
|
|
114
121
|
|
|
115
122
|
describe("turn 粒度热生效", () => {
|
|
116
123
|
it("turn_start 重读配置:指纹变化换新包装,未变零动作", async () => {
|
|
117
|
-
|
|
118
|
-
const { default: networkProxyExtension } = await import("../index.ts");
|
|
119
|
-
const { pi, emit } = createFakePi();
|
|
120
|
-
networkProxyExtension(pi);
|
|
124
|
+
const { emit } = await loadExtension();
|
|
121
125
|
const first = globalThis.fetch;
|
|
122
126
|
|
|
123
127
|
// 未变配置:同包装函数(不重建)
|
|
@@ -131,10 +135,7 @@ describe("turn 粒度热生效", () => {
|
|
|
131
135
|
});
|
|
132
136
|
|
|
133
137
|
it("pi 重装 globalThis.fetch(configureHttpDispatcher)后 turn_start 重新包装覆盖者", async () => {
|
|
134
|
-
|
|
135
|
-
const { default: networkProxyExtension } = await import("../index.ts");
|
|
136
|
-
const { pi, emit } = createFakePi();
|
|
137
|
-
networkProxyExtension(pi);
|
|
138
|
+
const { emit } = await loadExtension();
|
|
138
139
|
|
|
139
140
|
let piReinstalledCalls = 0;
|
|
140
141
|
const piReinstalled: FetchLike = async () => {
|
|
@@ -151,12 +152,9 @@ describe("turn 粒度热生效", () => {
|
|
|
151
152
|
});
|
|
152
153
|
|
|
153
154
|
it("turn_end flush 计数增量到 stats JSONL(recordDispatch → turn_end → 落盘行)", async () => {
|
|
154
|
-
|
|
155
|
-
const { default: networkProxyExtension } = await import("../index.ts");
|
|
155
|
+
const { emit } = await loadExtension();
|
|
156
156
|
// resetModules 后 index.ts 与此处 import 的 stats.ts 是同一模块图刷新的同一实例
|
|
157
157
|
const { recordDispatch, statsFilePath } = await import("../stats.ts");
|
|
158
|
-
const { pi, emit } = createFakePi();
|
|
159
|
-
networkProxyExtension(pi);
|
|
160
158
|
|
|
161
159
|
recordDispatch("api.anthropic.com", true);
|
|
162
160
|
recordDispatch("open.bigmodel.cn", false);
|
|
@@ -169,10 +167,7 @@ describe("turn 粒度热生效", () => {
|
|
|
169
167
|
});
|
|
170
168
|
|
|
171
169
|
it("enabled true→false:turn_start 后恢复原 fetch(禁用生效)", async () => {
|
|
172
|
-
|
|
173
|
-
const { default: networkProxyExtension } = await import("../index.ts");
|
|
174
|
-
const { pi, emit } = createFakePi();
|
|
175
|
-
networkProxyExtension(pi);
|
|
170
|
+
const { emit } = await loadExtension();
|
|
176
171
|
expect(globalThis.fetch).not.toBe(fakeFetch);
|
|
177
172
|
|
|
178
173
|
writeConfigFile(JSON.stringify({ ...ENABLED_CONFIG, enabled: false }));
|
package/src/fetch-wrapper.ts
CHANGED
|
@@ -49,11 +49,12 @@ type FetchInit = Parameters<FetchLike>[1];
|
|
|
49
49
|
* 经代理发出时的 init:保留调用方全部选项,附上本次请求的代理分发器。
|
|
50
50
|
* Omit 掉 Node 全局 RequestInit 自带的 dispatcher(undici-types 类型身份),避免与 npm
|
|
51
51
|
* undici 的 Dispatcher 形成 `Dispatcher & Dispatcher` 冲突;运行时由本字段整体覆盖。
|
|
52
|
+
* 签名自包含(不引用文件内私有别名 FetchInit)——导出签名的被引用类型必须可命名。
|
|
52
53
|
*/
|
|
53
|
-
export type ProxyFetchInit = Omit<NonNullable<
|
|
54
|
+
export type ProxyFetchInit = Omit<NonNullable<Parameters<FetchLike>[1]>, "dispatcher"> & { dispatcher: Dispatcher };
|
|
54
55
|
|
|
55
56
|
/** 代理 fetch 注入接缝:生产环境 = undici.fetch;单测用 fake 避免真实网络。 */
|
|
56
|
-
export type ProxyFetch = (input:
|
|
57
|
+
export type ProxyFetch = (input: Parameters<FetchLike>[0], init: ProxyFetchInit) => Promise<Response>;
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* 跨模块环境身份标记(Symbol.for 进程级注册表,jiti reload 后新旧环境共享):
|