chore: 初始化 dsh-enhancements 留档仓库

- 插件源码:dsh-usage(用户头像 + DeepSeek 官方 API 消耗面板)
- 工具:dl-gh.mjs(node fetch 下载 gh,绕开沙箱 curl 限制)
- 文档:PLAN.md 路线图 + mmx 视觉桥接补丁说明
- 资产:DeepSeek 鲸鱼图标
This commit is contained in:
江晨
2026-08-17 11:54:40 +08:00
commit fdafd5d6b3
10 changed files with 394 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# dsh-usage — 用户头像 + API 消耗面板
在 DeepSeek Harness 侧边栏底部(设置旁)添加一个 Codex 式**用户头像**入口,
点击弹出模态面板,展示 **DeepSeek 官方 API 余额/消耗**,并可一键打开官网用量页。
## 效果
- 侧边栏底部(设置按钮旁):圆形 🐋 头像按钮(窄栏只显图标,宽栏带"API 用量"文字)
- 点击 → 居中模态面板:
- 账户可用状态
- 各币种:总额 / 赠送余额 / 充值余额(来自官方 `GET https://api.deepseek.com/user/balance`
- 按钮:刷新 / 打开官网用量页 / 关闭
## 实现
| 端 | 机制 |
|---|---|
| Host | `harness.handle('usage/balance')`shell 运行 node 脚本读取 `%DSH_HOME%\.credentials.yaml``DEEPSEEK_API_KEY`,调用官方余额 API,返回 JSON(密钥不出进程日志);`usage/openPortal`:打开官网用量页 |
| Client | `sidebar.footer.action` 注册头像按钮;`shell.overlay` 注册模态面板;两个组件通过插件内微型 store 共享开关状态;样式用主题 CSS 变量(`--dsw-alias-*` |
## 为什么用官方 API
DeepSeek 官方提供余额接口(`/user/balance`),返回 `is_available` + `balance_infos[]`
用量明细官网暂无公开 API,故面板提供"打开官网用量页"platform.deepseek.com/usage)兜底。
## 文件
- `host.js` — Host 半(RPC + 官方 API 调用)
- `client.js` — Client 半(头像 + 模态 UI
- 部署方式:通过动态 Cordis 插件(`cordis_define` / `cordis_run`)或本仓库 `presets/` 下的预设文件
## 前置
- `%DSH_HOME%\.credentials.yaml` 中存在 `DEEPSEEK_API_KEY`
- 官方余额 API 需可访问(api.deepseek.com
+111
View File
@@ -0,0 +1,111 @@
// dsh-usage 插件 — Client 半
// 注册两处 UI
// sidebar.footer.action 'dsh-usage-avatar' — 设置旁的 🐋 用户头像按钮
// shell.overlay 'dsh-usage-modal' — 点击头像弹出的官方余额模态面板
// 两个组件通过插件内微型 storeopen + listeners)共享开关状态。
return {
apply(ctx) {
const slots = ctx.get('slots')
if (slots === undefined) return
styles.insert(`
.dsh-ua-avatar { display:flex; align-items:center; gap:8px; width:100%; padding:7px 10px; background:transparent; border:0; border-radius:8px; color:var(--dsw-alias-label-primary); cursor:pointer; font:inherit; }
.dsh-ua-avatar:hover { background:var(--dsw-alias-bg-layer-2); }
.dsh-ua-avatar.active { background:var(--dsw-alias-bg-layer-2); }
.dsh-ua-avatar-icon { width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:15px; flex:none; background:var(--dsw-alias-bg-layer-2); border:1px solid var(--dsw-alias-border-l1); }
.dsh-ua-avatar-label { font-size:12px; color:var(--dsw-alias-label-secondary); }
.dsh-ua-backdrop { position:fixed; inset:0; z-index:9999; background:rgba(0,0,0,.35); display:flex; align-items:center; justify-content:center; pointer-events:auto; }
.dsh-ua-modal { width:380px; max-width:92vw; background:var(--dsw-alias-bg-overlay); border:1px solid var(--dsw-alias-border-l1); border-radius:12px; box-shadow:0 12px 40px rgba(0,0,0,.3); padding:16px; color:var(--dsw-alias-label-primary); pointer-events:auto; font-size:13px; }
.dsh-ua-modal h3 { margin:0 0 12px; font-size:15px; display:flex; align-items:center; gap:8px; }
.dsh-ua-row { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--dsw-alias-border-l1); }
.dsh-ua-row:last-child { border-bottom:0; }
.dsh-ua-row .k { color:var(--dsw-alias-label-secondary); }
.dsh-ua-row .v { font-weight:600; }
.dsh-ua-ok { color:var(--dsw-alias-state-success-primary); }
.dsh-ua-err { color:var(--dsw-alias-state-error-primary); }
.dsh-ua-btns { display:flex; gap:8px; margin-top:14px; }
.dsh-ua-btn { flex:1; padding:7px 10px; border-radius:8px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1); color:var(--dsw-alias-label-primary); cursor:pointer; font:inherit; font-size:12px; }
.dsh-ua-btn:hover { background:var(--dsw-alias-bg-layer-2); }
.dsh-ua-btn.primary { background:var(--dsw-alias-brand-primary); color:#fff; border-color:transparent; }
`)
const store = { open: false, listeners: new Set() }
const setOpen = (v) => { store.open = v; store.listeners.forEach((fn) => fn()) }
const useOpen = () => {
const [s, setS] = React.useState(store.open)
React.useEffect(() => { store.listeners.add(setS); return () => store.listeners.delete(setS) }, [])
return s
}
const UsageModal = (props) => {
const [state, setState] = React.useState({ loading: true, data: null, error: null })
const load = () => {
setState({ loading: true, data: null, error: null })
host.call('usage/balance', {}).then((data) => {
setState({ loading: false, data: data, error: null })
}).catch((e) => {
setState({ loading: false, data: null, error: String((e && e.message) || e) })
})
}
React.useEffect(() => { load() }, [])
const openPortal = () => { host.call('usage/openPortal', {}) }
const data = state.data
const rows = []
if (data && Array.isArray(data.balance_infos)) {
data.balance_infos.forEach((b) => {
rows.push(React.createElement('div', { className: 'dsh-ua-row', key: b.currency + '-total' },
React.createElement('span', { className: 'k' }, '总额 (' + b.currency + ')'),
React.createElement('span', { className: 'v' }, String(b.total_balance))))
rows.push(React.createElement('div', { className: 'dsh-ua-row', key: b.currency + '-granted' },
React.createElement('span', { className: 'k' }, '赠送余额'),
React.createElement('span', { className: 'v' }, String(b.granted_balance))))
rows.push(React.createElement('div', { className: 'dsh-ua-row', key: b.currency + '-topped' },
React.createElement('span', { className: 'k' }, '充值余额'),
React.createElement('span', { className: 'v' }, String(b.topped_up_balance))))
})
}
let body = null
if (state.loading) {
body = React.createElement('div', { className: 'dsh-ua-row' }, React.createElement('span', { className: 'k' }, '正在查询官方余额…'))
} else if (state.error || (data && data.error)) {
body = React.createElement('div', { className: 'dsh-ua-err' }, String(state.error || (data && data.error)))
} else {
body = React.createElement('div', null,
React.createElement('div', { className: 'dsh-ua-row' },
React.createElement('span', { className: 'k' }, '账户可用'),
React.createElement('span', { className: 'v ' + (data.is_available ? 'dsh-ua-ok' : 'dsh-ua-err') }, data.is_available ? '可用' : '不可用')),
rows)
}
return React.createElement('div', { className: 'dsh-ua-backdrop', onClick: props.onClose },
React.createElement('div', { className: 'dsh-ua-modal', onClick: (e) => e.stopPropagation() },
React.createElement('h3', null, '🐋 DeepSeek API 用量',
React.createElement('span', { style: { marginLeft: 'auto', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, '官方余额')),
body,
React.createElement('div', { className: 'dsh-ua-btns' },
React.createElement('button', { className: 'dsh-ua-btn', onClick: load }, '刷新'),
React.createElement('button', { className: 'dsh-ua-btn', onClick: openPortal }, '打开官网用量页'),
React.createElement('button', { className: 'dsh-ua-btn primary', onClick: props.onClose }, '关闭'))))
}
slots.inject('sidebar.footer.action', () => slots.register(
{ name: 'sidebar.footer.action', id: 'dsh-usage-avatar', order: 100, label: 'API 用量' },
(props) => {
const open = useOpen()
return React.createElement('button', {
className: 'dsh-ua-avatar' + (open ? ' active' : ''),
title: 'API 用量 / 余额',
onClick: () => setOpen(true),
},
React.createElement('span', { className: 'dsh-ua-avatar-icon' }, '🐋'),
props.wide ? React.createElement('span', { className: 'dsh-ua-avatar-label' }, 'API 用量') : null)
}
))
slots.inject('shell.overlay', () => slots.register(
{ name: 'shell.overlay', id: 'dsh-usage-modal', order: 100 },
() => {
const open = useOpen()
if (!open) return null
return React.createElement(UsageModal, { onClose: () => setOpen(false) })
}
))
},
}
+47
View File
@@ -0,0 +1,47 @@
// dsh-usage 插件 — Host 半
// 通过 cordis_define 定义(动态 Cordis 插件)。
// 提供两个 Package-private RPC
// usage/balance — 读取 %DSH_HOME%\.credentials.yaml 的 DEEPSEEK_API_KEY
// 调用 DeepSeek 官方余额 APIGET /user/balance),返回 JSON。
// usage/openPortal— 用系统默认浏览器打开官网用量页。
// 密钥只在 node 子进程内读取与使用,绝不打印、不入库。
return {
inject: ['shell'],
apply(ctx) {
harness.handle('usage/balance', async (args) => {
const script = [
"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')
try {
const spec = ctx.shell.resolve({ command: 'node', args: ['-e', script] })
const res = await ctx.shell.run(spec)
const stdout = String(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: String(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', args: ['/c', 'start', '', 'https://platform.deepseek.com/usage'] })
await ctx.shell.run(spec)
return { ok: true }
} catch (e) {
return { ok: false, error: String((e && e.message) || e) }
}
})
},
}