@wszhoho/dsh-file-attachment 0.5.2 → 0.5.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-file-attachment
2
2
 
3
- DeepSeek Harness (dsh) web GUI 插件:在会话输入框中**拖入或 Ctrl+V 粘贴文档/图片(支持多文件,一次可拖入/粘贴多个文件)**、或通过 **📎 附件按钮上传**时,浏览器端批量读取全文,保存到**当前会话工作区根目录下的 `.dsh-file-attachment/`**,并在输入框光标处插入 `@<保存副本绝对路径>` 引用。agent 读取 `@路径` 时直接读到落盘副本,不再依赖原始文件位置。
3
+ DeepSeek Harness (dsh) web GUI 插件:在会话输入框中拖入或 Ctrl+V 粘贴文档/图片,或通过DSH本体 📎 附件按钮上传(插件已接管本体上传按钮),图片调用配置好的vlm模型自动进行图片识别。
4
4
 
5
5
  图片与文档走**同一条落盘管线**:图片在输入框**内联附件条**中以缩略图预览(点击放大),在聊天区渲染为**可点击放大的缩略图**;文档在附件条中以类型图标 + 文件名条目显示,在聊天区保持芯片样式。当前会话模型不支持多模态时,图片自动调用**可配置 VLM** 识别生成中文描述回填草稿,文本模型也能「看懂」图片。
6
6
 
@@ -21,7 +21,7 @@ DeepSeek Harness (dsh) web GUI 插件:在会话输入框中**拖入或 Ctrl+V
21
21
  **设置页**:「设置 → 文件附件」页两部分:
22
22
 
23
23
  - **可上传类型**:按 文档 / 代码 / 配置文件 三类增删扩展名(小写、不带点),图片恒可发送;
24
- - **多模态识别参数(VLM)**:Base URL / API Key / 模型 / 思考模式开关(默认禁用),仅当前会话模型不支持多模态时调用,未填 API Key 时静默跳过。
24
+ - **多模态识别参数(VLM)**:Base URL / API Key / 模型 / 思考模式开关(默认禁用)/ 超时时间(默认 60 秒),仅当前会话模型不支持多模态时调用,未填 API Key 时静默跳过。
25
25
 
26
26
  ![可上传类型设置](docs/settings-file-types.png)
27
27
 
@@ -97,7 +97,7 @@ packages/dsh-file-attachment/
97
97
  - **Host 半**:`webServer.register` 前缀路由 `/dsh-file-attachment`:
98
98
  - POST `/save`:接收 `{name, data(base64), sessionId}`,base64 解码后用 `node:fs/promises` 写盘到会话工作区 `.dsh-file-attachment/`,返回 `{ok, value:{path,dir,name,size}}`;
99
99
  - GET/POST `/config`:读写插件配置(可上传类型 + VLM 参数);
100
- - POST `/describe`:接收 `{dataUrl, prompt}`,调 VLM(OpenAI 兼容 `chat/completions`)识别图片,返回中文描述;
100
+ - POST `/describe`:接收 `{dataUrl, prompt}`,调 VLM(OpenAI 兼容 `chat/completions`)识别图片,返回中文描述与当前思考模式配置(`thinkingType`);
101
101
  - `describe_image` 工具(Host 注册):agent 需要理解 `@图片` 引用时调用——读已落盘图片 → VLM 识别 → 描述直接作为工具输出返回(UI 展示 + 模型可见)。
102
102
  - **Client 半**槽位:
103
103
  - `conversation.input.attachments`(shadow 原生附件条,priority -100):**内联附件条**,渲染自维护的附件登记表——图片为 data URL 缩略图(点击放大)、文档为类型图标 + 文件名;条目可移除(同步清除草稿 `@引用`),发送后自动清空,不依赖 dsh 附件草稿链路(文本模型可正常发送);
@@ -113,7 +113,7 @@ packages/dsh-file-attachment/
113
113
  - `lib/*.js`、`package.json`、`cordis.patch.yml`、`README.md` 当前均为 **UTF-8 无 BOM**(以仓库实测为准);编辑时保持原编码,不引入 BOM。
114
114
  - Host 半用 `webServer.register({ kind: 'prefix', path: '/dsh-file-attachment', handler })` 提供 `/save`、`/config`、`/describe` 三个路由,并经 `tools.register` 注册 `describe_image` 工具;纯 ESM 无构建,不依赖装饰器/远程反射。
115
115
  - Client 半保存用 `fetch('/dsh-file-attachment/save', { method: 'POST', body: JSON.stringify({ name, data, sessionId }) })`,信封为 `{ ok, value | error }`。
116
- - VLM 走 OpenAI 兼容 `chat/completions`(Base URL / API Key / 模型 / 思考模式可在设置页配置,默认关闭思考模式);未填 API Key 时识别静默跳过,不影响落盘。
116
+ - VLM 走 OpenAI 兼容 `chat/completions`(Base URL / API Key / 模型 / 思考模式 / 超时时间可在设置页配置,思考模式默认关闭,超时默认 60 秒);请求按 MiMo 官方格式发送 `thinking: { type: "enabled"|"disabled" }`(扁平 `thinkingType` 会被 API 忽略,而 mimo-v2.5 默认开启深度思考,导致识别极慢——实测 206 秒 vs 关闭后 22~73 秒,服务延迟波动较大,若偶发超时可调大);超时后工具调用中止并明确报错「VLM 请求超时(N 秒)」,不再挂死会话;`describe_image` 工具输出 JSON 附带 `thinkingType`(enabled/disabled),直观展示当前思考模式配置。未填 API Key 时识别静默跳过,不影响落盘。
117
117
  - 项目根 = 会话工作区(`session.header.cwd` → `sandboxPolicy.workspaceRoot` → `process.cwd()` 兜底)。
118
118
  - 50MB 上限两侧一致(client 跳过 + host 校验)。
119
119
 
package/lib/client.js CHANGED
@@ -40,7 +40,7 @@ window.__ModuleLoader__.load({
40
40
  let noticeTimer = null
41
41
  const NOTICE_MS = 4000
42
42
  const MAX_BYTES = 50 * 1024 * 1024
43
- const MAX_DIM = 640 // 图片缩放:最长边上限(GIF 例外,原样保动画)
43
+ const MAX_DIM = 640 // 图片缩放:宽或高任一边 >640 时等比缩小(GIF 例外,原样保动画)
44
44
  // 可上传类型(三类:文档/代码/配置文件);图片恒允许(image/*)。
45
45
  // 首启为默认值,apply/设置页 fetch 持久化配置(~/.dsh/file-attachment.json)后覆盖。
46
46
  const DEFAULT_TYPES = {
@@ -49,8 +49,8 @@ window.__ModuleLoader__.load({
49
49
  config: ['json', 'yml', 'yaml', 'toml', 'ini', 'conf', 'cfg', 'env', 'properties', 'xml', 'html', 'css'],
50
50
  }
51
51
  const allowedTypes = { doc: DEFAULT_TYPES.doc.slice(), code: DEFAULT_TYPES.code.slice(), config: DEFAULT_TYPES.config.slice() }
52
- // 多模态 VLM 识别参数(仅当前模型非多模态时调用):baseURL/apiKey/model/thinkingType
53
- let vlmCfg = { baseURL: 'https://api.xiaomimimo.com/v1', apiKey: '', model: 'mimo-v2.5', thinkingType: 'disabled' }
52
+ // 多模态 VLM 识别参数(仅当前模型非多模态时调用):baseURL/apiKey/model/thinkingType/timeout
53
+ let vlmCfg = { baseURL: 'https://api.xiaomimimo.com/v1', apiKey: '', model: 'mimo-v2.5', thinkingType: 'disabled', timeout: 60 }
54
54
  let allowedExts = null // Set<ext>,rebuildAllowed 填充;上传校验用
55
55
  let acceptString = 'image/*' // 文件选择器 <input accept>,buildAccept 生成;劫持📎打开前赋值
56
56
  // 规范化扩展名列表(小写/去点/去重/仅 [a-z0-9]),与 host 侧同逻辑
@@ -135,12 +135,15 @@ window.__ModuleLoader__.load({
135
135
  const timer = setInterval(scan, 1500)
136
136
  return { mo, timer }
137
137
  }
138
- // 客户端侧多模态参数规范化(空值回退默认,与 host normalizeMimo 一致)
138
+ // 客户端侧多模态参数规范化(空值回退默认,与 host normalizeVlm 一致)
139
139
  function normalizeVlmClient(raw) {
140
- const d = { baseURL: 'https://api.xiaomimimo.com/v1', apiKey: '', model: 'mimo-v2.5', thinkingType: 'disabled' }
140
+ const d = { baseURL: 'https://api.xiaomimimo.com/v1', apiKey: '', model: 'mimo-v2.5', thinkingType: 'disabled', timeout: 60 }
141
141
  if (raw === null || typeof raw !== 'object') return d
142
142
  const pick = (k) => (typeof raw[k] === 'string' && raw[k] !== '') ? raw[k] : d[k]
143
- return { baseURL: pick('baseURL'), apiKey: pick('apiKey'), model: pick('model'), thinkingType: raw.thinkingType === 'enabled' ? 'enabled' : 'disabled' }
143
+ // 超时(秒):正整数 1~600,非法回退默认
144
+ const t = Number(raw && raw.timeout)
145
+ const timeout = Number.isFinite(t) && t >= 1 && t <= 600 ? Math.floor(t) : d.timeout
146
+ return { baseURL: pick('baseURL'), apiKey: pick('apiKey'), model: pick('model'), thinkingType: raw.thinkingType === 'enabled' ? 'enabled' : 'disabled', timeout }
144
147
  }
145
148
  // 应用配置:更新 allowedTypes → 重建校验 Set + accept(劫持📎在打开选择器时读取 acceptString)
146
149
  function applyConfig(cfg) {
@@ -268,7 +271,7 @@ window.__ModuleLoader__.load({
268
271
  throw new Error(errMsg)
269
272
  }
270
273
 
271
- // ---- 图片处理:扩展名 / GIF 判定 / 加载 / 缩放(≤MAX_DIMGIF 原样保动画,PNG 保透明) ----
274
+ // ---- 图片处理:扩展名 / GIF 判定 / 加载 / 缩放(宽或高任一边 >MAX_DIM 时处理,GIF 原样保动画,PNG 保透明) ----
272
275
  function fileExt(name) {
273
276
  const s = String(name == null ? '' : name)
274
277
  const dot = s.lastIndexOf('.')
@@ -286,7 +289,7 @@ window.__ModuleLoader__.load({
286
289
  img.src = url
287
290
  })
288
291
  }
289
- // 缩放:最长边 > maxDim 时等比缩小;返回 { data, name }(data 为 Blob/File)。
292
+ // 缩放:宽或高任一边 > maxDim 时等比缩小(两边都不超过 maxDim 时原样);返回 { data, name }(data 为 Blob/File)。
290
293
  // GIF 原样(canvas 取首帧丢动画 + 不支持 gif 输出);PNG 输出 PNG 保透明;其余 JPEG 0.85。
291
294
  // 缩放失败/无需缩放时回退原文件。
292
295
  async function downscaleImage(file, maxDim) {
@@ -565,10 +568,11 @@ window.__ModuleLoader__.load({
565
568
  const it = pending[i]
566
569
  try {
567
570
  if (it.isImage) {
568
- // 图片:落盘 @路径 纯文本 + 附件条登记(方案 B:走文件引用链路,
569
- // 不触发 dsh 的 image 能力检查,文本模型可正常发送到历史)
570
- const data = it.file
571
- const name = it.name
571
+ // 图片:落盘前等比缩小(宽或高任一边 >MAX_DIM 才处理;GIF 原样保动画),
572
+ // 再走 @路径 纯文本 + 附件条登记(方案 B:不触发 dsh 的 image 能力检查)
573
+ const sized = await downscaleImage(it.file, MAX_DIM)
574
+ const data = sized.data
575
+ const name = sized.name
572
576
  if (data === null || typeof data.size !== 'number' || data.size === 0) throw new Error('size')
573
577
  if (data.size > MAX_BYTES) throw new Error('size')
574
578
  const buf = await data.arrayBuffer()
@@ -1139,6 +1143,7 @@ window.__ModuleLoader__.load({
1139
1143
  const [vlmApiKey, setVlmApiKey] = react.useState('')
1140
1144
  const [vlmModel, setVlmModel] = react.useState('')
1141
1145
  const [vlmThinking, setVlmThinking] = react.useState(false)
1146
+ const [vlmTimeout, setVlmTimeout] = react.useState('30')
1142
1147
  const [docIn, setDocIn] = react.useState('')
1143
1148
  const [codeIn, setCodeIn] = react.useState('')
1144
1149
  const [cfgIn, setCfgIn] = react.useState('')
@@ -1156,6 +1161,7 @@ window.__ModuleLoader__.load({
1156
1161
  setVlmApiKey(vlmCfg.apiKey)
1157
1162
  setVlmModel(vlmCfg.model)
1158
1163
  setVlmThinking(vlmCfg.thinkingType === 'enabled')
1164
+ setVlmTimeout(String(vlmCfg.timeout || 60))
1159
1165
  })
1160
1166
  return () => { live = false }
1161
1167
  }, [])
@@ -1168,7 +1174,7 @@ window.__ModuleLoader__.load({
1168
1174
  async function save() {
1169
1175
  setSaving(true); setSaved(false)
1170
1176
  try {
1171
- const body = { doc: doc, code: code, config: config, vlm: { baseURL: vlmBaseURL, apiKey: vlmApiKey, model: vlmModel, thinkingType: vlmThinking ? 'enabled' : 'disabled' } }
1177
+ const body = { doc: doc, code: code, config: config, vlm: { baseURL: vlmBaseURL, apiKey: vlmApiKey, model: vlmModel, thinkingType: vlmThinking ? 'enabled' : 'disabled', timeout: Math.min(600, Math.max(1, Math.floor(Number(vlmTimeout) || 60))) } }
1172
1178
  const r = await fetch('/dsh-file-attachment/config', {
1173
1179
  method: 'POST',
1174
1180
  headers: { 'Content-Type': 'application/json' },
@@ -1201,6 +1207,8 @@ window.__ModuleLoader__.load({
1201
1207
  react.createElement('input', { value: vlmApiKey, onChange: (e) => setVlmApiKey(e.target.value), type: 'password', placeholder: 'sk-...', style: { padding: '6px 8px', borderRadius: '6px', border: '1px solid rgba(128,128,128,0.3)', background: 'transparent', color: 'inherit', fontSize: '12px' } }),
1202
1208
  react.createElement('span', { style: { fontSize: '12px' } }, t('vlm.model')),
1203
1209
  react.createElement('input', { value: vlmModel, onChange: (e) => setVlmModel(e.target.value), placeholder: 'mimo-v2.5', style: { padding: '6px 8px', borderRadius: '6px', border: '1px solid rgba(128,128,128,0.3)', background: 'transparent', color: 'inherit', fontSize: '12px' } }),
1210
+ react.createElement('span', { style: { fontSize: '12px' } }, t('vlm.timeout')),
1211
+ react.createElement('input', { value: vlmTimeout, onChange: (e) => setVlmTimeout(e.target.value), placeholder: '60', title: t('vlm.timeoutHint'), style: { padding: '6px 8px', borderRadius: '6px', border: '1px solid rgba(128,128,128,0.3)', background: 'transparent', color: 'inherit', fontSize: '12px' } }),
1204
1212
  react.createElement('span', { style: { fontSize: '12px' } }),
1205
1213
  react.createElement('label', { style: { display: 'inline-flex', alignItems: 'center', gap: '6px', fontSize: '12px' } },
1206
1214
  react.createElement('input', { type: 'checkbox', checked: vlmThinking, onChange: (e) => setVlmThinking(e.target.checked) }),
@@ -1241,6 +1249,8 @@ window.__ModuleLoader__.load({
1241
1249
  'vlm.apiKey': 'API Key',
1242
1250
  'vlm.model': '模型',
1243
1251
  'vlm.thinking': '启用思考模式(默认禁用)',
1252
+ 'vlm.timeout': '超时(秒)',
1253
+ 'vlm.timeoutHint': '图片识别超时时间,默认 60 秒;识别慢或卡顿时可调大',
1244
1254
  'save': '保存',
1245
1255
  'saving': '保存中…',
1246
1256
  'saved': '已保存',
@@ -1257,6 +1267,8 @@ window.__ModuleLoader__.load({
1257
1267
  'vlm.apiKey': 'API Key',
1258
1268
  'vlm.model': 'Model',
1259
1269
  'vlm.thinking': 'Enable thinking mode (disabled by default)',
1270
+ 'vlm.timeout': 'Timeout (s)',
1271
+ 'vlm.timeoutHint': 'Image recognition timeout, default 60s; increase if recognition is slow or hangs',
1260
1272
  'save': 'Save',
1261
1273
  'saving': 'Saving…',
1262
1274
  'saved': 'Saved',
package/lib/index.js CHANGED
@@ -143,17 +143,22 @@ const VLM_DEFAULTS = {
143
143
  apiKey: '',
144
144
  model: 'mimo-v2.5',
145
145
  thinkingType: 'disabled',
146
+ timeout: 60, // 识别超时(秒)。实测 VLM 延迟波动大(12~73s),30s 会误杀多数请求
146
147
  }
147
148
 
148
149
  /** 规范化多模态参数(缺省/空值回退默认,trim 去空白)。 */
149
150
  function normalizeVlm(raw) {
150
151
  const r = (raw && typeof raw === 'object') ? raw : {}
151
152
  const pick = (key) => (typeof r[key] === 'string' && r[key].trim() !== '' ? r[key].trim() : VLM_DEFAULTS[key])
153
+ // 超时(秒):正整数 1~600,非法回退默认
154
+ const t = Number(r && r.timeout)
155
+ const timeout = Number.isFinite(t) && t >= 1 && t <= 600 ? Math.floor(t) : VLM_DEFAULTS.timeout
152
156
  return {
153
157
  baseURL: pick('baseURL'),
154
158
  apiKey: pick('apiKey'),
155
159
  model: pick('model'),
156
160
  thinkingType: pick('thinkingType'),
161
+ timeout,
157
162
  }
158
163
  }
159
164
 
@@ -219,19 +224,47 @@ async function handleSave(root, body) {
219
224
  return { path: target, dir, name: safe, size: bytes.length }
220
225
  }
221
226
 
227
+ // 仅思考模型识别:这类模型(旗舰推理版 / 带 thinking 后缀)始终思考、不可关闭,
228
+ // 传 thinking.type='disabled' 会报错(Kimi k2.7-code、GLM-5.3、Qwen3-VL-*-Thinking 等)。
229
+ function isThinkingOnlyModel(model) {
230
+ const m = String(model || '').toLowerCase()
231
+ if (/-thinking\b/.test(m)) return true
232
+ if (/kimi-k3\b/.test(m)) return true
233
+ if (/kimi-k2\.7-code/.test(m)) return true
234
+ if (/^glm-5\.3/.test(m)) return true
235
+ return false
236
+ }
237
+
238
+ // 思考参数适配器:各家 OpenAI 兼容 API 的「思考开关」字段形式不统一。
239
+ // 按 model 名称判断(不依赖 baseURL host——中转站/代理转发时 host 不可靠,而 model 字段稳定跟随请求)。
240
+ // - 仅思考模型 → 不传任何参数(服务端默认开启思考;传 disabled 会报错)
241
+ // - 通义千问 Qwen / QVQ(含 VL 视觉)→ 扁平布尔 enable_thinking
242
+ // - 其余主流(小米 MiMo / DeepSeek / 智谱 GLM / Kimi 官方)→ 嵌套对象 thinking:{type}
243
+ function buildThinkingParams(vlm) {
244
+ if (isThinkingOnlyModel(vlm.model)) return {}
245
+ const wantEnabled = vlm.thinkingType === 'enabled'
246
+ const model = String(vlm.model || '').toLowerCase()
247
+ if (model.includes('qwen') || model.includes('qvq')) {
248
+ return { enable_thinking: wantEnabled }
249
+ }
250
+ return { thinking: { type: wantEnabled ? 'enabled' : 'disabled' } }
251
+ }
252
+
222
253
  /**
223
254
  * 调 VLM(OpenAI 兼容 chat/completions)识别图片,返回中文描述。
224
- * @param vlm - 多模态参数 { baseURL, apiKey, model, thinkingType }(来自配置)。
255
+ * @param vlm - 多模态参数 { baseURL, apiKey, model, thinkingType, timeout }(来自配置,timeout 单位秒)。
225
256
  * @param dataUrl - base64 data URL(形如 data:image/png;base64,xxx)。
226
257
  * @returns 图片的简洁中文描述。
227
- * @throws VLM 请求失败或未返回有效内容时抛错。
258
+ * @throws VLM 请求失败或未返回有效内容时抛错;超时抛「VLM 请求超时」。
228
259
  */
229
260
  async function describeImage(vlm, dataUrl, prompt) {
230
261
  const url = vlm.baseURL.replace(/\/+$/u, '') + '/chat/completions'
231
262
  const userPrompt = (typeof prompt === 'string' && prompt.trim() !== '') ? prompt.trim() : '请描述这张图片。'
263
+ // 思考开关:各家 OpenAI 兼容 API 字段形式不统一,由 buildThinkingParams 按 model 名称适配
264
+ // (Qwen/QVQ → 扁平 enable_thinking;MiMo/DeepSeek/GLM/Kimi → 嵌套 thinking.type;仅思考模型 → 不传)。
232
265
  const payload = {
233
266
  model: vlm.model,
234
- thinkingType: vlm.thinkingType,
267
+ ...buildThinkingParams(vlm),
235
268
  messages: [
236
269
  { role: 'system', content: '你是图片描述助手,用简洁中文回答。' },
237
270
  {
@@ -243,22 +276,35 @@ async function describeImage(vlm, dataUrl, prompt) {
243
276
  },
244
277
  ],
245
278
  }
246
- const resp = await fetch(url, {
247
- method: 'POST',
248
- headers: {
249
- 'content-type': 'application/json',
250
- 'authorization': 'Bearer ' + vlm.apiKey,
251
- },
252
- body: JSON.stringify(payload),
253
- })
254
- if (!resp.ok) {
255
- const text = await resp.text().catch(() => '')
256
- throw new Error('VLM 请求失败 ' + resp.status + (text !== '' ? ' ' + text.slice(0, 200) : ''))
279
+ const timeoutSec = (Number.isFinite(vlm.timeout) && vlm.timeout >= 1 && vlm.timeout <= 600) ? vlm.timeout : VLM_DEFAULTS.timeout
280
+ const controller = new AbortController()
281
+ const timer = setTimeout(() => controller.abort(), timeoutSec * 1000)
282
+ try {
283
+ const resp = await fetch(url, {
284
+ method: 'POST',
285
+ headers: {
286
+ 'content-type': 'application/json',
287
+ 'authorization': 'Bearer ' + vlm.apiKey,
288
+ },
289
+ body: JSON.stringify(payload),
290
+ signal: controller.signal,
291
+ })
292
+ if (!resp.ok) {
293
+ const text = await resp.text().catch(() => '')
294
+ throw new Error('VLM 请求失败 ' + resp.status + (text !== '' ? ' ' + text.slice(0, 200) : ''))
295
+ }
296
+ const data = await resp.json()
297
+ const content = data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content
298
+ if (typeof content !== 'string' || content === '') throw new Error('VLM 未返回有效内容')
299
+ return content
300
+ } catch (err) {
301
+ if (err && err.name === 'AbortError') {
302
+ throw new Error('VLM 请求超时(' + timeoutSec + ' 秒),可在「设置→文件附件」调大超时时间')
303
+ }
304
+ throw err
305
+ } finally {
306
+ clearTimeout(timer)
257
307
  }
258
- const data = await resp.json()
259
- const content = data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content
260
- if (typeof content !== 'string' || content === '') throw new Error('VLM 未返回有效内容')
261
- return content
262
308
  }
263
309
 
264
310
  /** 注册 /dsh-file-attachment 前缀路由(save POST)。 */
@@ -327,7 +373,7 @@ function registerRoutes(ctx) {
327
373
  }
328
374
  try {
329
375
  const description = await describeImage(vlm, dataUrl, body.prompt)
330
- json(res, { ok: true, value: { description } })
376
+ json(res, { ok: true, value: { description, thinkingType: vlm.thinkingType } })
331
377
  return
332
378
  } catch (err) {
333
379
  const message = err && err.message ? err.message : '识别失败'
@@ -383,6 +429,11 @@ function registerTools(ctx) {
383
429
  type: 'string',
384
430
  description: '可选。告诉多模态模型要关注什么(如"识别图中的文字"、"描述 UI 布局")。不填则默认"请描述这张图片"。',
385
431
  },
432
+ thinkingType: {
433
+ type: 'string',
434
+ enum: ['disabled', 'enabled'],
435
+ description: 'VLM 思考模式开关:disabled=关闭(默认)/ enabled=开启。本工具默认关闭思考以加速识别,请填 disabled。',
436
+ },
386
437
  },
387
438
  output: {
388
439
  schema: {
@@ -391,11 +442,13 @@ function registerTools(ctx) {
391
442
  properties: {
392
443
  ok: { type: 'boolean', required: true },
393
444
  description: { type: 'string' },
445
+ thinkingType: { type: 'string', description: '当前 VLM 思考模式配置:enabled=开启 / disabled=关闭' },
394
446
  },
395
447
  },
396
448
  render: (_args, value) => {
449
+ const thinkingText = (value && value.thinkingType === 'enabled') ? '开启' : '关闭'
397
450
  if (value && value.ok === true && typeof value.description === 'string' && value.description !== '') {
398
- return [{ type: 'text', text: '图片识别结果:' + value.description }]
451
+ return [{ type: 'text', text: '图片识别结果:' + value.description + '(思考模式:' + thinkingText + ')' }]
399
452
  }
400
453
  return [{ type: 'text', text: (value && value.ok === true) ? '(图片已识别)' : '(图片识别失败)' }]
401
454
  },
@@ -413,7 +466,11 @@ function registerTools(ctx) {
413
466
  const description = await describeImage(cfg.vlm, dataUrl, args.prompt)
414
467
  // 识别结果直接作为工具输出返回:UI 直接展示描述文本,
415
468
  // 同时该输出就是模型可见内容(tool/result 的 content),无需额外 deferContext 注入。
416
- return { ok: true, description }
469
+ // 注意 thinkingType 的双重角色:
470
+ // - 输入参数(args.thinkingType):模型按 description 填充,仅用于让「工具调用 JSON」直观可见思考开关,
471
+ // 实际调用 VLM 时不读它(真实开关以 cfg.vlm.thinkingType 为准,见 describeImage 内的 thinking.type)。
472
+ // - 输出(此处返回值):回填配置真实值,使结果与输入展示一致、可核对。
473
+ return { ok: true, description, thinkingType: cfg.vlm.thinkingType }
417
474
  },
418
475
  }))
419
476
  console.log('[dsh-file-attachment] describe_image tool registered')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wszhoho/dsh-file-attachment",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "文件附件:拖拽/粘贴/上传文件(支持多文件);图片与文档统一落盘到 .dsh-file-attachment 并以 @绝对路径 引用发送(文本模型可正常使用);输入框内联显示图片缩略图预览与文件条目,聊天区图片同样渲染为可点击放大的缩略图、文件保持芯片样式;非多模态模型下图片自动调用可配置 VLM 识别生成中文描述回填草稿;文档/代码/配置文件可上传类型可在设置页配置;支持 PC 与移动端浏览器",
5
5
  "keywords": [
6
6
  "dsh",