Files
dsh-enhancements/plugins/dsh-usage/host.js
T
江晨 c33fe31f49 fix: 插件最终版(stdin 机制)+ 踩坑经验归档
- dsh-scheduler: shell 请求无 args 字段,改 stdin 传脚本 + JS 字面量持久化(Unicode 安全)
- dsh-usage: 余额查询同样改 stdin 机制
- 记录 CollectedOutput.text / sandboxPolicy danger-full-access / exitCode / workdir 等关键经验
2026-08-17 12:06:47 +08:00

60 lines
3.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// dsh-usage 插件 — Host 半(最终版,pkg-11
// 提供两个 Package-private RPC
// usage/balance — 读取 %DSH_HOME%\.credentials.yaml 的 DEEPSEEK_API_KEY
// 调用 DeepSeek 官方余额 APIGET /user/balance),返回 JSON。
// usage/openPortal— 用系统默认浏览器打开官网用量页。
//
// 关键技术点(踩坑记录):
// 1. ShellExecRequest 没有 args 字段!node 脚本必须走 stdincommand:'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) }
}
})
},
}