// OpenCode Go 用量面板 — Client 半区 // // 用法:把本文件内容作为 cordis_define 的 code.client 传入(函数体)。 // 注册到 shell.overlay:右下角可拖动 FAB 胶囊 + 可拖拽/缩放/最大化的悬浮仪表盘。 // 数据经 host.call('ocgo-usage:fetch') 或同源 fetch('/ocgo-usage/fetch') 获取, // 60s 自动刷新,打开面板时立即刷新。 // 面板位置/大小/FAB 位置/语言通过 localStorage 持久化; // 语言初始跟随 DSH 全局 locale(未手动选择时),面板标题栏按钮可切换中/英。 return { apply(ctx) { const slots = ctx.get('slots') const timer = ctx.get('timer') const localeSvc = ctx.get('locale') if (slots === undefined) return // ---- 中英字典 ---- const I18N = { zh: { 'title': 'OpenCode Go 用量', 'view.dsh': 'DSH', 'view.official': '官方', 'dsh.loading': 'DSH 会话扫描中(全量读取会话事件,约 10-60 秒)…', 'official.err': '官方数据不可用: {e}', 'official.loading': '官方数据加载中(增量/全量抓取)…', 'official.errConfig': '尚未配置官方凭据。请在浏览器中打开 opencode.ai 的 usage 页面,从开发者工具复制 auth Cookie 和 workspaceId,填入下方并保存。凭据只保存在本机。', 'official.errWs': '无法解析 workspaceId(cookie 可能已过期,请重新登录 opencode.ai)。', 'official.retry': '🔄 重试提取', 'official.paste': '手动填写官方凭据:', 'official.cookiePh': 'auth Cookie Value(只填值,不要 auth=)', 'official.widPh': 'workspaceId(只填 wrk_xxx)', 'official.save': '保存并刷新', 'official.saved': '已保存,刷新中…', 'foot.official': '官方账户级 usage.list · 更新 {t}', 'foot.officialTrunc': '官方数据截断(超出抓取页数上限)', 'src.quota': '配额 API', 'stat.today': '今日', 'stat.month': '本月', 'stat.total': '累计', 'stat.reqs': '{n} 次 · {t} tok', 'stat.tok': '{i} in / {o} out · cache {c}', 'donut.rolling': '滚动配额', 'donut.weekly': '本周', 'donut.month': '本月', 'donut.resets': '{t} 重置', 'donut.resetsUnknown': '重置时间未知', 'donut.limited': '⚠ 已限流', 'donut.resetIn': '{s} 后重置', 'pace.projected': '预计 {p}%', 'pace.emptyAt': '· 预计耗尽 {t}', 'warn.banner': '配额即将耗尽: {s}', 'stat.vsUp': '较昨日 +{d}%', 'stat.vsDown': '较昨日 {d}%', 'stat.vsFlat': '较昨日持平', 'btn.export': '导出', 'btn.exportTitle': '导出 CSV', 'quota.err': '配额查询失败: {e}', 'model.title': '按模型 · 共 {n} 个 · 点击展开分项', 'model.click': '点击查看费用分项', 'model.reqs': '{n} 次 · {t}', 'model.split': '输入 {a} · 输出 {b} · cache 读 {c} · cache 写 {d}', 'model.official': '金额为官方 cost(无分项)', 'model.tok': '{i} in / {o} out · cache {c} 读 / {w} 写', 'model.src': '来源: {s}', 'trend.title': '花费趋势', 'trend.7d': '7天', 'trend.14d': '14天', 'trend.30d': '30天', 'trend.day': '{d} {n} 次 {c}', 'recent.title': '最近会话', 'recent.none': '(无标题)', 'foot.src.dsh': 'DSH 会话事件', 'foot.matched': '官方回填 {n} 条', 'foot.est': '金额: 官方 cost + 定价估算', 'foot.upd': '更新 {t}', 'foot.int': '60s 自动刷新', 'update.available': '发现新版本 v{latest}(当前 v{current})——git pull 后重启 DSH 生效', 'foot.recon': '官方窗口 {o} · 本地明细 {l}', 'foot.reconTitle': '官方配额按用量单位计(部分模型限时 2×,×$60 仅是参考换算);本地为 usage.list 逐请求美元明细——两口径不可直接对比,差 {d}', 'load': '加载中…', 'err.title': '数据不可用', 'err.local': '本地数据接口 {s}', 'fab.loading': 'OpenCode Go 加载中…', 'fab.retry': 'OpenCode Go 重试', 'fab.rolling': '滚动 {p}%', 'fab.week': ' · 周 {p}%', 'fab.month': ' · 月 {p}%', 'fab.hint': ' (拖动移动,点击打开)', 'btn.refresh': '刷新', 'btn.max': '最大化', 'btn.restore': '还原', 'btn.close': '关闭', 'resize.e': '拖拽调整宽度', 'resize.s': '拖拽调整高度', 'resize.se': '拖拽缩放', 'lang.switch': '切换语言', }, en: { 'title': 'OpenCode Go Usage', 'view.dsh': 'DSH', 'view.official': 'Official', 'dsh.loading': 'Scanning DSH sessions (reading all session events, ~10-60s)…', 'official.err': 'Official data unavailable: {e}', 'official.loading': 'Loading official data (incremental/full fetch)…', 'official.errConfig': 'Official credentials are not configured. Open the opencode.ai usage page, copy the auth cookie and workspaceId from DevTools, then save them below. Credentials stay on this machine.', 'official.errWs': 'Cannot resolve workspaceId (cookie may be expired — sign in to opencode.ai again).', 'official.retry': '🔄 Retry extraction', 'official.paste': 'Enter official credentials:', 'official.cookiePh': 'auth Cookie Value (value only, no auth=)', 'official.widPh': 'workspaceId (only the wrk_xxx ID)', 'official.save': 'Save & refresh', 'official.saved': 'Saved, refreshing…', 'foot.official': 'Official account-level usage.list · updated {t}', 'foot.officialTrunc': 'Official data truncated (over fetch page cap)', 'src.quota': 'Quota API', 'stat.today': 'Today', 'stat.month': 'This month', 'stat.total': 'Total', 'stat.reqs': '{n} reqs · {t} tok', 'stat.tok': '{i} in / {o} out · cache {c}', 'donut.rolling': 'Rolling', 'donut.weekly': 'Week', 'donut.month': 'Month', 'donut.resets': 'resets {t}', 'donut.resetsUnknown': 'Reset time unknown', 'donut.limited': '⚠ rate-limited', 'donut.resetIn': 'resets in {s}', 'pace.projected': 'Projected {p}%', 'pace.emptyAt': ' · likely empty {t}', 'warn.banner': 'Quota nearly exhausted: {s}', 'stat.vsUp': '+{d}% vs yesterday', 'stat.vsDown': '{d}% vs yesterday', 'stat.vsFlat': 'flat vs yesterday', 'btn.export': 'Export', 'btn.exportTitle': 'Export CSV', 'quota.err': 'Quota query failed: {e}', 'model.title': 'By model · {n} total · click for details', 'model.click': 'Click for cost details', 'model.reqs': '{n} reqs · {t}', 'model.split': 'In {a} · Out {b} · Cache read {c} · Cache write {d}', 'model.official': 'Official cost (no breakdown)', 'model.tok': '{i} in / {o} out · cache {c} r / {w} w', 'model.src': 'Sources: {s}', 'trend.title': 'Cost trend', 'trend.7d': '7d', 'trend.14d': '14d', 'trend.30d': '30d', 'trend.day': '{d} {n} reqs {c}', 'recent.title': 'Recent sessions', 'recent.none': '(untitled)', 'foot.src.dsh': 'DSH session events', 'foot.matched': 'official-matched {n}', 'foot.est': 'Cost: official + estimated', 'foot.upd': 'Updated {t}', 'foot.int': 'Auto-refresh 60s', 'update.available': 'Update available: v{latest} (current v{current}) — git pull & restart DSH', 'foot.recon': 'Official window {o} · Local detail {l}', 'foot.reconTitle': 'Official quota is metered in usage units (some models count 2x; ×$60 is only a reference conversion); local is per-request USD from usage.list — different bases, not directly comparable, diff {d}', 'load': 'Loading…', 'err.title': 'Data unavailable', 'err.local': 'Local data endpoint {s}', 'fab.loading': 'OpenCode Go loading…', 'fab.retry': 'OpenCode Go retry', 'fab.rolling': 'Rolling {p}%', 'fab.week': ' · Wk {p}%', 'fab.month': ' · Mo {p}%', 'fab.hint': ' (drag to move, click to open)', 'btn.refresh': 'Refresh', 'btn.max': 'Maximize', 'btn.restore': 'Restore', 'btn.close': 'Close', 'resize.e': 'Drag to resize width', 'resize.s': 'Drag to resize height', 'resize.se': 'Drag to resize', 'lang.switch': 'Switch language', }, } const STYLE_TEXT = ` @keyframes ocgo-in { from { opacity: 0; transform: scale(.97) translateY(4px); } to { opacity: 1; transform: none; } } .ocgo-fab { position: fixed; right: 16px; bottom: 84px; z-index: 9999; display: flex; align-items: center; gap: 6px; padding: 8px 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px; cursor: grab; user-select: none; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary); background: linear-gradient(135deg, color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), transparent), var(--dsw-alias-bg-overlay); box-shadow: 0 4px 16px rgba(0,0,0,.18); white-space: nowrap; transition: filter .15s ease, border-color .3s ease, background .3s ease; } .ocgo-fab:hover { filter: brightness(1.12); } .ocgo-fab:active { cursor: grabbing; } .ocgo-fab.ocgo-warn-mid { border-color: var(--dsw-alias-state-warn-primary); } .ocgo-fab.ocgo-warn-hi { border-color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent), var(--dsw-alias-bg-overlay); } .ocgo-fab .ocgo-fab-sub { font-weight: 500; font-size: 10px; opacity: .8; } .ocgo-panel { position: fixed; z-index: 9999; display: flex; flex-direction: column; width: 400px; min-width: 300px; max-width: 92vw; height: 560px; min-height: 260px; max-height: 84vh; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-alias-bg-base); box-shadow: 0 12px 48px rgba(0,0,0,.32); overflow: hidden; animation: ocgo-in .16s ease; } .ocgo-panel.ocgo-max { left: 8px; top: 8px; right: 8px; bottom: 8px; width: auto; height: auto; max-width: none; max-height: none; } .ocgo-titlebar { display: flex; align-items: center; gap: 8px; padding: 8px 10px; cursor: move; user-select: none; background: linear-gradient(135deg, color-mix(in srgb, var(--dsw-alias-brand-primary) 16%, transparent), transparent), var(--dsw-alias-bg-layer-1); border-bottom: 1px solid var(--dsw-alias-border-l1); } .ocgo-title { font-size: 13px; font-weight: 700; color: var(--dsw-alias-label-primary); letter-spacing: .02em; } .ocgo-spacer { flex: 1; } .ocgo-ibtn { background: none; border: none; cursor: pointer; color: var(--dsw-alias-label-secondary); display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border-radius: 6px; transition: background .15s ease, color .15s ease; } .ocgo-ibtn:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); } .ocgo-body { flex: 1; overflow: auto; padding: 10px; display: flex; flex-direction: column; gap: 10px; font-size: 12px; color: var(--dsw-alias-label-secondary); } .ocgo-viewrow { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; } .ocgo-seg { display: flex; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; overflow: hidden; } .ocgo-seg-btn { background: none; border: none; cursor: pointer; padding: 4px 12px; font-size: 12px; color: var(--dsw-alias-label-secondary); transition: background .15s ease; } .ocgo-seg-btn.on { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent); color: var(--dsw-alias-label-primary); font-weight: 600; } .ocgo-src { font-size: 10px; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); } .ocgo-src.ok { color: var(--dsw-alias-state-success-primary); } .ocgo-src.miss { color: var(--dsw-alias-label-secondary); opacity: .6; } .ocgo-stats { display: flex; gap: 8px; } .ocgo-stat { flex: 1; display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: 10px; border: 1px solid var(--dsw-alias-border-l1); background: linear-gradient(160deg, color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent), transparent 70%), var(--dsw-alias-bg-layer-1); } .ocgo-stat-label { font-size: 10px; color: var(--dsw-alias-label-secondary); } .ocgo-stat-value { font-size: 18px; font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -.02em; color: var(--dsw-alias-label-primary); } .ocgo-stat-sub { font-size: 10px; color: var(--dsw-alias-label-secondary); opacity: .85; } .ocgo-quota { display: flex; gap: 12px; justify-content: space-around; padding: 8px; border-radius: 10px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); } .ocgo-donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 2px; } .ocgo-donut { width: 76px; height: 76px; } .ocgo-donut-val { fill: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 800; } .ocgo-donut-lbl { fill: var(--dsw-alias-label-secondary); font-size: 9px; } .ocgo-donut-time { font-size: 9px; color: var(--dsw-alias-label-secondary); } .ocgo-panel2 { display: flex; flex-direction: column; gap: 6px; padding: 8px 10px; border-radius: 10px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); } .ocgo-ptitle { font-size: 10px; font-weight: 700; color: var(--dsw-alias-label-secondary); text-transform: uppercase; letter-spacing: .06em; } .ocgo-mrow { display: flex; align-items: center; gap: 8px; cursor: pointer; border-radius: 6px; padding: 2px 4px; } .ocgo-mrow:hover { background: var(--dsw-alias-bg-layer-2); } .ocgo-mname { width: 120px; flex: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); font-size: 11px; } .ocgo-mbar { flex: 1; height: 8px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; } .ocgo-mbar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, color-mix(in srgb, var(--dsw-alias-brand-primary) 50%, transparent), var(--dsw-alias-brand-primary)); } .ocgo-mreq { width: 58px; flex: none; text-align: right; color: var(--dsw-alias-label-secondary); font-size: 10px; } .ocgo-mcost { width: 66px; flex: none; text-align: right; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary); font-weight: 600; font-size: 11px; } .ocgo-mdetail { display: flex; flex-direction: column; gap: 2px; padding: 4px 6px 6px 6px; font-size: 10px; color: var(--dsw-alias-label-secondary); border-left: 2px solid var(--dsw-alias-border-l2); margin-left: 4px; } .ocgo-prow { display: flex; align-items: center; gap: 8px; } .ocgo-pname { width: 84px; flex: none; color: var(--dsw-alias-label-primary); font-size: 11px; } .ocgo-pbar { flex: 1; height: 8px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; } .ocgo-pbar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, color-mix(in srgb, var(--dsw-alias-brand-primary) 40%, transparent), var(--dsw-alias-brand-primary)); } .ocgo-pcost { width: 66px; flex: none; text-align: right; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary); font-weight: 600; font-size: 11px; } .ocgo-days { display: flex; align-items: flex-end; gap: 4px; height: 76px; padding-top: 4px; } .ocgo-day { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 2px; height: 100%; } .ocgo-day-fill { width: 100%; max-width: 20px; border-radius: 3px 3px 0 0; background: linear-gradient(180deg, color-mix(in srgb, var(--dsw-alias-brand-primary) 25%, transparent), var(--dsw-alias-brand-primary)); transition: height .3s ease; } .ocgo-day-lbl { font-size: 8px; color: var(--dsw-alias-label-secondary); } .ocgo-srow { display: flex; align-items: baseline; gap: 8px; } .ocgo-sname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); font-size: 11px; } .ocgo-stime { color: var(--dsw-alias-label-secondary); font-size: 10px; flex: none; } .ocgo-scost { font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary); font-weight: 600; font-size: 11px; flex: none; } .ocgo-foot { display: flex; align-items: center; gap: 8px; font-size: 10px; color: var(--dsw-alias-label-secondary); opacity: .75; flex-wrap: wrap; } .ocgo-foot .ocgo-warn { color: var(--dsw-alias-state-warn-primary); opacity: 1; } .ocgo-err { display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); } .ocgo-loading { color: var(--dsw-alias-label-secondary); padding: 8px 4px; } .ocgo-edge-e { position: absolute; top: 0; right: 0; width: 8px; height: 100%; cursor: ew-resize; } .ocgo-edge-s { position: absolute; left: 0; bottom: 0; width: 100%; height: 8px; cursor: ns-resize; } .ocgo-resize { position: absolute; right: 0; bottom: 0; width: 26px; height: 26px; cursor: nwse-resize; } .ocgo-resize::before, .ocgo-resize::after { content: ''; position: absolute; right: 6px; bottom: 6px; border-right: 2px solid var(--dsw-alias-label-secondary); border-bottom: 2px solid var(--dsw-alias-label-secondary); } .ocgo-resize::before { width: 9px; height: 9px; opacity: .3; } .ocgo-resize::after { width: 5px; height: 5px; opacity: .6; } ` // 样式注入:动态模式下用 `styles` 全局(fiber 卸载自动移除); // 静态 bundle 模式下 styles 不可用,降级为直接挂