// 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) }) } )) }, }