- dsh-scheduler: shell 请求无 args 字段,改 stdin 传脚本 + JS 字面量持久化(Unicode 安全) - dsh-usage: 余额查询同样改 stdin 机制 - 记录 CollectedOutput.text / sandboxPolicy danger-full-access / exitCode / workdir 等关键经验
60 lines
3.1 KiB
JavaScript
60 lines
3.1 KiB
JavaScript
// 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) }
|
||
}
|
||
})
|
||
},
|
||
}
|