dsh-commandcode-usage 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/client.js +331 -0
- package/index.js +166 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zouxiaoyang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# dsh-commandcode-usage
|
|
2
|
+
|
|
3
|
+
CommandCode 用量与余额面板 — 在 DSH(DeepSeek Harness)侧边栏底部显示 CommandCode API 用量、信用额度、5小时/每周窗口。
|
|
4
|
+
|
|
5
|
+
A CommandCode usage & balance panel for DeepSeek Harness — shows API usage, credit balance, and 5-hour/weekly windows from the sidebar footer.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features · 功能
|
|
10
|
+
|
|
11
|
+
- **CommandCode 用量**:请求次数、成功率、Token 消耗、总花费
|
|
12
|
+
- **信用额度**:月额度、5小时窗口、每周窗口,进度条可视化
|
|
13
|
+
- **余额与订阅**:套餐信息、订阅状态、计费周期
|
|
14
|
+
- **主题自适应**:跟随 DSH 的浅色/深色主题自动切换
|
|
15
|
+
- **侧边栏入口**:在 codex-ui 侧边栏底部提供"⚡ 用量"按钮,点击弹出面板
|
|
16
|
+
|
|
17
|
+
## Install · 安装
|
|
18
|
+
|
|
19
|
+
### 方式一:插件市场(推荐)
|
|
20
|
+
|
|
21
|
+
打开 DSH **设置 → 插件市场**,搜索 `dsh-commandcode-usage`,一键安装。
|
|
22
|
+
|
|
23
|
+
### 方式二:npm
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dsh plugin --profile web add dsh-commandcode-usage
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 方式三:手动(双半包)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# 1. 安装 npm 包
|
|
33
|
+
npm install dsh-commandcode-usage
|
|
34
|
+
|
|
35
|
+
# 2. 在 cordis.patch.yml 注册
|
|
36
|
+
# - id: dsh-commandcode-usage
|
|
37
|
+
# name: 'dsh-commandcode-usage'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration · 配置
|
|
41
|
+
|
|
42
|
+
插件通过 `cordis.patch.yml` 的 `config` 字段配置:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
- id: dsh-commandcode-usage
|
|
46
|
+
name: 'dsh-commandcode-usage'
|
|
47
|
+
config:
|
|
48
|
+
# API key(可选,默认自动从环境变量/配置文件读取)
|
|
49
|
+
apiKey: 'your-commandcode-api-key'
|
|
50
|
+
# API 基础地址(可选,默认官方)
|
|
51
|
+
apiBase: 'https://api.commandcode.ai'
|
|
52
|
+
# 环境变量名(可选,默认 COMMANDCODE_API_KEY)
|
|
53
|
+
apiKeyEnv: 'COMMANDCODE_API_KEY'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### API key 解析顺序
|
|
57
|
+
|
|
58
|
+
1. 插件 `config.apiKey`
|
|
59
|
+
2. 环境变量 `COMMANDCODE_API_KEY`
|
|
60
|
+
3. `~/.dsh/.credentials.yaml` 中的 `COMMANDCODE_API_KEY`
|
|
61
|
+
4. `launchctl getenv COMMANDCODE_API_KEY`
|
|
62
|
+
5. 其他常见配置文件(`~/.config/commandcode/` 等)
|
|
63
|
+
|
|
64
|
+
## Usage · 使用
|
|
65
|
+
|
|
66
|
+
安装并重启后,在 codex-ui 侧边栏底部点击 **"⚡ 用量"** 按钮即可查看:
|
|
67
|
+
|
|
68
|
+
- 请求次数 / 成功率 / 总花费(概览卡片)
|
|
69
|
+
- 5小时窗口 / 每周窗口用量(进度条)
|
|
70
|
+
- 月额度剩余(进度条)
|
|
71
|
+
- 账号、套餐、更新时间(页脚)
|
|
72
|
+
|
|
73
|
+
同时注册了 `commandcode_usage` 工具,agent 可在对话中查询:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
查询我的 CommandCode 用量
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Development · 开发
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git clone https://github.com/zouxiaoyang/dsh-commandcode-usage.git
|
|
83
|
+
cd dsh-commandcode-usage
|
|
84
|
+
npm install
|
|
85
|
+
# 本地开发:软链到 profile
|
|
86
|
+
ln -sfn $PWD ~/.dsh/profiles/web/node_modules/dsh-commandcode-usage
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/client.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usage-panel 客户端半:在 codex-ui 侧边栏底部注入"⚡ 用量"按钮,
|
|
3
|
+
* 点击弹出 CommandCode 用量面板(精致版 UI)。
|
|
4
|
+
*
|
|
5
|
+
* 主题自适应:复用 codex-ui 的 body[data-ds-dark-theme] 机制 —— 面板自带
|
|
6
|
+
* 主题变量,浅色/深色自动切换。
|
|
7
|
+
*
|
|
8
|
+
* 数据获取:ctx.connection.rpc.call("/dsh-usage", "report", {})
|
|
9
|
+
*/
|
|
10
|
+
window.__ModuleLoader__.load({
|
|
11
|
+
id: "dsh-commandcode-usage",
|
|
12
|
+
factory: function (require) {
|
|
13
|
+
var module = { exports: {} };
|
|
14
|
+
var exports = module.exports;
|
|
15
|
+
|
|
16
|
+
var POLL_MS = 5 * 60 * 1000; // 数据缓存 5 分钟
|
|
17
|
+
var PANEL_CLS = "dsh-usage-panel";
|
|
18
|
+
var BTN_CLS = "dsh-usage-btn";
|
|
19
|
+
|
|
20
|
+
// ---------- 注入主题样式(精致版) ----------
|
|
21
|
+
var STYLE_ID = "dsh-usage-panel-style";
|
|
22
|
+
function injectStyles() {
|
|
23
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
24
|
+
var css = [
|
|
25
|
+
// 主题变量
|
|
26
|
+
"." + PANEL_CLS + "{",
|
|
27
|
+
" --up-bg:#ffffff;--up-fg:#1a1d1c;--up-fg2:#5c6360;--up-fg3:#9aa09d;",
|
|
28
|
+
" --up-border:rgba(20,60,50,.10);--up-hover:#f0f6f4;",
|
|
29
|
+
" --up-shadow:0 12px 40px rgba(20,60,50,.14);--up-bar-bg:#eef2f1;",
|
|
30
|
+
" --up-accent:#10b981;--up-accent-soft:rgba(16,185,129,.12);",
|
|
31
|
+
" --up-card:#f8faf9;",
|
|
32
|
+
"}",
|
|
33
|
+
"body[data-ds-dark-theme] ." + PANEL_CLS + "{",
|
|
34
|
+
" --up-bg:#1e2422;--up-fg:#e8ecea;--up-fg2:#a0a8a4;--up-fg3:#6b7470;",
|
|
35
|
+
" --up-border:rgba(255,255,255,.08);--up-hover:#262e2b;",
|
|
36
|
+
" --up-shadow:0 12px 40px rgba(0,0,0,.4);--up-bar-bg:#2a3230;",
|
|
37
|
+
" --up-accent:#34d399;--up-accent-soft:rgba(52,211,153,.14);",
|
|
38
|
+
" --up-card:#232a28;",
|
|
39
|
+
"}",
|
|
40
|
+
// 面板容器
|
|
41
|
+
"." + PANEL_CLS + "{",
|
|
42
|
+
" position:fixed;z-index:9999;right:12px;top:49px;width:300px;",
|
|
43
|
+
" max-height:78vh;overflow-y:auto;overflow-x:hidden;",
|
|
44
|
+
" background:var(--up-bg);",
|
|
45
|
+
" border:1px solid var(--up-border);",
|
|
46
|
+
" border-radius:16px;",
|
|
47
|
+
" box-shadow:var(--up-shadow);",
|
|
48
|
+
" padding:0;font-family:var(--dcu-font,Inter,system-ui);",
|
|
49
|
+
" color:var(--up-fg);",
|
|
50
|
+
" display:none;",
|
|
51
|
+
" scrollbar-width:thin;scrollbar-color:var(--up-fg3) transparent;",
|
|
52
|
+
"}",
|
|
53
|
+
"." + PANEL_CLS + "::-webkit-scrollbar{width:6px}",
|
|
54
|
+
"." + PANEL_CLS + "::-webkit-scrollbar-thumb{background:var(--up-fg3);border-radius:3px}",
|
|
55
|
+
// 头部
|
|
56
|
+
"." + PANEL_CLS + " .up-head{display:flex;align-items:center;gap:8px;padding:14px 16px 12px;border-bottom:1px solid var(--up-border);background:linear-gradient(180deg,var(--up-accent-soft),transparent)}",
|
|
57
|
+
"." + PANEL_CLS + " .up-head-dot{width:8px;height:8px;border-radius:50%;background:var(--up-accent);box-shadow:0 0 0 3px var(--up-accent-soft);flex:none}",
|
|
58
|
+
"." + PANEL_CLS + " .up-head-title{font-size:13px;font-weight:700;letter-spacing:.2px;flex:1}",
|
|
59
|
+
"." + PANEL_CLS + " .up-head-sub{font-size:11px;color:var(--up-fg3);font-weight:400;margin-top:1px}",
|
|
60
|
+
"." + PANEL_CLS + " .up-icon-btn{appearance:none;border:0;background:transparent;cursor:pointer;width:26px;height:26px;border-radius:7px;display:grid;place-items:center;color:var(--up-fg3);font-size:13px;transition:background .15s,color .15s}",
|
|
61
|
+
"." + PANEL_CLS + " .up-icon-btn:hover{background:var(--up-hover);color:var(--up-fg)}",
|
|
62
|
+
// 概览卡片
|
|
63
|
+
"." + PANEL_CLS + " .up-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;padding:12px 16px}",
|
|
64
|
+
"." + PANEL_CLS + " .up-stat{background:var(--up-card);border:1px solid var(--up-border);border-radius:10px;padding:8px 6px;text-align:center}",
|
|
65
|
+
"." + PANEL_CLS + " .up-stat-num{font-size:16px;font-weight:700;color:var(--up-fg);line-height:1.2}",
|
|
66
|
+
"." + PANEL_CLS + " .up-stat-lbl{font-size:10px;color:var(--up-fg3);margin-top:2px;letter-spacing:.3px}",
|
|
67
|
+
// 分区
|
|
68
|
+
"." + PANEL_CLS + " .up-section{padding:4px 16px 8px}",
|
|
69
|
+
"." + PANEL_CLS + " .up-section-title{font-size:10px;font-weight:700;color:var(--up-fg3);text-transform:uppercase;letter-spacing:1px;margin:8px 0 4px}",
|
|
70
|
+
// 进度行
|
|
71
|
+
"." + PANEL_CLS + " .up-progress{margin:6px 0}",
|
|
72
|
+
"." + PANEL_CLS + " .up-progress-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}",
|
|
73
|
+
"." + PANEL_CLS + " .up-progress-label{font-size:12px;color:var(--up-fg2);font-weight:500}",
|
|
74
|
+
"." + PANEL_CLS + " .up-progress-val{font-size:11px;color:var(--up-fg3);font-variant-numeric:tabular-nums}",
|
|
75
|
+
"." + PANEL_CLS + " .up-progress-track{height:8px;border-radius:4px;background:var(--up-bar-bg);overflow:hidden;position:relative}",
|
|
76
|
+
"." + PANEL_CLS + " .up-progress-fill{height:100%;border-radius:4px;transition:width .5s cubic-bezier(.4,0,.2,1);position:relative}",
|
|
77
|
+
"." + PANEL_CLS + " .up-progress-fill::after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,rgba(255,255,255,.25),transparent 60%);border-radius:4px}",
|
|
78
|
+
// 页脚
|
|
79
|
+
"." + PANEL_CLS + " .up-foot{padding:10px 16px 12px;border-top:1px solid var(--up-border);display:flex;align-items:center;justify-content:space-between}",
|
|
80
|
+
"." + PANEL_CLS + " .up-foot-left{font-size:10px;color:var(--up-fg3)}",
|
|
81
|
+
"." + PANEL_CLS + " .up-foot-badge{font-size:10px;font-weight:600;color:var(--up-accent);background:var(--up-accent-soft);padding:2px 8px;border-radius:99px}",
|
|
82
|
+
// 加载/错误
|
|
83
|
+
"." + PANEL_CLS + " .up-loading{padding:24px 16px;text-align:center;font-size:12px;color:var(--up-fg3)}",
|
|
84
|
+
"." + PANEL_CLS + " .up-spinner{width:22px;height:22px;border:2px solid var(--up-bar-bg);border-top-color:var(--up-accent);border-radius:50%;margin:0 auto 8px;animation:up-spin .8s linear infinite}",
|
|
85
|
+
"@keyframes up-spin{to{transform:rotate(360deg)}}",
|
|
86
|
+
"." + PANEL_CLS + " .up-error{padding:24px 16px;text-align:center;font-size:12px;color:#e5484d}",
|
|
87
|
+
"." + PANEL_CLS + " .up-retry{margin-top:10px;padding:5px 14px;border-radius:8px;border:1px solid var(--up-border);background:var(--up-card);cursor:pointer;font-size:12px;color:var(--up-fg)}",
|
|
88
|
+
// 按钮
|
|
89
|
+
"." + BTN_CLS + "{",
|
|
90
|
+
" --up-fg2:#4e5253;--up-hover:#dfe8e5;",
|
|
91
|
+
" display:flex;align-items:center;gap:8px;width:100%;min-height:36px;",
|
|
92
|
+
" padding:0 4px;border-radius:8px;background:transparent;border:0;",
|
|
93
|
+
" cursor:pointer;color:var(--up-fg2);",
|
|
94
|
+
" font-size:14px;text-align:left;font-family:var(--dcu-font,Inter,system-ui);",
|
|
95
|
+
"}",
|
|
96
|
+
"body[data-ds-dark-theme] ." + BTN_CLS + "{",
|
|
97
|
+
" --up-fg2:#b9bab9;--up-hover:#303432;",
|
|
98
|
+
"}",
|
|
99
|
+
"." + BTN_CLS + ":hover{background:var(--up-hover);}",
|
|
100
|
+
"." + BTN_CLS + " .up-btn-icon{width:20px;height:20px;display:grid;place-items:center;font-size:14px;}",
|
|
101
|
+
].join("\n");
|
|
102
|
+
var style = document.createElement("style");
|
|
103
|
+
style.id = STYLE_ID;
|
|
104
|
+
style.textContent = css;
|
|
105
|
+
document.head.appendChild(style);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------- 格式化 ----------
|
|
109
|
+
function fmtMoney(v) { return "$" + (typeof v === "number" ? v.toFixed(2) : "—"); }
|
|
110
|
+
function fmtNum(v) { return typeof v === "number" ? v.toLocaleString() : "—"; }
|
|
111
|
+
function fmtCompact(v) {
|
|
112
|
+
if (typeof v !== "number") return "—";
|
|
113
|
+
if (v >= 1e9) return (v / 1e9).toFixed(1) + "B";
|
|
114
|
+
if (v >= 1e6) return (v / 1e6).toFixed(1) + "M";
|
|
115
|
+
if (v >= 1e3) return (v / 1e3).toFixed(1) + "K";
|
|
116
|
+
return String(v);
|
|
117
|
+
}
|
|
118
|
+
function pct(used, cap) { return cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0; }
|
|
119
|
+
function barColor(p) { return p >= 80 ? "#e5484d" : p >= 50 ? "#f5a623" : "var(--up-accent)"; }
|
|
120
|
+
|
|
121
|
+
// ---------- RPC 调 host ----------
|
|
122
|
+
function callUsage(ctx) {
|
|
123
|
+
return new Promise(function (resolve) {
|
|
124
|
+
try {
|
|
125
|
+
var rpc = ctx && ctx.connection && ctx.connection.rpc;
|
|
126
|
+
if (rpc && typeof rpc.call === "function") {
|
|
127
|
+
rpc.call("/dsh-usage", "report", {}).then(function (res) {
|
|
128
|
+
if (res && res.ok && res.value) resolve(res.value);
|
|
129
|
+
else resolve({ error: (res && (res.error || (res.value && res.value.error))) || "RPC 返回异常" });
|
|
130
|
+
}).catch(function (e) { resolve({ error: String(e && e.message || e) }); });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
resolve({ error: "RPC 通道不可用" });
|
|
134
|
+
} catch (e) { resolve({ error: String(e && e.message || e) }); }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------- 精致面板 ----------
|
|
139
|
+
function buildPanel(ctx) {
|
|
140
|
+
var wrap = document.createElement("div");
|
|
141
|
+
wrap.className = PANEL_CLS;
|
|
142
|
+
var state = { data: null, error: null, loading: false };
|
|
143
|
+
|
|
144
|
+
// 头部(固定结构,不随 render 重建)
|
|
145
|
+
var head = document.createElement("div");
|
|
146
|
+
head.className = "up-head";
|
|
147
|
+
var dot = document.createElement("span");
|
|
148
|
+
dot.className = "up-head-dot";
|
|
149
|
+
var headTitles = document.createElement("div");
|
|
150
|
+
headTitles.style.cssText = "flex:1;min-width:0";
|
|
151
|
+
var headTitle = document.createElement("div");
|
|
152
|
+
headTitle.className = "up-head-title";
|
|
153
|
+
headTitle.textContent = "CommandCode";
|
|
154
|
+
var headSub = document.createElement("div");
|
|
155
|
+
headSub.className = "up-head-sub";
|
|
156
|
+
headSub.textContent = "用量 · 余额";
|
|
157
|
+
headTitles.appendChild(headTitle); headTitles.appendChild(headSub);
|
|
158
|
+
var refreshBtn = document.createElement("button");
|
|
159
|
+
refreshBtn.className = "up-icon-btn";
|
|
160
|
+
refreshBtn.textContent = "↻";
|
|
161
|
+
refreshBtn.title = "刷新";
|
|
162
|
+
refreshBtn.onclick = load;
|
|
163
|
+
head.appendChild(dot); head.appendChild(headTitles); head.appendChild(refreshBtn);
|
|
164
|
+
wrap.appendChild(head);
|
|
165
|
+
|
|
166
|
+
// 主体(render 时重建)
|
|
167
|
+
var body = document.createElement("div");
|
|
168
|
+
wrap.appendChild(body);
|
|
169
|
+
|
|
170
|
+
function el(tag, cls, text) {
|
|
171
|
+
var e = document.createElement(tag);
|
|
172
|
+
if (cls) e.className = cls;
|
|
173
|
+
if (text !== undefined && text !== null) e.textContent = text;
|
|
174
|
+
return e;
|
|
175
|
+
}
|
|
176
|
+
function statRow(num, label) {
|
|
177
|
+
var s = el("div", "up-stat");
|
|
178
|
+
s.appendChild(el("div", "up-stat-num", num));
|
|
179
|
+
s.appendChild(el("div", "up-stat-lbl", label));
|
|
180
|
+
return s;
|
|
181
|
+
}
|
|
182
|
+
function progressRow(label, used, cap) {
|
|
183
|
+
var p = pct(used, cap);
|
|
184
|
+
var pr = el("div", "up-progress");
|
|
185
|
+
var top = el("div", "up-progress-top");
|
|
186
|
+
top.appendChild(el("span", "up-progress-label", label));
|
|
187
|
+
top.appendChild(el("span", "up-progress-val", fmtMoney(used) + " / " + fmtMoney(cap) + " · " + p + "%"));
|
|
188
|
+
pr.appendChild(top);
|
|
189
|
+
var track = el("div", "up-progress-track");
|
|
190
|
+
var fill = el("div", "up-progress-fill");
|
|
191
|
+
fill.style.width = p + "%";
|
|
192
|
+
fill.style.background = barColor(p);
|
|
193
|
+
track.appendChild(fill);
|
|
194
|
+
pr.appendChild(track);
|
|
195
|
+
return pr;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function render() {
|
|
199
|
+
body.textContent = "";
|
|
200
|
+
|
|
201
|
+
if (state.loading) {
|
|
202
|
+
var ld = el("div", "up-loading");
|
|
203
|
+
ld.appendChild(el("div", "up-spinner"));
|
|
204
|
+
ld.appendChild(el("div", "", "加载中…"));
|
|
205
|
+
body.appendChild(ld);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (state.error) {
|
|
209
|
+
var er = el("div", "up-error", "加载失败:" + state.error);
|
|
210
|
+
var retry = el("button", "up-retry", "重试");
|
|
211
|
+
retry.onclick = load;
|
|
212
|
+
er.appendChild(retry);
|
|
213
|
+
body.appendChild(er);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
var d = state.data || {};
|
|
218
|
+
var account = d.account && d.account.user || {};
|
|
219
|
+
var usage = d.usage || {};
|
|
220
|
+
var credits = d.credits && d.credits.credits || {};
|
|
221
|
+
var win = d.credits && d.credits.windowLimits || {};
|
|
222
|
+
var sub = d.subscription && d.subscription.data || {};
|
|
223
|
+
|
|
224
|
+
// 概览卡片:请求 / 成功 / 花费
|
|
225
|
+
if (usage.totalCount !== undefined) {
|
|
226
|
+
var stats = el("div", "up-stats");
|
|
227
|
+
stats.appendChild(statRow(fmtCompact(usage.totalCount), "请求"));
|
|
228
|
+
stats.appendChild(statRow((usage.successRate !== undefined ? usage.successRate + "%" : "—"), "成功率"));
|
|
229
|
+
stats.appendChild(statRow(fmtMoney(usage.totalCost), "花费"));
|
|
230
|
+
body.appendChild(stats);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 窗口用量分区
|
|
234
|
+
var secWin = el("div", "up-section");
|
|
235
|
+
secWin.appendChild(el("div", "up-section-title", "窗口用量"));
|
|
236
|
+
if (win.fiveHour) secWin.appendChild(progressRow("5小时", win.fiveHour.used, win.fiveHour.cap));
|
|
237
|
+
if (win.weekly) secWin.appendChild(progressRow("每周", win.weekly.used, win.weekly.cap));
|
|
238
|
+
body.appendChild(secWin);
|
|
239
|
+
|
|
240
|
+
// 信用分区
|
|
241
|
+
if (credits.monthlyCredits !== undefined) {
|
|
242
|
+
var secCred = el("div", "up-section");
|
|
243
|
+
secCred.appendChild(el("div", "up-section-title", "月额度"));
|
|
244
|
+
var usedMonthly = win.weekly ? win.weekly.used : (usage.totalCredits || 0);
|
|
245
|
+
var capMonthly = usedMonthly + credits.monthlyCredits;
|
|
246
|
+
secCred.appendChild(progressRow("剩余", credits.monthlyCredits, capMonthly));
|
|
247
|
+
body.appendChild(secCred);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 底部:套餐 + 更新时间
|
|
251
|
+
var foot = el("div", "up-foot");
|
|
252
|
+
var footL = el("div", "up-foot-left");
|
|
253
|
+
footL.textContent = (account.userName || "—") + " · " + (d.fetchedAt ? new Date(d.fetchedAt).toLocaleTimeString() : "");
|
|
254
|
+
foot.appendChild(footL);
|
|
255
|
+
if (sub.planId) foot.appendChild(el("span", "up-foot-badge", sub.planId));
|
|
256
|
+
body.appendChild(foot);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function load() {
|
|
260
|
+
state.loading = true; state.error = null;
|
|
261
|
+
render();
|
|
262
|
+
callUsage(ctx).then(function (res) {
|
|
263
|
+
state.loading = false;
|
|
264
|
+
if (res && res.error) state.error = res.error;
|
|
265
|
+
else {
|
|
266
|
+
try { state.data = typeof res === "string" ? JSON.parse(res) : res; }
|
|
267
|
+
catch (e) { state.error = "解析失败"; }
|
|
268
|
+
}
|
|
269
|
+
render();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 点击面板外部关闭
|
|
274
|
+
function onDocClick(ev) {
|
|
275
|
+
if (!wrap.contains(ev.target) && !btn.contains(ev.target)) hide();
|
|
276
|
+
}
|
|
277
|
+
function show() { load(); wrap.style.display = "block"; document.addEventListener("click", onDocClick); }
|
|
278
|
+
function hide() { wrap.style.display = "none"; document.removeEventListener("click", onDocClick); }
|
|
279
|
+
function toggle() { if (wrap.style.display === "none") show(); else hide(); }
|
|
280
|
+
|
|
281
|
+
// 定时刷新
|
|
282
|
+
var timer = setInterval(function () { if (wrap.style.display !== "none") load(); }, POLL_MS);
|
|
283
|
+
|
|
284
|
+
return { wrap: wrap, toggle: toggle };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------- 按钮 ----------
|
|
288
|
+
function buildButton(panel) {
|
|
289
|
+
var btn = document.createElement("button");
|
|
290
|
+
btn.type = "button";
|
|
291
|
+
btn.className = BTN_CLS;
|
|
292
|
+
var icon = document.createElement("span");
|
|
293
|
+
icon.className = "up-btn-icon";
|
|
294
|
+
icon.textContent = "⚡";
|
|
295
|
+
var label = document.createElement("span");
|
|
296
|
+
label.textContent = "用量";
|
|
297
|
+
btn.appendChild(icon); btn.appendChild(label);
|
|
298
|
+
btn.onclick = function (e) { e.stopPropagation(); panel.toggle(); };
|
|
299
|
+
return btn;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ---------- apply ----------
|
|
303
|
+
exports.apply = function apply(ctx) {
|
|
304
|
+
if (typeof document === "undefined") return;
|
|
305
|
+
if (document.documentElement.dataset.dshUsagePanel === "on") return;
|
|
306
|
+
document.documentElement.dataset.dshUsagePanel = "on";
|
|
307
|
+
|
|
308
|
+
injectStyles();
|
|
309
|
+
|
|
310
|
+
var panel = buildPanel(ctx);
|
|
311
|
+
var btn = buildButton(panel);
|
|
312
|
+
document.body.appendChild(panel.wrap);
|
|
313
|
+
|
|
314
|
+
function tryAttach() {
|
|
315
|
+
var foot = document.querySelector(".dcu-footer-actions");
|
|
316
|
+
if (foot && !foot.contains(btn)) {
|
|
317
|
+
foot.appendChild(btn);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
var tries = 0;
|
|
323
|
+
var timer = setInterval(function () {
|
|
324
|
+
if (tryAttach() || tries > 40) clearInterval(timer);
|
|
325
|
+
tries++;
|
|
326
|
+
}, 250);
|
|
327
|
+
};
|
|
328
|
+
exports.inject = ["slots", "connection"];
|
|
329
|
+
return module.exports;
|
|
330
|
+
}
|
|
331
|
+
});
|
package/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// dsh-commandcode-usage 宿主半:注册 commandcode_usage 工具 + /dsh-usage RPC channel。
|
|
2
|
+
// 数据来自 CommandCode /alpha/* 端点(whoami、usage/summary、billing/credits、
|
|
3
|
+
// billing/subscriptions)。
|
|
4
|
+
//
|
|
5
|
+
// API key 解析顺序(通用化,兼容不同用户环境):
|
|
6
|
+
// 1. 插件 config.apiKey(cordis.patch.yml 里配置,优先级最高)
|
|
7
|
+
// 2. 环境变量 COMMANDCODE_API_KEY
|
|
8
|
+
// 3. ~/.dsh/.credentials.yaml 里的 COMMANDCODE_API_KEY
|
|
9
|
+
// 4. launchctl getenv COMMANDCODE_API_KEY
|
|
10
|
+
// 5. 其他常见配置文件(~/.config/commandcode/credentials.json 等)
|
|
11
|
+
'use strict';
|
|
12
|
+
const { readFileSync } = require('node:fs');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
module.exports.name = 'dsh-commandcode-usage';
|
|
17
|
+
module.exports.inject = ['tools', 'connection'];
|
|
18
|
+
|
|
19
|
+
// 4 个 /alpha 端点(来自 @mars-sea/dsh-commandcode-provider 源码 adapter.ts)
|
|
20
|
+
const ENDPOINTS = [
|
|
21
|
+
{ key: 'account', path: '/alpha/whoami' },
|
|
22
|
+
{ key: 'usage', path: '/alpha/usage/summary' },
|
|
23
|
+
{ key: 'credits', path: '/alpha/billing/credits' },
|
|
24
|
+
{ key: 'subscription', path: '/alpha/billing/subscriptions' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// 默认配置
|
|
28
|
+
const DEFAULT_CONFIG = {
|
|
29
|
+
apiBase: 'https://api.commandcode.ai',
|
|
30
|
+
apiKeyEnv: 'COMMANDCODE_API_KEY',
|
|
31
|
+
timeoutMs: 12000,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 从 yaml 里提取 key(宽松匹配)
|
|
35
|
+
function keyFromYaml(raw) {
|
|
36
|
+
const m = raw.match(/COMMANDCODE_API_KEY:\s*['"]?([^'"\n\s]+)['"]?/);
|
|
37
|
+
return m ? m[1] : '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 尝试多个候选配置文件
|
|
41
|
+
function readApiKeyFromFiles() {
|
|
42
|
+
const home = os.homedir();
|
|
43
|
+
const candidates = [
|
|
44
|
+
path.join(home, '.dsh', '.credentials.yaml'),
|
|
45
|
+
path.join(home, '.config', 'commandcode', 'credentials.yaml'),
|
|
46
|
+
path.join(home, '.config', 'commandcode', 'config.json'),
|
|
47
|
+
path.join(home, '.commandcode', 'credentials.json'),
|
|
48
|
+
];
|
|
49
|
+
for (const file of candidates) {
|
|
50
|
+
try {
|
|
51
|
+
const raw = readFileSync(file, 'utf8');
|
|
52
|
+
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
53
|
+
const k = keyFromYaml(raw);
|
|
54
|
+
if (k) return k;
|
|
55
|
+
} else {
|
|
56
|
+
try {
|
|
57
|
+
const d = JSON.parse(raw);
|
|
58
|
+
const k = d && (d.apiKey || d.api_key || d.COMMANDCODE_API_KEY || (d.refs && d.refs.COMMANDCODE_API_KEY));
|
|
59
|
+
if (k) return String(k);
|
|
60
|
+
} catch { /* not json */ }
|
|
61
|
+
}
|
|
62
|
+
} catch { /* file not found */ }
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readApiKey(config) {
|
|
68
|
+
// 1. 插件 config(patch 里配置)
|
|
69
|
+
if (config.apiKey) return config.apiKey;
|
|
70
|
+
// 2. 环境变量
|
|
71
|
+
const envName = config.apiKeyEnv || DEFAULT_CONFIG.apiKeyEnv;
|
|
72
|
+
if (process.env[envName]) return process.env[envName];
|
|
73
|
+
// 3. 配置文件
|
|
74
|
+
const fromFiles = readApiKeyFromFiles();
|
|
75
|
+
if (fromFiles) return fromFiles;
|
|
76
|
+
// 4. launchctl getenv
|
|
77
|
+
try {
|
|
78
|
+
const { execFileSync } = require('node:child_process');
|
|
79
|
+
const out = execFileSync('launchctl', ['getenv', envName], { encoding: 'utf8' }).trim();
|
|
80
|
+
if (out) return out;
|
|
81
|
+
} catch {}
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function fetchJson(url, apiKey, config) {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timer = setTimeout(() => controller.abort(), config.timeoutMs || DEFAULT_CONFIG.timeoutMs);
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(url, {
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: 'Bearer ' + apiKey,
|
|
93
|
+
'x-command-code-version': '1.0.0',
|
|
94
|
+
'x-cli-environment': 'production',
|
|
95
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
99
|
+
return await res.json();
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function fetchUsageReport(config) {
|
|
106
|
+
const apiKey = readApiKey(config);
|
|
107
|
+
if (!apiKey) {
|
|
108
|
+
return { error: '未找到 COMMANDCODE_API_KEY(可通过 cordis.patch.yml config.apiKey 配置,或设置环境变量)' };
|
|
109
|
+
}
|
|
110
|
+
const base = config.apiBase || DEFAULT_CONFIG.apiBase;
|
|
111
|
+
const results = {};
|
|
112
|
+
const failures = [];
|
|
113
|
+
for (const ep of ENDPOINTS) {
|
|
114
|
+
try {
|
|
115
|
+
results[ep.key] = await fetchJson(base + ep.path, apiKey, config);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
failures.push(ep.path + ': ' + (e instanceof Error ? e.message : String(e)));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { fetchedAt: new Date().toISOString(), failures, ...results };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 同步 apply:避免 async + await 导致 ctx.connection 状态不完整
|
|
124
|
+
module.exports.apply = function apply(ctx, rawConfig) {
|
|
125
|
+
const config = Object.assign({}, DEFAULT_CONFIG, rawConfig || {});
|
|
126
|
+
|
|
127
|
+
// 1. 注册工具(agent 可用 / 对话可查)
|
|
128
|
+
try {
|
|
129
|
+
const { defineTool } = require('@deepseek-ai/dsh-tools');
|
|
130
|
+
ctx.tools.register(defineTool({
|
|
131
|
+
name: 'commandcode_usage',
|
|
132
|
+
description: '查询 CommandCode 账号用量与余额(/alpha/whoami、usage/summary、billing/credits、billing/subscriptions)。返回账号信息、累计用量、信用额度、5小时/每周窗口、订阅套餐。',
|
|
133
|
+
parameters: {},
|
|
134
|
+
output: {
|
|
135
|
+
schema: { type: 'string' },
|
|
136
|
+
render: (_args, value) => [{ type: 'text', text: value }],
|
|
137
|
+
},
|
|
138
|
+
async execute() {
|
|
139
|
+
const report = await fetchUsageReport(config);
|
|
140
|
+
return report.error ? report.error : JSON.stringify(report, null, 2);
|
|
141
|
+
},
|
|
142
|
+
}));
|
|
143
|
+
console.log('[dsh-commandcode-usage] 已注册 commandcode_usage 工具');
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.error('[dsh-commandcode-usage] 工具注册失败:', e instanceof Error ? e.message : String(e));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2. 注册 RPC channel(client 侧面板取数)
|
|
149
|
+
try {
|
|
150
|
+
const rpc = ctx && ctx.connection && ctx.connection.rpc;
|
|
151
|
+
if (rpc && typeof rpc.handle === 'function') {
|
|
152
|
+
rpc.handle('/dsh-usage', async (endpoint, _payload, _signal) => {
|
|
153
|
+
if (endpoint === 'report') {
|
|
154
|
+
const report = await fetchUsageReport(config);
|
|
155
|
+
return { ok: !report.error, value: report };
|
|
156
|
+
}
|
|
157
|
+
return { ok: false, error: 'unknown endpoint: ' + endpoint };
|
|
158
|
+
}, { authority: 'loopback' });
|
|
159
|
+
console.log('[dsh-commandcode-usage] 已注册 RPC channel /dsh-usage');
|
|
160
|
+
} else {
|
|
161
|
+
console.log('[dsh-commandcode-usage] 无 rpc.handle 可用,跳过 RPC channel');
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.error('[dsh-commandcode-usage] RPC 注册失败:', e instanceof Error ? e.message : String(e));
|
|
165
|
+
}
|
|
166
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-commandcode-usage",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CommandCode 用量与余额面板 — 在 codex-ui 侧边栏底部显示 CommandCode API 用量、信用额度、5小时/每周窗口。 · CommandCode usage & balance panel for DeepSeek Harness: shows API usage, credit balance, 5-hour/weekly windows from the sidebar footer.",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js",
|
|
8
|
+
"./client": "./client.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"client.js",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"dsh",
|
|
19
|
+
"deepseek-harness",
|
|
20
|
+
"commandcode",
|
|
21
|
+
"usage",
|
|
22
|
+
"balance",
|
|
23
|
+
"plugin"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"dsh": {
|
|
30
|
+
"client": {
|
|
31
|
+
"platform": "web",
|
|
32
|
+
"inject": [
|
|
33
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
34
|
+
"@deepseek-ai/dsh-client-locale",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
41
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.5"
|
|
42
|
+
}
|
|
43
|
+
}
|