New:Socket for Asana Is Now Available.Learn more
Get Started

dsh-vision-recognizer

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dsh-vision-recognizer - npm Package Compare versions

Comparing version
0.1.1
to
0.1.2
+45
-1
client/client.js

@@ -48,2 +48,6 @@ window.__ModuleLoader__.load({ id: "dsh-vision-recognizer", factory: (require) => {

saved: '已保存,立即生效',
test: '测试',
testing: '测试中…',
testOk: '连接正常',
testFailed: '测试失败',
saveFailed: '保存失败',

@@ -81,2 +85,6 @@ loadFailed: '配置加载失败,请稍后重试',

saved: 'Saved — takes effect immediately',
test: 'Test',
testing: 'Testing…',
testOk: 'Connection OK',
testFailed: 'Test failed',
saveFailed: 'Save failed',

@@ -106,2 +114,3 @@ loadFailed: 'Failed to load configuration',

.vrg-btn:disabled{opacity:.6;cursor:default}
.vrg-btn-ghost{background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-primary,#1f2328)}
.vrg-status{font-size:12px}

@@ -136,2 +145,3 @@ .vrg-status.ok{color:var(--dsw-alias-state-success-primary,#16a34a)}

const [saving, setSaving] = useState(false)
const [testing, setTesting] = useState(false)
const [status, setStatus] = useState(null)

@@ -172,2 +182,32 @@

const runTest = useCallback(() => {
if (form === null || testing) return
setTesting(true)
setStatus(null)
fetch('/dsh-vision-recognizer/test', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
provider: form.provider,
apiKey: form.apiKey,
model: form.model,
baseURL: form.baseURL,
}),
})
.then((res) => res.text().then((text) => {
let body = {}
try { body = text ? JSON.parse(text) : {} } catch { body = { error: text.slice(0, 200) } }
return body
}))
.then((body) => {
if (body.ok) {
setStatus({ kind: 'ok', message: t('testOk') + (typeof body.durationMs === 'number' ? ' · ' + body.durationMs + 'ms' : '') })
} else {
setStatus({ kind: 'err', message: t('testFailed') + (body.error ? ': ' + body.error : '') })
}
})
.catch((error) => setStatus({ kind: 'err', message: t('testFailed') + ': ' + String(error) }))
.finally(() => setTesting(false))
}, [form, testing, t])
const save = useCallback(() => {

@@ -206,2 +246,5 @@ if (form === null || saving) return

setStatus({ kind: 'ok', message: t('saved') })
// Auto-test the just-saved config so the user immediately sees
// whether the provider / key / model actually works.
runTest()
} else {

@@ -213,3 +256,3 @@ setStatus({ kind: 'err', message: t('saveFailed') + (body.error ? ': ' + body.error : '') })

.finally(() => setSaving(false))
}, [form, saving, t])
}, [form, saving, t, runTest])

@@ -302,2 +345,3 @@ if (loadError) return h('p', { className: 'vrg-intro' }, t('loadFailed'))

h('button', { className: 'vrg-btn', disabled: saving, onClick: save }, saving ? t('saving') : t('save')),
h('button', { className: 'vrg-btn vrg-btn-ghost', disabled: testing, onClick: runTest }, testing ? t('testing') : t('test')),
status !== null && h('span', { className: 'vrg-status ' + status.kind }, status.message)))

@@ -304,0 +348,0 @@ }

@@ -19,2 +19,7 @@ /**

/** Short prompt used by the API-key test request. */
export const TEST_PROMPT = 'Reply with exactly: OK';
/** A 32x32 solid-color PNG used by the API-key test (valid for every vision model, cheap). */
export const TEST_IMAGE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVR4nO3OIQEAAAgDMBrShPpIiHEzMb/q2UsqAQEBAQEBAQEBAQEBAQGBdOABt9TMl+gurJYAAAAASUVORK5CYII=';
/** The transcription prompt: a complete, faithful text rendition for a text-only recipient. */

@@ -73,2 +78,5 @@ export const TRANSCRIBE_PROMPT = `You are an image-to-text transcription service. The recipient is a text-only LLM that cannot see the image. Produce a faithful, complete text rendition covering:

}
if (status === 400 && /image|invalid/i.test(text) && /length|width|dimension|size|larger than|smaller than|resolution/i.test(text)) {
return { kind: 'image_dimensions', hint: 'the image does not meet the model\'s dimension requirements — it is probably too small (each side must usually be larger than ~10px); use a normal screenshot or resize the image' };
}
if (status === 400 && /context|length|too (large|long)|token/i.test(text)) {

@@ -118,3 +126,3 @@ return { kind: 'context_too_large', hint: 'input is too large for this model — try a smaller image or a longer-context model' };

/** Build the OpenAI-compatible request for a transcription. */
export function buildOpenAIRequest(attempt, base64, mediaType) {
export function buildOpenAIRequest(attempt, base64, mediaType, prompt = TRANSCRIBE_PROMPT) {
return {

@@ -133,3 +141,3 @@ url: `${attempt.baseURL.replace(/\/+$/, '')}/chat/completions`,

{ type: 'image_url', image_url: { url: `data:${mediaType};base64,${base64}` } },
{ type: 'text', text: TRANSCRIBE_PROMPT },
{ type: 'text', text: prompt },
],

@@ -142,3 +150,3 @@ }],

/** Build the Anthropic Messages request for a transcription. */
export function buildAnthropicRequest(attempt, base64, mediaType) {
export function buildAnthropicRequest(attempt, base64, mediaType, prompt = TRANSCRIBE_PROMPT) {
return {

@@ -159,3 +167,3 @@ url: `${attempt.baseURL.replace(/\/+$/, '')}/messages`,

{ type: 'image', source: { type: 'base64', media_type: mediaType, data: base64 } },
{ type: 'text', text: TRANSCRIBE_PROMPT },
{ type: 'text', text: prompt },
],

@@ -188,3 +196,3 @@ }],

/** Send the transcription request for already-read image bytes; returns the text. */
export async function transcribeRequest(attempt, data, mediaType, signal) {
export async function transcribeRequest(attempt, data, mediaType, signal, prompt = TRANSCRIBE_PROMPT) {
const base64 = Buffer.from(data).toString('base64');

@@ -196,3 +204,3 @@ const effectiveTimeout = isLocalBaseURL(attempt.baseURL)

const parse = attempt.protocol === PROTOCOL_ANTHROPIC ? parseAnthropicResponse : parseOpenAIResponse;
const request = build(attempt, base64, mediaType);
const request = build(attempt, base64, mediaType, prompt);
const post = () => fetch(request.url, {

@@ -232,2 +240,20 @@ method: 'POST',

/**
* Verify a provider connection: sends a tiny valid image with a short prompt
* through the configured protocol and throws on any failure, so the Settings
* UI can report exactly why the key/endpoint/model does not work.
* @param attempt - resolved attempt (provider/model/baseURL/apiKey).
* @returns the resolved attempt on success.
*/
export async function testProvider(attempt) {
const apiKey = resolveApiKey(attempt);
if (apiKey === '' && !isLocalBaseURL(attempt.baseURL)) {
throw new Error('no API key configured — enter the key in Settings → Plugins → 识图, or export the provider env var');
}
const effective = { ...attempt, apiKey };
const data = Buffer.from(TEST_IMAGE_BASE64, 'base64');
await transcribeRequest(effective, data, 'image/png', undefined, TEST_PROMPT);
return effective;
}
/**
* Fully resolve one config entry (primary or fallback) into an attempt object.

@@ -237,4 +263,3 @@ * @param entry - {provider?, model?, baseURL?, apiKey?} plus shared defaults.

*/
export function resolveAttempt(entry, shared) {
const preset = providerById(entry.provider ?? 'custom');
export function resolveAttempt(entry, shared) { const preset = providerById(entry.provider ?? 'custom');
const baseURL = (entry.baseURL && entry.baseURL !== '') ? entry.baseURL : preset.baseURL;

@@ -241,0 +266,0 @@ const model = (entry.model && entry.model !== '') ? entry.model : preset.defaultModel;

+69
-26

@@ -157,17 +157,47 @@ /**

// 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, {
providers: providerCatalog(),
config: redactedConfig(state),
});
return;
}
if (request.method === 'POST') {
// The config route is a single exact route whose handler dispatches by HTTP
// method — the webserver rejects duplicate (kind, path) registrations, so
// registering GET and POST as two routes would throw and leave POST dead.
// The test route is a separate path and may be its own registration.
const disposers = [
hostCtx.webServer.register({
kind: 'exact',
path: '/dsh-vision-recognizer/config',
handler: async (request, response) => {
if (request.method === 'GET') {
sendJson(response, 200, {
providers: providerCatalog(),
config: redactedConfig(state),
});
return;
}
if (request.method === 'POST') {
if (!sameOrigin(request)) {
sendJson(response, 403, { error: 'untrusted origin' });
return;
}
try {
const body = await readJsonBody(request);
const patch = core.sanitizePatch(body);
const next = persistConfig({ ...state.config, ...patch });
state.config = next;
sendJson(response, 200, { ok: true, config: redactedConfig(state) });
} catch (error) {
sendJson(response, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });
}
return;
}
response.writeHead(405, { allow: 'GET, POST' });
response.end();
},
}),
hostCtx.webServer.register({
kind: 'exact',
path: '/dsh-vision-recognizer/test',
handler: async (request, response) => {
if (request.method !== 'POST') {
response.writeHead(405, { allow: 'POST' });
response.end();
return;
}
if (!sameOrigin(request)) {

@@ -179,15 +209,28 @@ sendJson(response, 403, { error: 'untrusted origin' });

const body = await readJsonBody(request);
const patch = core.sanitizePatch(body);
const next = persistConfig({ ...state.config, ...patch });
state.config = next;
sendJson(response, 200, { ok: true, config: redactedConfig(state) });
const c = state.config;
const candidate = {
provider: typeof body.provider === 'string' && body.provider !== '' ? body.provider : c.provider,
model: typeof body.model === 'string' ? body.model : c.model,
baseURL: typeof body.baseURL === 'string' ? body.baseURL : c.baseURL,
apiKey: typeof body.apiKey === 'string' && body.apiKey !== '' ? body.apiKey : c.apiKey,
};
const shared = { maxTokens: 32, timeoutMs: Math.min(c.timeoutMs ?? DEFAULT_TIMEOUT_MS, 30_000) };
const attempt = core.resolveAttempt(candidate, shared);
const started = Date.now();
await core.testProvider(attempt);
sendJson(response, 200, {
ok: true,
durationMs: Date.now() - started,
model: attempt.model,
baseURL: attempt.baseURL,
});
} catch (error) {
sendJson(response, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });
sendJson(response, 200, { ok: false, error: error instanceof Error ? error.message : String(error) });
}
return;
}
response.writeHead(405, { allow: 'GET, POST' });
response.end();
},
});
},
}),
];
return () => {
for (const dispose of disposers) dispose();
};
}

@@ -194,0 +237,0 @@

{
"name": "dsh-vision-recognizer",
"version": "0.1.1",
"version": "0.1.2",
"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,