autowonder 0.2.144 → 0.2.145
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/README.md +10 -10
- package/bin/cli.js +14 -2
- package/bin/token-proxy.js +158 -45
- package/package.json +1 -1
- package/vendor/autowonder-daemon-darwin-amd64 +0 -0
- package/vendor/autowonder-daemon-darwin-arm64 +0 -0
- package/vendor/autowonder-daemon-linux-amd64 +0 -0
- package/vendor/autowonder-daemon-linux-arm64 +0 -0
- package/vendor/autowonder-daemon-win32-amd64.exe +0 -0
- package/vendor/autowonder-daemon-win32-arm64.exe +0 -0
package/README.md
CHANGED
|
@@ -6,16 +6,16 @@ AutoWonder 本地 agent runtime。安装后启动 daemon,持续轮询本地 as
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# Qoder CLI(模型使用 provider model ID;Context Window 使用固定档位)
|
|
9
|
-
npx -y autowonder@0.2.
|
|
9
|
+
npx -y autowonder@0.2.145 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qoder --model qmodel_latest --reasoning-effort medium --context-window 260000
|
|
10
10
|
|
|
11
11
|
# Qoder CLI CN
|
|
12
|
-
npx -y autowonder@0.2.
|
|
12
|
+
npx -y autowonder@0.2.145 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
|
|
13
13
|
|
|
14
14
|
# Claude Code
|
|
15
|
-
npx -y autowonder@0.2.
|
|
15
|
+
npx -y autowonder@0.2.145 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider claude
|
|
16
16
|
|
|
17
17
|
# Codex CLI
|
|
18
|
-
npx -y autowonder@0.2.
|
|
18
|
+
npx -y autowonder@0.2.145 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider codex --model gpt-5.5 --reasoning-effort medium
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
`connect` 会安装当前 npm 包内置的 daemon,并使用页面生成的 WebSocket endpoint、Token 和执行器 ID 建立连接。选择 Qoder 时需要 Node.js 20+;runtime 会在缺少 `qodercli` 时自动通过 npm 安装,并在尚未登录时打开 Qoder 浏览器登录。Claude Code 和 Codex CLI 仍需提前安装并登录。runtime 会继承当前用户的 HOME、环境变量和 CLI 登录状态。
|
|
@@ -66,7 +66,7 @@ mv "$queue/.assignment.tmp" "$queue/assignment.json"
|
|
|
66
66
|
也可以直接提交到本地 API:
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
npx -y autowonder@0.2.
|
|
69
|
+
npx -y autowonder@0.2.145 dispatch ./assignment.json
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
## 上传工单附件
|
|
@@ -74,7 +74,7 @@ npx -y autowonder@0.2.144 dispatch ./assignment.json
|
|
|
74
74
|
把本地需求/设计文档直接上传到工单,避免在会话里传递大文件:
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
|
-
npx -y autowonder@0.2.
|
|
77
|
+
npx -y autowonder@0.2.145 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
上传令牌通过 MCP 工具 `autowonder.workitem_cli_upload_token` 签发,经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 环境变量传入。`--file` 可重复指定多个文件;`--json` 输出机器可读结果。失败时按错误类型返回不同退出码(401→3、403→4、404→5、409→6、413→7、其他 4xx→8、网络/重定向→9)。
|
|
@@ -84,7 +84,7 @@ npx -y autowonder@0.2.144 workitem upload --server-url <autowonder-server-url> -
|
|
|
84
84
|
把需求/设计文档上传到 7x24 定时任务,用法与工单附件一致(同一套上传令牌与预检规则):
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
npx -y autowonder@0.2.
|
|
87
|
+
npx -y autowonder@0.2.145 scheduled-task upload --server-url <autowonder-server-url> --scheduled-task-id <id> --file <filepath-1> --file <filepath-2> --json
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
上传令牌同样通过 `autowonder.workitem_cli_upload_token` 签发(令牌为用户级,对当前可修改的任意定时任务生效),经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 传入;退出码约定与 `workitem upload` 相同。
|
|
@@ -92,8 +92,8 @@ npx -y autowonder@0.2.144 scheduled-task upload --server-url <autowonder-server-
|
|
|
92
92
|
## 管理 daemon
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
npx -y autowonder@0.2.
|
|
96
|
-
npx -y autowonder@0.2.
|
|
95
|
+
npx -y autowonder@0.2.145 status
|
|
96
|
+
npx -y autowonder@0.2.145 stop
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
默认 API 是 `http://127.0.0.1:34989`,日志位于 `~/.autowonder/daemon.log`。npm 包不包含任何 agent、MCP 或服务端凭证。
|
|
@@ -101,7 +101,7 @@ npx -y autowonder@0.2.144 stop
|
|
|
101
101
|
## 调试
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
npx -y autowonder@0.2.
|
|
104
|
+
npx -y autowonder@0.2.145 connect ... --debug
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
`--debug` 会让 CLI 以 `AUTOWONDER_DEBUG=1` 启动 daemon,把完整的 daemon 与 agent 活动(runtime 事件流、provider 事件解码、重试退避与会话门等待、空闲时的阻塞点)同时输出到控制台和会话调试日志。调试日志不做脱敏,仅用于本机排障;不传 `--debug` 时该环境变量不会注入。
|
package/bin/cli.js
CHANGED
|
@@ -438,9 +438,16 @@ async function cmdConnect(args) {
|
|
|
438
438
|
const proxyPort = parseInt(process.env.TOKEN_PROXY_PORT || "19876", 10);
|
|
439
439
|
const caCertPath = path.join(AUTOWONDER_HOME, "token-tap", "ca-cert.pem");
|
|
440
440
|
try {
|
|
441
|
+
const proxyLogPath = path.join(AUTOWONDER_HOME, "token-tap", "proxy-diag.log");
|
|
442
|
+
const proxyStdio = flags.debug
|
|
443
|
+
? ["ignore", fs.openSync(proxyLogPath, "a"), fs.openSync(proxyLogPath, "a")]
|
|
444
|
+
: ["ignore", "ignore", "ignore"];
|
|
445
|
+
const proxyEnv = { ...process.env };
|
|
446
|
+
if (flags.debug) proxyEnv.AUTOWONDER_DEBUG = "1";
|
|
441
447
|
tokenProxyChild = spawn(process.execPath, [tokenProxyScript, "start", "--port", String(proxyPort)], {
|
|
442
448
|
detached: true,
|
|
443
|
-
stdio:
|
|
449
|
+
stdio: proxyStdio,
|
|
450
|
+
env: proxyEnv,
|
|
444
451
|
});
|
|
445
452
|
tokenProxyChild.unref();
|
|
446
453
|
|
|
@@ -913,12 +920,17 @@ function ensureWindowsUTF8() {
|
|
|
913
920
|
process.env.PYTHONUTF8 = process.env.PYTHONUTF8 || "1";
|
|
914
921
|
process.env.JAVA_TOOL_OPTIONS = process.env.JAVA_TOOL_OPTIONS || "-Dfile.encoding=UTF-8";
|
|
915
922
|
if (!process.env.GIT_CONFIG_COUNT) {
|
|
916
|
-
process.env.GIT_CONFIG_COUNT = "
|
|
923
|
+
process.env.GIT_CONFIG_COUNT = "3";
|
|
917
924
|
process.env.GIT_CONFIG_KEY_0 = "i18n.logOutputEncoding";
|
|
918
925
|
process.env.GIT_CONFIG_VALUE_0 = "utf-8";
|
|
919
926
|
process.env.GIT_CONFIG_KEY_1 = "i18n.commitEncoding";
|
|
920
927
|
process.env.GIT_CONFIG_VALUE_1 = "utf-8";
|
|
928
|
+
process.env.GIT_CONFIG_KEY_2 = "core.longpaths";
|
|
929
|
+
process.env.GIT_CONFIG_VALUE_2 = "true";
|
|
921
930
|
}
|
|
931
|
+
// Persist core.longpaths in git global config so all git operations
|
|
932
|
+
// (including checkpoint restore) can handle paths > MAX_PATH (260 chars).
|
|
933
|
+
spawnSync("git", ["config", "--global", "core.longpaths", "true"], { stdio: "ignore", timeout: 5000 });
|
|
922
934
|
}
|
|
923
935
|
|
|
924
936
|
// ─── Main ────────────────────────────────────────────────────────────
|
package/bin/token-proxy.js
CHANGED
|
@@ -36,15 +36,17 @@ const CA_KEY_PATH = path.join(DATA_DIR, 'ca-key.pem');
|
|
|
36
36
|
const CA_CERT_PATH = path.join(DATA_DIR, 'ca-cert.pem');
|
|
37
37
|
const USAGE_LOG_PATH = path.join(DATA_DIR, 'token-usage.jsonl');
|
|
38
38
|
const USAGE_LOG_OLD = path.join(DATA_DIR, 'token-usage.old.jsonl');
|
|
39
|
+
const RAW_LOG_DIR = path.join(DATA_DIR, 'raw');
|
|
40
|
+
const DEBUG = process.env.AUTOWONDER_DEBUG === '1';
|
|
39
41
|
|
|
40
42
|
// Qoder/QoderCN LLM API hosts to sniff (others are tunneled without inspection).
|
|
41
43
|
// Only LLM endpoints are listed — non-LLM APIs (openapi.*) are excluded to avoid
|
|
42
44
|
// unnecessary MITM overhead on high-frequency config/auth calls.
|
|
43
45
|
const SNIFF_HOSTS = new Set([
|
|
44
46
|
// Qoder (international)
|
|
45
|
-
'api3.qoder.sh', 'api2.qoder.sh', 'api.qoder.sh',
|
|
47
|
+
'api3.qoder.sh', 'api2.qoder.sh', 'api.qoder.sh', 'openapi.qoder.sh',
|
|
46
48
|
// QoderCN (China)
|
|
47
|
-
'gateway.qoder.com.cn',
|
|
49
|
+
'gateway.qoder.com.cn', 'openapi.qoder.com.cn',
|
|
48
50
|
'api3.qodercn.com', 'api2.qodercn.com', 'api.qodercn.com',
|
|
49
51
|
]);
|
|
50
52
|
|
|
@@ -163,15 +165,16 @@ function extractUsageFromResponse(buffer) {
|
|
|
163
165
|
// 1. Qoder SSE (current): data:{"headers":{...},"body":"{...\"usage\":{...}}"}
|
|
164
166
|
// 2. Standard OpenAI SSE: data: {"choices":[...],"usage":{...}}
|
|
165
167
|
// 3. NDJSON streaming: {"choices":[...],"usage":{...}}\n
|
|
166
|
-
// 4. Single JSON response: {"choices":[...],"usage":{...}}
|
|
167
|
-
//
|
|
168
|
-
// Strategy: scan lines backwards for "prompt_tokens", try multiple parse strategies.
|
|
168
|
+
// 4. Single JSON response (non-streaming): {"choices":[...],"usage":{...}}
|
|
169
|
+
// 5. Regex extraction: find "usage":{...} anywhere in buffer
|
|
169
170
|
try {
|
|
170
171
|
const text = buffer.toString('utf8');
|
|
171
172
|
const lines = text.split('\n');
|
|
173
|
+
|
|
174
|
+
// Strategy 1-3: line-based parsing (backwards scan)
|
|
172
175
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
173
176
|
const line = lines[i].trim();
|
|
174
|
-
if (!line.includes('
|
|
177
|
+
if (!line.includes('"usage"') && !line.includes('\\"usage\\"')) continue;
|
|
175
178
|
|
|
176
179
|
// Strategy 1: Qoder nested SSE — data:{"headers":...,"body":"<escaped json>"}
|
|
177
180
|
if (line.startsWith('data:')) {
|
|
@@ -182,16 +185,67 @@ function extractUsageFromResponse(buffer) {
|
|
|
182
185
|
if (usage2) return usage2;
|
|
183
186
|
}
|
|
184
187
|
|
|
185
|
-
// Strategy 3:
|
|
188
|
+
// Strategy 3: Qoder nested without "data:" prefix — {"headers":...,"body":"..."}
|
|
189
|
+
if (line.startsWith('{') && line.includes('"body"')) {
|
|
190
|
+
const usage3a = tryQoderNestedSSE(line);
|
|
191
|
+
if (usage3a) return usage3a;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Strategy 4: Plain JSON line (NDJSON or single response)
|
|
186
195
|
const usage3 = tryDirectJSON(line);
|
|
187
196
|
if (usage3) return usage3;
|
|
188
197
|
}
|
|
198
|
+
|
|
199
|
+
// Strategy 4: Regex extraction — find usage JSON object directly in buffer
|
|
200
|
+
// Handles cases where the response is not line-delimited or spans multiple chunks
|
|
201
|
+
const usageMatch = text.match(/"usage"\s*:\s*\{[^}]*"prompt_tokens"\s*:\s*\d+[^}]*\}/);
|
|
202
|
+
if (usageMatch) {
|
|
203
|
+
const usage4 = tryExtractUsageObject(usageMatch[0], text);
|
|
204
|
+
if (usage4) return usage4;
|
|
205
|
+
}
|
|
206
|
+
// Also check input_tokens variant
|
|
207
|
+
const usageMatch2 = text.match(/"usage"\s*:\s*\{[^}]*"input_tokens"\s*:\s*\d+[^}]*\}/);
|
|
208
|
+
if (usageMatch2) {
|
|
209
|
+
const usage5 = tryExtractUsageObject(usageMatch2[0], text);
|
|
210
|
+
if (usage5) return usage5;
|
|
211
|
+
}
|
|
189
212
|
} catch (_) {
|
|
190
213
|
// Silent: parsing failure only loses token data, never breaks traffic
|
|
191
214
|
}
|
|
192
215
|
return null;
|
|
193
216
|
}
|
|
194
217
|
|
|
218
|
+
function tryExtractUsageObject(usageStr, fullText) {
|
|
219
|
+
// Extract usage from a regex-matched "usage":{...} fragment
|
|
220
|
+
// Handle nested objects like prompt_tokens_details by using a greedy match
|
|
221
|
+
try {
|
|
222
|
+
// Try to parse just the usage value (strip the key prefix)
|
|
223
|
+
let jsonStr = usageStr.replace(/^"usage"\s*:\s*/, '');
|
|
224
|
+
// Handle nested braces (prompt_tokens_details, completion_tokens_details)
|
|
225
|
+
// Re-extract with nested brace awareness
|
|
226
|
+
const startIdx = fullText.indexOf(usageStr);
|
|
227
|
+
if (startIdx >= 0) {
|
|
228
|
+
const usageStart = fullText.indexOf('{', startIdx + 7);
|
|
229
|
+
if (usageStart >= 0) {
|
|
230
|
+
let depth = 0, end = usageStart;
|
|
231
|
+
for (let i = usageStart; i < fullText.length && i < usageStart + 2000; i++) {
|
|
232
|
+
if (fullText[i] === '{') depth++;
|
|
233
|
+
else if (fullText[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
234
|
+
}
|
|
235
|
+
jsonStr = fullText.slice(usageStart, end + 1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const u = JSON.parse(jsonStr);
|
|
239
|
+
if (u && (u.prompt_tokens !== undefined || u.input_tokens !== undefined)) {
|
|
240
|
+
// Try to find model from nearby context
|
|
241
|
+
const modelMatch = fullText.match(/"model"\s*:\s*"([^"]+)"/);
|
|
242
|
+
const model = modelMatch ? modelMatch[1] : 'unknown';
|
|
243
|
+
return buildUsageRecord(u, model);
|
|
244
|
+
}
|
|
245
|
+
} catch (_) {}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
195
249
|
function tryQoderNestedSSE(data) {
|
|
196
250
|
try {
|
|
197
251
|
const outer = JSON.parse(data);
|
|
@@ -239,25 +293,32 @@ function appendUsage(usage) {
|
|
|
239
293
|
}
|
|
240
294
|
}
|
|
241
295
|
|
|
242
|
-
// ─── Dispatch ID extraction from Proxy-Authorization
|
|
296
|
+
// ─── Dispatch/Step ID extraction from Proxy-Authorization ───────────────────
|
|
243
297
|
|
|
244
|
-
function
|
|
245
|
-
// daemon sets HTTPS_PROXY=http://d-{dispatchId}:x@host:port
|
|
246
|
-
// → client sends Proxy-Authorization: Basic <base64("d-{id}:x")>
|
|
298
|
+
function extractDispatchInfo(req) {
|
|
299
|
+
// daemon sets HTTPS_PROXY=http://d-{dispatchId}-s-{stepId}:x@host:port
|
|
300
|
+
// → client sends Proxy-Authorization: Basic <base64("d-{id}-s-{step}:x")>
|
|
247
301
|
const auth = req.headers['proxy-authorization'] || '';
|
|
248
|
-
if (!auth.startsWith('Basic ')) return null;
|
|
302
|
+
if (!auth.startsWith('Basic ')) return { dispatchId: null, stepId: null };
|
|
249
303
|
try {
|
|
250
304
|
const decoded = Buffer.from(auth.slice(6), 'base64').toString('utf8');
|
|
251
305
|
const [user] = decoded.split(':');
|
|
252
|
-
if (user
|
|
306
|
+
if (!user || !user.startsWith('d-')) return { dispatchId: null, stepId: null };
|
|
307
|
+
const payload = user.slice(2); // remove "d-" prefix
|
|
308
|
+
const stepIdx = payload.indexOf('-s-');
|
|
309
|
+
if (stepIdx >= 0) {
|
|
310
|
+
return { dispatchId: payload.slice(0, stepIdx), stepId: payload.slice(stepIdx + 3) };
|
|
311
|
+
}
|
|
312
|
+
return { dispatchId: payload, stepId: null };
|
|
253
313
|
} catch (_) {}
|
|
254
|
-
return null;
|
|
314
|
+
return { dispatchId: null, stepId: null };
|
|
255
315
|
}
|
|
256
316
|
|
|
257
317
|
// ─── Forward Proxy (CONNECT method) ─────────────────────────────────────────
|
|
258
318
|
|
|
259
319
|
function startProxy(port) {
|
|
260
320
|
const ca = ensureCA();
|
|
321
|
+
const dbg = DEBUG ? (...args) => console.log(...args) : () => {};
|
|
261
322
|
|
|
262
323
|
const server = http.createServer((req, res) => {
|
|
263
324
|
// Non-CONNECT requests: reject (we only handle CONNECT tunnels)
|
|
@@ -265,40 +326,47 @@ function startProxy(port) {
|
|
|
265
326
|
res.end('Only CONNECT supported');
|
|
266
327
|
});
|
|
267
328
|
|
|
329
|
+
let connSeq = 0;
|
|
268
330
|
server.on('connect', (req, clientSocket, head) => {
|
|
331
|
+
const seq = ++connSeq;
|
|
332
|
+
const ts = () => new Date().toISOString().slice(11, 23);
|
|
269
333
|
const [hostname, portStr] = req.url.split(':');
|
|
270
334
|
const remotePort = parseInt(portStr) || 443;
|
|
271
335
|
const shouldSniff = SNIFF_HOSTS.has(hostname);
|
|
272
|
-
const dispatchId =
|
|
336
|
+
const { dispatchId, stepId } = extractDispatchInfo(req);
|
|
337
|
+
|
|
338
|
+
dbg(`[${ts()}] CONNECT #${seq} ${hostname}:${remotePort} sniff=${shouldSniff} dispatch=${dispatchId || 'none'} step=${stepId || 'none'}`);
|
|
273
339
|
|
|
274
340
|
if (!shouldSniff) {
|
|
275
|
-
|
|
341
|
+
dbg(`[${ts()}] #${seq} → BLIND TUNNEL (not in SNIFF_HOSTS)`);
|
|
276
342
|
const upstream = net.connect(remotePort, hostname, () => {
|
|
277
343
|
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
278
344
|
upstream.write(head);
|
|
279
345
|
upstream.pipe(clientSocket);
|
|
280
346
|
clientSocket.pipe(upstream);
|
|
281
347
|
});
|
|
282
|
-
upstream.on('error', () => { try { clientSocket.end(); } catch (_) {} });
|
|
283
|
-
clientSocket.on('error', () => { try { upstream.end(); } catch (_) {} });
|
|
348
|
+
upstream.on('error', (e) => { dbg(`[${ts()}] #${seq} blind upstream err: ${e.message}`); try { clientSocket.end(); } catch (_) {} });
|
|
349
|
+
clientSocket.on('error', (e) => { dbg(`[${ts()}] #${seq} blind client err: ${e.message}`); try { upstream.end(); } catch (_) {} });
|
|
284
350
|
return;
|
|
285
351
|
}
|
|
286
352
|
|
|
287
353
|
// MITM for Qoder API hosts: decrypt, sniff usage, re-encrypt
|
|
288
354
|
const hostCert = getHostCert(hostname, ca);
|
|
289
355
|
if (!hostCert) {
|
|
290
|
-
|
|
356
|
+
dbg(`[${ts()}] #${seq} → BLIND TUNNEL (cert generation failed for ${hostname})`);
|
|
291
357
|
const upstream = net.connect(remotePort, hostname, () => {
|
|
292
358
|
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
293
359
|
upstream.write(head);
|
|
294
360
|
upstream.pipe(clientSocket);
|
|
295
361
|
clientSocket.pipe(upstream);
|
|
296
362
|
});
|
|
297
|
-
upstream.on('error', () => { try { clientSocket.end(); } catch (_) {} });
|
|
298
|
-
clientSocket.on('error', () => { try { upstream.end(); } catch (_) {} });
|
|
363
|
+
upstream.on('error', (e) => { dbg(`[${ts()}] #${seq} cert-fail upstream err: ${e.message}`); try { clientSocket.end(); } catch (_) {} });
|
|
364
|
+
clientSocket.on('error', (e) => { dbg(`[${ts()}] #${seq} cert-fail client err: ${e.message}`); try { upstream.end(); } catch (_) {} });
|
|
299
365
|
return;
|
|
300
366
|
}
|
|
301
367
|
|
|
368
|
+
dbg(`[${ts()}] #${seq} → MITM (cert OK for ${hostname})`);
|
|
369
|
+
|
|
302
370
|
// Accept TLS from client (presenting our per-host cert signed by our CA)
|
|
303
371
|
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
304
372
|
|
|
@@ -308,26 +376,71 @@ function startProxy(port) {
|
|
|
308
376
|
cert: hostCert.cert,
|
|
309
377
|
});
|
|
310
378
|
|
|
379
|
+
let tlsClientReady = false;
|
|
380
|
+
tlsClient.on('secure', () => { tlsClientReady = true; dbg(`[${ts()}] #${seq} TLS-client handshake OK`); });
|
|
381
|
+
|
|
311
382
|
// Connect to real upstream
|
|
312
383
|
const tlsUpstream = tls.connect({ host: hostname, port: remotePort, servername: hostname });
|
|
313
384
|
|
|
385
|
+
let tlsUpstreamReady = false;
|
|
386
|
+
tlsUpstream.on('secureConnect', () => { tlsUpstreamReady = true; dbg(`[${ts()}] #${seq} TLS-upstream handshake OK → ${hostname}`); });
|
|
387
|
+
|
|
314
388
|
let responseBuffer = Buffer.alloc(0);
|
|
315
|
-
let
|
|
389
|
+
let usageCount = 0;
|
|
390
|
+
let totalBytes = 0;
|
|
391
|
+
let rawFd = null;
|
|
392
|
+
|
|
393
|
+
// Raw response logging: only in debug mode to avoid disk bloat
|
|
394
|
+
if (DEBUG) {
|
|
395
|
+
try {
|
|
396
|
+
fs.mkdirSync(RAW_LOG_DIR, { recursive: true });
|
|
397
|
+
const rawFile = path.join(RAW_LOG_DIR, `${new Date().toISOString().slice(0,10)}_${seq}_${hostname}.raw`);
|
|
398
|
+
rawFd = fs.openSync(rawFile, 'w');
|
|
399
|
+
} catch (_) {}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Precise trigger: "usage" must co-occur with "prompt_tokens"/"input_tokens"
|
|
403
|
+
// This avoids false positives when model output TEXT contains "prompt_tokens"
|
|
404
|
+
function hasUsageSignal(text) {
|
|
405
|
+
if (!text.includes('"usage"') && !text.includes('\\"usage\\"')) return false;
|
|
406
|
+
return text.includes('prompt_tokens') || text.includes('input_tokens');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// lineBuffer accumulates complete lines; incomplete trailing data stays for next chunk
|
|
410
|
+
let lineRemainder = '';
|
|
411
|
+
|
|
412
|
+
function processLines(lines) {
|
|
413
|
+
// Extract ALL usage records from a batch of complete lines (not just the last one)
|
|
414
|
+
for (const line of lines.split('\n')) {
|
|
415
|
+
const trimmed = line.trim();
|
|
416
|
+
if (!trimmed) continue;
|
|
417
|
+
if (!hasUsageSignal(trimmed)) continue;
|
|
418
|
+
const buf = Buffer.from(trimmed, 'utf8');
|
|
419
|
+
const usage = extractUsageFromResponse(buf);
|
|
420
|
+
if (usage) {
|
|
421
|
+
usageCount++;
|
|
422
|
+
if (dispatchId) usage.dispatch_id = dispatchId;
|
|
423
|
+
if (stepId) usage.step_id = stepId;
|
|
424
|
+
appendUsage(usage);
|
|
425
|
+
console.log(`[${ts()}] #${seq} ✓ USAGE CAPTURED (#${usageCount}) credits=${usage.credits} model=${usage.model} in=${usage.input_tokens} out=${usage.output_tokens}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
316
429
|
|
|
317
430
|
tlsUpstream.on('data', (chunk) => {
|
|
431
|
+
totalBytes += chunk.length;
|
|
318
432
|
try { tlsClient.write(chunk); } catch (_) {}
|
|
433
|
+
if (rawFd !== null) { try { fs.writeSync(rawFd, chunk); } catch (_) {} }
|
|
319
434
|
try {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (usage) {
|
|
326
|
-
if (dispatchId) usage.dispatch_id = dispatchId;
|
|
327
|
-
appendUsage(usage);
|
|
328
|
-
usageExtracted = true;
|
|
329
|
-
}
|
|
435
|
+
const text = lineRemainder + chunk.toString('utf8');
|
|
436
|
+
const lastNL = text.lastIndexOf('\n');
|
|
437
|
+
if (lastNL === -1) {
|
|
438
|
+
lineRemainder = text.length > 65536 ? text.slice(-65536) : text;
|
|
439
|
+
return;
|
|
330
440
|
}
|
|
441
|
+
const complete = text.slice(0, lastNL);
|
|
442
|
+
lineRemainder = text.slice(lastNL + 1);
|
|
443
|
+
processLines(complete);
|
|
331
444
|
} catch (_) {}
|
|
332
445
|
});
|
|
333
446
|
|
|
@@ -336,25 +449,21 @@ function startProxy(port) {
|
|
|
336
449
|
});
|
|
337
450
|
|
|
338
451
|
const cleanup = () => {
|
|
339
|
-
// Final
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
} catch (_) {}
|
|
348
|
-
}
|
|
349
|
-
responseBuffer = Buffer.alloc(0);
|
|
452
|
+
// Final extraction from any remaining data (lineRemainder may hold last line without trailing \n)
|
|
453
|
+
try {
|
|
454
|
+
if (lineRemainder) processLines(lineRemainder);
|
|
455
|
+
} catch (_) {}
|
|
456
|
+
lineRemainder = '';
|
|
457
|
+
dbg(`[${ts()}] #${seq} CLOSED ${hostname} bytes=${totalBytes} usages=${usageCount} tlsClient=${tlsClientReady} tlsUpstream=${tlsUpstreamReady}`);
|
|
458
|
+
if (rawFd !== null) { try { fs.closeSync(rawFd); } catch (_) {} rawFd = null; }
|
|
350
459
|
try { tlsClient.end(); } catch (_) {}
|
|
351
460
|
try { tlsUpstream.end(); } catch (_) {}
|
|
352
461
|
};
|
|
353
462
|
|
|
354
463
|
tlsClient.on('end', cleanup);
|
|
355
|
-
tlsClient.on('error', () => { cleanup(); });
|
|
464
|
+
tlsClient.on('error', (e) => { dbg(`[${ts()}] #${seq} TLS-client ERROR: ${e.message}`); cleanup(); });
|
|
356
465
|
tlsUpstream.on('end', cleanup);
|
|
357
|
-
tlsUpstream.on('error', () => { cleanup(); });
|
|
466
|
+
tlsUpstream.on('error', (e) => { dbg(`[${ts()}] #${seq} TLS-upstream ERROR: ${e.message}`); cleanup(); });
|
|
358
467
|
|
|
359
468
|
if (head.length > 0) tlsClient.emit('data', head);
|
|
360
469
|
});
|
|
@@ -467,3 +576,7 @@ Files:
|
|
|
467
576
|
CA cert: ${CA_CERT_PATH}
|
|
468
577
|
Usage log: ${USAGE_LOG_PATH}`);
|
|
469
578
|
}
|
|
579
|
+
|
|
580
|
+
if (require.main !== module) {
|
|
581
|
+
module.exports = { extractUsageFromResponse, extractDispatchInfo, buildUsageRecord };
|
|
582
|
+
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|