feat: register dsh-watch settings namespace + harness.balance (apiKey) + npm pack
This commit is contained in:
@@ -49,11 +49,21 @@ DeepSeek Harness 插件:把当前 Harness 变成一个可被 DSHWatch 手机/
|
|||||||
userId: "someone"
|
userId: "someone"
|
||||||
token: "shared-secret"
|
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:
|
透传 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
|
- session.list create history prompt cancel rename fork models selectModel attachment
|
||||||
- goal.* subagent.* workspace.* host.*
|
- goal.* subagent.* workspace.* host.*
|
||||||
- skill.list agentPreset.* llm.* settings.* credentials.*
|
- skill.list agentPreset.* llm.* settings.* credentials.*
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+49
-13
@@ -1,8 +1,10 @@
|
|||||||
import { createServer } from 'node:http';
|
import { createServer } from 'node:http';
|
||||||
import { WebSocket, WebSocketServer } from 'ws';
|
import { WebSocket, WebSocketServer } from 'ws';
|
||||||
import z from '@deepseek-ai/schemastery';
|
import z from '@deepseek-ai/schemastery';
|
||||||
|
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
|
||||||
|
|
||||||
const name = 'dsh-watch';
|
const name = 'dsh-watch';
|
||||||
|
const NS = settingsNamespace('dsh-watch');
|
||||||
|
|
||||||
const Config = z.object({
|
const Config = z.object({
|
||||||
mode: z.union([z.const('direct'), z.const('relay')]).default('relay'),
|
mode: z.union([z.const('direct'), z.const('relay')]).default('relay'),
|
||||||
@@ -10,6 +12,7 @@ const Config = z.object({
|
|||||||
serverUrl: z.string().default(''),
|
serverUrl: z.string().default(''),
|
||||||
userId: z.string().default(''),
|
userId: z.string().default(''),
|
||||||
token: z.string().default(''),
|
token: z.string().default(''),
|
||||||
|
apiKey: z.string().default(''),
|
||||||
reconnectMs: z.number().min(500).default(5000),
|
reconnectMs: z.number().min(500).default(5000),
|
||||||
timeoutMs: z.number().min(1000).default(30000),
|
timeoutMs: z.number().min(1000).default(30000),
|
||||||
// Direct mode
|
// Direct mode
|
||||||
@@ -21,6 +24,17 @@ function apply(ctx, config) {
|
|||||||
let api;
|
let api;
|
||||||
ctx.inject(['apiProxy'], (c) => { api = c.apiProxy; });
|
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();
|
const directClients = new Set();
|
||||||
let relayWs = null;
|
let relayWs = null;
|
||||||
let httpServer = null;
|
let httpServer = null;
|
||||||
@@ -40,7 +54,7 @@ function apply(ctx, config) {
|
|||||||
|
|
||||||
function pushEvent(event) {
|
function pushEvent(event) {
|
||||||
const frame = { type: 'event', event };
|
const frame = { type: 'event', event };
|
||||||
if (config.mode === 'direct') {
|
if (current.mode === 'direct') {
|
||||||
for (const client of directClients) safeSend(client, frame);
|
for (const client of directClients) safeSend(client, frame);
|
||||||
} else {
|
} else {
|
||||||
safeSend(relayWs, frame);
|
safeSend(relayWs, frame);
|
||||||
@@ -49,7 +63,7 @@ function apply(ctx, config) {
|
|||||||
|
|
||||||
function pushStatus(status) {
|
function pushStatus(status) {
|
||||||
const frame = { type: 'status', status };
|
const frame = { type: 'status', status };
|
||||||
if (config.mode === 'direct') {
|
if (current.mode === 'direct') {
|
||||||
for (const client of directClients) safeSend(client, frame);
|
for (const client of directClients) safeSend(client, frame);
|
||||||
} else {
|
} else {
|
||||||
safeSend(relayWs, frame);
|
safeSend(relayWs, frame);
|
||||||
@@ -97,6 +111,7 @@ function apply(ctx, config) {
|
|||||||
if (method === 'harness.status') return computeStatus();
|
if (method === 'harness.status') return computeStatus();
|
||||||
if (method === 'harness.prompt') return harnessPrompt(params);
|
if (method === 'harness.prompt') return harnessPrompt(params);
|
||||||
if (method === 'harness.cancel') return harnessCancel();
|
if (method === 'harness.cancel') return harnessCancel();
|
||||||
|
if (method === 'harness.balance') return harnessBalance();
|
||||||
const dot = method.indexOf('.');
|
const dot = method.indexOf('.');
|
||||||
if (dot <= 0) throw new Error('unknown method ' + method);
|
if (dot <= 0) throw new Error('unknown method ' + method);
|
||||||
const domain = method.slice(0, dot);
|
const domain = method.slice(0, dot);
|
||||||
@@ -137,6 +152,27 @@ function apply(ctx, config) {
|
|||||||
return api.sessions.cancel({ sessionId });
|
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) {
|
function handleRequest(msg, respond) {
|
||||||
invoke(msg.method || '', msg.params || {}).then((result) => {
|
invoke(msg.method || '', msg.params || {}).then((result) => {
|
||||||
respond({ type: 'response', requestId: msg.requestId, ok: true, result });
|
respond({ type: 'response', requestId: msg.requestId, ok: true, result });
|
||||||
@@ -151,7 +187,7 @@ function apply(ctx, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isAuthed(msg) {
|
function isAuthed(msg) {
|
||||||
return config.token && msg.token === config.token;
|
return current.token && msg.token === current.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Direct mode ----------
|
// ---------- Direct mode ----------
|
||||||
@@ -192,7 +228,7 @@ function apply(ctx, config) {
|
|||||||
const url = new URL(req.url || '/', 'http://localhost');
|
const url = new URL(req.url || '/', 'http://localhost');
|
||||||
if (url.pathname === '/health') {
|
if (url.pathname === '/health') {
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||||
@@ -200,8 +236,8 @@ function apply(ctx, config) {
|
|||||||
});
|
});
|
||||||
wss = new WebSocketServer({ server: httpServer });
|
wss = new WebSocketServer({ server: httpServer });
|
||||||
wss.on('connection', handleDirectConnection);
|
wss.on('connection', handleDirectConnection);
|
||||||
httpServer.listen(config.port, config.host, () => {
|
httpServer.listen(current.port, current.host, () => {
|
||||||
log('direct mode listening on ws://' + config.host + ':' + config.port + '/ws');
|
log('direct mode listening on ws://' + current.host + ':' + current.port + '/ws');
|
||||||
sendStatus();
|
sendStatus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -209,12 +245,12 @@ function apply(ctx, config) {
|
|||||||
// ---------- Relay mode ----------
|
// ---------- Relay mode ----------
|
||||||
function connectRelay() {
|
function connectRelay() {
|
||||||
if (closed || (relayWs && relayWs.readyState === WebSocket.OPEN)) return;
|
if (closed || (relayWs && relayWs.readyState === WebSocket.OPEN)) return;
|
||||||
const w = new WebSocket(config.serverUrl);
|
const w = new WebSocket(current.serverUrl);
|
||||||
relayWs = w;
|
relayWs = w;
|
||||||
w.on('open', () => {
|
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();
|
sendStatus();
|
||||||
log('relay connected to ' + config.serverUrl + ' as ' + config.userId);
|
log('relay connected to ' + current.serverUrl + ' as ' + current.userId);
|
||||||
});
|
});
|
||||||
w.on('message', (raw) => {
|
w.on('message', (raw) => {
|
||||||
let msg;
|
let msg;
|
||||||
@@ -240,7 +276,7 @@ function apply(ctx, config) {
|
|||||||
reconnectTimer = setTimeout(() => {
|
reconnectTimer = setTimeout(() => {
|
||||||
reconnectTimer = null;
|
reconnectTimer = null;
|
||||||
connectRelay();
|
connectRelay();
|
||||||
}, config.reconnectMs);
|
}, current.reconnectMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Shared event hooks ----------
|
// ---------- Shared event hooks ----------
|
||||||
@@ -277,14 +313,14 @@ function apply(ctx, config) {
|
|||||||
if (httpServer) { try { httpServer.close(); } catch {} httpServer = null; }
|
if (httpServer) { try { httpServer.close(); } catch {} httpServer = null; }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.mode === 'direct') {
|
if (current.mode === 'direct') {
|
||||||
if (!config.port || !config.token) {
|
if (!current.port || !current.token) {
|
||||||
warn('direct mode requires port and token; bridge stays idle.');
|
warn('direct mode requires port and token; bridge stays idle.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
startDirect();
|
startDirect();
|
||||||
} else {
|
} 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.');
|
warn('relay mode requires serverUrl/userId/token; bridge stays idle.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -29,7 +29,8 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@deepseek-ai/cordis": "^4.0.1",
|
"@deepseek-ai/cordis": "^4.0.1",
|
||||||
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
"@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": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|||||||
Reference in New Issue
Block a user