diff --git a/README.md b/README.md index 3e2864a..ea31817 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,21 @@ DeepSeek Harness 插件:把当前 Harness 变成一个可被 DSHWatch 手机/ userId: "someone" token: "shared-secret" +## DSH 设置页(手表/手机设置) + +插件注册了 dsh-watch 设置命名空间。装好后,在 DSH 的 设置 -> 插件(或对应插件设置区)里可填: + +- mode: direct / relay +- serverUrl / userId / token +- apiKey(DeepSeek API Key,用于 harness.balance 直接查余额;留空则用 DEEPSEEK_API_KEY 环境变量/credentials) +- reconnectMs / timeoutMs +- host / port(direct 模式) + ## 暴露方法 透传 DSH apiProxy: -- harness.status / harness.prompt / harness.cancel +- harness.status / harness.prompt / harness.cancel / harness.balance - session.list create history prompt cancel rename fork models selectModel attachment - goal.* subagent.* workspace.* host.* - skill.list agentPreset.* llm.* settings.* credentials.* diff --git a/dist/dsh-watch-0.1.0.tgz b/dist/dsh-watch-0.1.0.tgz new file mode 100644 index 0000000..57f3a08 Binary files /dev/null and b/dist/dsh-watch-0.1.0.tgz differ diff --git a/lib/index.js b/lib/index.js index 364f4ae..6b0767f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,8 +1,10 @@ import { createServer } from 'node:http'; import { WebSocket, WebSocketServer } from 'ws'; import z from '@deepseek-ai/schemastery'; +import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'; const name = 'dsh-watch'; +const NS = settingsNamespace('dsh-watch'); const Config = z.object({ mode: z.union([z.const('direct'), z.const('relay')]).default('relay'), @@ -10,6 +12,7 @@ const Config = z.object({ serverUrl: z.string().default(''), userId: z.string().default(''), token: z.string().default(''), + apiKey: z.string().default(''), reconnectMs: z.number().min(500).default(5000), timeoutMs: z.number().min(1000).default(30000), // Direct mode @@ -21,6 +24,17 @@ function apply(ctx, config) { let api; ctx.inject(['apiProxy'], (c) => { api = c.apiProxy; }); + // 可设置项:注册到 DSH 设置页(手表/手机设置) + let current = config; + try { + installSettingsSection(ctx, NS, Config, config, { + setSource: (source) => { current = source; }, + onChange: () => {} + }); + } catch (e) { + ctx.logger.warn('[dsh-watch] settings section unavailable: ' + String(e && e.message || e)); + } + const directClients = new Set(); let relayWs = null; let httpServer = null; @@ -40,7 +54,7 @@ function apply(ctx, config) { function pushEvent(event) { const frame = { type: 'event', event }; - if (config.mode === 'direct') { + if (current.mode === 'direct') { for (const client of directClients) safeSend(client, frame); } else { safeSend(relayWs, frame); @@ -49,7 +63,7 @@ function apply(ctx, config) { function pushStatus(status) { const frame = { type: 'status', status }; - if (config.mode === 'direct') { + if (current.mode === 'direct') { for (const client of directClients) safeSend(client, frame); } else { safeSend(relayWs, frame); @@ -97,6 +111,7 @@ function apply(ctx, config) { if (method === 'harness.status') return computeStatus(); if (method === 'harness.prompt') return harnessPrompt(params); if (method === 'harness.cancel') return harnessCancel(); + if (method === 'harness.balance') return harnessBalance(); const dot = method.indexOf('.'); if (dot <= 0) throw new Error('unknown method ' + method); const domain = method.slice(0, dot); @@ -137,6 +152,27 @@ function apply(ctx, config) { return api.sessions.cancel({ sessionId }); } + async function harnessBalance() { + const key = (current.apiKey && current.apiKey.trim()) || (await resolveApiKey()); + if (!key) throw new Error('no DeepSeek API key configured (set dsh-watch apiKey or DEEPSEEK_API_KEY)'); + const res = await fetch('https://api.deepseek.com/user/balance', { + headers: { Authorization: 'Bearer ' + key, Accept: 'application/json' } + }); + if (!res.ok) throw new Error('balance request failed: ' + res.status); + return await res.json(); + } + + async function resolveApiKey() { + try { + const credentials = ctx.get('credentials'); + if (credentials) { + const hit = await credentials.resolve('DEEPSEEK_API_KEY'); + if (hit && hit.value) return hit.value; + } + } catch (e) { /* ignore */ } + return process.env.DEEPSEEK_API_KEY || ''; + } + function handleRequest(msg, respond) { invoke(msg.method || '', msg.params || {}).then((result) => { respond({ type: 'response', requestId: msg.requestId, ok: true, result }); @@ -151,7 +187,7 @@ function apply(ctx, config) { } function isAuthed(msg) { - return config.token && msg.token === config.token; + return current.token && msg.token === current.token; } // ---------- Direct mode ---------- @@ -192,7 +228,7 @@ function apply(ctx, config) { const url = new URL(req.url || '/', 'http://localhost'); if (url.pathname === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ ok: true, mode: 'direct', clients: directClients.size, port: config.port })); + res.end(JSON.stringify({ ok: true, mode: 'direct', clients: directClients.size, port: current.port })); return; } res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); @@ -200,8 +236,8 @@ function apply(ctx, config) { }); wss = new WebSocketServer({ server: httpServer }); wss.on('connection', handleDirectConnection); - httpServer.listen(config.port, config.host, () => { - log('direct mode listening on ws://' + config.host + ':' + config.port + '/ws'); + httpServer.listen(current.port, current.host, () => { + log('direct mode listening on ws://' + current.host + ':' + current.port + '/ws'); sendStatus(); }); } @@ -209,12 +245,12 @@ function apply(ctx, config) { // ---------- Relay mode ---------- function connectRelay() { if (closed || (relayWs && relayWs.readyState === WebSocket.OPEN)) return; - const w = new WebSocket(config.serverUrl); + const w = new WebSocket(current.serverUrl); relayWs = w; w.on('open', () => { - safeSend(w, { type: 'hello', role: 'dsh', userId: config.userId, token: config.token }); + safeSend(w, { type: 'hello', role: 'dsh', userId: current.userId, token: current.token }); sendStatus(); - log('relay connected to ' + config.serverUrl + ' as ' + config.userId); + log('relay connected to ' + current.serverUrl + ' as ' + current.userId); }); w.on('message', (raw) => { let msg; @@ -240,7 +276,7 @@ function apply(ctx, config) { reconnectTimer = setTimeout(() => { reconnectTimer = null; connectRelay(); - }, config.reconnectMs); + }, current.reconnectMs); } // ---------- Shared event hooks ---------- @@ -277,14 +313,14 @@ function apply(ctx, config) { if (httpServer) { try { httpServer.close(); } catch {} httpServer = null; } }); - if (config.mode === 'direct') { - if (!config.port || !config.token) { + if (current.mode === 'direct') { + if (!current.port || !current.token) { warn('direct mode requires port and token; bridge stays idle.'); return; } startDirect(); } else { - if (!config.serverUrl || !config.userId || !config.token) { + if (!current.serverUrl || !current.userId || !current.token) { warn('relay mode requires serverUrl/userId/token; bridge stays idle.'); return; } diff --git a/package.json b/package.json index c8b3d4a..57631a1 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", - "@deepseek-ai/schemastery": "^3.18.1" + "@deepseek-ai/schemastery": "^3.18.1", + "@deepseek-ai/dsh-settings": "^0.1.1-rc.2" }, "engines": { "node": ">=18"