From 863005e7b12a30235918e52ca48787b73c9e0b78 Mon Sep 17 00:00:00 2001 From: jiangchen <1703935164@qq.com> Date: Tue, 1 Sep 2026 21:10:19 +0800 Subject: [PATCH] feat: register dsh-watch settings namespace + harness.balance (apiKey) + npm pack --- README.md | 12 +++++++- dist/dsh-watch-0.1.0.tgz | Bin 0 -> 6018 bytes lib/index.js | 62 +++++++++++++++++++++++++++++++-------- package.json | 3 +- 4 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 dist/dsh-watch-0.1.0.tgz 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 0000000000000000000000000000000000000000..57f3a08eb11d9c1fa066b32a6ff28381e8661ef6 GIT binary patch literal 6018 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 { 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"