dsh-vision-recognizer
Advanced tools
+39
-9
@@ -26,2 +26,3 @@ window.__ModuleLoader__.load({ id: "dsh-vision-recognizer", factory: (require) => { | ||
| providerLabel: '供应商', | ||
| providerHint: '选择图片转译使用的视觉模型供应商。可在下方覆盖模型与接口地址。', | ||
| apiKeyLabel: 'API Key', | ||
@@ -32,9 +33,15 @@ apiKeyHint: '留空则读取该供应商的环境变量;本地 Ollama 无需 Key。', | ||
| modelLabel: '模型', | ||
| modelHint: '视觉模型 ID。留空使用该供应商的默认模型。', | ||
| modelPlaceholder: '留空使用供应商默认', | ||
| baseUrlLabel: '接口地址', | ||
| baseUrlHint: 'OpenAI 兼容接口地址(…/chat/completions 会自动拼接)。留空使用该供应商的默认地址。', | ||
| baseUrlPlaceholder: '留空使用供应商默认', | ||
| maxTokensLabel: '最大输出 token', | ||
| maxTokensHint: '视觉模型单次输出的 token 上限,越大转译越详尽、也越贵。', | ||
| timeoutLabel: '超时(毫秒)', | ||
| timeoutHint: '请求超时时间。本地/匿名端点会被强制限制在 20 秒内。', | ||
| markerLabel: '转译标记', | ||
| markerHint: '转译文本的前缀标记,DeepSeek 靠它区分「这是图片转译的内容」。默认 [图片转译]。', | ||
| autoOllamaLabel: '自动探测本地 Ollama', | ||
| autoOllamaHint: '启动时探测本机 http://localhost:11434 的 Ollama,检测到则作为兜底——图片不出本机、无需 Key。', | ||
| save: '保存', | ||
@@ -53,2 +60,3 @@ saving: '保存中…', | ||
| providerLabel: 'Provider', | ||
| providerHint: 'Vision provider used to transcribe attached images. Model and endpoint can be overridden below.', | ||
| apiKeyLabel: 'API key', | ||
@@ -59,9 +67,15 @@ apiKeyHint: 'Leave blank to read the provider env var; local Ollama needs none.', | ||
| modelLabel: 'Model', | ||
| modelHint: 'Vision model id. Leave blank for the provider default.', | ||
| modelPlaceholder: 'Leave blank for the provider default', | ||
| baseUrlLabel: 'Endpoint', | ||
| baseUrlHint: 'OpenAI-compatible endpoint (…/chat/completions is appended). Leave blank for the provider default.', | ||
| baseUrlPlaceholder: 'Leave blank for the provider default', | ||
| maxTokensLabel: 'Max output tokens', | ||
| maxTokensHint: 'Output token cap for the vision model; larger is more thorough but costs more.', | ||
| timeoutLabel: 'Timeout (ms)', | ||
| timeoutHint: 'Request timeout. Local/anonymous endpoints are hard-capped at 20s.', | ||
| markerLabel: 'Transcription marker', | ||
| markerHint: 'Text prefix prepended to each transcription, so DeepSeek can tell image transcriptions apart. Default [图片转译].', | ||
| autoOllamaLabel: 'Auto-detect local Ollama', | ||
| autoOllamaHint: 'Probe local Ollama at http://localhost:11434 at startup and use it as a fallback — images never leave your machine and no key is needed.', | ||
| save: 'Save', | ||
@@ -97,2 +111,5 @@ saving: 'Saving…', | ||
| .vrg-status.err{color:var(--dsw-alias-state-error-primary,#dc2626)} | ||
| .vrg-tip{position:relative;display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;border-radius:99px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);color:var(--dsw-alias-label-tertiary,#9ca3af);font-size:10px;font-weight:600;line-height:1;cursor:help;margin-left:6px;vertical-align:middle;user-select:none} | ||
| .vrg-tip .vrg-tiptext{position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%) translateY(-2px);background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:8px;padding:8px 10px;font-size:12px;font-weight:400;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2328);width:max-content;max-width:280px;white-space:normal;text-align:left;z-index:60;opacity:0;pointer-events:none;transition:opacity .12s ease,transform .12s ease;box-shadow:0 6px 20px rgba(0,0,0,.14)} | ||
| .vrg-tip:hover .vrg-tiptext,.vrg-tip:focus-visible .vrg-tiptext{opacity:1;transform:translateX(-50%) translateY(0)} | ||
| ` | ||
@@ -109,2 +126,9 @@ | ||
| /** A small "?" circle whose hover/focus reveals a tooltip explaining the field. */ | ||
| function HelpTip({ text }) { | ||
| return h('span', { className: 'vrg-tip', tabIndex: 0, 'aria-label': text }, | ||
| '?', | ||
| h('span', { className: 'vrg-tiptext' }, text)) | ||
| } | ||
| function VisionPanel({ t, locale }) { | ||
@@ -168,3 +192,7 @@ const [data, setData] = useState(null) | ||
| }) | ||
| .then((res) => res.json().then((body) => ({ status: res.status, body }))) | ||
| .then((res) => res.text().then((text) => { | ||
| let body = {} | ||
| try { body = text ? JSON.parse(text) : {} } catch { body = { error: text.slice(0, 200) } } | ||
| return { status: res.status, body } | ||
| })) | ||
| .then(({ status: httpStatus, body }) => { | ||
@@ -200,3 +228,3 @@ if (httpStatus === 200 && body.ok) { | ||
| h('div', { className: 'vrg-field' }, | ||
| h('label', { className: 'vrg-label' }, t('providerLabel')), | ||
| h('label', { className: 'vrg-label' }, t('providerLabel'), h(HelpTip, { text: t('providerHint') })), | ||
| h('select', { | ||
@@ -212,3 +240,4 @@ className: 'vrg-input', | ||
| h('span', { className: 'vrg-badge' + (form.apiKeyConfigured ? ' on' : '') }, | ||
| form.apiKeyConfigured ? t('apiKeyConfigured') : t('apiKeyUnset'))), | ||
| form.apiKeyConfigured ? t('apiKeyConfigured') : t('apiKeyUnset')), | ||
| h(HelpTip, { text: t('apiKeyHint') })), | ||
| h('input', { | ||
@@ -225,3 +254,3 @@ className: 'vrg-input', | ||
| h('div', { className: 'vrg-field' }, | ||
| h('label', { className: 'vrg-label' }, t('modelLabel')), | ||
| h('label', { className: 'vrg-label' }, t('modelLabel'), h(HelpTip, { text: t('modelHint') })), | ||
| h('input', { | ||
@@ -234,3 +263,3 @@ className: 'vrg-input', | ||
| h('div', { className: 'vrg-field' }, | ||
| h('label', { className: 'vrg-label' }, t('baseUrlLabel')), | ||
| h('label', { className: 'vrg-label' }, t('baseUrlLabel'), h(HelpTip, { text: t('baseUrlHint') })), | ||
| h('input', { | ||
@@ -244,3 +273,3 @@ className: 'vrg-input', | ||
| h('div', { className: 'vrg-field' }, | ||
| h('label', { className: 'vrg-label' }, t('maxTokensLabel')), | ||
| h('label', { className: 'vrg-label' }, t('maxTokensLabel'), h(HelpTip, { text: t('maxTokensHint') })), | ||
| h('input', { | ||
@@ -253,3 +282,3 @@ className: 'vrg-input', | ||
| h('div', { className: 'vrg-field' }, | ||
| h('label', { className: 'vrg-label' }, t('timeoutLabel')), | ||
| h('label', { className: 'vrg-label' }, t('timeoutLabel'), h(HelpTip, { text: t('timeoutHint') })), | ||
| h('input', { | ||
@@ -262,3 +291,3 @@ className: 'vrg-input', | ||
| h('div', { className: 'vrg-field' }, | ||
| h('label', { className: 'vrg-label' }, t('markerLabel')), | ||
| h('label', { className: 'vrg-label' }, t('markerLabel'), h(HelpTip, { text: t('markerHint') })), | ||
| h('input', { | ||
@@ -275,3 +304,4 @@ className: 'vrg-input', | ||
| }), | ||
| t('autoOllamaLabel')), | ||
| t('autoOllamaLabel'), | ||
| h(HelpTip, { text: t('autoOllamaHint') })), | ||
| form.provider === 'custom' && h('p', { className: 'vrg-hint' }, t('requiredHint')), | ||
@@ -278,0 +308,0 @@ h('div', { className: 'vrg-actions' }, |
+17
-27
@@ -157,12 +157,10 @@ /** | ||
| const disposers = [ | ||
| hostCtx.webServer.register({ | ||
| kind: 'exact', | ||
| path: '/dsh-vision-recognizer/config', | ||
| handler: async (request, response) => { | ||
| if (request.method !== 'GET') { | ||
| response.writeHead(405, { allow: 'GET' }); | ||
| response.end(); | ||
| return; | ||
| } | ||
| // Single exact route; the handler dispatches by HTTP method. The webserver | ||
| // rejects duplicate (kind, path) registrations, so registering GET and POST | ||
| // as two routes would throw and leave the POST handler dead. | ||
| return hostCtx.webServer.register({ | ||
| kind: 'exact', | ||
| path: '/dsh-vision-recognizer/config', | ||
| handler: async (request, response) => { | ||
| if (request.method === 'GET') { | ||
| sendJson(response, 200, { | ||
@@ -172,13 +170,5 @@ providers: providerCatalog(), | ||
| }); | ||
| }, | ||
| }), | ||
| hostCtx.webServer.register({ | ||
| kind: 'exact', | ||
| path: '/dsh-vision-recognizer/config', | ||
| handler: async (request, response) => { | ||
| if (request.method !== 'POST') { | ||
| response.writeHead(405, { allow: 'POST' }); | ||
| response.end(); | ||
| return; | ||
| } | ||
| return; | ||
| } | ||
| if (request.method === 'POST') { | ||
| if (!sameOrigin(request)) { | ||
@@ -197,8 +187,8 @@ sendJson(response, 403, { error: 'untrusted origin' }); | ||
| } | ||
| }, | ||
| }), | ||
| ]; | ||
| return () => { | ||
| for (const dispose of disposers) dispose(); | ||
| }; | ||
| return; | ||
| } | ||
| response.writeHead(405, { allow: 'GET, POST' }); | ||
| response.end(); | ||
| }, | ||
| }); | ||
| } | ||
@@ -205,0 +195,0 @@ |
+1
-1
| { | ||
| "name": "dsh-vision-recognizer", | ||
| "version": "0.1.0", | ||
| "version": "0.1.1", | ||
| "description": "DeepSeek Harness image recognition plugin: keep DeepSeek as the conversation brain while a configurable vision model (15+ providers, OpenAI-compatible + Anthropic) transcribes attached images. Configurable from Settings → Plugins.", | ||
@@ -5,0 +5,0 @@ "private": false, |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
69069
4.66%1080
2.08%