cc-vision-hook 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/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/README.zh-CN.md +158 -0
- package/dist/cache.d.ts +70 -0
- package/dist/cache.js +164 -0
- package/dist/cache.js.map +1 -0
- package/dist/claudeSettings.d.ts +31 -0
- package/dist/claudeSettings.js +115 -0
- package/dist/claudeSettings.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +106 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/configCmd.d.ts +18 -0
- package/dist/commands/configCmd.js +104 -0
- package/dist/commands/configCmd.js.map +1 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +60 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/install.d.ts +16 -0
- package/dist/commands/install.js +48 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/status.d.ts +9 -0
- package/dist/commands/status.js +35 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/testImage.d.ts +11 -0
- package/dist/commands/testImage.js +40 -0
- package/dist/commands/testImage.js.map +1 -0
- package/dist/commands/toggle.d.ts +10 -0
- package/dist/commands/toggle.js +21 -0
- package/dist/commands/toggle.js.map +1 -0
- package/dist/config.d.ts +72 -0
- package/dist/config.js +94 -0
- package/dist/config.js.map +1 -0
- package/dist/extract.d.ts +32 -0
- package/dist/extract.js +105 -0
- package/dist/extract.js.map +1 -0
- package/dist/hooks/postToolUse.d.ts +27 -0
- package/dist/hooks/postToolUse.js +53 -0
- package/dist/hooks/postToolUse.js.map +1 -0
- package/dist/hooks/userPromptSubmit.d.ts +29 -0
- package/dist/hooks/userPromptSubmit.js +64 -0
- package/dist/hooks/userPromptSubmit.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/pasteScanner.d.ts +37 -0
- package/dist/pasteScanner.js +68 -0
- package/dist/pasteScanner.js.map +1 -0
- package/dist/sdk/index.d.ts +12 -0
- package/dist/sdk/index.js +12 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/seenTracker.d.ts +23 -0
- package/dist/seenTracker.js +46 -0
- package/dist/seenTracker.js.map +1 -0
- package/dist/vision.d.ts +40 -0
- package/dist/vision.js +87 -0
- package/dist/vision.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户粘贴图片场景的扫描器:复用 Claude Code 官方在用户粘贴图片时会提前落盘的旁路事实。
|
|
3
|
+
*
|
|
4
|
+
* 依赖的实现细节(非官方承诺的稳定 API,best-effort,不做版本防御):
|
|
5
|
+
* `~/.claude/image-cache/<session_id>/<pasteId>.<ext>`
|
|
6
|
+
* 该路径已通过泄露源码 `src/utils/imageStore.ts` 的 `getImageStoreDir()` 源码级确认,
|
|
7
|
+
* `session_id` 与 hook input 里的顶层 `session_id` 字段是同一个值。
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
10
|
+
import { join, extname } from "node:path";
|
|
11
|
+
import { getClaudeHomeDir } from "./config.js";
|
|
12
|
+
const EXT_TO_MIME = {
|
|
13
|
+
".png": "image/png",
|
|
14
|
+
".jpg": "image/jpeg",
|
|
15
|
+
".jpeg": "image/jpeg",
|
|
16
|
+
".webp": "image/webp",
|
|
17
|
+
".gif": "image/gif",
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* 拼出指定 session 的 image-cache 目录路径。
|
|
21
|
+
*
|
|
22
|
+
* @param sessionId - Claude Code hook input 里的 `session_id` 字段
|
|
23
|
+
* @returns 该 session 对应的图片缓存目录绝对路径
|
|
24
|
+
*/
|
|
25
|
+
export const getImageCacheDir = (sessionId) => join(getClaudeHomeDir(), "image-cache", sessionId);
|
|
26
|
+
/**
|
|
27
|
+
* 扫描指定 session 的 image-cache 目录,列出所有图片文件(按修改时间升序)。
|
|
28
|
+
* 目录不存在时(如该 session 从未粘贴过图片)静默返回空数组,不抛错——
|
|
29
|
+
* 这是预期中的常见情况,不是异常。
|
|
30
|
+
*
|
|
31
|
+
* @param sessionId - Claude Code hook input 里的 `session_id` 字段
|
|
32
|
+
* @returns 该 session 目录下所有可识别的图片文件列表
|
|
33
|
+
*/
|
|
34
|
+
export const scanPastedImages = async (sessionId) => {
|
|
35
|
+
const dir = getImageCacheDir(sessionId);
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = await readdir(dir);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const name of entries) {
|
|
45
|
+
const ext = extname(name).toLowerCase();
|
|
46
|
+
const mimeType = EXT_TO_MIME[ext];
|
|
47
|
+
if (!mimeType)
|
|
48
|
+
continue;
|
|
49
|
+
const fullPath = join(dir, name);
|
|
50
|
+
try {
|
|
51
|
+
const s = await stat(fullPath);
|
|
52
|
+
results.push({ path: fullPath, mimeType, mtimeMs: s.mtimeMs });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// 文件可能在扫描过程中被清理,忽略
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
results.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
59
|
+
return results;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* 读取指定图片文件的字节内容。
|
|
63
|
+
*
|
|
64
|
+
* @param path - 图片文件绝对路径
|
|
65
|
+
* @returns 图片二进制数据
|
|
66
|
+
*/
|
|
67
|
+
export const readPastedImageBytes = async (path) => readFile(path);
|
|
68
|
+
//# sourceMappingURL=pasteScanner.js.map
|
|
@@ -0,0 +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;AAE/C,MAAM,WAAW,GAA2B;IAC1C,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;CACpB,CAAC;AASF;;;;;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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cvh SDK 导出:供其他 Node/Bun 项目直接内置调用(不需要 shell 调 CLI)。
|
|
3
|
+
* 例如某个上层 CLI/Agent 工具若要集成 cvh 的能力判断/内置调用,可以直接 import 这里的函数。
|
|
4
|
+
*/
|
|
5
|
+
export type { CvhConfig, CvhProvider } from "../config.js";
|
|
6
|
+
export { DEFAULT_CONFIG, loadConfig, saveConfig, resolveConfig } from "../config.js";
|
|
7
|
+
export { installHooks, uninstallHooks, checkHooksInstalled } from "../claudeSettings.js";
|
|
8
|
+
export { extractImages, type ExtractedImage } from "../extract.js";
|
|
9
|
+
export { describeImage } from "../vision.js";
|
|
10
|
+
export { computeImageId, findCacheByBytes, putCacheEntry, getCacheEntry, getCachedImageBytes, type CacheEntry } from "../cache.js";
|
|
11
|
+
export { handleUserPromptSubmit, type UserPromptSubmitInput, type HookOutput } from "../hooks/userPromptSubmit.js";
|
|
12
|
+
export { handlePostToolUse, type PostToolUseInput } from "../hooks/postToolUse.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cvh SDK 导出:供其他 Node/Bun 项目直接内置调用(不需要 shell 调 CLI)。
|
|
3
|
+
* 例如某个上层 CLI/Agent 工具若要集成 cvh 的能力判断/内置调用,可以直接 import 这里的函数。
|
|
4
|
+
*/
|
|
5
|
+
export { DEFAULT_CONFIG, loadConfig, saveConfig, resolveConfig } from "../config.js";
|
|
6
|
+
export { installHooks, uninstallHooks, checkHooksInstalled } from "../claudeSettings.js";
|
|
7
|
+
export { extractImages } from "../extract.js";
|
|
8
|
+
export { describeImage } from "../vision.js";
|
|
9
|
+
export { computeImageId, findCacheByBytes, putCacheEntry, getCacheEntry, getCachedImageBytes } from "../cache.js";
|
|
10
|
+
export { handleUserPromptSubmit } from "../hooks/userPromptSubmit.js";
|
|
11
|
+
export { handlePostToolUse } from "../hooks/postToolUse.js";
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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;AAEzF,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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session 级"已展示过的图片"标记,避免 `UserPromptSubmit` 在同一 session 内每次提交
|
|
3
|
+
* 都把之前已经处理过的粘贴图片重新塞进 `additionalContext`(图片本身走全局内容 hash 缓存,
|
|
4
|
+
* 不需要重新调视觉模型,但如果不做这层过滤,上下文会随对话轮数不断膨胀重复信息)。
|
|
5
|
+
*
|
|
6
|
+
* 存储位置:`~/.claude/cc-vision-hook/seen/<session_id>.json`,内容是已展示过的 image_id 数组。
|
|
7
|
+
* 不设 TTL——生命周期跟随 session,session 目录本身很小(几十个 id 字符串),无需主动清理,
|
|
8
|
+
* 后续如需要可以在 doctor/cleanup 命令里一并按 mtime 清掉太旧的标记文件。
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* 读取指定 session 已经展示过的 image_id 集合。
|
|
12
|
+
*
|
|
13
|
+
* @param sessionId - Claude Code session id
|
|
14
|
+
* @returns 已展示过的 image_id 集合,文件不存在时返回空集合
|
|
15
|
+
*/
|
|
16
|
+
export declare const loadSeenImageIds: (sessionId: string) => Promise<Set<string>>;
|
|
17
|
+
/**
|
|
18
|
+
* 把一批新的 image_id 追加写入 session 的"已展示"标记文件。
|
|
19
|
+
*
|
|
20
|
+
* @param sessionId - Claude Code session id
|
|
21
|
+
* @param newIds - 本次新展示的 image_id 列表
|
|
22
|
+
*/
|
|
23
|
+
export declare const markImagesSeen: (sessionId: string, newIds: string[]) => Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session 级"已展示过的图片"标记,避免 `UserPromptSubmit` 在同一 session 内每次提交
|
|
3
|
+
* 都把之前已经处理过的粘贴图片重新塞进 `additionalContext`(图片本身走全局内容 hash 缓存,
|
|
4
|
+
* 不需要重新调视觉模型,但如果不做这层过滤,上下文会随对话轮数不断膨胀重复信息)。
|
|
5
|
+
*
|
|
6
|
+
* 存储位置:`~/.claude/cc-vision-hook/seen/<session_id>.json`,内容是已展示过的 image_id 数组。
|
|
7
|
+
* 不设 TTL——生命周期跟随 session,session 目录本身很小(几十个 id 字符串),无需主动清理,
|
|
8
|
+
* 后续如需要可以在 doctor/cleanup 命令里一并按 mtime 清掉太旧的标记文件。
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { getCvhHomeDir } from "./config.js";
|
|
13
|
+
const getSeenPath = (sessionId) => join(getCvhHomeDir(), "seen", `${sessionId}.json`);
|
|
14
|
+
/**
|
|
15
|
+
* 读取指定 session 已经展示过的 image_id 集合。
|
|
16
|
+
*
|
|
17
|
+
* @param sessionId - Claude Code session id
|
|
18
|
+
* @returns 已展示过的 image_id 集合,文件不存在时返回空集合
|
|
19
|
+
*/
|
|
20
|
+
export const loadSeenImageIds = async (sessionId) => {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readFile(getSeenPath(sessionId), "utf8");
|
|
23
|
+
const ids = JSON.parse(raw);
|
|
24
|
+
return new Set(ids);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return new Set();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 把一批新的 image_id 追加写入 session 的"已展示"标记文件。
|
|
32
|
+
*
|
|
33
|
+
* @param sessionId - Claude Code session id
|
|
34
|
+
* @param newIds - 本次新展示的 image_id 列表
|
|
35
|
+
*/
|
|
36
|
+
export const markImagesSeen = async (sessionId, newIds) => {
|
|
37
|
+
if (newIds.length === 0)
|
|
38
|
+
return;
|
|
39
|
+
const existing = await loadSeenImageIds(sessionId);
|
|
40
|
+
for (const id of newIds)
|
|
41
|
+
existing.add(id);
|
|
42
|
+
const path = getSeenPath(sessionId);
|
|
43
|
+
await mkdir(join(getCvhHomeDir(), "seen"), { recursive: true });
|
|
44
|
+
await writeFile(path, JSON.stringify([...existing]));
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=seenTracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seenTracker.js","sourceRoot":"","sources":["../src/seenTracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,GAAG,CAAC,SAAiB,EAAU,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,SAAS,OAAO,CAAC,CAAC;AAEtG;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,SAAiB,EAAwB,EAAE;IAChF,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC;QAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAa,CAAC;QACxC,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,GAAG,EAAE,CAAC;IACnB,CAAC;AACH,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,SAAiB,EAAE,MAAgB,EAAiB,EAAE;IACzF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAChC,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,SAAS,CAAC,CAAC;IACnD,KAAK,MAAM,EAAE,IAAI,MAAM;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;AACvD,CAAC,CAAC"}
|
package/dist/vision.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 视觉解析层:统一通过 Vercel AI SDK 对接四个上游 provider,屏蔽各家协议格式差异。
|
|
3
|
+
*
|
|
4
|
+
* Provider 映射:
|
|
5
|
+
* - "oai" -> @ai-sdk/openai 的 Chat Completions 模型(显式 openai.chat(modelId))
|
|
6
|
+
* - "responses" -> @ai-sdk/openai 的 Responses API 模型(openai.responses(modelId))
|
|
7
|
+
* - "anthropic" -> @ai-sdk/anthropic
|
|
8
|
+
* - "gemini" -> @ai-sdk/google
|
|
9
|
+
* 保留 cvh 自定义的 provider 命名(不直接暴露 AI SDK 包名),降低用户配置心智负担。
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ 实测踩坑记录:当前 `ai`/`@ai-sdk/openai`
|
|
12
|
+
* 版本下,`openai(modelId)` 这种默认调用方式解析出的 provider 字符串是 `"openai.responses"`
|
|
13
|
+
* (即默认就是 Responses API,不是 Chat Completions)。如果 "oai" 分支直接写
|
|
14
|
+
* `openai(config.model)`,会导致 "oai" 和 "responses" 两个配置项实际调用的是同一个 API,
|
|
15
|
+
* "oai" 选项形同虚设。必须显式调用 `openai.chat(modelId)` 才能真正拿到 Chat Completions 模型。
|
|
16
|
+
*/
|
|
17
|
+
import type { CvhConfig } from "./config.js";
|
|
18
|
+
/**
|
|
19
|
+
* 按配置解析出对应的 AI SDK 语言模型实例。
|
|
20
|
+
*
|
|
21
|
+
* 导出(而非模块内部私有)是为了支持单测:AI SDK 的模型实例暴露 `.modelId`/`.provider`
|
|
22
|
+
* 等只读字段,可以在不发起真实网络请求的前提下验证 provider 映射是否正确
|
|
23
|
+
* (见 tests/vision.test.ts),避免每次跑测试都要真实调用视觉模型 API。
|
|
24
|
+
*
|
|
25
|
+
* @param config - 当前生效的 cvh 配置(含 provider/model/baseUrl/apiKey)
|
|
26
|
+
* @returns AI SDK 的 LanguageModel 实例,供 generateText 使用
|
|
27
|
+
* @throws 当 provider 是未知值时抛出错误
|
|
28
|
+
*/
|
|
29
|
+
export declare const resolveModel: (config: CvhConfig) => import("@ai-sdk/provider").LanguageModelV3;
|
|
30
|
+
/**
|
|
31
|
+
* 调用视觉模型解析一张图片,返回文字描述(或针对具体问题的回答)。
|
|
32
|
+
*
|
|
33
|
+
* @param base64 - 图片的 base64 编码数据(不含 data URI 前缀)
|
|
34
|
+
* @param mimeType - 图片 MIME 类型,如 "image/png"
|
|
35
|
+
* @param config - 当前生效的 cvh 配置
|
|
36
|
+
* @param question - 可选的具体提问(用于 vision_ask 追问场景),省略时使用通用描述提示词
|
|
37
|
+
* @returns 视觉模型的文字回答
|
|
38
|
+
* @throws 当上游调用失败或超时时抛出错误,调用方需要自行捕获并降级处理
|
|
39
|
+
*/
|
|
40
|
+
export declare const describeImage: (base64: string, mimeType: string, config: CvhConfig, question?: string) => Promise<string>;
|
package/dist/vision.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 视觉解析层:统一通过 Vercel AI SDK 对接四个上游 provider,屏蔽各家协议格式差异。
|
|
3
|
+
*
|
|
4
|
+
* Provider 映射:
|
|
5
|
+
* - "oai" -> @ai-sdk/openai 的 Chat Completions 模型(显式 openai.chat(modelId))
|
|
6
|
+
* - "responses" -> @ai-sdk/openai 的 Responses API 模型(openai.responses(modelId))
|
|
7
|
+
* - "anthropic" -> @ai-sdk/anthropic
|
|
8
|
+
* - "gemini" -> @ai-sdk/google
|
|
9
|
+
* 保留 cvh 自定义的 provider 命名(不直接暴露 AI SDK 包名),降低用户配置心智负担。
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ 实测踩坑记录:当前 `ai`/`@ai-sdk/openai`
|
|
12
|
+
* 版本下,`openai(modelId)` 这种默认调用方式解析出的 provider 字符串是 `"openai.responses"`
|
|
13
|
+
* (即默认就是 Responses API,不是 Chat Completions)。如果 "oai" 分支直接写
|
|
14
|
+
* `openai(config.model)`,会导致 "oai" 和 "responses" 两个配置项实际调用的是同一个 API,
|
|
15
|
+
* "oai" 选项形同虚设。必须显式调用 `openai.chat(modelId)` 才能真正拿到 Chat Completions 模型。
|
|
16
|
+
*/
|
|
17
|
+
import { generateText } from "ai";
|
|
18
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
19
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
20
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
21
|
+
/** 默认的通用图片描述提示词(未指定追问问题时使用)。 */
|
|
22
|
+
const DEFAULT_DESCRIBE_PROMPT = "请简要描述这张图片的内容,包含关键的颜色、文字、布局等信息,控制在 200 字以内。";
|
|
23
|
+
/**
|
|
24
|
+
* 按配置解析出对应的 AI SDK 语言模型实例。
|
|
25
|
+
*
|
|
26
|
+
* 导出(而非模块内部私有)是为了支持单测:AI SDK 的模型实例暴露 `.modelId`/`.provider`
|
|
27
|
+
* 等只读字段,可以在不发起真实网络请求的前提下验证 provider 映射是否正确
|
|
28
|
+
* (见 tests/vision.test.ts),避免每次跑测试都要真实调用视觉模型 API。
|
|
29
|
+
*
|
|
30
|
+
* @param config - 当前生效的 cvh 配置(含 provider/model/baseUrl/apiKey)
|
|
31
|
+
* @returns AI SDK 的 LanguageModel 实例,供 generateText 使用
|
|
32
|
+
* @throws 当 provider 是未知值时抛出错误
|
|
33
|
+
*/
|
|
34
|
+
export const resolveModel = (config) => {
|
|
35
|
+
switch (config.provider) {
|
|
36
|
+
case "oai": {
|
|
37
|
+
const openai = createOpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl });
|
|
38
|
+
// 显式用 .chat(),不要用 openai(modelId) 默认调用——见上方模块注释,默认调用在当前
|
|
39
|
+
// AI SDK 版本下实际解析成 Responses API,会让 "oai"/"responses" 两个 provider 选项失去区分度。
|
|
40
|
+
return openai.chat(config.model);
|
|
41
|
+
}
|
|
42
|
+
case "responses": {
|
|
43
|
+
const openai = createOpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl });
|
|
44
|
+
return openai.responses(config.model);
|
|
45
|
+
}
|
|
46
|
+
case "anthropic": {
|
|
47
|
+
const anthropic = createAnthropic({ apiKey: config.apiKey, baseURL: config.baseUrl });
|
|
48
|
+
return anthropic(config.model);
|
|
49
|
+
}
|
|
50
|
+
case "gemini": {
|
|
51
|
+
const google = createGoogleGenerativeAI({ apiKey: config.apiKey, baseURL: config.baseUrl });
|
|
52
|
+
return google(config.model);
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`未知的 provider: ${config.provider}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* 调用视觉模型解析一张图片,返回文字描述(或针对具体问题的回答)。
|
|
60
|
+
*
|
|
61
|
+
* @param base64 - 图片的 base64 编码数据(不含 data URI 前缀)
|
|
62
|
+
* @param mimeType - 图片 MIME 类型,如 "image/png"
|
|
63
|
+
* @param config - 当前生效的 cvh 配置
|
|
64
|
+
* @param question - 可选的具体提问(用于 vision_ask 追问场景),省略时使用通用描述提示词
|
|
65
|
+
* @returns 视觉模型的文字回答
|
|
66
|
+
* @throws 当上游调用失败或超时时抛出错误,调用方需要自行捕获并降级处理
|
|
67
|
+
*/
|
|
68
|
+
export const describeImage = async (base64, mimeType, config, question) => {
|
|
69
|
+
const model = resolveModel(config);
|
|
70
|
+
const dataUrl = `data:${mimeType};base64,${base64}`;
|
|
71
|
+
const { text } = await generateText({
|
|
72
|
+
model,
|
|
73
|
+
messages: [
|
|
74
|
+
{
|
|
75
|
+
role: "user",
|
|
76
|
+
content: [
|
|
77
|
+
{ type: "image", image: dataUrl },
|
|
78
|
+
{ type: "text", text: question ?? DEFAULT_DESCRIBE_PROMPT },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
maxOutputTokens: config.maxTokens,
|
|
83
|
+
abortSignal: AbortSignal.timeout(config.timeoutMs),
|
|
84
|
+
});
|
|
85
|
+
return text;
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=vision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vision.js","sourceRoot":"","sources":["../src/vision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAG1D,gCAAgC;AAChC,MAAM,uBAAuB,GAAG,4CAA4C,CAAC;AAE7E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAiB,EAAE,EAAE;IAChD,QAAQ,MAAM,CAAC,QAAQ,EAAE,CAAC;QACxB,KAAK,KAAK,CAAC,CAAC,CAAC;YACX,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YAChF,wDAAwD;YACxD,0EAA0E;YAC1E,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnC,CAAC;QACD,KAAK,WAAW,CAAC,CAAC,CAAC;YACjB,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YAChF,OAAO,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACxC,CAAC;QACD,KAAK,WAAW,CAAC,CAAC,CAAC;YACjB,MAAM,SAAS,GAAG,eAAe,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YACtF,OAAO,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,MAAM,GAAG,wBAAwB,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YAC5F,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;QACD;YACE,MAAM,IAAI,KAAK,CAAC,iBAAiB,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IACxD,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAChC,MAAc,EACd,QAAgB,EAChB,MAAiB,EACjB,QAAiB,EACA,EAAE;IACnB,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,QAAQ,QAAQ,WAAW,MAAM,EAAE,CAAC;IACpD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,YAAY,CAAC;QAClC,KAAK;QACL,QAAQ,EAAE;YACR;gBACE,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE;oBACP,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;oBACjC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,IAAI,uBAAuB,EAAE;iBAC5D;aACF;SACF;QACD,eAAe,EAAE,MAAM,CAAC,SAAS;QACjC,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC;KACnD,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-vision-hook",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"claude-code",
|
|
9
|
+
"claude-code-hook",
|
|
10
|
+
"vision",
|
|
11
|
+
"image",
|
|
12
|
+
"multimodal",
|
|
13
|
+
"ai-sdk"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/RainSunMe/cc-vision-hook#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/RainSunMe/cc-vision-hook.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/RainSunMe/cc-vision-hook/issues"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"provenance": true
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"cvh": "./dist/cli.js",
|
|
29
|
+
"cc-vision-hook": "./dist/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./dist/index.js",
|
|
33
|
+
"./sdk": "./dist/sdk/index.js"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"dev": "tsx src/cli.ts",
|
|
38
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
39
|
+
"test": "bun test",
|
|
40
|
+
"changeset": "changeset",
|
|
41
|
+
"version-packages": "changeset version",
|
|
42
|
+
"ci": "tsc --noEmit -p tsconfig.json && bun test && bun run build",
|
|
43
|
+
"smoke": "node dist/cli.js help"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@ai-sdk/anthropic": "^3.0.81",
|
|
47
|
+
"@ai-sdk/google": "^3.0.0",
|
|
48
|
+
"@ai-sdk/openai": "^3.0.0",
|
|
49
|
+
"ai": "^6.0.194"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@changesets/cli": "^2.31.0",
|
|
53
|
+
"@types/node": "^24.0.0",
|
|
54
|
+
"typescript": "^6.0.3"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=20"
|
|
58
|
+
},
|
|
59
|
+
"files": [
|
|
60
|
+
"dist",
|
|
61
|
+
"README.md",
|
|
62
|
+
"README.zh-CN.md",
|
|
63
|
+
"LICENSE",
|
|
64
|
+
"CHANGELOG.md"
|
|
65
|
+
]
|
|
66
|
+
}
|