Files
dsh-enhancements/plugins/dsh-usage/client.js
T
江晨 fdafd5d6b3 chore: 初始化 dsh-enhancements 留档仓库
- 插件源码:dsh-usage(用户头像 + DeepSeek 官方 API 消耗面板)
- 工具:dl-gh.mjs(node fetch 下载 gh,绕开沙箱 curl 限制)
- 文档:PLAN.md 路线图 + mmx 视觉桥接补丁说明
- 资产:DeepSeek 鲸鱼图标
2026-08-17 11:54:40 +08:00

112 lines
6.8 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 插件 — 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) })
}
))
},
}