- 插件源码:dsh-usage(用户头像 + DeepSeek 官方 API 消耗面板) - 工具:dl-gh.mjs(node fetch 下载 gh,绕开沙箱 curl 限制) - 文档:PLAN.md 路线图 + mmx 视觉桥接补丁说明 - 资产:DeepSeek 鲸鱼图标
112 lines
6.8 KiB
JavaScript
112 lines
6.8 KiB
JavaScript
// dsh-usage 插件 — Client 半
|
||
// 注册两处 UI:
|
||
// sidebar.footer.action 'dsh-usage-avatar' — 设置旁的 🐋 用户头像按钮
|
||
// shell.overlay 'dsh-usage-modal' — 点击头像弹出的官方余额模态面板
|
||
// 两个组件通过插件内微型 store(open + 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) })
|
||
}
|
||
))
|
||
},
|
||
}
|