@agent-proxy/zcode 0.20.3 → 0.21.0

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { bindHostMethods, withHostServices } from "@agent-proxy/plugin-api";
1
+ import { bindHostMethods, getHostServices, withHostServices } from "@agent-proxy/plugin-api";
2
2
  import path from "node:path";
3
3
  import { isIP } from "node:net";
4
4
  import { resolvePaths } from "@agent-proxy/plugin-api";
@@ -306,6 +306,11 @@ function buildZcodeAdapter(config, dependencies = {}) {
306
306
  const incoming = new URL(request.url);
307
307
  const group = logGroupFromPath(incoming.pathname);
308
308
  const zcode = isZcodeModel(input.model);
309
+ // 响应体由 HTTP 层在本插件的 withHostServices 之外泵送,流中段(onComplete、
310
+ // 视觉续跑腿)再调 getHostServices 会抛「插件主机上下文未初始化」——这也是
311
+ // 插件交换日志长期缺失的根因。捕获本次请求的主机上下文供流内回调显式重入。
312
+ const host = getHostServices();
313
+ const inHostContext = (action) => withHostServices(host, action);
309
314
  // 渠道(zai/bigmodel)只在转发时按当前套餐快照判定,用于鉴权与日志分流;模型 ID 不携带渠道。
310
315
  let family;
311
316
  let key = "";
@@ -349,24 +354,27 @@ function buildZcodeAdapter(config, dependencies = {}) {
349
354
  logged = true;
350
355
  const durationMs = Date.now() - start;
351
356
  const at = incoming.pathname + incoming.search;
352
- logExchange(sink, group, {
353
- requestTime, method: request.method, url: at,
354
- reqHeaders: request.headers, reqBody: redactValue(input),
355
- status, resHeaders: headers, resBody: redact(body), upstreamUrl, upstreamRequestHeaders, upstreamRequestBody, durationMs,
356
- }, "zcode");
357
- // 与网关主链一样,进程日志里每条请求恰好一行:逻辑错误也算错误,走错误摘要。
358
- if (status >= 400 || logicalError) {
359
- logGatewayError(sink?.processLog, {
357
+ // onComplete 等流内调用点已在主机上下文之外,落盘前显式重入。
358
+ inHostContext(() => {
359
+ logExchange(sink, group, {
360
360
  requestTime, method: request.method, url: at,
361
- status: logicalError && status < 400 ? 502 : status,
362
- message: redact(logicalError ?? body), upstreamUrl, durationMs,
363
- });
364
- }
365
- else {
366
- logRequestSummary(sink?.processLog, {
367
- requestTime, method: request.method, url: at, status, upstreamUrl, durationMs,
368
- });
369
- }
361
+ reqHeaders: request.headers, reqBody: redactValue(input),
362
+ status, resHeaders: headers, resBody: redact(body), upstreamUrl, upstreamRequestHeaders, upstreamRequestBody, durationMs,
363
+ }, "zcode");
364
+ // 与网关主链一样,进程日志里每条请求恰好一行:逻辑错误也算错误,走错误摘要。
365
+ if (status >= 400 || logicalError) {
366
+ logGatewayError(sink?.processLog, {
367
+ requestTime, method: request.method, url: at,
368
+ status: logicalError && status < 400 ? 502 : status,
369
+ message: redact(logicalError ?? body), upstreamUrl, durationMs,
370
+ });
371
+ }
372
+ else {
373
+ logRequestSummary(sink?.processLog, {
374
+ requestTime, method: request.method, url: at, status, upstreamUrl, durationMs,
375
+ });
376
+ }
377
+ });
370
378
  };
371
379
  const fail = (status, message, type) => {
372
380
  const response = zcodeError(status, redact(message), type);
@@ -468,6 +476,7 @@ function buildZcodeAdapter(config, dependencies = {}) {
468
476
  // 图片识别适配:请求含图片时把被吸收的 analyze_image 调用落到网关执行并续跑上游。
469
477
  let gatewayTools;
470
478
  let continuationLegs = 0;
479
+ const visionAttemptNotes = [];
471
480
  if (translated.vision) {
472
481
  gatewayTools = {
473
482
  maxContinuations: 3,
@@ -495,6 +504,24 @@ function buildZcodeAdapter(config, dependencies = {}) {
495
504
  signal: abort.signal,
496
505
  // 错误正文脱敏必须在截断之前发生(见 ZcodeExecutorOptions.redact)。
497
506
  redact,
507
+ // 执行腿不经过网关交换日志:每次尝试的结局写进程日志,并在收尾时
508
+ // 汇入交换日志尾部,服务端瞬时故障(如 MCP 1210)才能事后取证。
509
+ onAttempt: (summary) => {
510
+ visionAttemptNotes.push(`attempt ${summary.attempt}/${summary.maxAttempts} ${summary.ok ? "ok" : "failed"}: ${summary.detail}`);
511
+ const entry = {
512
+ requestTime, method: "POST",
513
+ url: `${incoming.pathname}${incoming.search} [analyze_image ${summary.attempt}/${summary.maxAttempts}]`,
514
+ durationMs: summary.durationMs, upstreamUrl: routedUrl,
515
+ };
516
+ inHostContext(() => {
517
+ if (summary.ok) {
518
+ logRequestSummary(sink?.processLog, { ...entry, status: 200 });
519
+ }
520
+ else {
521
+ logGatewayError(sink?.processLog, { ...entry, status: 502, message: `[analyze_image] ${summary.detail}` });
522
+ }
523
+ });
524
+ },
498
525
  });
499
526
  }
500
527
  catch (error) {
@@ -596,8 +623,9 @@ function buildZcodeAdapter(config, dependencies = {}) {
596
623
  // 剥离的内置工具与图片识别续跑腿数写进交换日志,避免“静默降级”无从排查。
597
624
  const stripped = translated.dropped.length ? `\n--- stripped built-in tools: ${translated.dropped.join(", ")} ---` : "";
598
625
  const visionLegs = continuationLegs > 0 ? `\n--- analyze_image continuation legs: ${continuationLegs} ---` : "";
626
+ const visionAttempts = visionAttemptNotes.length ? `\n--- analyze_image attempts ---\n${visionAttemptNotes.join("\n")}` : "";
599
627
  if (!mapResult)
600
- log(stream ? 200 : payload.status === "failed" ? 502 : 200, stream ? `${chunks}${stripped}${visionLegs}\n--- response.${payload.status} ---\n${JSON.stringify(redactValue(payload))}` : `${JSON.stringify(redactValue(payload))}${stripped}${visionLegs}`, new Headers({ "content-type": stream ? "text/event-stream" : "application/json" }), payload.status === "failed" ? JSON.stringify(payload.error ?? "响应失败") : undefined);
628
+ log(stream ? 200 : payload.status === "failed" ? 502 : 200, stream ? `${chunks}${stripped}${visionLegs}${visionAttempts}\n--- response.${payload.status} ---\n${JSON.stringify(redactValue(payload))}` : `${JSON.stringify(redactValue(payload))}${stripped}${visionLegs}${visionAttempts}`, new Headers({ "content-type": stream ? "text/event-stream" : "application/json" }), payload.status === "failed" ? JSON.stringify(payload.error ?? "响应失败") : undefined);
601
629
  },
602
630
  });
603
631
  if (!mapResult)
package/dist/response.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createClientResponse } from "@agent-proxy/llm-bridge";
1
+ import { createClientResponse, remapUndeclaredToolName } from "@agent-proxy/llm-bridge";
2
2
  import { encodeZcodeThinking, isZcodeRecord } from "./wire.js";
3
3
  import { gatewayToolCallNarration, gatewayToolResultNarration } from "./vision.js";
4
4
  /** z.ai 的服务端搜索按 web_search_prime/search_query 形状返回 query;兼容 Anthropic 标准的 query。 */
@@ -483,9 +483,18 @@ export async function createZcodeResponse(upstream, options) {
483
483
  const tool = normalized.find((value) => value.type === "tool_call_start");
484
484
  const call = record(tool?.tool_call, "tool call");
485
485
  const name = requiredString(call.name, "tool name");
486
- const mapped = options.tools.get(name);
487
- if (!mapped)
488
- throw new Error(`ZCode 返回了未声明的工具:${name}`);
486
+ let mapped = options.tools.get(name);
487
+ if (!mapped) {
488
+ // 上游可能回吐与折叠声明名仅分隔符/哈希不同的调用名:骨架唯一匹配时
489
+ // 对回声明名;完全无法对应的调用按未知块忽略,不再整条断流。
490
+ const remapped = remapUndeclaredToolName(name, options.tools);
491
+ if (remapped !== undefined)
492
+ mapped = options.tools.get(remapped);
493
+ }
494
+ if (!mapped) {
495
+ ignoredBlocks.add(index);
496
+ return result;
497
+ }
489
498
  if (mapped.gateway) {
490
499
  if (!options.gatewayTools)
491
500
  throw new Error(`ZCode 网关工具 ${name} 缺少执行钩子`);
package/dist/vision.d.ts CHANGED
@@ -40,6 +40,12 @@ export declare function collectZcodeImages(messages: unknown): ZcodeRequestImage
40
40
  export declare function matchZcodeAnalyzeImage(source: unknown, images: ZcodeRequestImage[]): ZcodeRequestImage | undefined;
41
41
  /** 执行器 SSE 把公告与结果拼接成文本;结果通常跟在最后一个 result_summary 标记后。 */
42
42
  export declare function analysisFromExecutorText(text: string): string;
43
+ /**
44
+ * 服务端 analyze_image 执行失败的识别:失败时 result_summary 的首个文本条目以
45
+ * `MCP error <code>` 开头(实测瞬时故障返回 `MCP error 400: … "图片输入格式/解析错误"`)。
46
+ * 只检查摘要片段开头的一小段,避免把「内容恰好描述 MCP 报错的截图」的识别成功误判为失败。
47
+ */
48
+ export declare function serverToolResultFailed(text: string): boolean;
43
49
  export interface ZcodeExecutorOptions {
44
50
  url: string;
45
51
  model: string;
@@ -57,6 +63,17 @@ export interface ZcodeExecutorOptions {
57
63
  * 边界时,先截断再脱敏会留下可还原的前缀片段。由调用方(zcode.ts)注入。
58
64
  */
59
65
  redact?: (text: string) => string;
66
+ /** 每次尝试结束(成功、服务端失败或异常)后的观测回调;用于进程日志留痕。 */
67
+ onAttempt?: (summary: ZcodeExecutorAttempt) => void;
68
+ }
69
+ /** 一次 analyze_image 执行尝试的结果摘要;detail 已由调用方脱敏。 */
70
+ export interface ZcodeExecutorAttempt {
71
+ attempt: number;
72
+ maxAttempts: number;
73
+ /** false 表示服务端执行失败(MCP 错误结果)或本次尝试抛错。 */
74
+ ok: boolean;
75
+ durationMs: number;
76
+ detail: string;
60
77
  }
61
78
  /** 网关侧执行 analyze_image:以 Claude Code 指纹信封侧请求,服务端在同一条 SSE 内完成识别。 */
62
79
  export declare function executeZcodeAnalyzeImage(options: ZcodeExecutorOptions): Promise<string>;
package/dist/vision.js CHANGED
@@ -117,6 +117,26 @@ export function analysisFromExecutorText(text) {
117
117
  const unwrapped = tail.replace(/^[::*`\s"']+/, "").replace(/[`"']+$/, "").trim();
118
118
  return unwrapped || text.trim();
119
119
  }
120
+ /**
121
+ * 服务端 analyze_image 执行失败的识别:失败时 result_summary 的首个文本条目以
122
+ * `MCP error <code>` 开头(实测瞬时故障返回 `MCP error 400: … "图片输入格式/解析错误"`)。
123
+ * 只检查摘要片段开头的一小段,避免把「内容恰好描述 MCP 报错的截图」的识别成功误判为失败。
124
+ */
125
+ export function serverToolResultFailed(text) {
126
+ const marker = "analyze_image_result_summary";
127
+ const index = text.lastIndexOf(marker);
128
+ const segment = (index >= 0 ? text.slice(index + marker.length) : text).slice(0, 200).replace(/\\/g, "");
129
+ return /mcp error \d+/i.test(segment);
130
+ }
131
+ /** 观测回调必须不致命:落日志失败不得影响识别执行本身。 */
132
+ function reportAttempt(options, summary) {
133
+ try {
134
+ options.onAttempt?.(summary);
135
+ }
136
+ catch {
137
+ // 依赖插件主机上下文的日志实现可能在流中段抛错;观察钩子静默降级。
138
+ }
139
+ }
120
140
  async function attemptOnce(options) {
121
141
  const headers = await options.headers();
122
142
  const timeout = AbortSignal.timeout(options.attemptTimeoutMs ?? 120_000);
@@ -191,11 +211,31 @@ export async function executeZcodeAnalyzeImage(options) {
191
211
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
192
212
  if (options.signal?.aborted)
193
213
  throw new ZcodeVisionError("analyze_image 执行已取消");
214
+ const startedAt = Date.now();
194
215
  try {
195
- return await attemptOnce(options);
216
+ const result = await attemptOnce(options);
217
+ // 服务端把 MCP 错误当正常结果流回(信封本身 HTTP 200):识别为失败并重试,
218
+ // 实测该类失败为上游瞬时故障,同图同链路稍后即可成功。
219
+ if (serverToolResultFailed(result)) {
220
+ const detail = options.redact ? options.redact(result.slice(0, 200)) : result.slice(0, 200);
221
+ reportAttempt(options, { attempt, maxAttempts, ok: false, durationMs: Date.now() - startedAt, detail });
222
+ lastError = new ZcodeVisionError(detail);
223
+ if (options.signal?.aborted)
224
+ throw lastError;
225
+ continue;
226
+ }
227
+ reportAttempt(options, {
228
+ attempt, maxAttempts, ok: true, durationMs: Date.now() - startedAt,
229
+ detail: `识别成功(图片 ${options.image.identifier},${Math.trunc(options.image.data.length * 3 / 4 / 1024)}KB)`,
230
+ });
231
+ return result;
196
232
  }
197
233
  catch (error) {
198
234
  lastError = error;
235
+ reportAttempt(options, {
236
+ attempt, maxAttempts, ok: false, durationMs: Date.now() - startedAt,
237
+ detail: options.redact ? options.redact(error instanceof Error ? error.message : String(error)).slice(0, 200) : String(error).slice(0, 200),
238
+ });
199
239
  if (options.signal?.aborted)
200
240
  throw error;
201
241
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-proxy/zcode",
3
- "version": "0.20.3",
3
+ "version": "0.21.0",
4
4
  "description": "Agent Proxy zcode 插件;保留基线转换行为。",
5
5
  "type": "module",
6
6
  "exports": {
@@ -35,11 +35,11 @@
35
35
  "url": "git+https://github.com/aceHubert/codex-cliproxy-gateway.git"
36
36
  },
37
37
  "devDependencies": {
38
- "@agent-proxy/models": "0.20.2"
38
+ "@agent-proxy/models": "0.21.0"
39
39
  },
40
40
  "dependencies": {
41
- "@agent-proxy/llm-bridge": "0.20.3",
42
- "@agent-proxy/plugin-api": "0.20.2"
41
+ "@agent-proxy/llm-bridge": "0.21.0",
42
+ "@agent-proxy/plugin-api": "0.21.0"
43
43
  },
44
- "gitHead": "b5922449d4fc9c2952b324031aee4f254e1b4d14"
44
+ "gitHead": "59b68f3e371fec94cfc4854303e34fc5d8427e3b"
45
45
  }