@agent-proxy/zcode 0.0.0-stage → 0.11.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/dist/vision.js ADDED
@@ -0,0 +1,204 @@
1
+ /** zcode 链路图片识别适配:analyze_image 声明、图片清单、标识反查与网关侧执行器。 */
2
+ import { createHash } from "node:crypto";
3
+ import executorTemplateJson from "./vision-template.json";
4
+ import { isZcodeRecord } from "./wire.js";
5
+ export const ANALYZE_IMAGE_TOOL_NAME = "analyze_image";
6
+ export class ZcodeVisionError extends Error {
7
+ constructor(message) { super(message); this.name = "ZcodeVisionError"; }
8
+ }
9
+ /**
10
+ * z.ai 内置工具的调用与结果是以普通助手 Markdown 流式输出的(实测 web_search_prime:
11
+ * `**🌐 Z.ai Built-in Tool: …**` + Input 代码块 + `*Executing on server...*`,随后另起一条
12
+ * `**Output:**\n**…_result_summary:** [...]`)。网关代执行 analyze_image 时复刻同一形状,
13
+ * Codex 才会把它渲染成与 web_search_prime 相同的 "Z.ai Built-in Tool" 卡片。
14
+ */
15
+ export function gatewayToolCallNarration(call) {
16
+ return [
17
+ `**🌐 Z.ai Built-in Tool: ${call.name}**`,
18
+ "",
19
+ "**Input:**",
20
+ "```json",
21
+ JSON.stringify(call.input),
22
+ "```",
23
+ "*Executing on server...*",
24
+ "",
25
+ ].join("\n");
26
+ }
27
+ /** 与 z.ai 的 result_summary 旁白同形:结果作为 JSON 文本块出现在 Output 卡片里。 */
28
+ export function gatewayToolResultNarration(name, result) {
29
+ return `**Output:**\n**${name}_result_summary:** [{"text": ${JSON.stringify(result)}, "type": "text"}]`;
30
+ }
31
+ /**
32
+ * 与 z.ai MCP 形状对齐(imageSource + prompt);显式声明后模型会确定性地调用图片识别,
33
+ * 并在 imageSource 里携带服务端改写给它的图片 URL。
34
+ */
35
+ export const ANALYZE_IMAGE_TOOL_DECLARATION = {
36
+ name: ANALYZE_IMAGE_TOOL_NAME,
37
+ description: "Analyze an image using advanced AI vision models with comprehensive understanding capabilities. Only supports remote URL.",
38
+ input_schema: {
39
+ type: "object",
40
+ properties: {
41
+ imageSource: { type: "string", description: "Remote URL to the image (supports PNG, JPG, JPEG)" },
42
+ prompt: { type: "string", description: "Detailed text prompt describing what to analyze, extract, or understand from the image." },
43
+ },
44
+ required: ["imageSource", "prompt"],
45
+ additionalProperties: false,
46
+ },
47
+ };
48
+ export const ANALYZE_IMAGE_SYSTEM_NOTE = [
49
+ "## 图片识别适配",
50
+ "当前模型无法直接查看图片像素;请求中的图片已由服务端上传并替换为 CDN URL(图片附近会给出以 maas-log-prod 开头的 URL,URL 文件名即图片标识)。",
51
+ `请使用 ${ANALYZE_IMAGE_TOOL_NAME} 工具识别图片:imageSource 传图片 URL,prompt 传具体识别要求。`,
52
+ "不要假装能看到图片内容,也不要用本地 OCR 命令替代;识别结果返回后再整理作答。",
53
+ ].join("\n");
54
+ /**
55
+ * 图片字节的内容标识。非安全用途:z.ai CDN 以图片字节摘要的 32 位十六进制作为 URL
56
+ * 文件名(实测确认),网关用同一约定把模型回传的 URL 反查回本地 base64,不做完整性校验。
57
+ */
58
+ export function imageIdentifier(data) {
59
+ return createHash("md5").update(Buffer.from(data, "base64")).digest("hex");
60
+ }
61
+ /** 遍历翻译后的 Anthropic messages,收集全部 base64 图片(按标识去重)。 */
62
+ export function collectZcodeImages(messages) {
63
+ const images = new Map();
64
+ const visitBlock = (block) => {
65
+ if (!isZcodeRecord(block))
66
+ return;
67
+ if (block.type === "image" && isZcodeRecord(block.source)) {
68
+ const source = block.source;
69
+ if (source.type === "base64" && typeof source.data === "string" && source.data) {
70
+ const mediaType = typeof source.media_type === "string" && source.media_type ? source.media_type : "image/png";
71
+ const identifier = imageIdentifier(source.data);
72
+ if (!images.has(identifier))
73
+ images.set(identifier, { identifier, media_type: mediaType, data: source.data });
74
+ }
75
+ return;
76
+ }
77
+ // tool_result 的图片在 content 数组内(view_image 等工具的返回形态)。
78
+ if (block.type === "tool_result" && Array.isArray(block.content))
79
+ block.content.forEach(visitBlock);
80
+ };
81
+ const visitContent = (content) => {
82
+ if (Array.isArray(content))
83
+ content.forEach(visitBlock);
84
+ else if (isZcodeRecord(content))
85
+ visitContent(content.content);
86
+ };
87
+ if (Array.isArray(messages)) {
88
+ for (const message of messages) {
89
+ if (isZcodeRecord(message))
90
+ visitContent(message.content);
91
+ }
92
+ }
93
+ return [...images.values()];
94
+ }
95
+ /** 模型回传的 imageSource(CDN URL 或其他引用)反查请求图片;单图时无条件兜底。 */
96
+ export function matchZcodeAnalyzeImage(source, images) {
97
+ if (images.length === 0)
98
+ return undefined;
99
+ if (images.length === 1)
100
+ return images[0];
101
+ if (typeof source !== "string" || !source)
102
+ return undefined;
103
+ const match = source.match(/([0-9a-f]{32})\.(?:png|jpe?g|webp|gif|bmp)/i);
104
+ if (!match)
105
+ return undefined;
106
+ const identifier = match[1].toLowerCase();
107
+ return images.find((image) => image.identifier === identifier);
108
+ }
109
+ const executorTemplate = executorTemplateJson;
110
+ /** 执行器 SSE 把公告与结果拼接成文本;结果通常跟在最后一个 result_summary 标记后。 */
111
+ export function analysisFromExecutorText(text) {
112
+ const marker = "analyze_image_result_summary";
113
+ const index = text.lastIndexOf(marker);
114
+ if (index < 0)
115
+ return text.trim();
116
+ const tail = text.slice(index + marker.length).trim();
117
+ const unwrapped = tail.replace(/^[::*`\s"']+/, "").replace(/[`"']+$/, "").trim();
118
+ return unwrapped || text.trim();
119
+ }
120
+ async function attemptOnce(options) {
121
+ const headers = await options.headers();
122
+ const timeout = AbortSignal.timeout(options.attemptTimeoutMs ?? 120_000);
123
+ const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
124
+ const instruction = `使用 analyze_image 工具识别这张图片,按以下要求输出:${options.prompt}`;
125
+ const body = {
126
+ model: options.model,
127
+ max_tokens: 4096,
128
+ stream: true,
129
+ // 完整 Claude Code 指纹(system+tools)是服务端代执行的触发条件,模板逐字来自实测请求。
130
+ system: executorTemplate.system,
131
+ tools: executorTemplate.tools,
132
+ messages: [{
133
+ role: "user",
134
+ content: [
135
+ { type: "image", source: { type: "base64", media_type: options.image.media_type, data: options.image.data } },
136
+ { type: "text", text: instruction },
137
+ ],
138
+ }],
139
+ };
140
+ const response = await options.fetchImpl(options.url, {
141
+ method: "POST", headers, body: JSON.stringify(body), redirect: "manual", signal,
142
+ });
143
+ if (!response.ok || !response.body) {
144
+ const text = await response.text().catch(() => "");
145
+ // 先对完整正文脱敏、后截断:截断会把跨边界的 key 切成脱敏匹配不到的前缀片段。
146
+ const sanitized = options.redact ? options.redact(text) : text;
147
+ throw new ZcodeVisionError(`执行信封上游返回 HTTP ${response.status}${sanitized ? `:${sanitized.slice(0, 200)}` : ""}`);
148
+ }
149
+ const reader = response.body.getReader();
150
+ const decoder = new TextDecoder();
151
+ let buffer = "";
152
+ let text = "";
153
+ let executed = false;
154
+ while (true) {
155
+ const { value, done } = await reader.read();
156
+ if (done)
157
+ break;
158
+ buffer += decoder.decode(value, { stream: true });
159
+ let index;
160
+ while ((index = buffer.indexOf("\n")) >= 0) {
161
+ const line = buffer.slice(0, index).trim();
162
+ buffer = buffer.slice(index + 1);
163
+ if (!line.startsWith("data:"))
164
+ continue;
165
+ const payload = line.slice(5).trim();
166
+ if (!payload || payload === "[DONE]")
167
+ continue;
168
+ let frame;
169
+ try {
170
+ frame = JSON.parse(payload);
171
+ }
172
+ catch {
173
+ continue;
174
+ }
175
+ const block = isZcodeRecord(frame.content_block) ? frame.content_block : undefined;
176
+ if (frame.type === "content_block_start" && block?.type === "server_tool_use" && block.name === ANALYZE_IMAGE_TOOL_NAME)
177
+ executed = true;
178
+ const delta = isZcodeRecord(frame.delta) ? frame.delta : undefined;
179
+ if (frame.type === "content_block_delta" && delta?.type === "text_delta" && typeof delta.text === "string")
180
+ text += delta.text;
181
+ }
182
+ }
183
+ if (!executed)
184
+ throw new ZcodeVisionError("执行信封未触发服务端 analyze_image");
185
+ return analysisFromExecutorText(text);
186
+ }
187
+ /** 网关侧执行 analyze_image:以 Claude Code 指纹信封侧请求,服务端在同一条 SSE 内完成识别。 */
188
+ export async function executeZcodeAnalyzeImage(options) {
189
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
190
+ let lastError;
191
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
192
+ if (options.signal?.aborted)
193
+ throw new ZcodeVisionError("analyze_image 执行已取消");
194
+ try {
195
+ return await attemptOnce(options);
196
+ }
197
+ catch (error) {
198
+ lastError = error;
199
+ if (options.signal?.aborted)
200
+ throw error;
201
+ }
202
+ }
203
+ throw new ZcodeVisionError(`analyze_image 执行失败(${maxAttempts} 次尝试):${lastError instanceof Error ? lastError.message : String(lastError)}`);
204
+ }
package/dist/wire.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /** ZCode 协议共享类型;元数据不包含授权凭据。 */
2
+ export interface ZcodeToolDefinition {
3
+ name: string;
4
+ custom: boolean;
5
+ namespace?: string;
6
+ /** 网关代执行标记:该工具由网关在上游续跑中执行并吸收,不回传客户端。 */
7
+ gateway?: "analyze_image";
8
+ }
9
+ export type ZcodeToolMap = Map<string, ZcodeToolDefinition>;
10
+ export interface ZcodeTranslatedRequest {
11
+ body: Record<string, unknown>;
12
+ tools: ZcodeToolMap;
13
+ /** 被剥离的服务器内置工具类型名,供日志与降级说明使用。 */
14
+ dropped: string[];
15
+ /** 请求含图片且已声明网关代执行的 analyze_image 工具。 */
16
+ vision: boolean;
17
+ /** 请求内收集到的 base64 图片清单(按内容标识去重),供代执行反查。 */
18
+ images: import("./vision.ts").ZcodeRequestImage[];
19
+ }
20
+ export declare function isZcodeRecord(value: unknown): value is Record<string, unknown>;
21
+ /** 本地不透明回传载荷,不代表 OpenAI 的加密思考格式。 */
22
+ export declare function encodeZcodeThinking(model: string, blocks: Record<string, unknown>[]): string;
23
+ export declare function decodeZcodeThinking(value: unknown, model: string): Record<string, unknown>[] | undefined;
package/dist/wire.js ADDED
@@ -0,0 +1,25 @@
1
+ export function isZcodeRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ const THINKING_PREFIX = "zcode-thinking-v1:";
5
+ /** 本地不透明回传载荷,不代表 OpenAI 的加密思考格式。 */
6
+ export function encodeZcodeThinking(model, blocks) {
7
+ return THINKING_PREFIX + Buffer.from(JSON.stringify({ model, blocks }), "utf8").toString("base64");
8
+ }
9
+ export function decodeZcodeThinking(value, model) {
10
+ if (typeof value !== "string" || !value.startsWith(THINKING_PREFIX))
11
+ return undefined;
12
+ try {
13
+ const data = JSON.parse(Buffer.from(value.slice(THINKING_PREFIX.length), "base64").toString("utf8"));
14
+ if (!isZcodeRecord(data) || data.model !== model || !Array.isArray(data.blocks))
15
+ return undefined;
16
+ if (!data.blocks.every((block) => isZcodeRecord(block)
17
+ && ((block.type === "thinking" && typeof block.thinking === "string" && typeof block.signature === "string")
18
+ || (block.type === "redacted_thinking" && typeof block.data === "string"))))
19
+ return undefined;
20
+ return data.blocks;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@agent-proxy/zcode",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.11.1",
4
+ "description": "Agent Proxy zcode 插件;保留基线转换行为。",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./*": {
13
+ "types": "./dist/*.d.ts",
14
+ "import": "./dist/*.js",
15
+ "default": "./dist/*.js"
16
+ },
17
+ "./*.json": "./dist/*.json"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "bun ../../scripts/build-library.ts",
25
+ "typecheck": "tsc --noEmit -p tsconfig.json",
26
+ "prepack": "bun ../../scripts/verify-package.ts ."
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/aceHubert/codex-cliproxy-gateway.git"
35
+ },
36
+ "devDependencies": {
37
+ "@agent-proxy/models": "0.11.1"
38
+ },
39
+ "dependencies": {
40
+ "@agent-proxy/llm-bridge": "0.11.1",
41
+ "@agent-proxy/plugin-api": "0.11.1"
42
+ },
43
+ "gitHead": "affd71836c6cb82ec474edc5efa46146eb92398f"
44
+ }