@router-agent/infra 0.0.0-stage → 0.3.32

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,164 @@
1
+ /**
2
+ * 图片转文字(多模态模型)的基础设施。
3
+ *
4
+ * 只依赖 OpenAI 兼容的 `/chat/completions` 接口,不引入新 SDK:
5
+ * 飞书图片先由消息层下载成 base64,再交给这里转成可进入研判的文本。
6
+ */
7
+ export const DEFAULT_VISION_PROMPT = [
8
+ '把这张聊天图片转换成可以直接进入研发研判的纯文本:',
9
+ '1. 逐字转写图片里所有可见文字,保留报错信息、日志、代码、命令行和表格结构;',
10
+ '2. 如果图片是界面截图,说明这是哪个页面/功能区域,以及图里表现出的异常或操作意图;',
11
+ '3. 如果图片是设计稿或流程图,按层级概述它的结构与关键信息。',
12
+ '看不清的内容写「[看不清]」,不要猜测或编造。',
13
+ '直接输出纯文本,不要加 Markdown 代码块,也不要解释你在做什么。',
14
+ ].join('\n');
15
+ const DEFAULT_TIMEOUT_MS = 60_000;
16
+ const DEFAULT_MAX_CHARS = 2_000;
17
+ const DEFAULT_MAX_TOKENS = 1_500;
18
+ /** 把 OpenAI 兼容的 baseUrl 规范成 chat/completions 地址。 */
19
+ export function chatCompletionsUrl(baseUrl) {
20
+ const clean = baseUrl.trim().replace(/\/+$/, '');
21
+ if (!clean)
22
+ throw new Error('image:vision-base-url-missing');
23
+ if (clean.endsWith('/chat/completions'))
24
+ return clean;
25
+ return `${clean}/chat/completions`;
26
+ }
27
+ /** 从 `chat/completions` 的返回里取 assistant 文本;兼容字符串与分段数组。 */
28
+ export function parseChatCompletionText(payload) {
29
+ const root = payload;
30
+ const content = root?.choices?.[0]?.message?.content;
31
+ if (typeof content === 'string')
32
+ return content.trim();
33
+ if (Array.isArray(content)) {
34
+ return content
35
+ .map((part) => (part && part.type === 'text' ? String(part.text ?? '') : ''))
36
+ .join('')
37
+ .trim();
38
+ }
39
+ return '';
40
+ }
41
+ /**
42
+ * 创建视觉转写器。
43
+ *
44
+ * 结构性失败(模型不支持图片、接口不存在)会熔断:同一进程里后续图片直接快速失败,
45
+ * 避免每张图都等一次超时、刷一屏同样的错误。
46
+ */
47
+ export function createVisionTranscriber(model, opts = {}) {
48
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
49
+ const timeoutMs = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
50
+ const maxChars = opts.maxChars && opts.maxChars > 0 ? opts.maxChars : DEFAULT_MAX_CHARS;
51
+ const maxTokens = opts.maxTokens && opts.maxTokens > 0 ? opts.maxTokens : DEFAULT_MAX_TOKENS;
52
+ const prompt = opts.prompt?.trim() || DEFAULT_VISION_PROMPT;
53
+ const url = chatCompletionsUrl(model.baseUrl);
54
+ let unsupported = null;
55
+ // 一旦确认这个模型会把预算耗在 reasoning 上,后续图片直接关闭推理,省掉一次白跑的请求。
56
+ let reasoningOff = false;
57
+ /** 发一次请求;reasoningOff=true 时显式关闭推理模型的思考预算。 */
58
+ async function request(reasoningOff, image) {
59
+ const controller = new AbortController();
60
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
61
+ let response;
62
+ try {
63
+ response = await fetchImpl(url, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ ...(model.apiKey ? { Authorization: `Bearer ${model.apiKey}` } : {}),
68
+ },
69
+ body: JSON.stringify({
70
+ model: model.model,
71
+ temperature: model.temperature ?? 0.1,
72
+ max_tokens: maxTokens,
73
+ // 推理模型的思考 token 也算在 max_tokens 里;图片转写不需要推理,重试时直接关掉。
74
+ ...(reasoningOff ? { reasoning_effort: 'none' } : {}),
75
+ messages: [
76
+ {
77
+ role: 'user',
78
+ content: [
79
+ { type: 'text', text: prompt },
80
+ { type: 'image_url', image_url: { url: `data:${image.mimeType};base64,${image.data}` } },
81
+ ],
82
+ },
83
+ ],
84
+ }),
85
+ signal: controller.signal,
86
+ });
87
+ }
88
+ catch (error) {
89
+ if (isAbortError(error))
90
+ throw new Error(`image:vision-timeout(${timeoutMs}ms)`);
91
+ throw new Error(`image:vision-request-failed: ${errorText(error)}`);
92
+ }
93
+ finally {
94
+ clearTimeout(timer);
95
+ }
96
+ if (!response.ok) {
97
+ const detail = (await response.text().catch(() => '')).slice(0, 200);
98
+ // 404/415/422,或 400 且明确提到 image/vision,基本可判定为「这个模型/接口不接受图片」。
99
+ // 「图片太大」之类的 400 不熔断,换一张图仍可能成功。
100
+ const looksUnsupported = response.status === 404 || response.status === 415 || response.status === 422 ||
101
+ (response.status === 400 && /(not|doesn'?t|unsupported).{0,40}(support|image|vision)|modalit|multimodal/i.test(detail));
102
+ if (looksUnsupported)
103
+ unsupported = `http-${response.status}${detail ? `: ${detail}` : ''}`;
104
+ throw new Error(`image:vision-http-${response.status}${detail ? `: ${detail}` : ''}`);
105
+ }
106
+ try {
107
+ return await response.json();
108
+ }
109
+ catch (error) {
110
+ throw new Error(`image:vision-invalid-json: ${errorText(error)}`);
111
+ }
112
+ }
113
+ return {
114
+ async transcribe(image) {
115
+ if (unsupported)
116
+ throw new Error(`image:vision-unsupported: ${unsupported}`);
117
+ let payload = await request(reasoningOff, image);
118
+ let text = parseChatCompletionText(payload);
119
+ // 推理模型的 reasoning_content 会把 max_tokens 吃光,content 为空或半截就截断。
120
+ // 转写不需要思考,发现这种情况时明确关闭推理重试一次。
121
+ if (!reasoningOff && reasoningUsedUp(payload, text)) {
122
+ try {
123
+ const retried = await request(true, image);
124
+ const retriedText = parseChatCompletionText(retried);
125
+ if (retriedText.length > text.length) {
126
+ payload = retried;
127
+ text = retriedText;
128
+ reasoningOff = true;
129
+ }
130
+ }
131
+ catch {
132
+ // 部分 OpenAI 兼容服务不认识 reasoning_effort;重试失败保留首次结果。
133
+ }
134
+ }
135
+ if (!text)
136
+ throw new Error(`image:vision-empty(${completionDetail(payload)})`);
137
+ return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
138
+ },
139
+ };
140
+ }
141
+ /** 推理 token 是否占满了预算:有 reasoning 消耗,且 content 为空或因为长度被截断。 */
142
+ function reasoningUsedUp(payload, text) {
143
+ const root = payload;
144
+ const choice = root?.choices?.[0];
145
+ const finish = String(choice?.finish_reason ?? '');
146
+ const reasoning = Number(root?.usage?.completion_tokens_details?.reasoning_tokens ?? root?.usage?.reasoning_tokens ?? 0);
147
+ if (!Number.isFinite(reasoning) || reasoning <= 0)
148
+ return false;
149
+ return !text || finish === 'length';
150
+ }
151
+ /** 给空结果留一个可排查的细节:finish_reason 与 reasoning token 数。 */
152
+ function completionDetail(payload) {
153
+ const root = payload;
154
+ const finish = String(root?.choices?.[0]?.finish_reason ?? 'unknown');
155
+ const reasoning = Number(root?.usage?.completion_tokens_details?.reasoning_tokens ?? root?.usage?.reasoning_tokens ?? 0);
156
+ return `finish=${finish}, reasoning=${Number.isFinite(reasoning) ? reasoning : 0}`;
157
+ }
158
+ function isAbortError(error) {
159
+ return error instanceof Error && (error.name === 'AbortError' || /aborted/i.test(error.message));
160
+ }
161
+ function errorText(error) {
162
+ return error instanceof Error ? error.message : String(error);
163
+ }
164
+ //# sourceMappingURL=vision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vision.js","sourceRoot":"","sources":["../../../../packages/infra/src/vision.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@router-agent/infra",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.32",
4
+ "type": "module",
5
+ "description": "基础设施:日志、子进程、对话模型与图片识别",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/blankPen/router-agent.git",
10
+ "directory": "packages/infra"
11
+ },
12
+ "homepage": "https://github.com/blankPen/router-agent#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/blankPen/router-agent/issues"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./*.js": "./dist/*.js",
23
+ "./*": "./dist/*"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "types": "./dist/index.d.ts",
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org/"
32
+ }
33
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.