// dsh-usage 插件 — Host 半(最终版,pkg-11) // 提供两个 Package-private RPC: // usage/balance — 读取 %DSH_HOME%\.credentials.yaml 的 DEEPSEEK_API_KEY, // 调用 DeepSeek 官方余额 API(GET /user/balance),返回 JSON。 // usage/openPortal— 用系统默认浏览器打开官网用量页。 // // 关键技术点(踩坑记录): // 1. ShellExecRequest 没有 args 字段!node 脚本必须走 stdin(command:'node' + stdin:脚本), // 传 args 会被忽略导致"裸跑 node"零输出。 // 2. res.stdout 是 CollectedOutput 对象({text, truncated, spillPath}),不是字符串, // 用 .text 取值;String() 会得到 "[object Object]"。 // 3. 宿主 ctx.shell 默认按 workspace-write 沙箱执行,本机无可用沙箱后端会被拒绝, // 必须显式传 sandboxPolicy: sandboxPolicyService.resolve({mode:'danger-full-access'})。 // 4. 密钥只在 node 子进程内读取与使用,绝不打印、不入库。 return { inject: ['shell'], apply(ctx) { const sp = ctx.get('sandboxPolicy') const fullAccess = sp ? sp.resolve({ mode: 'danger-full-access' }) : { mode: 'danger-full-access' } const out = (o) => (o && typeof o === 'object' && 'text' in o) ? o.text : String(o || '') const balanceScript = [ "const fs = require('node:fs');", "const os = require('node:os');", "const path = require('node:path');", "const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');", "const file = path.join(home, '.credentials.yaml');", "let key = null;", "try { const txt = fs.readFileSync(file, 'utf8'); const m = txt.match(/DEEPSEEK_API_KEY:\\s*(\\S+)/); if (m) key = m[1]; } catch (e) {}", "if (!key) { console.log(JSON.stringify({ error: 'no-api-key', file: file })); process.exit(0); }", "fetch('https://api.deepseek.com/user/balance', { headers: { Authorization: 'Bearer ' + key, Accept: 'application/json' }, signal: AbortSignal.timeout(15000) })", " .then(r => r.json().then(j => console.log(JSON.stringify(Object.assign({ status: r.status }, j)))))", " .catch(e => console.log(JSON.stringify({ error: String((e && e.message) || e) })));", ].join('\n') harness.handle('usage/balance', async (args) => { try { const spec = ctx.shell.resolve({ command: 'node', stdin: balanceScript, sandboxPolicy: fullAccess }) const res = await ctx.shell.run(spec) const stdout = out(res.stdout) const lines = stdout.trim().split(/\r?\n/).filter(Boolean) if (lines.length) return JSON.parse(lines[lines.length - 1]) return { error: 'empty-output', stderr: out(res.stderr).slice(0, 400) } } catch (e) { return { error: String((e && e.message) || e) } } }) harness.handle('usage/openPortal', async () => { try { const spec = ctx.shell.resolve({ command: 'cmd /c start "" https://platform.deepseek.com/usage', sandboxPolicy: fullAccess }) await ctx.shell.run(spec) return { ok: true } } catch (e) { return { ok: false, error: String((e && e.message) || e) } } }) }, }