@honlnk/picsense 0.1.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 ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * picsense MCP 入口。
4
+ *
5
+ * 启动顺序:
6
+ * 1. 加载并校验配置(缺失必填项 → 友好报错到 stderr 后退出)。
7
+ * 2. 构造默认 provider(触发 provider 注册)。
8
+ * 3. 创建 SessionManager(含多轮迭代、并发隔离、24h 过期清理)。
9
+ * 4. 注册 3 个工具:analyze_images / list_sessions / analyze_document。
10
+ * 5. 经 stdio 传输与 MCP 客户端通信。
11
+ *
12
+ * 设计依据:docs/picsense-design.md §4.1、§6.3;docs/plan/05-mcp-entry.md。
13
+ */
14
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import { z } from 'zod';
17
+ import { loadConfig, ConfigError } from './utils/config.js';
18
+ import './providers/registry.js'; // 触发 provider 自注册
19
+ import { getProvider } from './providers/registry.js';
20
+ import { SessionManager } from './core/session-manager.js';
21
+ import { analyzeImages, ToolInputError } from './tools/analyze-images.js';
22
+ import { listSessions } from './tools/list-sessions.js';
23
+ import { analyzeDocument } from './tools/analyze-document.js';
24
+ import { analyzeVideo, VideoLoadError } from './tools/analyze-video.js';
25
+ import { SessionNotFoundError } from './core/session-manager.js';
26
+ /** 把结果对象包成 MCP 文本 content(标准做法)。 */
27
+ function asText(obj) {
28
+ return { content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }] };
29
+ }
30
+ /** 把错误包成 isError 的工具结果(MCP 标准的错误信号方式)。 */
31
+ function asError(message) {
32
+ return { content: [{ type: 'text', text: message }], isError: true };
33
+ }
34
+ async function main() {
35
+ let config;
36
+ try {
37
+ config = loadConfig();
38
+ }
39
+ catch (err) {
40
+ // 配置错误:直接退出,提示走 stderr(stdout 留给 JSON-RPC)。
41
+ if (err instanceof ConfigError) {
42
+ process.stderr.write(`[picsense] config error: ${err.message}\n`);
43
+ process.exit(1);
44
+ }
45
+ throw err;
46
+ }
47
+ // 构造默认 provider。
48
+ const providerCfg = config.providers[config.defaultProvider];
49
+ if (!providerCfg) {
50
+ // throw(而非 process.exit):返回类型为 never,让 TS 严格控制流收窄
51
+ // providerCfg 为 ProviderConfig,同时交由 main().catch 统一输出。
52
+ throw new ConfigError(`Default provider "${config.defaultProvider}" is not configured. ` +
53
+ `Please set the corresponding API_KEY and MODEL (see .env.example).`);
54
+ }
55
+ const provider = getProvider(config.defaultProvider, providerCfg, {
56
+ timeoutMs: config.timeoutMs,
57
+ });
58
+ // Session 管理器,启动定时清理。
59
+ const sessionManager = new SessionManager(provider, config.maxImageBytes, config.sessionTtlMs, config.sessionCleanupIntervalMs);
60
+ sessionManager.startCleanup();
61
+ const server = new McpServer({ name: 'picsense', version: '0.1.0' }, { capabilities: { tools: {} } });
62
+ // ===== 工具 1:analyze_images =====
63
+ server.registerTool('analyze_images', {
64
+ description: '识别图片内容。传入一张图片为单图识别,传入多张为批量或对比识别。' +
65
+ '支持多轮迭代——首次调用创建 session,后续调用传入 session_id 可在已有对话基础上追加提问。',
66
+ inputSchema: {
67
+ image_sources: z
68
+ .array(z.string())
69
+ .optional()
70
+ .describe('图片来源数组(传一张为单图识别,传多张为批量/对比),每项自动识别 http URL / 本地文件路径 / base64。首次调用创建 session 时必填;后续轮次传入 session_id 时可省略(沿用该 session 的图片)'),
71
+ prompt: z.string().describe('对图片的识别要求,根据当前任务意图编写'),
72
+ session_id: z
73
+ .string()
74
+ .optional()
75
+ .describe('传入已有 session 的 ID 以发起后续轮次;不传则创建新 session'),
76
+ },
77
+ }, async (args) => {
78
+ try {
79
+ const result = await analyzeImages(sessionManager, {
80
+ image_sources: args.image_sources,
81
+ prompt: args.prompt,
82
+ session_id: args.session_id,
83
+ });
84
+ return asText(result);
85
+ }
86
+ catch (err) {
87
+ return asError(formatError(err));
88
+ }
89
+ });
90
+ // ===== 工具 2:list_sessions =====
91
+ server.registerTool('list_sessions', {
92
+ description: '查看当前所有识别会话的列表,包括每个 session 的简介、关联图片和迭代轮数。用于回顾历史识别记录。',
93
+ }, async () => {
94
+ const result = await listSessions(sessionManager);
95
+ return asText(result);
96
+ });
97
+ // ===== 工具 3:analyze_document =====
98
+ server.registerTool('analyze_document', {
99
+ description: '解析文档,识别其中所有图片的内容,并在图片位置旁标注描述。返回标注后的完整文档。',
100
+ inputSchema: {
101
+ document: z.string().describe('文档 URL / HTML / markdown(自动识别)'),
102
+ },
103
+ }, async (args) => {
104
+ try {
105
+ const result = await analyzeDocument(provider, config.maxImageBytes, config.timeoutMs, { document: args.document });
106
+ return asText(result);
107
+ }
108
+ catch (err) {
109
+ return asError(formatError(err));
110
+ }
111
+ });
112
+ // ===== 工具 4:analyze_video =====
113
+ server.registerTool('analyze_video', {
114
+ description: '识别视频内容。把视频抽帧后发送给视觉模型分析(默认每秒抽 1 帧、最多 30 帧)。' +
115
+ '支持多轮迭代——首次调用创建 session,后续调用传入 session_id 可在已有对话基础上追加提问。',
116
+ inputSchema: {
117
+ video_source: z
118
+ .string()
119
+ .optional()
120
+ .describe('视频来源(http URL / 本地文件路径),自动识别。首次调用创建 session 时必填;后续轮次传入 session_id 时可省略(沿用该 session 的视频)'),
121
+ prompt: z.string().describe('对视频的识别要求,根据当前任务意图编写'),
122
+ session_id: z
123
+ .string()
124
+ .optional()
125
+ .describe('传入已有 session 的 ID 以发起后续轮次;不传则创建新 session'),
126
+ },
127
+ }, async (args) => {
128
+ try {
129
+ const result = await analyzeVideo(sessionManager, {
130
+ video_source: args.video_source,
131
+ prompt: args.prompt,
132
+ session_id: args.session_id,
133
+ }, config.maxVideoBytes, { fps: config.videoFps, maxFrames: config.videoMaxFrames });
134
+ return asText(result);
135
+ }
136
+ catch (err) {
137
+ return asError(formatError(err));
138
+ }
139
+ });
140
+ // stdio 传输。
141
+ const transport = new StdioServerTransport();
142
+ await server.connect(transport);
143
+ process.stderr.write(`[picsense] started (provider=${config.defaultProvider}, model=${providerCfg.model})\n`);
144
+ // 进程退出时清理定时器(session 内存态自然清空,无需持久化善后)。
145
+ const shutdown = () => {
146
+ sessionManager.stopCleanup();
147
+ };
148
+ process.on('SIGINT', () => {
149
+ shutdown();
150
+ process.exit(0);
151
+ });
152
+ process.on('SIGTERM', () => {
153
+ shutdown();
154
+ process.exit(0);
155
+ });
156
+ }
157
+ /** 把各类错误格式化成面向用户的提示。 */
158
+ function formatError(err) {
159
+ if (err instanceof SessionNotFoundError) {
160
+ return err.message + ' 该 session 可能已过期(超过 24 小时)或进程已重启。请重新发起首次识别。';
161
+ }
162
+ if (err instanceof ToolInputError) {
163
+ return `参数错误: ${err.message}`;
164
+ }
165
+ if (err instanceof VideoLoadError) {
166
+ return `视频加载失败: ${err.message}`;
167
+ }
168
+ if (err instanceof Error) {
169
+ return `识别失败: ${err.message}`;
170
+ }
171
+ return `识别失败: ${String(err)}`;
172
+ }
173
+ main().catch((err) => {
174
+ process.stderr.write(`[picsense] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
175
+ process.exit(1);
176
+ });
177
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * VisionProvider 抽象层。
3
+ *
4
+ * 设计依据:docs/picsense-design.md §5.2.2、§6.1 —— 参考 @systemmin/image-mcp 的接口设计,
5
+ * 新增 provider 只需实现 VisionProvider 接口并在 getProvider 工厂注册。
6
+ *
7
+ * 多轮对话历史布局(见 docs/plan/03-session-manager.md):
8
+ * - 首轮 user message 含图(images 非空)。
9
+ * - 后续轮次只有文字,provider 把 messages 拼成原生多轮 messages 数组发给视觉模型。
10
+ */
11
+ /** 已注册的 provider 工厂表。新增 provider 时在此注册即可。 */
12
+ const FACTORIES = {};
13
+ /** 供 provider 模块自注册用。 */
14
+ export function registerProvider(name, factory) {
15
+ FACTORIES[name] = factory;
16
+ }
17
+ /**
18
+ * 按 provider 名称构造实例。
19
+ *
20
+ * @param name provider 名称(如 'openai')。
21
+ * @param cfg 该 provider 的配置(API Key / model / baseUrl)。
22
+ * @param opts 跨 provider 的运行时选项。
23
+ */
24
+ export function getProvider(name, cfg, opts) {
25
+ const factory = FACTORIES[name];
26
+ if (!factory) {
27
+ throw new Error(`Unknown provider "${name}". Supported: ${Object.keys(FACTORIES).join(', ') || '(none registered yet)'}.`);
28
+ }
29
+ return factory(cfg, opts);
30
+ }
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA4CH,6CAA6C;AAC7C,MAAM,SAAS,GAA6C,EAAE,CAAC;AAE/D,yBAAyB;AACzB,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,OAAwB;IACrE,SAAS,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,IAAY,EACZ,GAAmB,EACnB,IAAkB;IAElB,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,qBAAqB,IAAI,iBAAiB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,uBAAuB,GAAG,CAC1G,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * OpenAI Responses API vision provider。
3
+ *
4
+ * 该 provider 使用 OpenAI 的 **Responses API**(/v1/responses,原生格式),
5
+ * 而非 Chat Completions API。区别:
6
+ * - 请求用 input 数组(而非 messages),content part 用 input_text / input_image。
7
+ * - 多图:首轮 user 的 content 数组里放多个 input_image + 一个 input_text。
8
+ * - 图片字段 image_url:可为 https URL(直传)或 data:image/xxx;base64,...(base64)。
9
+ * - 响应:output[].content[] 中 type=output_text 的 text 为正文。
10
+ *
11
+ * 调研确认(实测 vibebabo.com 网关 + gpt-5.6-sol):
12
+ * - endpoint: POST {baseUrl}/responses,其中 baseUrl 形如 https://api.openai.com/v1
13
+ * (若用户传入的 baseUrl 已带 /v1 则原样使用;若未带则补 /v1)。
14
+ * - input 支持多轮:数组里多条 user/assistant 消息,assistant 的 content 用
15
+ * [{type:'output_text', text}] 形态(已在实测中验证多轮记忆)。
16
+ *
17
+ * 多轮对话布局(见 docs/plan/03-session-manager.md):
18
+ * 图片只挂在首轮 user message 的 content(input_image),后续轮次纯文字。
19
+ */
20
+ import { registerProvider } from './index.js';
21
+ import { withRetry } from './retry.js';
22
+ export class ProviderHttpError extends Error {
23
+ status;
24
+ body;
25
+ constructor(message, status, body) {
26
+ super(message);
27
+ this.status = status;
28
+ this.body = body;
29
+ this.name = 'ProviderHttpError';
30
+ }
31
+ }
32
+ const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
33
+ /**
34
+ * 规范化 baseUrl:确保以 /v1 结尾(Responses API 路径为 /responses)。
35
+ * - 用户传 https://api.openai.com → 补 /v1
36
+ * - 用户传 https://api.openai.com/v1 → 原样
37
+ * - 用户传 https://gateway.com/v1/ → 去尾斜杠
38
+ */
39
+ function normalizeBaseUrl(raw) {
40
+ if (!raw)
41
+ return DEFAULT_BASE_URL;
42
+ let url = raw.replace(/\/+$/, ''); // 去尾斜杠
43
+ if (!/\/v\d+$/.test(url)) {
44
+ url += '/v1';
45
+ }
46
+ return url;
47
+ }
48
+ /**
49
+ * 把 VisionRequest 转换为 Responses API 的 input 数组。
50
+ *
51
+ * 规则:images 只附加到 messages 里第一条 user 消息的 content(首轮)。
52
+ */
53
+ function buildInput(req) {
54
+ const input = [];
55
+ // system 消息:仅当需要生成 summary 时,给一个极简结构指令。
56
+ // 不预设场景化 prompt(设计文档 §2.3)。
57
+ if (req.generateSummary) {
58
+ input.push({
59
+ role: 'system',
60
+ content: 'You are a vision assistant. After your description, append a one-sentence summary of this image session wrapped in <summary>...</summary> tags. The summary should capture the essence of what the user is analyzing.',
61
+ });
62
+ }
63
+ let imagesAttached = false;
64
+ for (const msg of req.messages) {
65
+ if (msg.role === 'user' && req.images.length > 0 && !imagesAttached) {
66
+ // 首条 user 消息:图片 + 文字。
67
+ const parts = [];
68
+ for (const img of req.images) {
69
+ parts.push({ type: 'input_image', image_url: img.url });
70
+ }
71
+ parts.push({ type: 'input_text', text: msg.content });
72
+ input.push({ role: 'user', content: parts });
73
+ imagesAttached = true;
74
+ }
75
+ else {
76
+ // user 后续轮:纯文字(用 input_text 包装,保持数组形态一致)。
77
+ // assistant 历史轮:用 output_text 包装。
78
+ const partType = msg.role === 'assistant' ? 'output_text' : 'input_text';
79
+ input.push({
80
+ role: msg.role,
81
+ content: [{ type: partType, text: msg.content }],
82
+ });
83
+ }
84
+ }
85
+ // 兜底:传了图片但 messages 里没有 user 消息(异常情况)。
86
+ if (req.images.length > 0 && !imagesAttached) {
87
+ const parts = req.images.map((img) => ({
88
+ type: 'input_image',
89
+ image_url: img.url,
90
+ }));
91
+ input.push({ role: 'user', content: parts });
92
+ }
93
+ return input;
94
+ }
95
+ /** 从 Responses API 返回中拆出 summary 和正文 description。 */
96
+ function extractText(data) {
97
+ const out = data.output;
98
+ if (!Array.isArray(out))
99
+ return '';
100
+ const texts = [];
101
+ for (const item of out) {
102
+ if (item.type === 'message' && Array.isArray(item.content)) {
103
+ for (const c of item.content) {
104
+ if (c.type === 'output_text' && typeof c.text === 'string') {
105
+ texts.push(c.text);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ return texts.join('\n').trim();
111
+ }
112
+ /** 从模型回复中拆出 summary 和正文 description。 */
113
+ function splitSummary(raw, expectSummary) {
114
+ if (!expectSummary) {
115
+ return { description: raw };
116
+ }
117
+ const match = raw.match(/<summary>([\s\S]*?)<\/summary>/i);
118
+ if (match) {
119
+ const summary = match[1].trim();
120
+ const description = raw.replace(match[0], '').trim();
121
+ return { description: description || raw.trim(), summary };
122
+ }
123
+ // 模型没按格式返回时,用首句兜底作 summary。
124
+ const firstLine = raw.split('\n').find((l) => l.trim()) ?? raw.slice(0, 80);
125
+ return { description: raw.trim(), summary: firstLine.trim().slice(0, 120) };
126
+ }
127
+ class OpenAIProvider {
128
+ apiKey;
129
+ model;
130
+ baseUrl;
131
+ timeoutMs;
132
+ name = 'openai';
133
+ constructor(apiKey, model, baseUrl, timeoutMs) {
134
+ this.apiKey = apiKey;
135
+ this.model = model;
136
+ this.baseUrl = baseUrl;
137
+ this.timeoutMs = timeoutMs;
138
+ }
139
+ async analyze(req) {
140
+ const input = buildInput(req);
141
+ const body = {
142
+ model: this.model,
143
+ input,
144
+ // 非流式,便于一次性拿到完整回复。
145
+ stream: false,
146
+ };
147
+ const raw = await withRetry(async () => {
148
+ const controller = new AbortController();
149
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
150
+ try {
151
+ const res = await fetch(`${this.baseUrl}/responses`, {
152
+ method: 'POST',
153
+ headers: {
154
+ 'Content-Type': 'application/json',
155
+ Authorization: `Bearer ${this.apiKey}`,
156
+ },
157
+ body: JSON.stringify(body),
158
+ signal: controller.signal,
159
+ });
160
+ if (!res.ok) {
161
+ const text = await res.text().catch(() => '');
162
+ throw new ProviderHttpError(`OpenAI Responses API error ${res.status}: ${text.slice(0, 500)}`, res.status, text);
163
+ }
164
+ const data = await res.json();
165
+ const content = extractText(data);
166
+ if (!content) {
167
+ throw new ProviderHttpError('OpenAI Responses API returned no text content', 200, JSON.stringify(data).slice(0, 500));
168
+ }
169
+ return { status: res.status, result: content };
170
+ }
171
+ finally {
172
+ clearTimeout(timer);
173
+ }
174
+ });
175
+ const { description, summary } = splitSummary(raw, req.generateSummary === true);
176
+ return summary ? { description, summary } : { description };
177
+ }
178
+ }
179
+ /** 工厂实现。 */
180
+ export const createOpenAIProvider = (cfg, opts) => {
181
+ return new OpenAIProvider(cfg.apiKey, cfg.model, normalizeBaseUrl(cfg.baseUrl), opts.timeoutMs);
182
+ };
183
+ // 模块加载时自注册。
184
+ registerProvider('openai', createOpenAIProvider);
185
+ //# sourceMappingURL=openai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/providers/openai.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Provider 注册中心。
3
+ *
4
+ * 各 provider 模块在 import 时通过 registerProvider() 自注册。
5
+ * 本文件集中 import 所有 provider,确保应用启动时它们都被加载。
6
+ * 入口/测试只需 import 本模块即可获得全部已注册 provider。
7
+ *
8
+ * 新增 provider 时:在下方加一行 import。
9
+ */
10
+ import './openai.js';
11
+ // 后续 provider 在此注册:
12
+ // import './qwen.js';
13
+ // import './kimi.js';
14
+ export { getProvider, registerProvider } from './index.js';
15
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/providers/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,aAAa,CAAC;AACrB,oBAAoB;AACpB,sBAAsB;AACtB,sBAAsB;AAEtB,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 通用重试工具。参考 @z_ai/mcp-server 的 withRetry(设计文档 附录 A.4):
3
+ * 最多重试 maxRetries 次,每次间隔 baseDelayMs,仅对网络/服务端错误重试。
4
+ */
5
+ /** 简单的 sleep。 */
6
+ export function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ /**
10
+ * 判断错误是否值得重试:网络错误或 5xx/429。
11
+ * 4xx(除 429)通常是请求本身的问题,重试无意义。
12
+ */
13
+ export function isRetryable(status, error) {
14
+ if (status !== undefined) {
15
+ if (status === 429)
16
+ return true;
17
+ if (status >= 500 && status < 600)
18
+ return true;
19
+ return false;
20
+ }
21
+ // 无 status(网络层错误,如 DNS/连接超时)—— 重试。
22
+ return error instanceof TypeError;
23
+ }
24
+ /**
25
+ * 带重试地执行 fn。
26
+ *
27
+ * @param fn 待执行函数。
28
+ * @param maxRetries 最大重试次数(不含首次)。
29
+ * @param baseDelayMs 首次重试前等待毫秒数;每次翻倍。
30
+ */
31
+ export async function withRetry(fn, maxRetries = 2, baseDelayMs = 1000) {
32
+ let lastError;
33
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
34
+ try {
35
+ const { result } = await fn();
36
+ return result;
37
+ }
38
+ catch (err) {
39
+ lastError = err;
40
+ // 错误对象上携带的 status 字段(见 OpenAIProvider 抛出的 ProviderHttpError)。
41
+ const status = err.status;
42
+ if (attempt < maxRetries && isRetryable(status, err)) {
43
+ await sleep(baseDelayMs * 2 ** attempt);
44
+ continue;
45
+ }
46
+ break;
47
+ }
48
+ }
49
+ throw lastError;
50
+ }
51
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/providers/retry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,iBAAiB;AACjB,MAAM,UAAU,KAAK,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,MAA0B,EAAE,KAAc;IACpE,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAChC,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;YAAE,OAAO,IAAI,CAAC;QAC/C,OAAO,KAAK,CAAC;IACf,CAAC;IACD,mCAAmC;IACnC,OAAO,KAAK,YAAY,SAAS,CAAC;AACpC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,EAAgD,EAChD,UAAU,GAAG,CAAC,EACd,WAAW,GAAG,IAAI;IAElB,IAAI,SAAkB,CAAC;IACvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,IAAI,CAAC;YACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,EAAE,CAAC;YAC9B,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,SAAS,GAAG,GAAG,CAAC;YAChB,8DAA8D;YAC9D,MAAM,MAAM,GAAI,GAA2B,CAAC,MAAM,CAAC;YACnD,IAAI,OAAO,GAAG,UAAU,IAAI,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;gBACrD,MAAM,KAAK,CAAC,WAAW,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;gBACxC,SAAS;YACX,CAAC;YACD,MAAM;QACR,CAAC;IACH,CAAC;IACD,MAAM,SAAS,CAAC;AAClB,CAAC"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * analyze_document 工具业务逻辑。
3
+ *
4
+ * 设计依据:docs/picsense-design.md §4.1、§4.2.5。
5
+ * - 输入文档(URL/HTML/markdown 自动识别)。
6
+ * - 识别其中所有图片,在图片位置旁标注描述。
7
+ * - 返回标注后的完整文档 + 识别图片数。
8
+ *
9
+ * 职责单一:输入文档、输出文档。识别 prompt 内置,不接收外部 prompt(§4.2.5)。
10
+ * 标注格式:图片后插入注释,不破坏原文档渲染:
11
+ * markdown: ![alt](url)\n<!-- image-vision: <desc> -->
12
+ * html: <img src="url"> 后插 <!-- image-vision: <desc> -->
13
+ *
14
+ * 策略:逐张串行识别(保守,避免 rate limit)。失败容错,原图保留并标注失败原因。
15
+ */
16
+ import { loadImage, ImageLoadError } from '../core/image-loader.js';
17
+ /** 内置识别 prompt(不接收外部 prompt)。 */
18
+ const DOC_IMAGE_PROMPT = 'Describe this image concisely and accurately.';
19
+ const HTML_IMG_RE = /<img\s+[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
20
+ const MD_IMG_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
21
+ export class DocumentFetchError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'DocumentFetchError';
25
+ }
26
+ }
27
+ export async function analyzeDocument(provider, maxImageBytes, timeoutMs, params) {
28
+ const doc = params.document?.trim();
29
+ if (!doc) {
30
+ return { document: params.document ?? '', images_analyzed: 0 };
31
+ }
32
+ // 自动识别文档形态:URL → 抓取;含 <img → HTML;否则 markdown。
33
+ let content = doc;
34
+ let isHtml = /<[a-z!][\s\S]*>/i.test(doc);
35
+ if (/^https?:\/\//i.test(doc)) {
36
+ content = await fetchDocument(doc, timeoutMs);
37
+ // 抓回来的内容形态重新判断。
38
+ isHtml = /<[a-z!][\s\S]*>/i.test(content) && /<img\s/i.test(content);
39
+ }
40
+ if (isHtml) {
41
+ return annotateHtml(provider, maxImageBytes, content);
42
+ }
43
+ return annotateMarkdown(provider, maxImageBytes, content);
44
+ }
45
+ /** 抓取 URL 文档正文。 */
46
+ async function fetchDocument(url, timeoutMs) {
47
+ const controller = new AbortController();
48
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
49
+ try {
50
+ const res = await fetch(url, {
51
+ signal: controller.signal,
52
+ headers: { Accept: 'text/html, text/markdown, text/plain, */*' },
53
+ });
54
+ if (!res.ok) {
55
+ throw new DocumentFetchError(`Failed to fetch document: HTTP ${res.status}`);
56
+ }
57
+ return await res.text();
58
+ }
59
+ finally {
60
+ clearTimeout(timer);
61
+ }
62
+ }
63
+ /** 标注 HTML 文档中的 <img>。 */
64
+ async function annotateHtml(provider, maxImageBytes, html) {
65
+ const matches = [];
66
+ let m;
67
+ HTML_IMG_RE.lastIndex = 0;
68
+ while ((m = HTML_IMG_RE.exec(html)) !== null) {
69
+ matches.push({ src: m[1], index: m.index, fullEnd: m.index + m[0].length });
70
+ }
71
+ if (matches.length === 0)
72
+ return { document: html, images_analyzed: 0 };
73
+ // 从后往前插入注释,避免影响前面的 index。
74
+ let result = html;
75
+ let analyzed = 0;
76
+ for (let i = matches.length - 1; i >= 0; i--) {
77
+ const { src, fullEnd } = matches[i];
78
+ const desc = await recognizeOne(provider, maxImageBytes, src);
79
+ const comment = `\n<!-- image-vision: ${escapeComment(desc)} -->`;
80
+ result = result.slice(0, fullEnd) + comment + result.slice(fullEnd);
81
+ if (!desc.startsWith('[识别失败'))
82
+ analyzed++;
83
+ }
84
+ return { document: result, images_analyzed: analyzed };
85
+ }
86
+ /** 标注 markdown 文档中的 ![]()。 */
87
+ async function annotateMarkdown(provider, maxImageBytes, md) {
88
+ const matches = [];
89
+ let m;
90
+ MD_IMG_RE.lastIndex = 0;
91
+ while ((m = MD_IMG_RE.exec(md)) !== null) {
92
+ matches.push({ full: m[0], index: m.index, end: m.index + m[0].length });
93
+ }
94
+ if (matches.length === 0)
95
+ return { document: md, images_analyzed: 0 };
96
+ let result = md;
97
+ let analyzed = 0;
98
+ for (let i = matches.length - 1; i >= 0; i--) {
99
+ // 提取 url:full 形如 ![alt](url)
100
+ const urlMatch = matches[i].full.match(/!\[[^\]]*\]\(([^)]+)\)/);
101
+ const url = urlMatch ? urlMatch[1].trim() : '';
102
+ const desc = await recognizeOne(provider, maxImageBytes, url);
103
+ const comment = `\n<!-- image-vision: ${escapeComment(desc)} -->`;
104
+ result = result.slice(0, matches[i].end) + comment + result.slice(matches[i].end);
105
+ if (!desc.startsWith('[识别失败'))
106
+ analyzed++;
107
+ }
108
+ return { document: result, images_analyzed: analyzed };
109
+ }
110
+ /** 识别单张图,失败返回错误占位(不抛出,保证整体流程继续)。 */
111
+ async function recognizeOne(provider, maxImageBytes, src) {
112
+ try {
113
+ const img = await loadImage(src, maxImageBytes);
114
+ const res = await provider.analyze({
115
+ images: [img],
116
+ messages: [{ role: 'user', content: DOC_IMAGE_PROMPT }],
117
+ generateSummary: false,
118
+ });
119
+ return res.description;
120
+ }
121
+ catch (err) {
122
+ const reason = err instanceof ImageLoadError ? err.message : err instanceof Error ? err.message : String(err);
123
+ return `[识别失败: ${reason}]`;
124
+ }
125
+ }
126
+ /** 转义注释内容中的 -->,避免破坏 HTML 注释。 */
127
+ function escapeComment(s) {
128
+ return s.replace(/-->/g, '--&gt;');
129
+ }
130
+ //# sourceMappingURL=analyze-document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-document.js","sourceRoot":"","sources":["../../src/tools/analyze-document.ts"],"names":[],"mappings":"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"}