@yinxe/opencode-tui-usage 0.0.6
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/README.md +285 -0
- package/dist/components.d.ts +20 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.jsx +22 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/quota/config.d.ts +4 -0
- package/dist/quota/config.d.ts.map +1 -0
- package/dist/quota/config.js +22 -0
- package/dist/quota/provider.d.ts +9 -0
- package/dist/quota/provider.d.ts.map +1 -0
- package/dist/quota/provider.js +9 -0
- package/dist/quota/providers/minimax.d.ts +13 -0
- package/dist/quota/providers/minimax.d.ts.map +1 -0
- package/dist/quota/providers/minimax.js +94 -0
- package/dist/quota/providers/opencode-go.d.ts +14 -0
- package/dist/quota/providers/opencode-go.d.ts.map +1 -0
- package/dist/quota/providers/opencode-go.js +85 -0
- package/dist/quota/service.d.ts +15 -0
- package/dist/quota/service.d.ts.map +1 -0
- package/dist/quota/service.js +47 -0
- package/dist/quota/types.d.ts +21 -0
- package/dist/quota/types.d.ts.map +1 -0
- package/dist/quota/types.js +1 -0
- package/dist/session-info.d.ts +8 -0
- package/dist/session-info.d.ts.map +1 -0
- package/dist/session-info.jsx +44 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.jsx +24 -0
- package/dist/usage.d.ts +14 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.jsx +162 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# OpenCode TUI Usage Plugin
|
|
2
|
+
|
|
3
|
+
OpenCode TUI 插件,在侧边栏显示用量和额度信息,支持多额度 provider。
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## 功能特性
|
|
8
|
+
|
|
9
|
+
- 📊 实时显示 Rolling / Weekly / Monthly 三种维度的额度使用情况
|
|
10
|
+
- 🎨 彩色标签:Rolling(绿) / Weekly(黄) / Monthly(蓝)
|
|
11
|
+
- 📈 进度条可视化展示使用比例
|
|
12
|
+
- 🔄 自动根据当前会话的 provider 切换数据源
|
|
13
|
+
- 🛠️ 支持扩展新的 provider 适配器
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
包发布在 npm 和 GitHub Packages,推荐从 npm 安装。
|
|
18
|
+
|
|
19
|
+
### 从 npm 安装(推荐)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @yinx-in/opencode-tui-usage
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 从 GitHub Packages 安装
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 设置 registry
|
|
29
|
+
npm config set @yinx-in:registry https://npm.pkg.github.com
|
|
30
|
+
|
|
31
|
+
# 登录(需要 GitHub Personal Access Token)
|
|
32
|
+
echo "YOUR_GITHUB_TOKEN" | npm login --registry=https://npm.pkg.github.com --username=YOUR_GITHUB_USERNAME --email=you@example.com
|
|
33
|
+
|
|
34
|
+
# 安装
|
|
35
|
+
npm install @yinx-in/opencode-tui-usage
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 配置插件
|
|
39
|
+
|
|
40
|
+
安装后,在 `~/.config/opencode/tui.json` 中添加:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
45
|
+
"plugin": ["@yinx-in/opencode-tui-usage"]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 本地安装(开发)
|
|
50
|
+
|
|
51
|
+
1. 克隆或下载此项目到本地
|
|
52
|
+
2. 在 `~/.config/opencode/tui.json` 中添加插件路径:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
57
|
+
"plugin": ["/absolute/path/to/opencode-tui-usage-plugin"]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
3. 重启 OpenCode 使插件生效
|
|
62
|
+
|
|
63
|
+
## 配置额度 Provider
|
|
64
|
+
|
|
65
|
+
插件支持多个额度 provider,会根据当前会话的 `providerID` 自动选择对应的适配器。
|
|
66
|
+
|
|
67
|
+
### MiniMax-CN
|
|
68
|
+
|
|
69
|
+
适用于 `providerID` 为 `minimax-cn-coding-plan` 的会话。
|
|
70
|
+
|
|
71
|
+
创建 `~/.config/opencode/usage.provider.json`:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"providers": {
|
|
76
|
+
"minimax-cn-coding-plan": {
|
|
77
|
+
"apiKey": "${MINIMAX_API_KEY}"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
设置环境变量:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export MINIMAX_API_KEY="your-api-key-here"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### OpenCode-Go
|
|
90
|
+
|
|
91
|
+
适用于 `providerID` 为 `opencode-go` 的会话。
|
|
92
|
+
|
|
93
|
+
在 `~/.config/opencode/usage.provider.json` 中添加:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"providers": {
|
|
98
|
+
"opencode-go": {
|
|
99
|
+
"cookie": "${OPENCODE_GO_AUTH_COOKIE}",
|
|
100
|
+
"workspaceId": "${OPENCODE_GO_WORKSPACE_ID}"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
设置环境变量:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
export OPENCODE_GO_AUTH_COOKIE="your-cookie"
|
|
110
|
+
export OPENCODE_GO_WORKSPACE_ID="wrk_xxxxxxxxxxxx"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 获取 OpenCode-Go 配置
|
|
114
|
+
|
|
115
|
+
1. 登录 https://opencode.ai
|
|
116
|
+
2. 打开浏览器开发者工具 → Network
|
|
117
|
+
3. 访问 `/workspace/{workspaceId}/usage` 页面
|
|
118
|
+
4. 找到 `_server` 请求,从 Request Headers 复制完整的 `cookie`
|
|
119
|
+
5. workspaceId 从 URL 中获取(格式:`wrk_` 开头)
|
|
120
|
+
|
|
121
|
+
## 开发
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# 安装依赖
|
|
125
|
+
npm install
|
|
126
|
+
|
|
127
|
+
# 类型检查
|
|
128
|
+
npm run lint
|
|
129
|
+
|
|
130
|
+
# 构建输出到 dist/
|
|
131
|
+
npm run build
|
|
132
|
+
|
|
133
|
+
# 监听模式(开发时使用)
|
|
134
|
+
npm run dev
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## 目录结构
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
src/
|
|
141
|
+
├── tui.tsx # 插件入口,注册 sidebar_content slot
|
|
142
|
+
├── usage.tsx # Usage 组件(彩色标签 + 进度条)
|
|
143
|
+
├── session-info.tsx # Session Info 组件
|
|
144
|
+
├── components.tsx # 可复用组件
|
|
145
|
+
└── quota/ # 额度服务
|
|
146
|
+
├── types.ts # QuotaData, QuotaResult 类型定义
|
|
147
|
+
├── provider.ts # QuotaProvider 接口
|
|
148
|
+
├── service.ts # QuotaService 管理多 provider
|
|
149
|
+
├── config.ts # 读取 usage.provider.json
|
|
150
|
+
└── providers/ # provider 适配器
|
|
151
|
+
├── minimax.ts
|
|
152
|
+
└── opencode-go.ts
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 添加新的 Provider 适配器
|
|
156
|
+
|
|
157
|
+
如果需要支持新的额度来源(如其他 AI provider),按以下步骤添加:
|
|
158
|
+
|
|
159
|
+
### 1. 创建适配器文件
|
|
160
|
+
|
|
161
|
+
在 `src/quota/providers/` 下创建 `{provider-name}.ts`:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import type { QuotaData, ProviderConfig } from "../types.js";
|
|
165
|
+
import { QuotaProvider, resolveEnvVar } from "../provider.js";
|
|
166
|
+
|
|
167
|
+
export class MyQuotaProvider implements QuotaProvider {
|
|
168
|
+
readonly name = "my-provider"; // 与 usage.provider.json 中的 key 对应
|
|
169
|
+
|
|
170
|
+
init(config: ProviderConfig, _credentials: Record<string, unknown>): void {
|
|
171
|
+
// 读取 config 并解析环境变量
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async fetchQuota(): Promise<QuotaData | null> {
|
|
175
|
+
// 调用 API 并返回 QuotaData 格式
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 2. 注册到 QuotaService
|
|
181
|
+
|
|
182
|
+
在 `src/quota/service.ts` 构造函数中添加:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
this.registerProvider(new MyQuotaProvider());
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 3. 添加配置
|
|
189
|
+
|
|
190
|
+
在 `~/.config/opencode/usage.provider.json` 中添加 provider 配置:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"providers": {
|
|
195
|
+
"my-provider": {
|
|
196
|
+
"apiKey": "${MY_API_KEY}"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 4. 测试
|
|
203
|
+
|
|
204
|
+
切换到对应 provider 的会话,检查侧边栏是否正常显示额度数据。
|
|
205
|
+
|
|
206
|
+
## 调试
|
|
207
|
+
|
|
208
|
+
查看插件日志:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
cat ~/.local/share/opencode/log/$(ls -t ~/.local/share/opencode/log/ | head -1) | grep -i "tui.plugin\|QuotaService\|MiniMaxCN\|OpenCodeGo"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
常见问题:
|
|
215
|
+
|
|
216
|
+
| 问题 | 原因 | 解决方案 |
|
|
217
|
+
|------|------|----------|
|
|
218
|
+
| 侧边栏无显示 | 缺少 `"oc-plugin": ["tui"]` | 检查 package.json |
|
|
219
|
+
| JSX 报错 | 缺少 pragma | 每个 .tsx 顶部加 `/** @jsxImportSource @opentui/solid */` |
|
|
220
|
+
| 显示 "No data" | provider 未注册或配置缺失 | 检查 usage.provider.json 和环境变量 |
|
|
221
|
+
| opencode-go 500 错误 | cookie 过期或 headers 不对 | 重新抓包获取最新 cookie |
|
|
222
|
+
|
|
223
|
+
## 技术栈
|
|
224
|
+
|
|
225
|
+
- **Solid.js** - 响应式 UI 框架
|
|
226
|
+
- **@opentui/solid** - TUI 组件库(`<box>`, `<text>` 等)
|
|
227
|
+
- **TypeScript** - 类型安全
|
|
228
|
+
|
|
229
|
+
## CI/CD 自动化发版
|
|
230
|
+
|
|
231
|
+
本项目使用 GitHub Actions 实现自动化发版。推送 `v*` 格式的 tag 后,**同时发布到 npm 和 GitHub Packages**。
|
|
232
|
+
|
|
233
|
+
### 发布流程
|
|
234
|
+
|
|
235
|
+
使用 `npm version` 管理版本号(会自动创建 tag):
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# 更新版本并创建 tag
|
|
239
|
+
npm version patch # 0.0.1 → 0.0.2
|
|
240
|
+
npm version minor # 0.0.1 → 0.1.0
|
|
241
|
+
npm version major # 0.0.1 → 1.0.0
|
|
242
|
+
|
|
243
|
+
# 推送 tag 触发 CI/CD
|
|
244
|
+
git push origin v0.0.3
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 自动触发的工作流
|
|
248
|
+
|
|
249
|
+
推送 tag 后,以下 job 会自动执行:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
build → publish-npm + publish-github
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
| Job | 目标 | 依赖 |
|
|
256
|
+
|-----|------|------|
|
|
257
|
+
| `build` | 安装依赖、构建项目、运行测试 | - |
|
|
258
|
+
| `publish-npm` | 发布到 [npm registry](https://www.npmjs.com/) | build |
|
|
259
|
+
| `publish-github` | 发布到 [GitHub Packages](https://github.com/Yinxe/opencode-tui-usage/packages) | build |
|
|
260
|
+
|
|
261
|
+
### 首次发版配置
|
|
262
|
+
|
|
263
|
+
1. **GitHub Packages 认证**
|
|
264
|
+
- 无需额外配置,使用内置 `GITHUB_TOKEN`
|
|
265
|
+
|
|
266
|
+
2. **npm 认证**(如需发布到 npm)
|
|
267
|
+
- 在 [npm.npmjs.com](https://www.npmjs.com/) 创建 Access Token
|
|
268
|
+
- 在 GitHub 仓库 Settings → Secrets and variables → Actions 添加 secret:
|
|
269
|
+
- Name: `npm_token`
|
|
270
|
+
- Secret: 你的 npm access token
|
|
271
|
+
|
|
272
|
+
3. **scoped 包配置**
|
|
273
|
+
- 包名 `@yinx-in/opencode-tui-usage` 已在 package.json 中配置
|
|
274
|
+
- `publishConfig.registry` 指定发布到哪个 registry
|
|
275
|
+
|
|
276
|
+
### 发布地址
|
|
277
|
+
|
|
278
|
+
| 平台 | 包名 | 地址 |
|
|
279
|
+
|------|------|------|
|
|
280
|
+
| npm | `@yinx-in/opencode-tui-usage` | https://www.npmjs.com/package/@yinx-in/opencode-tui-usage |
|
|
281
|
+
| GitHub Packages | `@yinx-in/opencode-tui-usage` | https://github.com/Yinxe/opencode-tui-usage/packages |
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { JSX } from "solid-js";
|
|
3
|
+
export interface LabelValueProps {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string | number;
|
|
6
|
+
labelColor?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function LabelValue(props: LabelValueProps): JSX.Element;
|
|
9
|
+
export interface TitleProps {
|
|
10
|
+
text: string;
|
|
11
|
+
color?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function Title(props: TitleProps): JSX.Element;
|
|
14
|
+
export interface ProgressBarProps {
|
|
15
|
+
value: number;
|
|
16
|
+
color?: string;
|
|
17
|
+
width?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function ProgressBar(props: ProgressBarProps): JSX.Element;
|
|
20
|
+
//# sourceMappingURL=components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,GAAG,CAAC,OAAO,CAQ9D;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,GAAG,CAAC,OAAO,CAEpD;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,GAAG,CAAC,OAAO,CAchE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function LabelValue(props) {
|
|
2
|
+
return (<box flexDirection="row" gap={1}>
|
|
3
|
+
<text fg={props.labelColor}>{props.label}</text>
|
|
4
|
+
<text>:</text>
|
|
5
|
+
<text>{props.value}</text>
|
|
6
|
+
</box>);
|
|
7
|
+
}
|
|
8
|
+
export function Title(props) {
|
|
9
|
+
return <text fg={props.color}>{props.text}</text>;
|
|
10
|
+
}
|
|
11
|
+
export function ProgressBar(props) {
|
|
12
|
+
const width = props.width ?? 20;
|
|
13
|
+
const filled = Math.round((props.value / 100) * width);
|
|
14
|
+
const empty = filled === 0 ? width - 1 : width - filled;
|
|
15
|
+
const barColor = props.color ?? '#6bcf7f';
|
|
16
|
+
return (<box flexDirection="row" gap={0}>
|
|
17
|
+
<text>[</text>
|
|
18
|
+
<text fg={barColor}>{'■'.repeat(filled)}</text>
|
|
19
|
+
<text>{' '.repeat(empty)}</text>
|
|
20
|
+
<text>]</text>
|
|
21
|
+
</box>);
|
|
22
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,WAAW,CAAC;AAE5B,OAAO,EAAE,GAAG,EAAE,CAAC;AACf,eAAe,GAAG,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProviderRegistry, ProviderConfig } from "./types.js";
|
|
2
|
+
export declare function readProviderConfig(): ProviderRegistry;
|
|
3
|
+
export declare function getProviderConfig(registry: ProviderRegistry, providerName: string): ProviderConfig | undefined;
|
|
4
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/quota/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AASnE,wBAAgB,kBAAkB,IAAI,gBAAgB,CAWrD;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAI5B"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { resolve, join } from "path";
|
|
3
|
+
const HOME_DIR = process.env.HOME || process.env.USERPROFILE || "";
|
|
4
|
+
const OPENCODE_CONFIG_DIR = join(HOME_DIR, ".config", "opencode");
|
|
5
|
+
export function readProviderConfig() {
|
|
6
|
+
try {
|
|
7
|
+
const configPath = resolve(OPENCODE_CONFIG_DIR, "usage.provider.json");
|
|
8
|
+
const content = readFileSync(configPath, "utf-8");
|
|
9
|
+
const data = JSON.parse(content);
|
|
10
|
+
console.log("[QuotaService] Config content:", JSON.stringify(data));
|
|
11
|
+
return data.providers || {};
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.warn("[QuotaService] Failed to read usage.provider.json:", error);
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function getProviderConfig(registry, providerName) {
|
|
19
|
+
console.log("[QuotaService] Looking up provider:", providerName);
|
|
20
|
+
console.log("[QuotaService] Registry keys:", Object.keys(registry));
|
|
21
|
+
return registry[providerName];
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { QuotaData, ProviderConfig } from "./types.js";
|
|
2
|
+
export interface QuotaProvider {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
init(config: ProviderConfig, credentials: Record<string, unknown>): void;
|
|
5
|
+
fetchQuota(): Promise<QuotaData | null>;
|
|
6
|
+
resolveEnvVar(value: string | undefined): string | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare const resolveEnvVar: (value: string | undefined) => string | undefined;
|
|
9
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/quota/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAEzE,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAExC,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;CAC9D;AAED,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAOlE,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { QuotaData, ProviderConfig } from "../types.js";
|
|
2
|
+
import { QuotaProvider } from "../provider.js";
|
|
3
|
+
export declare class MiniMaxCNQuotaProvider implements QuotaProvider {
|
|
4
|
+
readonly name = "minimax-cn-coding-plan";
|
|
5
|
+
private apiKey;
|
|
6
|
+
private baseUrl;
|
|
7
|
+
init(config: ProviderConfig, _credentials: Record<string, unknown>): void;
|
|
8
|
+
fetchQuota(): Promise<QuotaData | null>;
|
|
9
|
+
resolveEnvVar(value: string | undefined): string | undefined;
|
|
10
|
+
private mapResponseToQuotaData;
|
|
11
|
+
private formatDuration;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=minimax.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minimax.d.ts","sourceRoot":"","sources":["../../../src/quota/providers/minimax.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAwB9D,qBAAa,sBAAuB,YAAW,aAAa;IAC1D,QAAQ,CAAC,IAAI,4BAA4B;IAEzC,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAA8B;IAE7C,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAKnE,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA8C7C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS;IAI5D,OAAO,CAAC,sBAAsB;IA8C9B,OAAO,CAAC,cAAc;CAMvB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { resolveEnvVar } from "../provider.js";
|
|
2
|
+
export class MiniMaxCNQuotaProvider {
|
|
3
|
+
name = "minimax-cn-coding-plan";
|
|
4
|
+
apiKey;
|
|
5
|
+
baseUrl = "https://www.minimaxi.com";
|
|
6
|
+
init(config, _credentials) {
|
|
7
|
+
const apiKeyRaw = config.apiKey;
|
|
8
|
+
this.apiKey = resolveEnvVar(apiKeyRaw) || resolveEnvVar(config.apiKeyEnvVar);
|
|
9
|
+
}
|
|
10
|
+
async fetchQuota() {
|
|
11
|
+
if (!this.apiKey) {
|
|
12
|
+
console.warn("[MiniMaxCNQuotaProvider] Missing apiKey");
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(`${this.baseUrl}/v1/api/openplatform/coding_plan/remains`, {
|
|
17
|
+
method: "GET",
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
console.error(`[MiniMaxCNQuotaProvider] API error: ${response.status}`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const data = (await response.json());
|
|
28
|
+
if (data.base_resp?.status_code !== 0) {
|
|
29
|
+
console.error(`[MiniMaxCNQuotaProvider] API error: ${data.base_resp?.status_msg}`);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const codingPlanModels = data.model_remains.filter((m) => m.model_name.startsWith("MiniMax-M"));
|
|
33
|
+
if (codingPlanModels.length === 0) {
|
|
34
|
+
console.warn("[MiniMaxCNQuotaProvider] No coding plan models found");
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return this.mapResponseToQuotaData(codingPlanModels);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error("[MiniMaxCNQuotaProvider] Fetch failed:", error);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
resolveEnvVar(value) {
|
|
45
|
+
return resolveEnvVar(value);
|
|
46
|
+
}
|
|
47
|
+
mapResponseToQuotaData(models) {
|
|
48
|
+
let totalRollingAvailable = 0;
|
|
49
|
+
let totalRollingLimit = 0;
|
|
50
|
+
let rollingResetMs = 0;
|
|
51
|
+
let totalWeeklyAvailable = 0;
|
|
52
|
+
let totalWeeklyLimit = 0;
|
|
53
|
+
let weeklyResetMs = 0;
|
|
54
|
+
for (const m of models) {
|
|
55
|
+
totalRollingAvailable += m.current_interval_usage_count;
|
|
56
|
+
totalRollingLimit += m.current_interval_total_count;
|
|
57
|
+
totalWeeklyAvailable += m.current_weekly_usage_count;
|
|
58
|
+
totalWeeklyLimit += m.current_weekly_total_count;
|
|
59
|
+
rollingResetMs = Math.max(rollingResetMs, m.end_time);
|
|
60
|
+
weeklyResetMs = Math.max(weeklyResetMs, m.weekly_end_time);
|
|
61
|
+
}
|
|
62
|
+
const rollingUsed = Math.max(0, totalRollingLimit - totalRollingAvailable);
|
|
63
|
+
const weeklyUsed = Math.max(0, totalWeeklyLimit - totalWeeklyAvailable);
|
|
64
|
+
const rollingPercent = totalRollingLimit > 0
|
|
65
|
+
? Math.round((rollingUsed / totalRollingLimit) * 100)
|
|
66
|
+
: 0;
|
|
67
|
+
const weeklyPercent = totalWeeklyLimit > 0
|
|
68
|
+
? Math.round((weeklyUsed / totalWeeklyLimit) * 100)
|
|
69
|
+
: 0;
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const rollingResetSec = Math.max(0, Math.floor((rollingResetMs - now) / 1000));
|
|
72
|
+
const weeklyResetSec = Math.max(0, Math.floor((weeklyResetMs - now) / 1000));
|
|
73
|
+
return {
|
|
74
|
+
rolling: {
|
|
75
|
+
usage: rollingPercent,
|
|
76
|
+
reset: this.formatDuration(rollingResetSec),
|
|
77
|
+
},
|
|
78
|
+
weekly: {
|
|
79
|
+
usage: weeklyPercent,
|
|
80
|
+
reset: this.formatDuration(weeklyResetSec),
|
|
81
|
+
},
|
|
82
|
+
monthly: undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
formatDuration(seconds) {
|
|
86
|
+
if (seconds < 60)
|
|
87
|
+
return `${seconds}s`;
|
|
88
|
+
if (seconds < 3600)
|
|
89
|
+
return `${Math.round(seconds / 60)}m`;
|
|
90
|
+
if (seconds < 86400)
|
|
91
|
+
return `${Math.round(seconds / 3600)}h`;
|
|
92
|
+
return `${Math.round(seconds / 86400)}d`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { QuotaData, ProviderConfig } from "../types.js";
|
|
2
|
+
import { QuotaProvider } from "../provider.js";
|
|
3
|
+
export declare class OpenCodeGoQuotaProvider implements QuotaProvider {
|
|
4
|
+
readonly name = "opencode-go";
|
|
5
|
+
private cookie;
|
|
6
|
+
private workspaceId;
|
|
7
|
+
private serviceId;
|
|
8
|
+
private baseUrl;
|
|
9
|
+
init(config: ProviderConfig, _credentials: Record<string, unknown>): void;
|
|
10
|
+
fetchQuota(): Promise<QuotaData | null>;
|
|
11
|
+
resolveEnvVar(value: string | undefined): string | undefined;
|
|
12
|
+
private formatDuration;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=opencode-go.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-go.d.ts","sourceRoot":"","sources":["../../../src/quota/providers/opencode-go.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAiB9D,qBAAa,uBAAwB,YAAW,aAAa;IAC3D,QAAQ,CAAC,IAAI,iBAAiB;IAE9B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,SAAS,CAAsE;IACvF,OAAO,CAAC,OAAO,CAAyB;IAExC,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAKnE,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAsE7C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS;IAI5D,OAAO,CAAC,cAAc;CAMvB"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolveEnvVar } from "../provider.js";
|
|
2
|
+
export class OpenCodeGoQuotaProvider {
|
|
3
|
+
name = "opencode-go";
|
|
4
|
+
cookie;
|
|
5
|
+
workspaceId;
|
|
6
|
+
serviceId = "c7389bd0e731f80f49593e5ee53835475f4e28594dd6bd83eb229bab753498cd";
|
|
7
|
+
baseUrl = "https://opencode.ai";
|
|
8
|
+
init(config, _credentials) {
|
|
9
|
+
this.cookie = resolveEnvVar(config.cookie);
|
|
10
|
+
this.workspaceId = resolveEnvVar(config.workspaceId);
|
|
11
|
+
}
|
|
12
|
+
async fetchQuota() {
|
|
13
|
+
if (!this.cookie || !this.workspaceId) {
|
|
14
|
+
console.warn("[OpenCodeGoQuotaProvider] Missing cookie or workspaceId");
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const args = JSON.stringify({
|
|
18
|
+
t: { t: 9, i: 0, l: 1, a: [{ t: 1, s: this.workspaceId }], o: 0 },
|
|
19
|
+
f: 31,
|
|
20
|
+
m: [],
|
|
21
|
+
});
|
|
22
|
+
const url = `${this.baseUrl}/_server?id=${this.serviceId}&args=${encodeURIComponent(args)}`;
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
headers: {
|
|
27
|
+
accept: "*/*",
|
|
28
|
+
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,pt;q=0.5,pl;q=0.4",
|
|
29
|
+
cookie: this.cookie,
|
|
30
|
+
referer: `${this.baseUrl}/workspace/${this.workspaceId}/usage`,
|
|
31
|
+
"x-server-id": this.serviceId,
|
|
32
|
+
"x-server-instance": "server-fn:3",
|
|
33
|
+
priority: "u=1, i",
|
|
34
|
+
"sec-fetch-mode": "cors",
|
|
35
|
+
"sec-fetch-site": "same-origin",
|
|
36
|
+
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
console.error(`[OpenCodeGoQuotaProvider] API error: ${response.status}`);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const text = await response.text();
|
|
44
|
+
const rollingMatch = text.match(/rollingUsage:\$R\[1\]=\{status:"([^"]+)",resetInSec:(\d+),usagePercent:(\d+)\}/);
|
|
45
|
+
const weeklyMatch = text.match(/weeklyUsage:\$R\[2\]=\{status:"([^"]+)",resetInSec:(\d+),usagePercent:(\d+)\}/);
|
|
46
|
+
const monthlyMatch = text.match(/monthlyUsage:\$R\[3\]=\{status:"([^"]+)",resetInSec:(\d+),usagePercent:(\d+)\}/);
|
|
47
|
+
if (!rollingMatch || !weeklyMatch || !monthlyMatch) {
|
|
48
|
+
console.error("[OpenCodeGoQuotaProvider] Failed to parse response:", text.substring(0, 200));
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const rollingUsage = { status: rollingMatch[1], resetInSec: parseInt(rollingMatch[2], 10), usagePercent: parseInt(rollingMatch[3], 10) };
|
|
52
|
+
const weeklyUsage = { status: weeklyMatch[1], resetInSec: parseInt(weeklyMatch[2], 10), usagePercent: parseInt(weeklyMatch[3], 10) };
|
|
53
|
+
const monthlyUsage = { status: monthlyMatch[1], resetInSec: parseInt(monthlyMatch[2], 10), usagePercent: parseInt(monthlyMatch[3], 10) };
|
|
54
|
+
return {
|
|
55
|
+
rolling: {
|
|
56
|
+
usage: rollingUsage.usagePercent,
|
|
57
|
+
reset: this.formatDuration(rollingUsage.resetInSec),
|
|
58
|
+
},
|
|
59
|
+
weekly: {
|
|
60
|
+
usage: weeklyUsage.usagePercent,
|
|
61
|
+
reset: this.formatDuration(weeklyUsage.resetInSec),
|
|
62
|
+
},
|
|
63
|
+
monthly: monthlyUsage.status !== "unlimited"
|
|
64
|
+
? { usage: monthlyUsage.usagePercent, reset: this.formatDuration(monthlyUsage.resetInSec) }
|
|
65
|
+
: undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error("[OpenCodeGoQuotaProvider] Fetch failed:", error);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
resolveEnvVar(value) {
|
|
74
|
+
return resolveEnvVar(value);
|
|
75
|
+
}
|
|
76
|
+
formatDuration(seconds) {
|
|
77
|
+
if (seconds < 60)
|
|
78
|
+
return `${seconds}s`;
|
|
79
|
+
if (seconds < 3600)
|
|
80
|
+
return `${Math.round(seconds / 60)}m`;
|
|
81
|
+
if (seconds < 86400)
|
|
82
|
+
return `${Math.round(seconds / 3600)}h`;
|
|
83
|
+
return `${Math.round(seconds / 86400)}d`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { QuotaResult } from "./types.js";
|
|
2
|
+
import type { QuotaProvider } from "./provider.js";
|
|
3
|
+
export declare class QuotaService {
|
|
4
|
+
private providers;
|
|
5
|
+
private providerRegistry;
|
|
6
|
+
private activeProviderName;
|
|
7
|
+
private activeProvider;
|
|
8
|
+
private refreshCount;
|
|
9
|
+
constructor();
|
|
10
|
+
registerProvider(provider: QuotaProvider): void;
|
|
11
|
+
setActiveProvider(providerName: string): boolean;
|
|
12
|
+
getActiveProviderName(): string | null;
|
|
13
|
+
fetchQuota(): Promise<QuotaResult | null>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/quota/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAKnD,qBAAa,YAAY;IACvB,OAAO,CAAC,SAAS,CAAyC;IAC1D,OAAO,CAAC,gBAAgB,CAAwB;IAChD,OAAO,CAAC,kBAAkB,CAAuB;IACjD,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,YAAY,CAAK;;IAOzB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAI/C,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAmBhD,qBAAqB,IAAI,MAAM,GAAG,IAAI;IAIhC,UAAU,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;CAahD"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readProviderConfig, getProviderConfig } from "./config.js";
|
|
2
|
+
import { MiniMaxCNQuotaProvider } from "./providers/minimax.js";
|
|
3
|
+
import { OpenCodeGoQuotaProvider } from "./providers/opencode-go.js";
|
|
4
|
+
export class QuotaService {
|
|
5
|
+
providers = new Map();
|
|
6
|
+
providerRegistry = readProviderConfig();
|
|
7
|
+
activeProviderName = null;
|
|
8
|
+
activeProvider = null;
|
|
9
|
+
refreshCount = 0;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.registerProvider(new MiniMaxCNQuotaProvider());
|
|
12
|
+
this.registerProvider(new OpenCodeGoQuotaProvider());
|
|
13
|
+
}
|
|
14
|
+
registerProvider(provider) {
|
|
15
|
+
this.providers.set(provider.name, provider);
|
|
16
|
+
}
|
|
17
|
+
setActiveProvider(providerName) {
|
|
18
|
+
const provider = this.providers.get(providerName);
|
|
19
|
+
if (!provider) {
|
|
20
|
+
console.warn(`[QuotaService] Unknown provider: ${providerName}`);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const config = getProviderConfig(this.providerRegistry, providerName);
|
|
24
|
+
if (!config) {
|
|
25
|
+
console.warn(`[QuotaService] No config for provider: ${providerName}`);
|
|
26
|
+
}
|
|
27
|
+
provider.init(config || {}, {});
|
|
28
|
+
this.activeProviderName = providerName;
|
|
29
|
+
this.activeProvider = provider;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
getActiveProviderName() {
|
|
33
|
+
return this.activeProviderName;
|
|
34
|
+
}
|
|
35
|
+
async fetchQuota() {
|
|
36
|
+
if (!this.activeProvider) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
this.refreshCount++;
|
|
40
|
+
const quota = await this.activeProvider.fetchQuota();
|
|
41
|
+
return {
|
|
42
|
+
provider: this.activeProviderName,
|
|
43
|
+
quota: quota,
|
|
44
|
+
refreshCount: this.refreshCount,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface QuotaUsage {
|
|
2
|
+
usage: number;
|
|
3
|
+
reset: string;
|
|
4
|
+
}
|
|
5
|
+
export interface QuotaData {
|
|
6
|
+
rolling: QuotaUsage | undefined;
|
|
7
|
+
weekly: QuotaUsage | undefined;
|
|
8
|
+
monthly: QuotaUsage | undefined;
|
|
9
|
+
}
|
|
10
|
+
export interface QuotaResult {
|
|
11
|
+
provider: string;
|
|
12
|
+
quota: QuotaData;
|
|
13
|
+
refreshCount: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ProviderConfig {
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface ProviderRegistry {
|
|
19
|
+
[providerName: string]: ProviderConfig;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/quota/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC;IAChC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IAC/B,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,CAAC;CACxC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
3
|
+
import type { JSX } from "solid-js";
|
|
4
|
+
export declare function SessionInfoView(props: {
|
|
5
|
+
api: TuiPluginApi;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
}): JSX.Element;
|
|
8
|
+
//# sourceMappingURL=session-info.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-info.d.ts","sourceRoot":"","sources":["../src/session-info.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAcpC,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,GAAG,CAAC,OAAO,CA2Ed"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createSignal, createEffect, Show } from "solid-js";
|
|
2
|
+
import { LabelValue } from "./components.jsx";
|
|
3
|
+
export function SessionInfoView(props) {
|
|
4
|
+
const [data, setData] = createSignal(null);
|
|
5
|
+
createEffect(() => {
|
|
6
|
+
const sessionId = props.sessionId;
|
|
7
|
+
const messages = props.api.state.session.messages(sessionId);
|
|
8
|
+
const todos = props.api.state.session.todo(sessionId);
|
|
9
|
+
const diff = props.api.state.session.diff(sessionId);
|
|
10
|
+
const vcs = props.api.state.vcs;
|
|
11
|
+
const lastAssistantMsg = [...messages]
|
|
12
|
+
.reverse()
|
|
13
|
+
.find((m) => m.role === "assistant");
|
|
14
|
+
const lastModelInfo = lastAssistantMsg && "modelID" in lastAssistantMsg
|
|
15
|
+
? {
|
|
16
|
+
providerID: lastAssistantMsg.providerID,
|
|
17
|
+
modelID: lastAssistantMsg.modelID,
|
|
18
|
+
}
|
|
19
|
+
: null;
|
|
20
|
+
setData({
|
|
21
|
+
sessionId,
|
|
22
|
+
branch: vcs?.branch,
|
|
23
|
+
provider: lastModelInfo?.providerID ?? "None",
|
|
24
|
+
model: lastModelInfo?.modelID ?? "None",
|
|
25
|
+
messageCount: messages.length,
|
|
26
|
+
todoCount: todos.length,
|
|
27
|
+
diffCount: diff.length,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
return (<Show when={data()} fallback={<text>Loading...</text>}>
|
|
31
|
+
{() => {
|
|
32
|
+
const d = data();
|
|
33
|
+
return (<>
|
|
34
|
+
<LabelValue label="Session" value={d.sessionId.slice(0, 8) + "..."} labelColor="#6bcf7f"/>
|
|
35
|
+
<LabelValue label="Branch" value={d.branch ?? "N/A"} labelColor="#ffd93d"/>
|
|
36
|
+
<LabelValue label="Provider" value={d.provider} labelColor="#ff6b6b"/>
|
|
37
|
+
<LabelValue label="Model" value={d.model} labelColor="#74b9ff"/>
|
|
38
|
+
<LabelValue label="Messages" value={d.messageCount} labelColor="#a29bfe"/>
|
|
39
|
+
<LabelValue label="TODOs" value={d.todoCount} labelColor="#fd79a8"/>
|
|
40
|
+
<LabelValue label="Changes" value={d.diffCount} labelColor="#00cec9"/>
|
|
41
|
+
</>);
|
|
42
|
+
}}
|
|
43
|
+
</Show>);
|
|
44
|
+
}
|
package/dist/tui.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAa,eAAe,EAAE,MAAM,yBAAyB,CAAC;AA+B1E,QAAA,MAAM,MAAM,EAAE,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAG3C,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/tui.jsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { UsageView } from "./usage.jsx";
|
|
2
|
+
import { SessionInfoView } from "./session-info.jsx";
|
|
3
|
+
import { QuotaService } from "./quota/service.js";
|
|
4
|
+
const id = "opencode-tui-usage-plugin";
|
|
5
|
+
const tui = async (api) => {
|
|
6
|
+
const quotaService = new QuotaService();
|
|
7
|
+
api.slots.register({
|
|
8
|
+
order: 150,
|
|
9
|
+
slots: {
|
|
10
|
+
sidebar_content(_ctx, _props) {
|
|
11
|
+
return (<box gap={0}>
|
|
12
|
+
<UsageView quotaService={quotaService} api={api} sessionId={_props.session_id}/>
|
|
13
|
+
<SessionInfoView api={api} sessionId={_props.session_id}/>
|
|
14
|
+
</box>);
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
console.log(`[${id}] Plugin registered`);
|
|
19
|
+
};
|
|
20
|
+
const plugin = {
|
|
21
|
+
id,
|
|
22
|
+
tui,
|
|
23
|
+
};
|
|
24
|
+
export default plugin;
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { JSX } from "solid-js";
|
|
3
|
+
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
4
|
+
import type { QuotaResult } from "./quota/types.js";
|
|
5
|
+
export interface UsageViewProps {
|
|
6
|
+
quotaService: {
|
|
7
|
+
fetchQuota(): Promise<QuotaResult | null>;
|
|
8
|
+
setActiveProvider(providerName: string): boolean;
|
|
9
|
+
};
|
|
10
|
+
api: TuiPluginApi;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function UsageView(props: UsageViewProps): JSX.Element;
|
|
14
|
+
//# sourceMappingURL=usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAGpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAmBpD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE;QACZ,UAAU,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;QAC1C,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;KAClD,CAAC;IACF,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,GAAG,CAAC,OAAO,CA0L5D"}
|
package/dist/usage.jsx
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup } from "solid-js";
|
|
2
|
+
import { Title, ProgressBar } from "./components.jsx";
|
|
3
|
+
const REFRESH_INTERVAL = 60;
|
|
4
|
+
function formatDuration(totalSeconds) {
|
|
5
|
+
if (totalSeconds < 60) {
|
|
6
|
+
return `${totalSeconds}s`;
|
|
7
|
+
}
|
|
8
|
+
if (totalSeconds < 3600) {
|
|
9
|
+
const m = Math.floor(totalSeconds / 60);
|
|
10
|
+
const s = totalSeconds % 60;
|
|
11
|
+
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
12
|
+
}
|
|
13
|
+
const h = Math.floor(totalSeconds / 3600);
|
|
14
|
+
const m = Math.floor((totalSeconds % 3600) / 60);
|
|
15
|
+
const s = totalSeconds % 60;
|
|
16
|
+
return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
|
|
17
|
+
}
|
|
18
|
+
export function UsageView(props) {
|
|
19
|
+
const [result, setResult] = createSignal(null);
|
|
20
|
+
const [loading, setLoading] = createSignal(true);
|
|
21
|
+
const [currentProvider, setCurrentProvider] = createSignal(null);
|
|
22
|
+
const [currentModel, setCurrentModel] = createSignal(null);
|
|
23
|
+
const [refreshCountdown, setRefreshCountdown] = createSignal(REFRESH_INTERVAL);
|
|
24
|
+
const doRefresh = () => {
|
|
25
|
+
const providerID = currentProvider();
|
|
26
|
+
if (!providerID)
|
|
27
|
+
return;
|
|
28
|
+
setLoading(true);
|
|
29
|
+
props.quotaService.setActiveProvider(providerID);
|
|
30
|
+
props.quotaService.fetchQuota().then((data) => {
|
|
31
|
+
if (data && data.quota) {
|
|
32
|
+
setResult(data);
|
|
33
|
+
}
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}).catch(() => {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
createEffect(() => {
|
|
40
|
+
const sessionId = props.sessionId;
|
|
41
|
+
const messages = props.api.state.session.messages(sessionId);
|
|
42
|
+
if (!messages || messages.length === 0) {
|
|
43
|
+
setCurrentProvider(null);
|
|
44
|
+
setCurrentModel(null);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const lastAssistantMsg = [...messages]
|
|
48
|
+
.reverse()
|
|
49
|
+
.find((m) => m.role === "assistant");
|
|
50
|
+
if (!lastAssistantMsg) {
|
|
51
|
+
setCurrentProvider(null);
|
|
52
|
+
setCurrentModel(null);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!("providerID" in lastAssistantMsg)) {
|
|
56
|
+
setCurrentProvider(null);
|
|
57
|
+
setCurrentModel(null);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const providerID = lastAssistantMsg.providerID;
|
|
61
|
+
const modelID = "modelID" in lastAssistantMsg
|
|
62
|
+
? lastAssistantMsg.modelID
|
|
63
|
+
: "";
|
|
64
|
+
setCurrentProvider(providerID);
|
|
65
|
+
setCurrentModel(modelID);
|
|
66
|
+
});
|
|
67
|
+
createEffect(() => {
|
|
68
|
+
const providerID = currentProvider();
|
|
69
|
+
const modelID = currentModel();
|
|
70
|
+
if (!providerID) {
|
|
71
|
+
setResult(null);
|
|
72
|
+
setLoading(false);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setLoading(true);
|
|
76
|
+
props.quotaService.setActiveProvider(providerID);
|
|
77
|
+
props.quotaService.fetchQuota().then((data) => {
|
|
78
|
+
if (data && data.quota) {
|
|
79
|
+
setResult(data);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
setResult(null);
|
|
83
|
+
}
|
|
84
|
+
setLoading(false);
|
|
85
|
+
}).catch((error) => {
|
|
86
|
+
console.error("[UsageView] Failed to fetch quota:", error);
|
|
87
|
+
setResult(null);
|
|
88
|
+
setLoading(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
createEffect(() => {
|
|
92
|
+
setRefreshCountdown(REFRESH_INTERVAL);
|
|
93
|
+
const id = setInterval(() => {
|
|
94
|
+
setRefreshCountdown((r) => {
|
|
95
|
+
if (r <= 1) {
|
|
96
|
+
doRefresh();
|
|
97
|
+
return REFRESH_INTERVAL;
|
|
98
|
+
}
|
|
99
|
+
return r - 1;
|
|
100
|
+
});
|
|
101
|
+
}, 1000);
|
|
102
|
+
onCleanup(() => clearInterval(id));
|
|
103
|
+
});
|
|
104
|
+
return (<box flexDirection="column" gap={0}>
|
|
105
|
+
<Title text="Usage" color="#6bcf7f"/>
|
|
106
|
+
<text fg="#888">Provider: {currentProvider() ?? "Unknown"}</text>
|
|
107
|
+
{loading() ? (<>
|
|
108
|
+
<box flexDirection="column" gap={0}>
|
|
109
|
+
<box flexDirection="row" gap={1}>
|
|
110
|
+
<text fg="#6bcf7f">Rolling:</text>
|
|
111
|
+
<text fg="#888">Loading...</text>
|
|
112
|
+
</box>
|
|
113
|
+
<ProgressBar value={0} color="#6bcf7f"/>
|
|
114
|
+
</box>
|
|
115
|
+
<box flexDirection="column" gap={0}>
|
|
116
|
+
<box flexDirection="row" gap={1}>
|
|
117
|
+
<text fg="#ffd93d">Weekly:</text>
|
|
118
|
+
<text fg="#888">Loading...</text>
|
|
119
|
+
</box>
|
|
120
|
+
<ProgressBar value={0} color="#ffd93d"/>
|
|
121
|
+
</box>
|
|
122
|
+
<box flexDirection="column" gap={0}>
|
|
123
|
+
<box flexDirection="row" gap={1}>
|
|
124
|
+
<text fg="#4da6ff">Monthly:</text>
|
|
125
|
+
<text fg="#888">Loading...</text>
|
|
126
|
+
</box>
|
|
127
|
+
<ProgressBar value={0} color="#4da6ff"/>
|
|
128
|
+
</box>
|
|
129
|
+
</>) : result() && result().quota ? (<>
|
|
130
|
+
{result().quota.rolling ? (<box flexDirection="column" gap={0}>
|
|
131
|
+
<box flexDirection="row" gap={1}>
|
|
132
|
+
<text fg="#6bcf7f">Rolling:</text>
|
|
133
|
+
<text>{result().quota.rolling.usage}%</text>
|
|
134
|
+
<text fg="#888">reset {result().quota.rolling.reset}</text>
|
|
135
|
+
</box>
|
|
136
|
+
<ProgressBar value={result().quota.rolling.usage} color="#6bcf7f"/>
|
|
137
|
+
</box>) : (<text fg="#888">Rolling: N/A</text>)}
|
|
138
|
+
{result().quota.weekly ? (<box flexDirection="column" gap={0}>
|
|
139
|
+
<box flexDirection="row" gap={1}>
|
|
140
|
+
<text fg="#ffd93d">Weekly:</text>
|
|
141
|
+
<text>{result().quota.weekly.usage}%</text>
|
|
142
|
+
<text fg="#888">reset {result().quota.weekly.reset}</text>
|
|
143
|
+
</box>
|
|
144
|
+
<ProgressBar value={result().quota.weekly.usage} color="#ffd93d"/>
|
|
145
|
+
</box>) : (<text fg="#888">Weekly: N/A</text>)}
|
|
146
|
+
<box flexDirection="column" gap={0}>
|
|
147
|
+
<box flexDirection="row" gap={1}>
|
|
148
|
+
<text fg="#4da6ff">Monthly:</text>
|
|
149
|
+
{result().quota.monthly ? (<>
|
|
150
|
+
<text>{result().quota.monthly.usage}%</text>
|
|
151
|
+
<text fg="#888">reset {result().quota.monthly.reset}</text>
|
|
152
|
+
</>) : (<>
|
|
153
|
+
<text fg="#888">0%</text>
|
|
154
|
+
<text fg="#888">reset ∞</text>
|
|
155
|
+
</>)}
|
|
156
|
+
</box>
|
|
157
|
+
<ProgressBar value={result().quota.monthly?.usage ?? 0} color="#4da6ff"/>
|
|
158
|
+
</box>
|
|
159
|
+
<text fg="#888">{formatDuration(refreshCountdown())} Refresh #{result().refreshCount}</text>
|
|
160
|
+
</>) : (<text>No data</text>)}
|
|
161
|
+
</box>);
|
|
162
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yinxe/opencode-tui-usage",
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "OpenCode TUI 额度显示插件 - 在侧边栏显示用量和额度信息",
|
|
5
|
+
"repository": "github:Yinxe/opencode-tui-usage",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"oc-plugin": [
|
|
8
|
+
"tui"
|
|
9
|
+
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"registry": "https://registry.npmjs.org/",
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./tui": {
|
|
20
|
+
"types": "./dist/tui.d.ts",
|
|
21
|
+
"default": "./dist/tui.jsx"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"dev": "tsc --watch",
|
|
31
|
+
"lint": "tsc --noEmit",
|
|
32
|
+
"test": "echo test"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"opencode",
|
|
36
|
+
"opencode-plugin",
|
|
37
|
+
"opencode-tui",
|
|
38
|
+
"quota",
|
|
39
|
+
"usage"
|
|
40
|
+
],
|
|
41
|
+
"author": "Yinxe",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@opencode-ai/plugin": "^1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@opentui/core": "^0.2.2",
|
|
48
|
+
"@opentui/solid": "^0.2.2",
|
|
49
|
+
"solid-js": "^1.8.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.0.0",
|
|
53
|
+
"typescript": "^5.3.0",
|
|
54
|
+
"vitest": "^1.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|