@caesarloo/dsh-deepseek-account-vision 0.0.0-stage → 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 ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ 本项目的所有重要改动都记录在这里。版本号遵循语义化版本(主.次.修)。
4
+
5
+ ## 0.1.0 — 2026-10-19
6
+
7
+ 首个版本。npm 包名为 `@caesarloo/dsh-deepseek-account-vision`
8
+ (无 scope 的同义名已被他人占用,故加 scope 并保留 account 语义)。
9
+
10
+ ### 新增
11
+
12
+ - **`deepseek_vision` 工具**:主模型是任意模型时,都能用 DeepSeek 账号的
13
+ DeepSeek-V4.1-Flash(`deepseek-flash`,1017 版)识别图片——支持本地
14
+ PNG/JPEG/WebP/GIF 路径与 `http(s)` 图片 URL,可选 `prompt` 指定识别重点。
15
+ - **自动转译路由**:为每个已注册 provider 包一层 `<provider>-vision` 路由
16
+ (默认显示名 `<provider 名称> + 自动识图`),在 GUI 里给纯文本主模型贴图时,
17
+ 图片先由 DeepSeek-V4.1-Flash 转成文字,再交给原模型作答。
18
+ - `wrapAllProviders`(默认开)覆盖已注册与之后动态注册的 provider;
19
+ `innerProviders` 可改为只包裹指定 provider。
20
+ - `failureMode: placeholder`(默认)在转译失败时插入 `[图片转译失败: …]`
21
+ 并继续对话;`error` 则让整轮失败。
22
+ - **零密钥**:识图请求统一走 `ctx.llm.stream()` 的 DeepSeek 账号路由
23
+ (默认 `deepseek-account`),复用 DSH 的 DeepSeek 登录态,插件不保存凭据。
24
+ - **稳定与省费**:按图片内容 SHA-256 的进程内缓存、同键并发合并、
25
+ 失败图片 60 秒冷却、请求超时快速失败。
26
+ - **发布闸门**:`prepublishOnly` 先跑离线单测 + UTF-8 BOM 检查 + 本机指纹/凭据扫描
27
+ (`scripts/check-leaks.mjs`,规则自足、含路径写法变体;`.leak-patterns.local` 不进库)
28
+ + bundle 清单一致性检查(入口 / patch 随包 / insert name / engines.dsh);
29
+ 中英双语 README 通过结构与指纹审核。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Caesar Loo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,243 @@
1
- # Temporary Holding Version
1
+ # @caesarloo/dsh-deepseek-account-vision
2
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.
3
+ [English](#english) | [中文](#中文)
4
+
5
+ ---
6
+
7
+ ## English
8
+
9
+ Give **any** main model in DeepSeek Harness the vision of the DeepSeek account: recognize images with **DeepSeek-V4.1-Flash** (`deepseek-flash`, release 1017) — through a `deepseek_vision` tool, or automatically for images attached to a text-only chat model. No API key, no extra VLM account, no image library.
10
+
11
+ ### What it does
12
+
13
+ - Adds a model-facing tool `deepseek_vision`: pass a local PNG/JPEG/WebP/GIF path or an `http(s)` image URL, get back a complete textual rendition (verbatim OCR, layout, visual elements, key details).
14
+ - Answers with the **DeepSeek account route** (`deepseek-account` / `deepseek-flash` by default), so it reuses the DeepSeek login you already have in DSH — the plugin never stores credentials.
15
+ - Registers automatic image-transcription routes: for every registered provider it adds a `<provider>-vision` route that declares image input, turns each attached image into text with DeepSeek-V4.1-Flash, then delegates the conversation to the original model. A pure-text main model can finally "see" a pasted screenshot.
16
+ - Caches results by image content hash (in-process only), merges concurrent identical requests, cools down a failed image for 60 s, and fails fast instead of hanging.
17
+ - Works alongside everything: text-only conversations pass through untouched; the original model, tools and history shape stay the same.
18
+
19
+ ### Install
20
+
21
+ ```sh
22
+ dsh plugin --profile <profile> add @caesarloo/dsh-deepseek-account-vision
23
+ ```
24
+
25
+ From a local checkout, install by directory instead: `dsh plugin --profile <profile> add <project-directory>`.
26
+
27
+ Restart DSH once (bundle layers have no hot reload), then verify that `deepseek_vision` appears in the tool list of a new session. A patch-layer development install (`- insert:` with the plugin entry file) reloads hot without a restart.
28
+
29
+ Requirements: DSH `>= 0.2.1-rc.2`, a signed-in DeepSeek account, and the standard `llm` / `attachments` / `fs` services (all present in the shipped bundle).
30
+
31
+ ### Usage
32
+
33
+ **1. Tool — any main model can call it**
34
+
35
+ ```json
36
+ {
37
+ "image_path": "screenshot.png",
38
+ "prompt": "Transcribe every visible character and describe the layout"
39
+ }
40
+ ```
41
+
42
+ or, for a remote image:
43
+
44
+ ```json
45
+ { "image_url": "https://example.com/chart.png" }
46
+ ```
47
+
48
+ The tool returns `{ text, model, source, truncated }`. The image never enters the main model's context — only the transcription does. A custom `prompt` is passed to the vision model verbatim; leave it out to use the built-in full-transcription prompt.
49
+
50
+ **2. Automatic transcription route — paste images into a text-only chat**
51
+
52
+ Pick the route named **`<provider> + 自动识图`** (id `<provider>-vision`, configurable) in the model selector, then attach an image as usual. The image block is transcribed before the original adapter sees the request:
53
+
54
+ ```
55
+ GUI image ──▶ <provider>-vision route (declares image input → attachment admitted)
56
+ │ each image block is transcribed by DeepSeek-V4.1-Flash
57
+ ▼
58
+ original model (receives plain text, answers normally)
59
+ ```
60
+
61
+ Transcribed text is prefixed with `[图片转译]`. When transcription fails and `failureMode` is `placeholder` (default), an explicit `[图片转译失败: …]` marker is inserted and the conversation continues.
62
+
63
+ | Situation | What to use |
64
+ |---|---|
65
+ | Any model, image exists as a file or URL | `deepseek_vision` tool |
66
+ | Text-only model, image attached in the GUI | the `<provider>-vision` route |
67
+ | Main model already accepts images | no plugin needed for that turn |
68
+
69
+ ### Configuration
70
+
71
+ Defaults ship in `cordis.patch.yml`; override them in your profile's `cordis.patch.yml` with an `- id: dsh-deepseek-account-vision` entry. Unlisted keys fall back to the defaults below.
72
+
73
+ | Key | Default | Meaning |
74
+ |---|---|---|
75
+ | `visionProvider` | `deepseek-account` | Registered provider route used for image recognition |
76
+ | `visionModel` | `deepseek-flash` | Vision model id (DeepSeek-V4.1-Flash / 1017) |
77
+ | `visionMaxTokens` | `4096` | Output cap for the vision model |
78
+ | `timeoutMs` | `120000` | Per-image recognition timeout |
79
+ | `tool` | `true` | Register the `deepseek_vision` tool |
80
+ | `toolName` | `deepseek_vision` | Tool name exposed to models |
81
+ | `autoTranscribe` | `true` | Register automatic image-transcription routes |
82
+ | `wrapAllProviders` | `true` | Wrap every registered provider (including ones registered later) |
83
+ | `innerProviders` | `[]` | Providers to wrap when `wrapAllProviders=false` |
84
+ | `routeSuffix` | `-vision` | Route id suffix: `<provider><routeSuffix>` |
85
+ | `routeNameTemplate` | `{name} + 自动识图` | Route display name; `{name}` / `{provider}` are substituted |
86
+ | `marker` | `[图片转译]` | Prefix added to each transcription |
87
+ | `failureMode` | `placeholder` | `placeholder` keeps the turn alive; `error` fails the whole turn |
88
+ | `transcribePrompt` | `''` | Override the automatic-transcription prompt (empty = built-in) |
89
+ | `recognitionPrompt` | `''` | Override the `deepseek_vision` prompt (empty = built-in) |
90
+ | `cacheSize` | `200` | In-process result cache entries (0 disables) |
91
+ | `urlTimeoutMs` | `30000` | Timeout for downloading an `http(s)` image |
92
+
93
+ ### Dependencies
94
+
95
+ - **No API key, no separate VLM account.** Recognition goes through `ctx.llm.stream()` on the DeepSeek account route; DSH's account service owns the credentials.
96
+ - **No image library.** Attachment normalization (size, pixels, format validation) is done by DSH's attachment service; the plugin only ships `schemastery` for config.
97
+ - **Node built-ins only** beyond that (`crypto`, `path`, global `fetch` / `AbortSignal`).
98
+
99
+ ### Boundaries
100
+
101
+ - It does **not** turn the main model into a multimodal model: the original model still receives only text. It **does** make image input usable for it via transcription.
102
+ - It does **not** replace `read_image`: `read_image` hands the native image block to an image-capable model; `deepseek_vision` returns text so any model can consume it.
103
+ - It only accepts **PNG / JPEG / WebP / GIF** — no PDF, audio or video.
104
+ - Transcription is a lossy summary for dense screenshots: very small text or fine UI details may be missed. Use a stronger `visionModel` or a more explicit `prompt` when exactness matters.
105
+ - Images are sent to DeepSeek with the recognition request (that is the point) and are not otherwise persisted by the plugin; the result cache lives in process memory only.
106
+
107
+ ### Development
108
+
109
+ ```sh
110
+ pnpm install
111
+ npm test # unit tests + BOM + fingerprint/credential + bundle-manifest checks
112
+ npm run check-leaks # fingerprint scan only
113
+ ```
114
+
115
+ `prepublishOnly` runs `npm test`, so a package with a failing test, a BOM-prefixed source file, or a machine-specific fingerprint cannot be published. Publish explicitly against the official registry:
116
+
117
+ ```sh
118
+ npm publish --registry https://registry.npmjs.org/
119
+ ```
120
+
121
+ ### License
122
+
123
+ MIT
124
+
125
+ ---
126
+
127
+ ## 中文
128
+
129
+ 让 DeepSeek Harness 里的**任意主模型**都用上 DeepSeek 账号的视觉能力:用 **DeepSeek-V4.1-Flash**(`deepseek-flash`,1017 版)识别图片——既可以由模型调用 `deepseek_vision` 工具,也可以给纯文本主模型自动转译 GUI 里贴的图片。不需要 API Key,不需要另找视觉模型账号,不装图像库。
130
+
131
+ ### 它做什么
132
+
133
+ - 注册模型可见的工具 `deepseek_vision`:给一个本地 PNG/JPEG/WebP/GIF 路径或 `http(s)` 图片地址,返回完整文字转译(逐字 OCR、版面结构、视觉元素、关键细节)。
134
+ - 识图后端走 **DeepSeek 账号路由**(默认 `deepseek-account` / `deepseek-flash`),复用 DSH 里已登录的 DeepSeek 账号,插件自身不保存任何凭据。
135
+ - 注册自动转译路由:为每个已注册 provider 加一条 `<provider>-vision` 路由,对外声明支持图片输入,收到图片后先用 DeepSeek-V4.1-Flash 转成文字,再交给原模型作答。纯文本主模型也能“看到”粘贴的截图。
136
+ - 按图片内容哈希缓存(仅进程内)、合并并发的同图请求、失败图片冷却 60 秒、超时快速失败而不是卡死。
137
+ - 与现有能力并存:纯文本对话零改动直达原模型,原模型、工具与会话历史结构都不变。
138
+
139
+ ### 安装
140
+
141
+ ```sh
142
+ dsh plugin --profile <profile> add @caesarloo/dsh-deepseek-account-vision
143
+ ```
144
+
145
+ 本地检出目录安装:`dsh plugin --profile <profile> add <项目目录>`。
146
+
147
+ 安装后重启一次 DSH(bundle 层不支持热重载),再在新会话的工具列表里确认出现 `deepseek_vision`。开发期也可以用 patch 层 `- insert:` 指向插件入口文件装载,改完热生效、无需重启。
148
+
149
+ 依赖:DSH `>= 0.2.1-rc.2`、已登录的 DeepSeek 账号,以及标准的 `llm` / `attachments` / `fs` 服务(随官方 bundle 提供)。
150
+
151
+ ### 使用
152
+
153
+ **1. 工具——任意主模型都能调**
154
+
155
+ ```json
156
+ {
157
+ "image_path": "screenshot.png",
158
+ "prompt": "逐字识别所有可见文字,并说明版式"
159
+ }
160
+ ```
161
+
162
+ 远程图片:
163
+
164
+ ```json
165
+ { "image_url": "https://example.com/chart.png" }
166
+ ```
167
+
168
+ 返回 `{ text, model, source, truncated }`。图片本身不会进入主模型上下文,只有转译文字会。自定义 `prompt` 会原样交给识图模型;留空才使用内置的完整转译提示词。
169
+
170
+ **2. 自动转译路由——给纯文本模型直接贴图**
171
+
172
+ 在模型选择器里选 **`<provider> + 自动识图`**(id 为 `<provider>-vision`,可配置),然后照常附加图片。图片块会在原适配器收到请求之前被转译:
173
+
174
+ ```
175
+ GUI 图片 ──▶ <provider>-vision 路由(声明支持图片 → 附件放行)
176
+ │ 每个 image 块交给 DeepSeek-V4.1-Flash 转译
177
+ ▼
178
+ 原模型(收到纯文本,正常作答)
179
+ ```
180
+
181
+ 转译文本带 `[图片转译]` 前缀。转译失败且 `failureMode` 为默认的 `placeholder` 时,会插入 `[图片转译失败: …]` 占位文本并继续对话。
182
+
183
+ | 场景 | 用什么 |
184
+ |---|---|
185
+ | 任意模型,图片是文件或 URL | `deepseek_vision` 工具 |
186
+ | 纯文本模型,在 GUI 里贴图 | `<provider>-vision` 路由 |
187
+ | 主模型本来就支持图片 | 该轮不需要本插件 |
188
+
189
+ ### 配置
190
+
191
+ 默认值随 `cordis.patch.yml` 提供;在 profile 的 `cordis.patch.yml` 里用 `- id: dsh-deepseek-account-vision` 条目覆盖。未列出的键回落到下表默认值。
192
+
193
+ | 键 | 缺省 | 含义 |
194
+ |---|---|---|
195
+ | `visionProvider` | `deepseek-account` | 识图使用的已注册 provider 路由 |
196
+ | `visionModel` | `deepseek-flash` | 识图模型 id(DeepSeek-V4.1-Flash / 1017) |
197
+ | `visionMaxTokens` | `4096` | 识图模型输出上限 |
198
+ | `timeoutMs` | `120000` | 单张图片识图超时 |
199
+ | `tool` | `true` | 是否注册 `deepseek_vision` 工具 |
200
+ | `toolName` | `deepseek_vision` | 暴露给模型的工具名 |
201
+ | `autoTranscribe` | `true` | 是否注册自动转译路由 |
202
+ | `wrapAllProviders` | `true` | 包裹全部已注册 provider(含之后动态注册的) |
203
+ | `innerProviders` | `[]` | `wrapAllProviders=false` 时要包裹的 provider |
204
+ | `routeSuffix` | `-vision` | 路由 id 后缀:`<provider><routeSuffix>` |
205
+ | `routeNameTemplate` | `{name} + 自动识图` | 路由显示名;`{name}` / `{provider}` 会被替换 |
206
+ | `marker` | `[图片转译]` | 每条转译文本的前缀 |
207
+ | `failureMode` | `placeholder` | `placeholder` 保住本轮对话;`error` 让整轮失败 |
208
+ | `transcribePrompt` | `''` | 覆盖自动转译提示词(留空用内置) |
209
+ | `recognitionPrompt` | `''` | 覆盖 `deepseek_vision` 提示词(留空用内置) |
210
+ | `cacheSize` | `200` | 进程内结果缓存条数(0 关闭) |
211
+ | `urlTimeoutMs` | `30000` | 下载 `http(s)` 图片的超时 |
212
+
213
+ ### 依赖约定
214
+
215
+ - **无 API Key、无独立视觉账号**:识图经 `ctx.llm.stream()` 打在 DeepSeek 账号路由上,凭据由 DSH 账号服务持有。
216
+ - **无图像库**:尺寸/像素/格式归一化由 DSH 附件服务完成;插件自身只依赖 `schemastery` 做配置校验。
217
+ - 其余只用 Node 内建能力(`crypto`、`path`、全局 `fetch` / `AbortSignal`)。
218
+
219
+ ### 边界(明确不做)
220
+
221
+ - **不把主模型变成多模态模型**:原模型收到的仍然只有文字;本插件做的是“把图片转成文字再交给它”。
222
+ - **不替代 `read_image`**:`read_image` 把原生图片块交给支持图片的模型;`deepseek_vision` 返回文字,所以任何模型都能消费。
223
+ - 只接受 **PNG / JPEG / WebP / GIF**,不支持 PDF、音频、视频。
224
+ - 转译对密集截图是有损摘要:极小的字或精细 UI 细节可能丢失;要求精确时换更强的 `visionModel` 或把 `prompt` 写具体。
225
+ - 图片只随识图请求发送到 DeepSeek(这正是功能本身),插件不另行持久化;结果缓存只在进程内存中。
226
+
227
+ ### 开发与发布
228
+
229
+ ```sh
230
+ pnpm install
231
+ npm test # 单测 + BOM + 本机指纹/凭据 + bundle 清单一致性检查
232
+ npm run check-leaks # 只跑指纹扫描
233
+ ```
234
+
235
+ `prepublishOnly` 跑 `npm test`:单测失败、源码带 BOM、或命中本机指纹/凭据时都发不出去。发布时显式指定官方 registry:
236
+
237
+ ```sh
238
+ npm publish --registry https://registry.npmjs.org/
239
+ ```
240
+
241
+ ### License
242
+
243
+ MIT
@@ -0,0 +1,40 @@
1
+ # @caesarloo/dsh-deepseek-account-vision bundle patch:在 profile 树上插入一行插件。
2
+ # 只有当本包作为 profile 依赖被安装(例如
3
+ # `dsh plugin add @caesarloo/dsh-deepseek-account-vision`)时,这一行才会被组合进运行时。
4
+ #
5
+ # 默认策略:
6
+ # - 注册 deepseek_vision 工具:主模型是任意模型时都能调用它,用 DeepSeek 账号的
7
+ # DeepSeek-V4.1-Flash(deepseek-flash)识别本地图片路径或 http(s) 图片链接。
8
+ # - autoTranscribe: 为所有已注册的模型提供方各包一层“识图路由”
9
+ # (<provider>-vision),把附带的图片自动转译成文字后再交给原模型。
10
+ # 这样在 GUI 里给任意纯文本主模型贴图也能用。
11
+ # - 所有识图请求都走 DeepSeek 账号登录态(ctx.llm 的 deepseek-account 路由),
12
+ # 不需要任何 API Key。
13
+
14
+ - insert:
15
+ - id: dsh-deepseek-account-vision
16
+ name: '@caesarloo/dsh-deepseek-account-vision'
17
+ config:
18
+ # 识图后端(默认:DeepSeek 账号自带的 DeepSeek-V4.1-Flash)
19
+ visionProvider: deepseek-account
20
+ visionModel: deepseek-flash
21
+ visionMaxTokens: 4096
22
+ timeoutMs: 120000
23
+
24
+ # deepseek_vision 工具
25
+ tool: true
26
+ toolName: deepseek_vision
27
+
28
+ # 自动转译路由
29
+ autoTranscribe: true
30
+ wrapAllProviders: true
31
+ innerProviders: [] # wrapAllProviders=false 时只包裹这些 provider
32
+ routeSuffix: '-vision'
33
+ routeNameTemplate: '{name} + 自动识图'
34
+ marker: '[图片转译]'
35
+ failureMode: placeholder # placeholder | error
36
+ transcribePrompt: '' # 留空用内置提示词
37
+ recognitionPrompt: '' # 留空用内置提示词
38
+
39
+ cacheSize: 200
40
+ urlTimeoutMs: 30000
package/lib/cache.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 进程内缓存与同键并发合并。
3
+ *
4
+ * - `VisionCache`:有上限的 LRU(Map 插入序实现),按图片内容哈希去重;
5
+ * 从不落盘。
6
+ * - `SingleFlight`:同一张图片在同一时刻只发起一次识图请求,其余调用等待
7
+ * 同一个 Promise(一轮回复里同一张图出现多次时不会重复计费)。
8
+ *
9
+ * @module dsh-deepseek-account-vision/cache
10
+ */
11
+
12
+ /** 有上限的 LRU 缓存。`limit <= 0` 时退化为不缓存。 */
13
+ export class VisionCache {
14
+ /** @param {number} limit 最大条目数。 */
15
+ constructor(limit) {
16
+ this.limit = Number.isInteger(limit) && limit > 0 ? limit : 0;
17
+ /** @type {Map<string, unknown>} */
18
+ this.entries = new Map();
19
+ }
20
+
21
+ /** @param {string} key 缓存键。 @returns {unknown} 命中值或 undefined。 */
22
+ get(key) {
23
+ const value = this.entries.get(key);
24
+ if (value === undefined) return undefined;
25
+ // 命中后移到队尾,维持 LRU 顺序。
26
+ this.entries.delete(key);
27
+ this.entries.set(key, value);
28
+ return value;
29
+ }
30
+
31
+ /** @param {string} key 缓存键。 @param {unknown} value 缓存值。 */
32
+ set(key, value) {
33
+ if (this.limit <= 0) return;
34
+ if (this.entries.has(key)) this.entries.delete(key);
35
+ else if (this.entries.size >= this.limit) {
36
+ const oldest = this.entries.keys().next();
37
+ if (oldest.done !== true) this.entries.delete(oldest.value);
38
+ }
39
+ this.entries.set(key, value);
40
+ }
41
+
42
+ /** @param {string} key 缓存键。 */
43
+ delete(key) {
44
+ this.entries.delete(key);
45
+ }
46
+
47
+ /** 当前条目数。 */
48
+ get size() {
49
+ return this.entries.size;
50
+ }
51
+ }
52
+
53
+ /** 同键并发合并:相同 key 的并发调用共享同一个 Promise。 */
54
+ export class SingleFlight {
55
+ constructor() {
56
+ /** @type {Map<string, Promise<unknown>>} */
57
+ this.inflight = new Map();
58
+ }
59
+
60
+ /**
61
+ * @template T
62
+ * @param {string} key 合并键。
63
+ * @param {() => Promise<T>} task 真正执行的任务。
64
+ * @returns {Promise<T>}
65
+ */
66
+ run(key, task) {
67
+ const existing = this.inflight.get(key);
68
+ if (existing !== undefined) return /** @type {Promise<T>} */ (existing);
69
+ const promise = Promise.resolve()
70
+ .then(task)
71
+ .finally(() => {
72
+ this.inflight.delete(key);
73
+ });
74
+ this.inflight.set(key, promise);
75
+ return promise;
76
+ }
77
+ }
package/lib/config.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * dsh-deepseek-account-vision:配置、默认提示词与配置归一化。
3
+ *
4
+ * 本插件不自带任何 API Key:识图请求全部经 `ctx.llm` 的 DeepSeek 账号路由
5
+ * (默认 `deepseek-account` / `deepseek-flash`)发出,复用 DSH 里已登录的
6
+ * DeepSeek 账号凭据。这既是本插件与“直连第三方 VLM”的 dsh-vision-proxy 的
7
+ * 核心区别,也意味着用户不需要再配置 key。
8
+ *
9
+ * @module dsh-deepseek-account-vision/config
10
+ */
11
+ import z from 'schemastery';
12
+
13
+ /** 默认识图后端:DeepSeek 账号路由。 */
14
+ export const DEFAULT_VISION_PROVIDER = 'deepseek-account';
15
+ /** 默认识图模型:目录名 DeepSeek-V4.1-Flash(1017),同时支持文本与图片输入。 */
16
+ export const DEFAULT_VISION_MODEL = 'deepseek-flash';
17
+ /** 自动转译文本的默认前缀标记。 */
18
+ export const DEFAULT_MARKER = '[图片转译]';
19
+ /** 默认注册的工具名(模型可见,snake_case)。 */
20
+ export const DEFAULT_TOOL_NAME = 'deepseek_vision';
21
+ /** 识图路由默认后缀:`<innerProvider>-vision`。 */
22
+ export const DEFAULT_ROUTE_SUFFIX = '-vision';
23
+ /** 识图路由默认显示名模板。 */
24
+ export const DEFAULT_ROUTE_NAME = '{name} + 自动识图';
25
+
26
+ /** 工具默认提示词:面向“主模型看不到图,需要完整转译”的场景。 */
27
+ export const DEFAULT_RECOGNITION_PROMPT = `你正在直接观察用户给出的这张图片,你的输出会交给另一位看不到图片的模型阅读。请输出一份忠实、完整、结构化的文字结果:
28
+ 1. 一句话总览:这张图是什么(截图 / 照片 / 图表 / 海报 / 表情包 / 文档 / 界面等)以及主题。
29
+ 2. 全部可见文字:逐字转录,保留原语言、顺序与层级;表格按行列输出;不要概括、不要漏字。
30
+ 3. 版面与视觉结构:区域划分、主要对象、人物或动物、颜色、图标、界面控件、图表及其数据。
31
+ 4. 关键细节:数字、时间、地址、URL、代码、报错信息、按钮文案等。
32
+ 要求:只描述图片本身,不要寒暄;不要输出“如果看不到图”“可以想象成”之类的假设性描述;看不清的地方标注“(不清晰)”;不要臆造不存在的内容。`;
33
+
34
+ /** 自动转译路由默认提示词:结果会替换原图片块进入主模型上下文。 */
35
+ export const DEFAULT_TRANSCRIBE_PROMPT = `你正在直接观察用户给出的这张图片,你的转译会交给另一位看不到图片、需要据此回答用户的纯文本模型。
36
+ 请输出:
37
+ 1. 图片类型与一句话总览;
38
+ 2. 全部可见文字逐字转录(保留原语言与结构;表格按行列输出);
39
+ 3. 版面、结构、主要对象、颜色、图标、界面元素等关键视觉信息;
40
+ 4. 数字、时间、URL、代码等关键细节。
41
+ 要求:忠实、完整;不寒暄、不臆造;不要输出“如果看不到图”“可以想象成”之类的假设性描述;看不清处标注“(不清晰)”。输出语言与图中主要文字一致;图中没有文字时用中文。`;
42
+
43
+ /** 自定义工具提示词时不再追加任何指令:用户/主模型给什么就按什么识别。 */
44
+
45
+ /**
46
+ * 插件配置 schema。bundle patch 只写部分键时,未列出的键回落到这里的默认值。
47
+ */
48
+ export const Config = z.object({
49
+ visionProvider: z.string().default(DEFAULT_VISION_PROVIDER)
50
+ .description('识图后端使用的已注册 provider 路由;默认 DeepSeek 账号路由'),
51
+ visionModel: z.string().default(DEFAULT_VISION_MODEL)
52
+ .description('识图模型 id;默认 deepseek-flash(DeepSeek-V4.1-Flash / 1017,原生多模态)'),
53
+ visionMaxTokens: z.number().step(1).min(1).max(32_768).default(4096)
54
+ .description('识图模型的输出上限;思考型模型请留足预算'),
55
+ timeoutMs: z.number().step(1).min(1_000).max(300_000).default(120_000)
56
+ .description('单张图片识图请求的超时时间'),
57
+ tool: z.boolean().default(true)
58
+ .description('是否注册 deepseek_vision 工具(主模型是任意模型时都可调用)'),
59
+ toolName: z.string().default(DEFAULT_TOOL_NAME)
60
+ .description('注册的工具名(模型可见,建议 snake_case)'),
61
+ autoTranscribe: z.boolean().default(true)
62
+ .description('是否为模型提供方注册自动转译路由,把 GUI 附带的图片转成文字'),
63
+ wrapAllProviders: z.boolean().default(true)
64
+ .description('true = 自动包裹所有已注册 provider(含之后动态注册的);false = 只包裹 innerProviders'),
65
+ innerProviders: z.array(z.string()).default([])
66
+ .description('wrapAllProviders=false 时要包裹的 provider 路由 id 列表'),
67
+ routeSuffix: z.string().default(DEFAULT_ROUTE_SUFFIX)
68
+ .description('识图路由 id 后缀,最终 id 为 <innerProvider><routeSuffix>'),
69
+ routeNameTemplate: z.string().default(DEFAULT_ROUTE_NAME)
70
+ .description('识图路由显示名模板;{name} 会被替换成被包裹路由的名称'),
71
+ marker: z.string().default(DEFAULT_MARKER)
72
+ .description('自动转译文本的前缀标记'),
73
+ failureMode: z.union(['placeholder', 'error']).default('placeholder')
74
+ .description('自动转译失败时的行为:placeholder 插入失败占位文本并继续对话(默认),error 让整轮失败'),
75
+ transcribePrompt: z.string().default('')
76
+ .description('覆盖自动转译提示词;留空使用内置提示词'),
77
+ recognitionPrompt: z.string().default('')
78
+ .description('覆盖工具识图提示词;留空使用内置提示词'),
79
+ cacheSize: z.number().step(1).min(0).max(5_000).default(200)
80
+ .description('进程内识图结果缓存条数(按图片内容 SHA-256 去重;0 关闭)'),
81
+ urlTimeoutMs: z.number().step(1).min(1_000).max(120_000).default(30_000)
82
+ .description('deepseek_vision 拉取 http(s) 图片的超时时间'),
83
+ });
84
+
85
+ function requireNonEmptyString(key, value) {
86
+ if (typeof value !== 'string' || value.trim() === '') throw new Error(`dsh-deepseek-account-vision: config.${key} must be a non-empty string`);
87
+ return value.trim();
88
+ }
89
+
90
+ function requirePositiveInteger(key, value, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
91
+ if (!Number.isInteger(value) || value < min || value > max) throw new Error(`dsh-deepseek-account-vision: config.${key} must be an integer between ${min} and ${max}`);
92
+ return value;
93
+ }
94
+
95
+ /**
96
+ * 把 loader 解析后的配置(或测试/手工传入的部分配置)归一化为冻结对象。
97
+ * bundle patch 里只写少数字段时也能安全补全默认值。
98
+ * @param {Record<string, unknown>} [input] 原始配置。
99
+ * @returns {Readonly<Record<string, unknown>>} 归一化后的配置。
100
+ */
101
+ export function resolveConfig(input = {}) {
102
+ const raw = input === null || typeof input !== 'object' ? {} : input;
103
+ const pick = (key, fallback) => (raw[key] === undefined || raw[key] === null ? fallback : raw[key]);
104
+
105
+ const failureMode = pick('failureMode', 'placeholder');
106
+ if (failureMode !== 'placeholder' && failureMode !== 'error') {
107
+ throw new Error('dsh-deepseek-account-vision: config.failureMode must be "placeholder" or "error"');
108
+ }
109
+ const innerProviders = pick('innerProviders', []);
110
+ if (!Array.isArray(innerProviders) || innerProviders.some((id) => typeof id !== 'string' || id.trim() === '')) {
111
+ throw new Error('dsh-deepseek-account-vision: config.innerProviders must be an array of non-empty provider ids');
112
+ }
113
+ const wrapAllProviders = pick('wrapAllProviders', true);
114
+ if (typeof wrapAllProviders !== 'boolean') throw new Error('dsh-deepseek-account-vision: config.wrapAllProviders must be a boolean');
115
+ const tool = pick('tool', true);
116
+ if (typeof tool !== 'boolean') throw new Error('dsh-deepseek-account-vision: config.tool must be a boolean');
117
+ const autoTranscribe = pick('autoTranscribe', true);
118
+ if (typeof autoTranscribe !== 'boolean') throw new Error('dsh-deepseek-account-vision: config.autoTranscribe must be a boolean');
119
+
120
+ return Object.freeze({
121
+ visionProvider: requireNonEmptyString('visionProvider', pick('visionProvider', DEFAULT_VISION_PROVIDER)),
122
+ visionModel: requireNonEmptyString('visionModel', pick('visionModel', DEFAULT_VISION_MODEL)),
123
+ visionMaxTokens: requirePositiveInteger('visionMaxTokens', pick('visionMaxTokens', 4096), { max: 32_768 }),
124
+ timeoutMs: requirePositiveInteger('timeoutMs', pick('timeoutMs', 120_000), { min: 1_000, max: 300_000 }),
125
+ tool,
126
+ toolName: requireNonEmptyString('toolName', pick('toolName', DEFAULT_TOOL_NAME)),
127
+ autoTranscribe,
128
+ wrapAllProviders,
129
+ innerProviders: Object.freeze(innerProviders.map((id) => id.trim())),
130
+ routeSuffix: requireNonEmptyString('routeSuffix', pick('routeSuffix', DEFAULT_ROUTE_SUFFIX)),
131
+ routeNameTemplate: requireNonEmptyString('routeNameTemplate', pick('routeNameTemplate', DEFAULT_ROUTE_NAME)),
132
+ marker: typeof pick('marker', DEFAULT_MARKER) === 'string' ? pick('marker', DEFAULT_MARKER) : DEFAULT_MARKER,
133
+ failureMode,
134
+ transcribePrompt: typeof pick('transcribePrompt', '') === 'string' ? pick('transcribePrompt', '').trim() : '',
135
+ recognitionPrompt: typeof pick('recognitionPrompt', '') === 'string' ? pick('recognitionPrompt', '').trim() : '',
136
+ cacheSize: requirePositiveInteger('cacheSize', pick('cacheSize', 200), { min: 0, max: 5_000 }),
137
+ urlTimeoutMs: requirePositiveInteger('urlTimeoutMs', pick('urlTimeoutMs', 30_000), { min: 1_000, max: 120_000 }),
138
+ });
139
+ }
package/lib/errors.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 识图失败的分类与可操作提示。
3
+ *
4
+ * `ctx.llm.stream()` 的终止 `finish` 分片携带规范化后的 `LlmFailure`
5
+ * (稳定 code + message + status)。本模块只依据 code 分支,不解析 message 文本。
6
+ *
7
+ * @module dsh-deepseek-account-vision/errors
8
+ */
9
+
10
+ /** 识图失败。`code` 为稳定错误码,`hint` 为可操作的中文提示。 */
11
+ export class VisionError extends Error {
12
+ /**
13
+ * @param {string} message 面向诊断的错误信息。
14
+ * @param {string} code 稳定错误码。
15
+ * @param {string} [hint] 面向用户的可操作提示。
16
+ */
17
+ constructor(message, code, hint) {
18
+ super(hint === undefined ? message : `${message}(${hint})`);
19
+ this.name = 'VisionError';
20
+ this.code = code;
21
+ this.hint = hint;
22
+ }
23
+ }
24
+
25
+ /** 失败码 → 可操作提示。未列出的码保留原始 message。 */
26
+ const HINTS = {
27
+ ACCOUNT_SIGN_IN_REQUIRED: '请先在 DSH 里登录 DeepSeek 账号(设置 → 账号);登录后即可直接识图,无需 API Key',
28
+ NO_ADAPTER: 'visionProvider 指向的模型路由没有注册;检查插件配置里的 visionProvider',
29
+ MISSING_CREDENTIAL: 'DeepSeek 账号凭据缺失,请重新登录 DeepSeek 账号',
30
+ AUTH: 'DeepSeek 账号登录已失效,请重新登录账号',
31
+ ACCOUNT_TOKEN_INVALID: 'DeepSeek 账号登录已失效,请重新登录账号',
32
+ INVALID_CREDENTIAL: 'DeepSeek 账号凭据无效,请重新登录账号',
33
+ RATE_LIMIT: '识图请求被限流,请稍后重试',
34
+ QUOTA: '识图模型额度不足,请检查 DeepSeek 账号额度',
35
+ ACCOUNT_QUOTA: 'DeepSeek 账号余额不足,请充值后重试',
36
+ CONTEXT_WINDOW_EXCEEDED: '图片太大或上下文超限,请换更小的图片重试',
37
+ IMAGE_OFFLOAD_REQUIRED: '图片体积超出该模型的请求预算,请换更小的图片重试',
38
+ MODEL_UNSUPPORTED: '当前账号/DSH 版本不支持该识图模型,请检查 visionModel 或升级 DSH',
39
+ CLIENT_UPGRADE_REQUIRED: '该识图模型要求更高的 DSH 版本,请升级 DSH',
40
+ UNKNOWN_MODEL: '识图模型 id 不被该路由认识,请检查 visionModel',
41
+ TIMEOUT: '识图请求超时,请检查网络后重试',
42
+ ABORTED: '识图请求被取消',
43
+ EMPTY_RESPONSE: '识图模型没有返回任何文本,请换个提示词或换张图片重试',
44
+ NO_FINISH: '识图请求没有正常结束,请重试',
45
+ };
46
+
47
+ /**
48
+ * 把 `finish.failure` 映射成 `VisionError`。
49
+ * @param {{ code?: string, message?: string, status?: number } | undefined} failure 终止失败信息。
50
+ * @param {string} provider 识图 provider id。
51
+ * @param {string} model 识图 model id。
52
+ * @returns {VisionError}
53
+ */
54
+ export function failureToVisionError(failure, provider, model) {
55
+ const code = typeof failure?.code === 'string' && failure.code !== '' ? failure.code : 'LLM_ERROR';
56
+ const detail = typeof failure?.message === 'string' && failure.message !== '' ? failure.message : 'no failure message';
57
+ const status = typeof failure?.status === 'number' ? ` HTTP ${failure.status}` : '';
58
+ const message = `识图模型 ${provider}/${model} 调用失败 [${code}${status}]: ${detail}`;
59
+ const hint = HINTS[code];
60
+ return new VisionError(message, code, hint);
61
+ }
62
+
63
+ /**
64
+ * 把任意异常归一化为 `VisionError`(用于工具错误与占位文本)。
65
+ * @param {unknown} error 原始异常。
66
+ * @param {string} [fallbackCode] 无法识别时使用的错误码。
67
+ * @returns {VisionError}
68
+ */
69
+ export function toVisionError(error, fallbackCode = 'VISION_FAILED') {
70
+ if (error instanceof VisionError) return error;
71
+ const message = error instanceof Error ? error.message : String(error);
72
+ return new VisionError(`识图失败: ${message}`, fallbackCode);
73
+ }
74
+
75
+ /** 生成自动转译占位文本(不把整段错误堆栈塞进会话历史)。 */
76
+ export function failurePlaceholder(error) {
77
+ const visionError = toVisionError(error);
78
+ const reason = visionError.message.length > 240 ? `${visionError.message.slice(0, 240)}…` : visionError.message;
79
+ return `[图片转译失败: ${reason}]`;
80
+ }
package/lib/hash.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 用一行 `sha256` 摘要给图片/提示词做内容键(缓存与并发合并都靠它)。
3
+ * @module dsh-deepseek-account-vision/hash
4
+ */
5
+ import { createHash } from 'node:crypto';
6
+
7
+ /**
8
+ * @param {Uint8Array | string} data 待摘要的字节或字符串。
9
+ * @returns {string} 32 字节十六进制摘要。
10
+ */
11
+ export function sha256Hex(data) {
12
+ return createHash('sha256').update(data).digest('hex');
13
+ }