cc-vision-hook 0.1.1 → 0.2.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/CHANGELOG.md +23 -0
- package/README.md +31 -7
- package/README.zh-CN.md +31 -7
- package/dist/claudeMcp.d.ts +29 -0
- package/dist/claudeMcp.js +85 -0
- package/dist/claudeMcp.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +22 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/configCmd.js +18 -5
- package/dist/commands/configCmd.js.map +1 -1
- package/dist/commands/doctor.js +29 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +16 -0
- package/dist/commands/init.js +112 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install.d.ts +3 -1
- package/dist/commands/install.js +13 -5
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/mcpCmd.d.ts +29 -0
- package/dist/commands/mcpCmd.js +58 -0
- package/dist/commands/mcpCmd.js.map +1 -0
- package/dist/commands/status.js +6 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/testImage.js +1 -7
- package/dist/commands/testImage.js.map +1 -1
- package/dist/config.d.ts +8 -2
- package/dist/config.js +16 -1
- package/dist/config.js.map +1 -1
- package/dist/imageMime.d.ts +14 -0
- package/dist/imageMime.js +22 -0
- package/dist/imageMime.js.map +1 -0
- package/dist/mcpServer.d.ts +31 -0
- package/dist/mcpServer.js +141 -0
- package/dist/mcpServer.js.map +1 -0
- package/dist/pasteScanner.js +1 -7
- package/dist/pasteScanner.js.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/index.js.map +1 -1
- package/dist/vision.d.ts +13 -1
- package/dist/vision.js +58 -17
- package/dist/vision.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP stdio server:暴露 `vision_ask` / `vision_describe_image` / `vision_describe_data_url`
|
|
3
|
+
* 三个工具,让 Claude 主模型在对话中可以主动追问此前已被 Hook 处理过的图片,
|
|
4
|
+
* 或直接解析一张新的本地图片/data URL(不依赖磁盘缓存命中)。
|
|
5
|
+
*
|
|
6
|
+
* 设计对齐 docs/research/cc-vision-hook-design-2026-07-08.md §8.3:
|
|
7
|
+
* - `vision_ask`:按 image_id 从磁盘缓存取回原图字节,用 describeImage() 追问,
|
|
8
|
+
* 不写回缓存的 description 字段(避免用一次追问的针对性回答覆盖掉原本的通用描述)。
|
|
9
|
+
* - `vision_describe_image` / `vision_describe_data_url`:直接解析,不依赖历史缓存命中,
|
|
10
|
+
* 但解析结果仍然按内容 hash 落盘缓存,方便后续 `vision_ask` 追问同一张图。
|
|
11
|
+
*
|
|
12
|
+
* MCP 本身与 enabled 开关无关(见 claudeSettings.ts 顶部注释的既定设计:
|
|
13
|
+
* "cvh disable 不卸载 MCP,cvh enable 不安装 MCP")——但工具执行时仍然遵守 enabled
|
|
14
|
+
* 开关,因为调用视觉模型本身就是"处理图片"这件事,enabled=false 时应视为功能关闭。
|
|
15
|
+
*/
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { resolveConfig } from "./config.js";
|
|
21
|
+
import { describeImage } from "./vision.js";
|
|
22
|
+
import { getCacheEntry, getCachedImageBytes, putCacheEntry } from "./cache.js";
|
|
23
|
+
import { mimeFromPath } from "./imageMime.js";
|
|
24
|
+
/** data URI 正则:形如 `data:image/png;base64,xxxx`,与 extract.ts 保持一致。 */
|
|
25
|
+
const DATA_URI_RE = /^data:(image\/[a-z0-9.+_-]+);base64,(.+)$/i;
|
|
26
|
+
/**
|
|
27
|
+
* 构造一个 MCP 文本结果,成功/失败均用统一 shape 返回(`isError` 标志区分),
|
|
28
|
+
* 让 Agent 侧可以稳定判断调用是否成功,而不是靠解析文本内容猜测。
|
|
29
|
+
*
|
|
30
|
+
* @param text - 返回给 Agent 的文本内容
|
|
31
|
+
* @param isError - 是否是错误结果
|
|
32
|
+
* @returns 符合 MCP CallToolResult 结构的对象
|
|
33
|
+
*/
|
|
34
|
+
const textResult = (text, isError = false) => ({
|
|
35
|
+
content: [{ type: "text", text }],
|
|
36
|
+
...(isError ? { isError: true } : {}),
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* 创建并配置好三个视觉工具的 MCP Server 实例(尚未连接 transport)。
|
|
40
|
+
* 拆成独立函数是为了方便单测:可以直接调用 `server.server` 上的能力做集成测试,
|
|
41
|
+
* 不需要真的起一个 stdio 子进程。
|
|
42
|
+
*
|
|
43
|
+
* @returns 配置完毕、可直接 connect(transport) 的 McpServer 实例
|
|
44
|
+
*/
|
|
45
|
+
export const createVisionMcpServer = () => {
|
|
46
|
+
const server = new McpServer({ name: "cc-vision-hook", version: "0.1.1" });
|
|
47
|
+
server.registerTool("vision_ask", {
|
|
48
|
+
title: "追问已解析过的图片",
|
|
49
|
+
description: "针对此前已被 cvh(UserPromptSubmit/PostToolUse Hook 或本 MCP 的其他工具)处理过的图片," +
|
|
50
|
+
"向视觉模型追加提问(如询问图片局部细节、读取图中文字等)。需要先从 additionalContext 里的 " +
|
|
51
|
+
"image_vision/tool_image_vision 标签拿到 image_id。",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
imageId: z.string().describe("additionalContext 中提供的 image_id,形如 img_xxxxxxxxxxxxxxxx"),
|
|
54
|
+
question: z.string().describe("希望追问的具体问题"),
|
|
55
|
+
},
|
|
56
|
+
}, async ({ imageId, question }) => {
|
|
57
|
+
const entry = await getCacheEntry(imageId);
|
|
58
|
+
if (!entry) {
|
|
59
|
+
// 明确的文字错误而不是静默失败,呼应设计文档 §8.3:缓存过期/不存在时要让 Agent 知道原因。
|
|
60
|
+
return textResult(`未找到 image_id="${imageId}" 对应的缓存条目(可能已过期或从未存在)。请重新提供图片。`, true);
|
|
61
|
+
}
|
|
62
|
+
const bytes = await getCachedImageBytes(imageId);
|
|
63
|
+
if (!bytes) {
|
|
64
|
+
return textResult(`缓存元数据存在但原图字节缺失(image_id="${imageId}"),无法追问,请重新提供图片。`, true);
|
|
65
|
+
}
|
|
66
|
+
const config = await resolveConfig();
|
|
67
|
+
try {
|
|
68
|
+
const answer = await describeImage(bytes.toString("base64"), entry.mimeType, config, question);
|
|
69
|
+
return textResult(answer);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return textResult(`视觉模型调用失败:${error instanceof Error ? error.message : String(error)}`, true);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
server.registerTool("vision_describe_image", {
|
|
76
|
+
title: "解析本地图片文件",
|
|
77
|
+
description: "直接解析一张本地图片文件的内容,不依赖历史缓存命中。适用于 Agent 想主动查看某个本地文件的场景。",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
path: z.string().describe("本地图片文件的绝对或相对路径"),
|
|
80
|
+
question: z.string().optional().describe("可选:具体想问的问题,省略则给出通用描述"),
|
|
81
|
+
},
|
|
82
|
+
}, async ({ path, question }) => {
|
|
83
|
+
const mimeType = mimeFromPath(path);
|
|
84
|
+
if (!mimeType) {
|
|
85
|
+
return textResult(`不支持的图片格式:${path}(支持 png/jpg/jpeg/webp/gif)`, true);
|
|
86
|
+
}
|
|
87
|
+
let bytes;
|
|
88
|
+
try {
|
|
89
|
+
bytes = await readFile(path);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return textResult(`读取文件失败:${error instanceof Error ? error.message : String(error)}`, true);
|
|
93
|
+
}
|
|
94
|
+
const config = await resolveConfig();
|
|
95
|
+
try {
|
|
96
|
+
const description = await describeImage(bytes.toString("base64"), mimeType, config, question);
|
|
97
|
+
// 解析结果落盘缓存,方便后续用 vision_ask 追问同一张图(sourceTool 标记来源,便于排查)。
|
|
98
|
+
await putCacheEntry(bytes, mimeType, "mcp:vision_describe_image", description, config.cache.ttlDays);
|
|
99
|
+
return textResult(description);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return textResult(`视觉模型调用失败:${error instanceof Error ? error.message : String(error)}`, true);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
server.registerTool("vision_describe_data_url", {
|
|
106
|
+
title: "解析 data URL 形式的图片",
|
|
107
|
+
description: "直接解析一个 data URL(data:image/xxx;base64,...)形式的图片内容,不依赖历史缓存命中。",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
dataUrl: z.string().describe("完整的 data URL 字符串,形如 data:image/png;base64,xxxx"),
|
|
110
|
+
question: z.string().optional().describe("可选:具体想问的问题,省略则给出通用描述"),
|
|
111
|
+
},
|
|
112
|
+
}, async ({ dataUrl, question }) => {
|
|
113
|
+
const match = dataUrl.match(DATA_URI_RE);
|
|
114
|
+
if (!match?.[1] || !match[2]) {
|
|
115
|
+
return textResult(`不是合法的图片 data URL:${dataUrl.slice(0, 64)}...`, true);
|
|
116
|
+
}
|
|
117
|
+
const [, mimeType, base64] = match;
|
|
118
|
+
const config = await resolveConfig();
|
|
119
|
+
try {
|
|
120
|
+
const description = await describeImage(base64, mimeType, config, question);
|
|
121
|
+
await putCacheEntry(Buffer.from(base64, "base64"), mimeType, "mcp:vision_describe_data_url", description, config.cache.ttlDays);
|
|
122
|
+
return textResult(description);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return textResult(`视觉模型调用失败:${error instanceof Error ? error.message : String(error)}`, true);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return server;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* 以 stdio 模式启动 MCP server(由 Claude Code 作为子进程调起,`cvh mcp serve` 的入口)。
|
|
132
|
+
* 这是一个长驻进程,函数本身不会 resolve(除非 transport 关闭)。
|
|
133
|
+
*
|
|
134
|
+
* @returns 在 transport 关闭前不会 resolve 的 Promise
|
|
135
|
+
*/
|
|
136
|
+
export const serveMcpStdio = async () => {
|
|
137
|
+
const server = createVisionMcpServer();
|
|
138
|
+
const transport = new StdioServerTransport();
|
|
139
|
+
await server.connect(transport);
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=mcpServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpServer.js","sourceRoot":"","sources":["../src/mcpServer.ts"],"names":[],"mappings":"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"}
|
package/dist/pasteScanner.js
CHANGED
|
@@ -9,13 +9,7 @@
|
|
|
9
9
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
10
10
|
import { join, extname } from "node:path";
|
|
11
11
|
import { getClaudeHomeDir } from "./config.js";
|
|
12
|
-
|
|
13
|
-
".png": "image/png",
|
|
14
|
-
".jpg": "image/jpeg",
|
|
15
|
-
".jpeg": "image/jpeg",
|
|
16
|
-
".webp": "image/webp",
|
|
17
|
-
".gif": "image/gif",
|
|
18
|
-
};
|
|
12
|
+
import { EXT_TO_MIME } from "./imageMime.js";
|
|
19
13
|
/**
|
|
20
14
|
* 拼出指定 session 的 image-cache 目录路径。
|
|
21
15
|
*
|
package/dist/pasteScanner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pasteScanner.js","sourceRoot":"","sources":["../src/pasteScanner.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"pasteScanner.js","sourceRoot":"","sources":["../src/pasteScanner.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAS7C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAiB,EAAU,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC;AAElH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,SAAiB,EAA8B,EAAE;IACtF,MAAM,GAAG,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IACxC,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,OAAO,GAAsB,EAAE,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,QAAQ;YAAE,SAAS;QACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC/B,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACjE,CAAC;QAAC,MAAM,CAAC;YACP,mBAAmB;QACrB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;IAC9C,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EAAE,IAAY,EAAmB,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC"}
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
export type { CvhConfig, CvhProvider } from "../config.js";
|
|
6
6
|
export { DEFAULT_CONFIG, loadConfig, saveConfig, resolveConfig } from "../config.js";
|
|
7
7
|
export { installHooks, uninstallHooks, checkHooksInstalled } from "../claudeSettings.js";
|
|
8
|
+
export { installMcpServer, uninstallMcpServer, checkMcpInstalled } from "../claudeMcp.js";
|
|
8
9
|
export { extractImages, type ExtractedImage } from "../extract.js";
|
|
9
10
|
export { describeImage } from "../vision.js";
|
|
10
11
|
export { computeImageId, findCacheByBytes, putCacheEntry, getCacheEntry, getCachedImageBytes, type CacheEntry } from "../cache.js";
|
|
11
12
|
export { handleUserPromptSubmit, type UserPromptSubmitInput, type HookOutput } from "../hooks/userPromptSubmit.js";
|
|
12
13
|
export { handlePostToolUse, type PostToolUseInput } from "../hooks/postToolUse.js";
|
|
14
|
+
export { createVisionMcpServer, serveMcpStdio } from "../mcpServer.js";
|
package/dist/sdk/index.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { DEFAULT_CONFIG, loadConfig, saveConfig, resolveConfig } from "../config.js";
|
|
6
6
|
export { installHooks, uninstallHooks, checkHooksInstalled } from "../claudeSettings.js";
|
|
7
|
+
export { installMcpServer, uninstallMcpServer, checkMcpInstalled } from "../claudeMcp.js";
|
|
7
8
|
export { extractImages } from "../extract.js";
|
|
8
9
|
export { describeImage } from "../vision.js";
|
|
9
10
|
export { computeImageId, findCacheByBytes, putCacheEntry, getCacheEntry, getCachedImageBytes } from "../cache.js";
|
|
10
11
|
export { handleUserPromptSubmit } from "../hooks/userPromptSubmit.js";
|
|
11
12
|
export { handlePostToolUse } from "../hooks/postToolUse.js";
|
|
13
|
+
export { createVisionMcpServer, serveMcpStdio } from "../mcpServer.js";
|
|
12
14
|
//# sourceMappingURL=index.js.map
|
package/dist/sdk/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sdk/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAErF,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sdk/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAErF,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE1F,OAAO,EAAE,aAAa,EAAuB,MAAM,eAAe,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAmB,MAAM,aAAa,CAAC;AAEnI,OAAO,EAAE,sBAAsB,EAA+C,MAAM,8BAA8B,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAyB,MAAM,yBAAyB,CAAC;AAEnF,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/vision.d.ts
CHANGED
|
@@ -27,6 +27,17 @@ import type { CvhConfig } from "./config.js";
|
|
|
27
27
|
* @throws 当 provider 是未知值时抛出错误
|
|
28
28
|
*/
|
|
29
29
|
export declare const resolveModel: (config: CvhConfig) => import("@ai-sdk/provider").LanguageModelV3;
|
|
30
|
+
/**
|
|
31
|
+
* 各 provider 期望 `baseUrl` 已经带上的路径段前缀(AI SDK 不会帮你补全,传什么用什么)。
|
|
32
|
+
* 导出供 `commands/configCmd.ts`(设置时提前警告)和本文件(调用失败后补充排查提示)
|
|
33
|
+
* 共用同一份定义,避免两处维护导致漂移。
|
|
34
|
+
*
|
|
35
|
+
* 实测踩坑:`@ai-sdk/anthropic` 只有在 `baseURL` 恰好等于官方地址时才会自动补 "/v1",
|
|
36
|
+
* 自定义网关(如公司内部自建的兼容网关)传裸域名会直接拼成 "<baseUrl>/messages" 发出
|
|
37
|
+
* 404,且错误信息只有一句 "Not Found",完全看不出是 baseUrl 缺路径段——第一次真机验证
|
|
38
|
+
* 时就在这里卡了一次,加这段针对性提示,避免同样的坑被别的用户/未来的自己重新踩一遍。
|
|
39
|
+
*/
|
|
40
|
+
export declare const EXPECTED_BASE_URL_SEGMENT: Record<CvhConfig["provider"], string>;
|
|
30
41
|
/**
|
|
31
42
|
* 调用视觉模型解析一张图片,返回文字描述(或针对具体问题的回答)。
|
|
32
43
|
*
|
|
@@ -35,6 +46,7 @@ export declare const resolveModel: (config: CvhConfig) => import("@ai-sdk/provid
|
|
|
35
46
|
* @param config - 当前生效的 cvh 配置
|
|
36
47
|
* @param question - 可选的具体提问(用于 vision_ask 追问场景),省略时使用通用描述提示词
|
|
37
48
|
* @returns 视觉模型的文字回答
|
|
38
|
-
* @throws
|
|
49
|
+
* @throws 当上游调用失败或超时时抛出错误(404 + baseUrl 缺路径段场景会补充排查提示),
|
|
50
|
+
* 调用方需要自行捕获并降级处理
|
|
39
51
|
*/
|
|
40
52
|
export declare const describeImage: (base64: string, mimeType: string, config: CvhConfig, question?: string) => Promise<string>;
|
package/dist/vision.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `openai(config.model)`,会导致 "oai" 和 "responses" 两个配置项实际调用的是同一个 API,
|
|
15
15
|
* "oai" 选项形同虚设。必须显式调用 `openai.chat(modelId)` 才能真正拿到 Chat Completions 模型。
|
|
16
16
|
*/
|
|
17
|
-
import { generateText } from "ai";
|
|
17
|
+
import { generateText, APICallError } from "ai";
|
|
18
18
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
19
19
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
20
20
|
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
@@ -55,6 +55,41 @@ export const resolveModel = (config) => {
|
|
|
55
55
|
throw new Error(`未知的 provider: ${config.provider}`);
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* 各 provider 期望 `baseUrl` 已经带上的路径段前缀(AI SDK 不会帮你补全,传什么用什么)。
|
|
60
|
+
* 导出供 `commands/configCmd.ts`(设置时提前警告)和本文件(调用失败后补充排查提示)
|
|
61
|
+
* 共用同一份定义,避免两处维护导致漂移。
|
|
62
|
+
*
|
|
63
|
+
* 实测踩坑:`@ai-sdk/anthropic` 只有在 `baseURL` 恰好等于官方地址时才会自动补 "/v1",
|
|
64
|
+
* 自定义网关(如公司内部自建的兼容网关)传裸域名会直接拼成 "<baseUrl>/messages" 发出
|
|
65
|
+
* 404,且错误信息只有一句 "Not Found",完全看不出是 baseUrl 缺路径段——第一次真机验证
|
|
66
|
+
* 时就在这里卡了一次,加这段针对性提示,避免同样的坑被别的用户/未来的自己重新踩一遍。
|
|
67
|
+
*/
|
|
68
|
+
export const EXPECTED_BASE_URL_SEGMENT = {
|
|
69
|
+
oai: "/v1",
|
|
70
|
+
responses: "/v1",
|
|
71
|
+
anthropic: "/v1",
|
|
72
|
+
gemini: "/v1beta",
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* 当上游调用返回 404 且 `baseUrl` 明显缺少 provider 期望的路径段(如 `/v1`)时,
|
|
76
|
+
* 在错误信息里补一句排查提示,帮用户快速定位是配置问题而不是上游故障。
|
|
77
|
+
*
|
|
78
|
+
* @param error - `generateText()` 抛出的原始错误
|
|
79
|
+
* @param config - 当前生效的 cvh 配置
|
|
80
|
+
* @returns 补充了排查提示的 Error(未命中该场景时原样返回原始错误)
|
|
81
|
+
*/
|
|
82
|
+
const enhanceApiError = (error, config) => {
|
|
83
|
+
if (!(error instanceof APICallError) || error.statusCode !== 404)
|
|
84
|
+
return error;
|
|
85
|
+
const expectedSegment = EXPECTED_BASE_URL_SEGMENT[config.provider];
|
|
86
|
+
if (!config.baseUrl || config.baseUrl.includes(expectedSegment))
|
|
87
|
+
return error;
|
|
88
|
+
return new Error(`${error.message}(HTTP 404,请求地址:${error.url ?? "未知"})\n` +
|
|
89
|
+
`⚠️ 可能原因:baseUrl 缺少路径段 "${expectedSegment}"——AI SDK 不会自动补全自定义网关的 ` +
|
|
90
|
+
`baseUrl,需要手动带上完整路径前缀,例如 "${config.baseUrl}${expectedSegment}"。` +
|
|
91
|
+
`官方地址(如 https://api.openai.com)是例外,SDK 内部会特殊处理。`);
|
|
92
|
+
};
|
|
58
93
|
/**
|
|
59
94
|
* 调用视觉模型解析一张图片,返回文字描述(或针对具体问题的回答)。
|
|
60
95
|
*
|
|
@@ -63,25 +98,31 @@ export const resolveModel = (config) => {
|
|
|
63
98
|
* @param config - 当前生效的 cvh 配置
|
|
64
99
|
* @param question - 可选的具体提问(用于 vision_ask 追问场景),省略时使用通用描述提示词
|
|
65
100
|
* @returns 视觉模型的文字回答
|
|
66
|
-
* @throws
|
|
101
|
+
* @throws 当上游调用失败或超时时抛出错误(404 + baseUrl 缺路径段场景会补充排查提示),
|
|
102
|
+
* 调用方需要自行捕获并降级处理
|
|
67
103
|
*/
|
|
68
104
|
export const describeImage = async (base64, mimeType, config, question) => {
|
|
69
105
|
const model = resolveModel(config);
|
|
70
106
|
const dataUrl = `data:${mimeType};base64,${base64}`;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
try {
|
|
108
|
+
const { text } = await generateText({
|
|
109
|
+
model,
|
|
110
|
+
messages: [
|
|
111
|
+
{
|
|
112
|
+
role: "user",
|
|
113
|
+
content: [
|
|
114
|
+
{ type: "image", image: dataUrl },
|
|
115
|
+
{ type: "text", text: question ?? DEFAULT_DESCRIBE_PROMPT },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
maxOutputTokens: config.maxTokens,
|
|
120
|
+
abortSignal: AbortSignal.timeout(config.timeoutMs),
|
|
121
|
+
});
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
throw enhanceApiError(error, config);
|
|
126
|
+
}
|
|
86
127
|
};
|
|
87
128
|
//# sourceMappingURL=vision.js.map
|
package/dist/vision.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vision.js","sourceRoot":"","sources":["../src/vision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"vision.js","sourceRoot":"","sources":["../src/vision.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-vision-hook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Claude Code Hook toolkit that lets vision-blind (silently-ignoring) models understand pasted/tool-produced images by appending an AI-generated description as additionalContext.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
"@ai-sdk/anthropic": "^3.0.81",
|
|
47
47
|
"@ai-sdk/google": "^3.0.0",
|
|
48
48
|
"@ai-sdk/openai": "^3.0.0",
|
|
49
|
-
"
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
|
+
"ai": "^6.0.194",
|
|
51
|
+
"zod": "^4.4.3"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@changesets/cli": "^2.31.0",
|