@yinxe/opencode-tui-usage 1.0.0 → 1.0.2
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 +7 -7
- package/dist/components.d.ts +12 -0
- package/dist/components.d.ts.map +1 -1
- package/dist/components.jsx +13 -0
- package/dist/context-usage.d.ts +9 -0
- package/dist/context-usage.d.ts.map +1 -1
- package/dist/context-usage.jsx +18 -1
- package/dist/formatters.d.ts +25 -0
- package/dist/formatters.d.ts.map +1 -1
- package/dist/formatters.js +33 -0
- package/dist/quota/config.d.ts +10 -0
- package/dist/quota/config.d.ts.map +1 -1
- package/dist/quota/config.js +11 -3
- package/dist/quota/index.d.ts +5 -0
- package/dist/quota/index.d.ts.map +1 -0
- package/dist/quota/index.js +4 -0
- package/dist/quota/provider.d.ts +20 -1
- package/dist/quota/provider.d.ts.map +1 -1
- package/dist/quota/provider.js +6 -0
- package/dist/quota/providers/minimax.d.ts +7 -2
- package/dist/quota/providers/minimax.d.ts.map +1 -1
- package/dist/quota/providers/minimax.js +17 -15
- package/dist/quota/providers/opencode-go.d.ts +4 -2
- package/dist/quota/providers/opencode-go.d.ts.map +1 -1
- package/dist/quota/providers/opencode-go.js +25 -17
- package/dist/quota/service.d.ts +29 -0
- package/dist/quota/service.d.ts.map +1 -1
- package/dist/quota/service.js +35 -1
- package/dist/quota/types.d.ts +26 -0
- package/dist/quota/types.d.ts.map +1 -1
- package/dist/session-info.d.ts +4 -0
- package/dist/session-info.d.ts.map +1 -1
- package/dist/session-info.jsx +12 -3
- package/dist/tokens-usage.d.ts +6 -0
- package/dist/tokens-usage.d.ts.map +1 -1
- package/dist/tokens-usage.jsx +10 -3
- package/dist/usage.d.ts +4 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.jsx +39 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,20 +19,20 @@ OpenCode TUI 插件,在侧边栏显示用量和额度信息,支持多额度
|
|
|
19
19
|
### 从 npm 安装(推荐)
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm install @
|
|
22
|
+
npm install @yinxe/opencode-tui-usage
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
### 从 GitHub Packages 安装
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
# 设置 registry
|
|
29
|
-
npm config set @
|
|
29
|
+
npm config set @yinxe:registry https://npm.pkg.github.com
|
|
30
30
|
|
|
31
31
|
# 登录(需要 GitHub Personal Access Token)
|
|
32
32
|
echo "YOUR_GITHUB_TOKEN" | npm login --registry=https://npm.pkg.github.com --username=YOUR_GITHUB_USERNAME --email=you@example.com
|
|
33
33
|
|
|
34
34
|
# 安装
|
|
35
|
-
npm install @
|
|
35
|
+
npm install @yinxe/opencode-tui-usage
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
### 配置插件
|
|
@@ -42,7 +42,7 @@ npm install @yinx-in/opencode-tui-usage
|
|
|
42
42
|
```json
|
|
43
43
|
{
|
|
44
44
|
"$schema": "https://opencode.ai/tui.json",
|
|
45
|
-
"plugin": ["@
|
|
45
|
+
"plugin": ["@yinxe/opencode-tui-usage"]
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
@@ -270,15 +270,15 @@ build → publish-npm + publish-github
|
|
|
270
270
|
- Secret: 你的 npm access token
|
|
271
271
|
|
|
272
272
|
3. **scoped 包配置**
|
|
273
|
-
- 包名 `@
|
|
273
|
+
- 包名 `@yinxe/opencode-tui-usage` 已在 package.json 中配置
|
|
274
274
|
- `publishConfig.registry` 指定发布到哪个 registry
|
|
275
275
|
|
|
276
276
|
### 发布地址
|
|
277
277
|
|
|
278
278
|
| 平台 | 包名 | 地址 |
|
|
279
279
|
|------|------|------|
|
|
280
|
-
| npm | `@
|
|
281
|
-
| GitHub Packages | `@
|
|
280
|
+
| npm | `@yinxe/opencode-tui-usage` | https://www.npmjs.com/package/@yinxe/opencode-tui-usage |
|
|
281
|
+
| GitHub Packages | `@yinxe/opencode-tui-usage` | https://github.com/Yinxe/opencode-tui-usage/packages |
|
|
282
282
|
|
|
283
283
|
## License
|
|
284
284
|
|
package/dist/components.d.ts
CHANGED
|
@@ -5,16 +5,28 @@ export interface LabelValueProps {
|
|
|
5
5
|
value: string | number;
|
|
6
6
|
labelColor?: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* 标签-值组件
|
|
10
|
+
* 显示格式:标签: 值
|
|
11
|
+
*/
|
|
8
12
|
export declare function LabelValue(props: LabelValueProps): JSX.Element;
|
|
9
13
|
export interface TitleProps {
|
|
10
14
|
text: string;
|
|
11
15
|
color?: string;
|
|
12
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* 标题组件
|
|
19
|
+
* 显示带颜色的文本
|
|
20
|
+
*/
|
|
13
21
|
export declare function Title(props: TitleProps): JSX.Element;
|
|
14
22
|
export interface ProgressBarProps {
|
|
15
23
|
value: number;
|
|
16
24
|
color?: string;
|
|
17
25
|
width?: number;
|
|
18
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* 进度条组件
|
|
29
|
+
* 显示格式:[■■■■■■■■□□] 或类似
|
|
30
|
+
*/
|
|
19
31
|
export declare function ProgressBar(props: ProgressBarProps): JSX.Element;
|
|
20
32
|
//# sourceMappingURL=components.d.ts.map
|
package/dist/components.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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;;;GAGG;AACH,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;;;GAGG;AACH,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;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,GAAG,CAAC,OAAO,CAehE"}
|
package/dist/components.jsx
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 标签-值组件
|
|
3
|
+
* 显示格式:标签: 值
|
|
4
|
+
*/
|
|
1
5
|
export function LabelValue(props) {
|
|
2
6
|
return (<box flexDirection="row" gap={1}>
|
|
3
7
|
<text fg={props.labelColor}>{props.label}</text>
|
|
@@ -5,11 +9,20 @@ export function LabelValue(props) {
|
|
|
5
9
|
<text>{props.value}</text>
|
|
6
10
|
</box>);
|
|
7
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* 标题组件
|
|
14
|
+
* 显示带颜色的文本
|
|
15
|
+
*/
|
|
8
16
|
export function Title(props) {
|
|
9
17
|
return <text fg={props.color}>{props.text}</text>;
|
|
10
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* 进度条组件
|
|
21
|
+
* 显示格式:[■■■■■■■■□□] 或类似
|
|
22
|
+
*/
|
|
11
23
|
export function ProgressBar(props) {
|
|
12
24
|
const width = props.width ?? 20;
|
|
25
|
+
// 计算填充和空白的字符数
|
|
13
26
|
const filled = Math.round((props.value / 100) * width);
|
|
14
27
|
const empty = filled === 0 ? width - 1 : width - filled;
|
|
15
28
|
const barColor = props.color ?? '#6bcf7f';
|
package/dist/context-usage.d.ts
CHANGED
|
@@ -5,5 +5,14 @@ export interface ContextUsageViewProps {
|
|
|
5
5
|
api: TuiPluginApi;
|
|
6
6
|
sessionId: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Context Usage 视图组件
|
|
10
|
+
* 显示当前会话最新一条 assistant 消息的 context tokens 使用情况
|
|
11
|
+
*
|
|
12
|
+
* Context Tokens 计算方式:
|
|
13
|
+
* contextTokens = input + output + reasoning + cache.read + cache.write
|
|
14
|
+
*
|
|
15
|
+
* 注意:AI 回复期间 tokens 可能为 0,此时跳过该消息
|
|
16
|
+
*/
|
|
8
17
|
export declare function ContextUsageView(props: ContextUsageViewProps): JSX.Element;
|
|
9
18
|
//# sourceMappingURL=context-usage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-usage.d.ts","sourceRoot":"","sources":["../src/context-usage.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAKpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,GAAG,CAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"context-usage.d.ts","sourceRoot":"","sources":["../src/context-usage.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAKpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,GAAG,CAAC,OAAO,CAyE1E"}
|
package/dist/context-usage.jsx
CHANGED
|
@@ -2,6 +2,15 @@ import { createSignal, createEffect } from "solid-js";
|
|
|
2
2
|
import { Show } from "solid-js";
|
|
3
3
|
import { ProgressBar } from "./components.jsx";
|
|
4
4
|
import { formatNumber, formatPercent } from "./formatters.js";
|
|
5
|
+
/**
|
|
6
|
+
* Context Usage 视图组件
|
|
7
|
+
* 显示当前会话最新一条 assistant 消息的 context tokens 使用情况
|
|
8
|
+
*
|
|
9
|
+
* Context Tokens 计算方式:
|
|
10
|
+
* contextTokens = input + output + reasoning + cache.read + cache.write
|
|
11
|
+
*
|
|
12
|
+
* 注意:AI 回复期间 tokens 可能为 0,此时跳过该消息
|
|
13
|
+
*/
|
|
5
14
|
export function ContextUsageView(props) {
|
|
6
15
|
const [contextData, setContextData] = createSignal(null);
|
|
7
16
|
createEffect(() => {
|
|
@@ -14,22 +23,30 @@ export function ContextUsageView(props) {
|
|
|
14
23
|
let latestTokens = 0;
|
|
15
24
|
let latestTime = -Infinity;
|
|
16
25
|
let limit = 0;
|
|
26
|
+
// 遍历所有消息,找到最新一条有有效 tokens 的 assistant 消息
|
|
17
27
|
for (const msg of messages) {
|
|
18
28
|
if (msg.role !== "assistant" || !msg.tokens)
|
|
19
29
|
continue;
|
|
30
|
+
// 计算总 tokens(input + output + reasoning + cache)
|
|
20
31
|
const tokens = msg.tokens.input +
|
|
21
32
|
msg.tokens.output +
|
|
22
33
|
msg.tokens.reasoning +
|
|
23
34
|
msg.tokens.cache.read +
|
|
24
35
|
msg.tokens.cache.write;
|
|
36
|
+
// AI 回复期间 tokens 可能为 0,跳过
|
|
25
37
|
if (tokens <= 0)
|
|
26
38
|
continue;
|
|
39
|
+
// 使用消息完成时间或创建时间来判断新旧
|
|
27
40
|
const time = msg.time.completed ?? msg.time.created;
|
|
28
41
|
if (time > latestTime) {
|
|
29
42
|
latestTime = time;
|
|
30
43
|
latestTokens = tokens;
|
|
44
|
+
// 从 provider 列表中查找对应模型的 context limit
|
|
31
45
|
const provider = props.api.state.provider.find((p) => p.id === msg.providerID);
|
|
32
|
-
|
|
46
|
+
if (!provider) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const model = provider.models[msg.modelID];
|
|
33
50
|
limit = model?.limit?.context ?? 0;
|
|
34
51
|
}
|
|
35
52
|
}
|
package/dist/formatters.d.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 格式化工具函数
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 格式化数字为大写缩写
|
|
6
|
+
* 例如:1000 -> "1.0K", 1000000 -> "1.0M"
|
|
7
|
+
*/
|
|
1
8
|
export declare function formatNumber(n: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* 格式化金额为美元字符串
|
|
11
|
+
* 自动去掉尾部的零
|
|
12
|
+
* 例如:0.000001 -> "$0.000001", 0.001000 -> "$0.001"
|
|
13
|
+
*/
|
|
2
14
|
export declare function formatCost(cost: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* 格式化时间间隔为 mm:ss 或 hh:mm:ss 格式
|
|
17
|
+
* 例如:45 -> "45s", 90 -> "1:30", 3661 -> "1:01:01"
|
|
18
|
+
*/
|
|
3
19
|
export declare function formatDuration(totalSeconds: number): string;
|
|
20
|
+
/**
|
|
21
|
+
* 格式化百分比
|
|
22
|
+
* 例如:45.6 -> "46%"
|
|
23
|
+
*/
|
|
4
24
|
export declare function formatPercent(value: number): string;
|
|
25
|
+
/**
|
|
26
|
+
* 格式化时间间隔为紧凑格式(用于额度显示)
|
|
27
|
+
* 例如:45 -> "45s", 90 -> "2m", 3600 -> "1h", 86400 -> "1d"
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatDurationCompact(seconds: number): string;
|
|
5
30
|
//# sourceMappingURL=formatters.d.ts.map
|
package/dist/formatters.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatters.d.ts","sourceRoot":"","sources":["../src/formatters.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI9C;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAa3D;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
|
|
1
|
+
{"version":3,"file":"formatters.d.ts","sourceRoot":"","sources":["../src/formatters.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI9C;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAa3D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAK7D"}
|
package/dist/formatters.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 格式化工具函数
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 格式化数字为大写缩写
|
|
6
|
+
* 例如:1000 -> "1.0K", 1000000 -> "1.0M"
|
|
7
|
+
*/
|
|
1
8
|
export function formatNumber(n) {
|
|
2
9
|
if (n >= 1000000)
|
|
3
10
|
return (n / 1000000).toFixed(1) + "M";
|
|
@@ -5,12 +12,21 @@ export function formatNumber(n) {
|
|
|
5
12
|
return (n / 1000).toFixed(1) + "K";
|
|
6
13
|
return n.toString();
|
|
7
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* 格式化金额为美元字符串
|
|
17
|
+
* 自动去掉尾部的零
|
|
18
|
+
* 例如:0.000001 -> "$0.000001", 0.001000 -> "$0.001"
|
|
19
|
+
*/
|
|
8
20
|
export function formatCost(cost) {
|
|
9
21
|
if (cost === 0)
|
|
10
22
|
return "$0";
|
|
11
23
|
const formatted = cost.toFixed(6).replace(/\.?0+$/, "");
|
|
12
24
|
return "$" + formatted;
|
|
13
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* 格式化时间间隔为 mm:ss 或 hh:mm:ss 格式
|
|
28
|
+
* 例如:45 -> "45s", 90 -> "1:30", 3661 -> "1:01:01"
|
|
29
|
+
*/
|
|
14
30
|
export function formatDuration(totalSeconds) {
|
|
15
31
|
if (totalSeconds < 60) {
|
|
16
32
|
return `${totalSeconds}s`;
|
|
@@ -25,6 +41,23 @@ export function formatDuration(totalSeconds) {
|
|
|
25
41
|
const s = totalSeconds % 60;
|
|
26
42
|
return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
|
|
27
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* 格式化百分比
|
|
46
|
+
* 例如:45.6 -> "46%"
|
|
47
|
+
*/
|
|
28
48
|
export function formatPercent(value) {
|
|
29
49
|
return `${Math.round(value)}%`;
|
|
30
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* 格式化时间间隔为紧凑格式(用于额度显示)
|
|
53
|
+
* 例如:45 -> "45s", 90 -> "2m", 3600 -> "1h", 86400 -> "1d"
|
|
54
|
+
*/
|
|
55
|
+
export function formatDurationCompact(seconds) {
|
|
56
|
+
if (seconds < 60)
|
|
57
|
+
return `${seconds}s`;
|
|
58
|
+
if (seconds < 3600)
|
|
59
|
+
return `${Math.round(seconds / 60)}m`;
|
|
60
|
+
if (seconds < 86400)
|
|
61
|
+
return `${Math.round(seconds / 3600)}h`;
|
|
62
|
+
return `${Math.round(seconds / 86400)}d`;
|
|
63
|
+
}
|
package/dist/quota/config.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { ProviderRegistry, ProviderConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 读取并解析 usage.provider.json 配置文件
|
|
4
|
+
* @returns Provider 注册表,如果文件不存在或解析失败则返回空对象
|
|
5
|
+
*/
|
|
2
6
|
export declare function readProviderConfig(): ProviderRegistry;
|
|
7
|
+
/**
|
|
8
|
+
* 根据 provider 名称获取对应配置
|
|
9
|
+
* @param registry Provider 注册表
|
|
10
|
+
* @param providerName Provider 名称
|
|
11
|
+
* @returns 对应的配置,如果不存在则返回 undefined
|
|
12
|
+
*/
|
|
3
13
|
export declare function getProviderConfig(registry: ProviderRegistry, providerName: string): ProviderConfig | undefined;
|
|
4
14
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -1 +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;
|
|
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;AAWnE;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,gBAAgB,CAUrD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAE5B"}
|
package/dist/quota/config.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
2
|
import { resolve, join } from "path";
|
|
3
|
+
/** OpenCode 配置目录路径 */
|
|
3
4
|
const HOME_DIR = process.env.HOME || process.env.USERPROFILE || "";
|
|
4
5
|
const OPENCODE_CONFIG_DIR = join(HOME_DIR, ".config", "opencode");
|
|
6
|
+
/**
|
|
7
|
+
* 读取并解析 usage.provider.json 配置文件
|
|
8
|
+
* @returns Provider 注册表,如果文件不存在或解析失败则返回空对象
|
|
9
|
+
*/
|
|
5
10
|
export function readProviderConfig() {
|
|
6
11
|
try {
|
|
7
12
|
const configPath = resolve(OPENCODE_CONFIG_DIR, "usage.provider.json");
|
|
8
13
|
const content = readFileSync(configPath, "utf-8");
|
|
9
14
|
const data = JSON.parse(content);
|
|
10
|
-
console.log("[QuotaService] Config content:", JSON.stringify(data));
|
|
11
15
|
return data.providers || {};
|
|
12
16
|
}
|
|
13
17
|
catch (error) {
|
|
@@ -15,8 +19,12 @@ export function readProviderConfig() {
|
|
|
15
19
|
return {};
|
|
16
20
|
}
|
|
17
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* 根据 provider 名称获取对应配置
|
|
24
|
+
* @param registry Provider 注册表
|
|
25
|
+
* @param providerName Provider 名称
|
|
26
|
+
* @returns 对应的配置,如果不存在则返回 undefined
|
|
27
|
+
*/
|
|
18
28
|
export function getProviderConfig(registry, providerName) {
|
|
19
|
-
console.log("[QuotaService] Looking up provider:", providerName);
|
|
20
|
-
console.log("[QuotaService] Registry keys:", Object.keys(registry));
|
|
21
29
|
return registry[providerName];
|
|
22
30
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { QuotaService } from "./service.js";
|
|
2
|
+
export { QuotaProvider, resolveEnvVar } from "./provider.js";
|
|
3
|
+
export type { QuotaUsage, QuotaData, QuotaResult, ProviderConfig, ProviderRegistry } from "./types.js";
|
|
4
|
+
export { readProviderConfig, getProviderConfig } from "./config.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/quota/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACvG,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/quota/provider.d.ts
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import type { QuotaData, ProviderConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 额度 Provider 接口
|
|
4
|
+
* 所有额度数据提供者需实现此接口
|
|
5
|
+
*/
|
|
2
6
|
export interface QuotaProvider {
|
|
7
|
+
/** Provider 唯一标识,需与 usage.provider.json 中的 key 匹配 */
|
|
3
8
|
readonly name: string;
|
|
9
|
+
/**
|
|
10
|
+
* 初始化 Provider
|
|
11
|
+
* @param config 从 usage.provider.json 读取的 provider 配置
|
|
12
|
+
* @param credentials 凭证信息(暂未使用)
|
|
13
|
+
*/
|
|
4
14
|
init(config: ProviderConfig, credentials: Record<string, unknown>): void;
|
|
15
|
+
/**
|
|
16
|
+
* 获取额度数据
|
|
17
|
+
* @returns 额度数据,获取失败返回 null
|
|
18
|
+
*/
|
|
5
19
|
fetchQuota(): Promise<QuotaData | null>;
|
|
6
|
-
resolveEnvVar(value: string | undefined): string | undefined;
|
|
7
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* 解析环境变量引用
|
|
23
|
+
* 支持 ${ENV_VAR} 格式,从 process.env 读取实际值
|
|
24
|
+
* @param value 可能是 ${ENV_VAR} 格式的字符串
|
|
25
|
+
* @returns 解析后的值,如果未找到环境变量则返回 undefined
|
|
26
|
+
*/
|
|
8
27
|
export declare const resolveEnvVar: (value: string | undefined) => string | undefined;
|
|
9
28
|
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -1 +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;
|
|
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;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;;OAIG;IACH,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAEzE;;;OAGG;IACH,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;CACzC;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAOlE,CAAC"}
|
package/dist/quota/provider.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { QuotaData, ProviderConfig } from "../types.js";
|
|
2
2
|
import { QuotaProvider } from "../provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* MiniMax 额度 Provider 基类
|
|
5
|
+
* 用于处理 MiniMax 的 coding plan 额度数据
|
|
6
|
+
*/
|
|
3
7
|
declare class MiniMaxQuotaProvider implements QuotaProvider {
|
|
4
8
|
readonly name: string;
|
|
5
9
|
private apiKey;
|
|
@@ -8,13 +12,14 @@ declare class MiniMaxQuotaProvider implements QuotaProvider {
|
|
|
8
12
|
constructor(name: string, baseUrl: string);
|
|
9
13
|
init(config: ProviderConfig, _credentials: Record<string, unknown>): void;
|
|
10
14
|
fetchQuota(): Promise<QuotaData | null>;
|
|
11
|
-
|
|
15
|
+
/** 将 API 响应映射为 QuotaData 格式 */
|
|
12
16
|
private mapResponseToQuotaData;
|
|
13
|
-
private formatDuration;
|
|
14
17
|
}
|
|
18
|
+
/** MiniMax CN (国内版) Provider */
|
|
15
19
|
export declare class MiniMaxCNQuotaProvider extends MiniMaxQuotaProvider {
|
|
16
20
|
constructor();
|
|
17
21
|
}
|
|
22
|
+
/** MiniMax IO (海外版) Provider */
|
|
18
23
|
export declare class MiniMaxIOQuotaProvider extends MiniMaxQuotaProvider {
|
|
19
24
|
constructor();
|
|
20
25
|
}
|
|
@@ -1 +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;
|
|
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;AA2B9D;;;GAGG;AACH,cAAM,oBAAqB,YAAW,aAAa;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;gBAEX,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAMzC,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;IAiD7C,+BAA+B;IAC/B,OAAO,CAAC,sBAAsB;CAgD/B;AAED,gCAAgC;AAChC,qBAAa,sBAAuB,SAAQ,oBAAoB;;CAI/D;AAED,gCAAgC;AAChC,qBAAa,sBAAuB,SAAQ,oBAAoB;;CAI/D"}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { resolveEnvVar } from "../provider.js";
|
|
2
|
+
import { formatDurationCompact } from "../../formatters.js";
|
|
3
|
+
/**
|
|
4
|
+
* MiniMax 额度 Provider 基类
|
|
5
|
+
* 用于处理 MiniMax 的 coding plan 额度数据
|
|
6
|
+
*/
|
|
2
7
|
class MiniMaxQuotaProvider {
|
|
3
8
|
name;
|
|
4
9
|
apiKey;
|
|
@@ -11,7 +16,7 @@ class MiniMaxQuotaProvider {
|
|
|
11
16
|
}
|
|
12
17
|
init(config, _credentials) {
|
|
13
18
|
const apiKeyRaw = config.apiKey;
|
|
14
|
-
this.apiKey = resolveEnvVar(apiKeyRaw)
|
|
19
|
+
this.apiKey = resolveEnvVar(apiKeyRaw);
|
|
15
20
|
}
|
|
16
21
|
async fetchQuota() {
|
|
17
22
|
if (!this.apiKey) {
|
|
@@ -19,6 +24,7 @@ class MiniMaxQuotaProvider {
|
|
|
19
24
|
return null;
|
|
20
25
|
}
|
|
21
26
|
try {
|
|
27
|
+
// 调用 MiniMax 额度查询 API
|
|
22
28
|
const response = await fetch(`${this.baseUrl}/v1/api/openplatform/coding_plan/remains`, {
|
|
23
29
|
method: "GET",
|
|
24
30
|
headers: {
|
|
@@ -31,10 +37,12 @@ class MiniMaxQuotaProvider {
|
|
|
31
37
|
return null;
|
|
32
38
|
}
|
|
33
39
|
const data = (await response.json());
|
|
40
|
+
// 检查业务状态码
|
|
34
41
|
if (data.base_resp?.status_code !== 0) {
|
|
35
42
|
console.error(`${this.logTag} API error: ${data.base_resp?.status_msg}`);
|
|
36
43
|
return null;
|
|
37
44
|
}
|
|
45
|
+
// 只保留 MiniMax-M 开头的模型(coding plan 模型)
|
|
38
46
|
const codingPlanModels = data.model_remains.filter((m) => m.model_name.startsWith("MiniMax-M"));
|
|
39
47
|
if (codingPlanModels.length === 0) {
|
|
40
48
|
console.warn(`${this.logTag} No coding plan models found`);
|
|
@@ -47,9 +55,7 @@ class MiniMaxQuotaProvider {
|
|
|
47
55
|
return null;
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
|
-
|
|
51
|
-
return resolveEnvVar(value);
|
|
52
|
-
}
|
|
58
|
+
/** 将 API 响应映射为 QuotaData 格式 */
|
|
53
59
|
mapResponseToQuotaData(models) {
|
|
54
60
|
let totalRollingAvailable = 0;
|
|
55
61
|
let totalRollingLimit = 0;
|
|
@@ -57,6 +63,7 @@ class MiniMaxQuotaProvider {
|
|
|
57
63
|
let totalWeeklyAvailable = 0;
|
|
58
64
|
let totalWeeklyLimit = 0;
|
|
59
65
|
let weeklyResetMs = 0;
|
|
66
|
+
// 累加所有模型的额度
|
|
60
67
|
for (const m of models) {
|
|
61
68
|
totalRollingAvailable += m.current_interval_usage_count;
|
|
62
69
|
totalRollingLimit += m.current_interval_total_count;
|
|
@@ -65,6 +72,7 @@ class MiniMaxQuotaProvider {
|
|
|
65
72
|
rollingResetMs = Math.max(rollingResetMs, m.end_time);
|
|
66
73
|
weeklyResetMs = Math.max(weeklyResetMs, m.weekly_end_time);
|
|
67
74
|
}
|
|
75
|
+
// 计算已用量和百分比
|
|
68
76
|
const rollingUsed = Math.max(0, totalRollingLimit - totalRollingAvailable);
|
|
69
77
|
const weeklyUsed = Math.max(0, totalWeeklyLimit - totalWeeklyAvailable);
|
|
70
78
|
const rollingPercent = totalRollingLimit > 0
|
|
@@ -73,36 +81,30 @@ class MiniMaxQuotaProvider {
|
|
|
73
81
|
const weeklyPercent = totalWeeklyLimit > 0
|
|
74
82
|
? Math.round((weeklyUsed / totalWeeklyLimit) * 100)
|
|
75
83
|
: 0;
|
|
84
|
+
// 计算距离重置的时间
|
|
76
85
|
const now = Date.now();
|
|
77
86
|
const rollingResetSec = Math.max(0, Math.floor((rollingResetMs - now) / 1000));
|
|
78
87
|
const weeklyResetSec = Math.max(0, Math.floor((weeklyResetMs - now) / 1000));
|
|
79
88
|
return {
|
|
80
89
|
rolling: {
|
|
81
90
|
usage: rollingPercent,
|
|
82
|
-
reset:
|
|
91
|
+
reset: formatDurationCompact(rollingResetSec),
|
|
83
92
|
},
|
|
84
93
|
weekly: {
|
|
85
94
|
usage: weeklyPercent,
|
|
86
|
-
reset:
|
|
95
|
+
reset: formatDurationCompact(weeklyResetSec),
|
|
87
96
|
},
|
|
88
97
|
monthly: undefined,
|
|
89
98
|
};
|
|
90
99
|
}
|
|
91
|
-
formatDuration(seconds) {
|
|
92
|
-
if (seconds < 60)
|
|
93
|
-
return `${seconds}s`;
|
|
94
|
-
if (seconds < 3600)
|
|
95
|
-
return `${Math.round(seconds / 60)}m`;
|
|
96
|
-
if (seconds < 86400)
|
|
97
|
-
return `${Math.round(seconds / 3600)}h`;
|
|
98
|
-
return `${Math.round(seconds / 86400)}d`;
|
|
99
|
-
}
|
|
100
100
|
}
|
|
101
|
+
/** MiniMax CN (国内版) Provider */
|
|
101
102
|
export class MiniMaxCNQuotaProvider extends MiniMaxQuotaProvider {
|
|
102
103
|
constructor() {
|
|
103
104
|
super("minimax-cn-coding-plan", "https://www.minimaxi.com");
|
|
104
105
|
}
|
|
105
106
|
}
|
|
107
|
+
/** MiniMax IO (海外版) Provider */
|
|
106
108
|
export class MiniMaxIOQuotaProvider extends MiniMaxQuotaProvider {
|
|
107
109
|
constructor() {
|
|
108
110
|
super("minimax-coding-plan", "https://api.minimax.io");
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { QuotaData, ProviderConfig } from "../types.js";
|
|
2
2
|
import { QuotaProvider } from "../provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* OpenCode Go 额度 Provider
|
|
5
|
+
* 通过 opencode.ai 网页 API 获取用户额度信息
|
|
6
|
+
*/
|
|
3
7
|
export declare class OpenCodeGoQuotaProvider implements QuotaProvider {
|
|
4
8
|
readonly name = "opencode-go";
|
|
5
9
|
private cookie;
|
|
@@ -8,7 +12,5 @@ export declare class OpenCodeGoQuotaProvider implements QuotaProvider {
|
|
|
8
12
|
private baseUrl;
|
|
9
13
|
init(config: ProviderConfig, _credentials: Record<string, unknown>): void;
|
|
10
14
|
fetchQuota(): Promise<QuotaData | null>;
|
|
11
|
-
resolveEnvVar(value: string | undefined): string | undefined;
|
|
12
|
-
private formatDuration;
|
|
13
15
|
}
|
|
14
16
|
//# sourceMappingURL=opencode-go.d.ts.map
|
|
@@ -1 +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;
|
|
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;AAG9D;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,aAAa;IAC3D,QAAQ,CAAC,IAAI,iBAAiB;IAE9B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,WAAW,CAAqB;IAExC,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;IAMnE,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;CAkF9C"}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { resolveEnvVar } from "../provider.js";
|
|
2
|
+
import { formatDurationCompact } from "../../formatters.js";
|
|
3
|
+
/**
|
|
4
|
+
* OpenCode Go 额度 Provider
|
|
5
|
+
* 通过 opencode.ai 网页 API 获取用户额度信息
|
|
6
|
+
*/
|
|
2
7
|
export class OpenCodeGoQuotaProvider {
|
|
3
8
|
name = "opencode-go";
|
|
4
9
|
cookie;
|
|
5
10
|
workspaceId;
|
|
11
|
+
// 服务端点 ID,用于调用 opencode.ai 的内部 RPC 服务
|
|
6
12
|
serviceId = "c7389bd0e731f80f49593e5ee53835475f4e28594dd6bd83eb229bab753498cd";
|
|
7
13
|
baseUrl = "https://opencode.ai";
|
|
8
14
|
init(config, _credentials) {
|
|
15
|
+
// 从配置中读取 cookie 和 workspaceId,支持 ${ENV_VAR} 格式
|
|
9
16
|
this.cookie = resolveEnvVar(config.cookie);
|
|
10
17
|
this.workspaceId = resolveEnvVar(config.workspaceId);
|
|
11
18
|
}
|
|
@@ -14,6 +21,7 @@ export class OpenCodeGoQuotaProvider {
|
|
|
14
21
|
console.warn("[OpenCodeGoQuotaProvider] Missing cookie or workspaceId");
|
|
15
22
|
return null;
|
|
16
23
|
}
|
|
24
|
+
// 构建 RPC 调用参数
|
|
17
25
|
const args = JSON.stringify({
|
|
18
26
|
t: { t: 9, i: 0, l: 1, a: [{ t: 1, s: this.workspaceId }], o: 0 },
|
|
19
27
|
f: 31,
|
|
@@ -41,11 +49,22 @@ export class OpenCodeGoQuotaProvider {
|
|
|
41
49
|
return null;
|
|
42
50
|
}
|
|
43
51
|
const text = await response.text();
|
|
52
|
+
// 从响应文本中用正则提取 rolling/weekly/monthly 额度数据
|
|
53
|
+
// 响应格式如: rollingUsage:$R[1]={status:"active",resetInSec:3600,usagePercent:45}
|
|
44
54
|
const rollingMatch = text.match(/rollingUsage:\$R\[1\]=\{status:"([^"]+)",resetInSec:(\d+),usagePercent:(\d+)\}/);
|
|
45
55
|
const weeklyMatch = text.match(/weeklyUsage:\$R\[2\]=\{status:"([^"]+)",resetInSec:(\d+),usagePercent:(\d+)\}/);
|
|
46
56
|
const monthlyMatch = text.match(/monthlyUsage:\$R\[3\]=\{status:"([^"]+)",resetInSec:(\d+),usagePercent:(\d+)\}/);
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
// 分别检查每个字段的解析结果,提供更详细的错误信息
|
|
58
|
+
if (!rollingMatch) {
|
|
59
|
+
console.error("[OpenCodeGoQuotaProvider] Failed to parse rollingUsage");
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (!weeklyMatch) {
|
|
63
|
+
console.error("[OpenCodeGoQuotaProvider] Failed to parse weeklyUsage");
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (!monthlyMatch) {
|
|
67
|
+
console.error("[OpenCodeGoQuotaProvider] Failed to parse monthlyUsage");
|
|
49
68
|
return null;
|
|
50
69
|
}
|
|
51
70
|
const rollingUsage = { status: rollingMatch[1], resetInSec: parseInt(rollingMatch[2], 10), usagePercent: parseInt(rollingMatch[3], 10) };
|
|
@@ -54,14 +73,15 @@ export class OpenCodeGoQuotaProvider {
|
|
|
54
73
|
return {
|
|
55
74
|
rolling: {
|
|
56
75
|
usage: rollingUsage.usagePercent,
|
|
57
|
-
reset:
|
|
76
|
+
reset: formatDurationCompact(rollingUsage.resetInSec),
|
|
58
77
|
},
|
|
59
78
|
weekly: {
|
|
60
79
|
usage: weeklyUsage.usagePercent,
|
|
61
|
-
reset:
|
|
80
|
+
reset: formatDurationCompact(weeklyUsage.resetInSec),
|
|
62
81
|
},
|
|
82
|
+
// 如果 monthly 状态是 "unlimited" 则不显示
|
|
63
83
|
monthly: monthlyUsage.status !== "unlimited"
|
|
64
|
-
? { usage: monthlyUsage.usagePercent, reset:
|
|
84
|
+
? { usage: monthlyUsage.usagePercent, reset: formatDurationCompact(monthlyUsage.resetInSec) }
|
|
65
85
|
: undefined,
|
|
66
86
|
};
|
|
67
87
|
}
|
|
@@ -70,16 +90,4 @@ export class OpenCodeGoQuotaProvider {
|
|
|
70
90
|
return null;
|
|
71
91
|
}
|
|
72
92
|
}
|
|
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
93
|
}
|
package/dist/quota/service.d.ts
CHANGED
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
import type { QuotaResult } from "./types.js";
|
|
2
2
|
import type { QuotaProvider } from "./provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* 额度服务 - 管理所有 Provider 的注册和调用
|
|
5
|
+
*/
|
|
3
6
|
export declare class QuotaService {
|
|
7
|
+
/** 已注册的 Provider 映射表 */
|
|
4
8
|
private providers;
|
|
9
|
+
/** 从配置文件加载的 Provider 注册表 */
|
|
5
10
|
private providerRegistry;
|
|
11
|
+
/** 当前活跃的 Provider 名称 */
|
|
6
12
|
private activeProviderName;
|
|
13
|
+
/** 当前活跃的 Provider 实例 */
|
|
7
14
|
private activeProvider;
|
|
15
|
+
/** 刷新次数计数器 */
|
|
8
16
|
private refreshCount;
|
|
9
17
|
constructor();
|
|
18
|
+
/**
|
|
19
|
+
* 注册一个 Provider
|
|
20
|
+
* @param provider Provider 实例
|
|
21
|
+
*/
|
|
10
22
|
registerProvider(provider: QuotaProvider): void;
|
|
23
|
+
/**
|
|
24
|
+
* 设置当前活跃的 Provider
|
|
25
|
+
* 会调用 Provider 的 init 方法进行初始化
|
|
26
|
+
* @param providerName Provider 名称
|
|
27
|
+
* @returns 是否设置成功(Provider 是否已注册)
|
|
28
|
+
*/
|
|
11
29
|
setActiveProvider(providerName: string): boolean;
|
|
30
|
+
/** 获取当前活跃的 Provider 名称 */
|
|
12
31
|
getActiveProviderName(): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* 检查某个 Provider 是否已注册
|
|
34
|
+
* @param providerName Provider 名称
|
|
35
|
+
*/
|
|
13
36
|
isProviderSupported(providerName: string): boolean;
|
|
37
|
+
/** 获取所有已注册的 Provider 名称列表 */
|
|
14
38
|
getRegisteredProviderNames(): string[];
|
|
39
|
+
/** 获取所有已配置的 Provider 名称列表(从配置文件) */
|
|
15
40
|
getConfiguredProviderNames(): string[];
|
|
41
|
+
/**
|
|
42
|
+
* 获取当前 Provider 的额度数据
|
|
43
|
+
* @returns 额度结果,包含 provider 名称、额度数据和刷新计数
|
|
44
|
+
*/
|
|
16
45
|
fetchQuota(): Promise<QuotaResult | null>;
|
|
17
46
|
}
|
|
18
47
|
//# sourceMappingURL=service.d.ts.map
|
|
@@ -1 +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;;
|
|
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;;GAEG;AACH,qBAAa,YAAY;IACvB,wBAAwB;IACxB,OAAO,CAAC,SAAS,CAAyC;IAC1D,4BAA4B;IAC5B,OAAO,CAAC,gBAAgB,CAAwB;IAChD,wBAAwB;IACxB,OAAO,CAAC,kBAAkB,CAAuB;IACjD,wBAAwB;IACxB,OAAO,CAAC,cAAc,CAA8B;IACpD,cAAc;IACd,OAAO,CAAC,YAAY,CAAK;;IASzB;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAI/C;;;;;OAKG;IACH,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAoBhD,0BAA0B;IAC1B,qBAAqB,IAAI,MAAM,GAAG,IAAI;IAItC;;;OAGG;IACH,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAIlD,6BAA6B;IAC7B,0BAA0B,IAAI,MAAM,EAAE;IAItC,oCAAoC;IACpC,0BAA0B,IAAI,MAAM,EAAE;IAItC;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;CAgBhD"}
|
package/dist/quota/service.js
CHANGED
|
@@ -1,20 +1,39 @@
|
|
|
1
1
|
import { readProviderConfig, getProviderConfig } from "./config.js";
|
|
2
2
|
import { MiniMaxCNQuotaProvider, MiniMaxIOQuotaProvider } from "./providers/minimax.js";
|
|
3
3
|
import { OpenCodeGoQuotaProvider } from "./providers/opencode-go.js";
|
|
4
|
+
/**
|
|
5
|
+
* 额度服务 - 管理所有 Provider 的注册和调用
|
|
6
|
+
*/
|
|
4
7
|
export class QuotaService {
|
|
8
|
+
/** 已注册的 Provider 映射表 */
|
|
5
9
|
providers = new Map();
|
|
10
|
+
/** 从配置文件加载的 Provider 注册表 */
|
|
6
11
|
providerRegistry = readProviderConfig();
|
|
12
|
+
/** 当前活跃的 Provider 名称 */
|
|
7
13
|
activeProviderName = null;
|
|
14
|
+
/** 当前活跃的 Provider 实例 */
|
|
8
15
|
activeProvider = null;
|
|
16
|
+
/** 刷新次数计数器 */
|
|
9
17
|
refreshCount = 0;
|
|
10
18
|
constructor() {
|
|
19
|
+
// 注册支持的 Provider
|
|
11
20
|
this.registerProvider(new MiniMaxCNQuotaProvider());
|
|
12
21
|
this.registerProvider(new MiniMaxIOQuotaProvider());
|
|
13
22
|
this.registerProvider(new OpenCodeGoQuotaProvider());
|
|
14
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* 注册一个 Provider
|
|
26
|
+
* @param provider Provider 实例
|
|
27
|
+
*/
|
|
15
28
|
registerProvider(provider) {
|
|
16
29
|
this.providers.set(provider.name, provider);
|
|
17
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* 设置当前活跃的 Provider
|
|
33
|
+
* 会调用 Provider 的 init 方法进行初始化
|
|
34
|
+
* @param providerName Provider 名称
|
|
35
|
+
* @returns 是否设置成功(Provider 是否已注册)
|
|
36
|
+
*/
|
|
18
37
|
setActiveProvider(providerName) {
|
|
19
38
|
const provider = this.providers.get(providerName);
|
|
20
39
|
if (!provider) {
|
|
@@ -25,32 +44,47 @@ export class QuotaService {
|
|
|
25
44
|
if (!config) {
|
|
26
45
|
console.warn(`[QuotaService] No config for provider: ${providerName}`);
|
|
27
46
|
}
|
|
47
|
+
// 初始化 Provider,传入配置
|
|
28
48
|
provider.init(config || {}, {});
|
|
29
49
|
this.activeProviderName = providerName;
|
|
30
50
|
this.activeProvider = provider;
|
|
31
51
|
return true;
|
|
32
52
|
}
|
|
53
|
+
/** 获取当前活跃的 Provider 名称 */
|
|
33
54
|
getActiveProviderName() {
|
|
34
55
|
return this.activeProviderName;
|
|
35
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* 检查某个 Provider 是否已注册
|
|
59
|
+
* @param providerName Provider 名称
|
|
60
|
+
*/
|
|
36
61
|
isProviderSupported(providerName) {
|
|
37
62
|
return this.providers.has(providerName);
|
|
38
63
|
}
|
|
64
|
+
/** 获取所有已注册的 Provider 名称列表 */
|
|
39
65
|
getRegisteredProviderNames() {
|
|
40
66
|
return Array.from(this.providers.keys());
|
|
41
67
|
}
|
|
68
|
+
/** 获取所有已配置的 Provider 名称列表(从配置文件) */
|
|
42
69
|
getConfiguredProviderNames() {
|
|
43
70
|
return Object.keys(this.providerRegistry);
|
|
44
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* 获取当前 Provider 的额度数据
|
|
74
|
+
* @returns 额度结果,包含 provider 名称、额度数据和刷新计数
|
|
75
|
+
*/
|
|
45
76
|
async fetchQuota() {
|
|
46
77
|
if (!this.activeProvider) {
|
|
47
78
|
return null;
|
|
48
79
|
}
|
|
49
80
|
this.refreshCount++;
|
|
50
81
|
const quota = await this.activeProvider.fetchQuota();
|
|
82
|
+
if (!quota) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
51
85
|
return {
|
|
52
86
|
provider: this.activeProviderName,
|
|
53
|
-
quota
|
|
87
|
+
quota,
|
|
54
88
|
refreshCount: this.refreshCount,
|
|
55
89
|
};
|
|
56
90
|
}
|
package/dist/quota/types.d.ts
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 单个时间维度的额度使用情况
|
|
3
|
+
*/
|
|
1
4
|
export interface QuotaUsage {
|
|
5
|
+
/** 已使用百分比 (0-100) */
|
|
2
6
|
usage: number;
|
|
7
|
+
/** 距离重置的倒计时字符串,如 "2h", "30m", "5d" */
|
|
3
8
|
reset: string;
|
|
4
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* 额度数据结构
|
|
12
|
+
* 包含 Rolling(滚动周期)、Weekly(每周)、Monthly(每月) 三种维度的使用量
|
|
13
|
+
*/
|
|
5
14
|
export interface QuotaData {
|
|
15
|
+
/** 滚动周期额度(如当天 0 点开始的计数) */
|
|
6
16
|
rolling: QuotaUsage | undefined;
|
|
17
|
+
/** 本周额度 */
|
|
7
18
|
weekly: QuotaUsage | undefined;
|
|
19
|
+
/** 本月额度,部分 provider 可能无此维度 */
|
|
8
20
|
monthly: QuotaUsage | undefined;
|
|
9
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* 完整的额度查询结果
|
|
24
|
+
*/
|
|
10
25
|
export interface QuotaResult {
|
|
26
|
+
/** Provider 名称 */
|
|
11
27
|
provider: string;
|
|
28
|
+
/** 额度数据 */
|
|
12
29
|
quota: QuotaData;
|
|
30
|
+
/** 当前会话的刷新次数计数 */
|
|
13
31
|
refreshCount: number;
|
|
14
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* 单个 Provider 的配置格式
|
|
35
|
+
* 具体字段由各 Provider 自己定义,如 apiKey、cookie 等
|
|
36
|
+
*/
|
|
15
37
|
export interface ProviderConfig {
|
|
16
38
|
[key: string]: unknown;
|
|
17
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Provider 注册表
|
|
42
|
+
* key 为 provider 名称,value 为对应的配置
|
|
43
|
+
*/
|
|
18
44
|
export interface ProviderRegistry {
|
|
19
45
|
[providerName: string]: ProviderConfig;
|
|
20
46
|
}
|
|
@@ -1 +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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/quota/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,2BAA2B;IAC3B,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC;IAChC,WAAW;IACX,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IAC/B,8BAA8B;IAC9B,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,kBAAkB;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW;IACX,KAAK,EAAE,SAAS,CAAC;IACjB,kBAAkB;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,CAAC;CACxC"}
|
package/dist/session-info.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
3
3
|
import type { JSX } from "solid-js";
|
|
4
|
+
/**
|
|
5
|
+
* Session Info 视图组件
|
|
6
|
+
* 显示当前会话的基本信息:Session ID、Branch、Provider、Model、消息数等
|
|
7
|
+
*/
|
|
4
8
|
export declare function SessionInfoView(props: {
|
|
5
9
|
api: TuiPluginApi;
|
|
6
10
|
sessionId: string;
|
|
@@ -1 +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;
|
|
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;AAepC;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,GAAG,CAAC,OAAO,CAgFd"}
|
package/dist/session-info.jsx
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createSignal, createEffect, Show } from "solid-js";
|
|
2
2
|
import { LabelValue } from "./components.jsx";
|
|
3
|
+
/**
|
|
4
|
+
* Session Info 视图组件
|
|
5
|
+
* 显示当前会话的基本信息:Session ID、Branch、Provider、Model、消息数等
|
|
6
|
+
*/
|
|
3
7
|
export function SessionInfoView(props) {
|
|
4
8
|
const [data, setData] = createSignal(null);
|
|
5
9
|
createEffect(() => {
|
|
@@ -8,9 +12,14 @@ export function SessionInfoView(props) {
|
|
|
8
12
|
const todos = props.api.state.session.todo(sessionId);
|
|
9
13
|
const diff = props.api.state.session.diff(sessionId);
|
|
10
14
|
const vcs = props.api.state.vcs;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
// 从后向前查找最后一个 assistant 消息以获取 provider 和 model 信息
|
|
16
|
+
let lastAssistantMsg = null;
|
|
17
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
18
|
+
if (messages[i].role === "assistant") {
|
|
19
|
+
lastAssistantMsg = messages[i];
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
const lastModelInfo = lastAssistantMsg && "modelID" in lastAssistantMsg
|
|
15
24
|
? {
|
|
16
25
|
providerID: lastAssistantMsg.providerID,
|
package/dist/tokens-usage.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { JSX } from "solid-js";
|
|
3
3
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
4
|
+
/** 单个模型的 token 统计 */
|
|
4
5
|
export interface TokenStats {
|
|
5
6
|
providerID: string;
|
|
6
7
|
modelID: string;
|
|
@@ -16,5 +17,10 @@ export interface TokensUsageViewProps {
|
|
|
16
17
|
api: TuiPluginApi;
|
|
17
18
|
sessionId: string;
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Tokens Usage 视图组件
|
|
22
|
+
* 统计当前会话中所有 assistant 消息的 token 消耗
|
|
23
|
+
* 按模型分组显示累计数据
|
|
24
|
+
*/
|
|
19
25
|
export declare function TokensUsageView(props: TokensUsageViewProps): JSX.Element;
|
|
20
26
|
//# sourceMappingURL=tokens-usage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens-usage.d.ts","sourceRoot":"","sources":["../src/tokens-usage.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAIpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG5D,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"tokens-usage.d.ts","sourceRoot":"","sources":["../src/tokens-usage.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAIpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG5D,qBAAqB;AACrB,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAaD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,GAAG,CAAC,OAAO,CAsIxE"}
|
package/dist/tokens-usage.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSignal, createEffect, Show, For } from "solid-js";
|
|
2
2
|
import { Title } from "./components.jsx";
|
|
3
3
|
import { formatNumber, formatCost } from "./formatters.js";
|
|
4
|
+
/** 内联指标组件:显示 "标签: 值" 格式 */
|
|
4
5
|
function InlineMetric(props) {
|
|
5
6
|
return (<box flexDirection="row" gap={0}>
|
|
6
7
|
<text fg={props.color}>{props.label}</text>
|
|
@@ -8,6 +9,11 @@ function InlineMetric(props) {
|
|
|
8
9
|
<text>{props.value}</text>
|
|
9
10
|
</box>);
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Tokens Usage 视图组件
|
|
14
|
+
* 统计当前会话中所有 assistant 消息的 token 消耗
|
|
15
|
+
* 按模型分组显示累计数据
|
|
16
|
+
*/
|
|
11
17
|
export function TokensUsageView(props) {
|
|
12
18
|
const [stats, setStats] = createSignal([]);
|
|
13
19
|
const [totals, setTotals] = createSignal(null);
|
|
@@ -21,14 +27,15 @@ export function TokensUsageView(props) {
|
|
|
21
27
|
setIsLoading(false);
|
|
22
28
|
return;
|
|
23
29
|
}
|
|
30
|
+
// 按 (providerID, modelID) 分组统计
|
|
24
31
|
const grouped = new Map();
|
|
25
|
-
let lastInput = 0;
|
|
26
32
|
messages.forEach((msg) => {
|
|
27
33
|
if (msg.role !== "assistant")
|
|
28
34
|
return;
|
|
29
35
|
const assistantMsg = msg;
|
|
30
36
|
if (!assistantMsg.tokens)
|
|
31
37
|
return;
|
|
38
|
+
// 以 providerID::modelID 作为分组 key
|
|
32
39
|
const key = `${assistantMsg.providerID || "unknown"}::${assistantMsg.modelID || "unknown"}`;
|
|
33
40
|
if (!grouped.has(key)) {
|
|
34
41
|
grouped.set(key, {
|
|
@@ -43,6 +50,7 @@ export function TokensUsageView(props) {
|
|
|
43
50
|
messageCount: 0,
|
|
44
51
|
});
|
|
45
52
|
}
|
|
53
|
+
// 累加各项数据
|
|
46
54
|
const stat = grouped.get(key);
|
|
47
55
|
stat.totalCost += assistantMsg.cost || 0;
|
|
48
56
|
stat.input += assistantMsg.tokens.input || 0;
|
|
@@ -51,8 +59,8 @@ export function TokensUsageView(props) {
|
|
|
51
59
|
stat.cacheRead += assistantMsg.tokens.cache?.read || 0;
|
|
52
60
|
stat.cacheWrite += assistantMsg.tokens.cache?.write || 0;
|
|
53
61
|
stat.messageCount += 1;
|
|
54
|
-
lastInput = assistantMsg.tokens.input || 0;
|
|
55
62
|
});
|
|
63
|
+
// 计算总计
|
|
56
64
|
let totalInput = 0, totalOutput = 0, totalReasoning = 0;
|
|
57
65
|
let totalCacheRead = 0, totalCacheWrite = 0, totalCost = 0;
|
|
58
66
|
grouped.forEach((stat) => {
|
|
@@ -71,7 +79,6 @@ export function TokensUsageView(props) {
|
|
|
71
79
|
cacheRead: totalCacheRead,
|
|
72
80
|
cacheWrite: totalCacheWrite,
|
|
73
81
|
cost: totalCost,
|
|
74
|
-
currentInput: lastInput,
|
|
75
82
|
});
|
|
76
83
|
setIsLoading(false);
|
|
77
84
|
});
|
package/dist/usage.d.ts
CHANGED
|
@@ -13,5 +13,9 @@ export interface UsageViewProps {
|
|
|
13
13
|
api: TuiPluginApi;
|
|
14
14
|
sessionId: string;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Usage Quota 视图组件
|
|
18
|
+
* 显示 Rolling/Weekly/Monthly 三种维度的额度使用情况
|
|
19
|
+
*/
|
|
16
20
|
export declare function UsageView(props: UsageViewProps): JSX.Element;
|
|
17
21
|
//# sourceMappingURL=usage.d.ts.map
|
package/dist/usage.d.ts.map
CHANGED
|
@@ -1 +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;AAIpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;
|
|
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;AAIpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAKpD,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;QACjD,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;QACnD,0BAA0B,IAAI,MAAM,EAAE,CAAC;QACvC,0BAA0B,IAAI,MAAM,EAAE,CAAC;KACxC,CAAC;IACF,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAyCD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,GAAG,CAAC,OAAO,CA4N5D"}
|
package/dist/usage.jsx
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { createSignal, createEffect, onCleanup } from "solid-js";
|
|
2
2
|
import { Title, ProgressBar } from "./components.jsx";
|
|
3
3
|
import { formatDuration } from "./formatters.js";
|
|
4
|
+
/** 额度刷新间隔(秒) */
|
|
4
5
|
const REFRESH_INTERVAL = 60;
|
|
6
|
+
/**
|
|
7
|
+
* 空状态组件
|
|
8
|
+
* 根据不同情况显示对应的提示信息
|
|
9
|
+
*/
|
|
5
10
|
function EmptyState(props) {
|
|
6
11
|
if (!props.provider) {
|
|
7
12
|
return <text fg="#888">No LLM activity detected</text>;
|
|
@@ -20,6 +25,10 @@ function EmptyState(props) {
|
|
|
20
25
|
}
|
|
21
26
|
return <text fg="#888">No quota data available</text>;
|
|
22
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Usage Quota 视图组件
|
|
30
|
+
* 显示 Rolling/Weekly/Monthly 三种维度的额度使用情况
|
|
31
|
+
*/
|
|
23
32
|
export function UsageView(props) {
|
|
24
33
|
const [result, setResult] = createSignal(null);
|
|
25
34
|
const [loading, setLoading] = createSignal(true);
|
|
@@ -28,20 +37,26 @@ export function UsageView(props) {
|
|
|
28
37
|
const [refreshCountdown, setRefreshCountdown] = createSignal(REFRESH_INTERVAL);
|
|
29
38
|
const [providerSupported, setProviderSupported] = createSignal(true);
|
|
30
39
|
const [fetchError, setFetchError] = createSignal(null);
|
|
40
|
+
// 请求 ID 计数器,用于处理竞态条件
|
|
41
|
+
let currentRequestId = 0;
|
|
42
|
+
// 从 v1.0.0 恢复的刷新逻辑:只在 provider/model 组合变化时刷新
|
|
31
43
|
const doRefresh = () => {
|
|
32
44
|
const providerID = currentProvider();
|
|
33
45
|
if (!providerID)
|
|
34
46
|
return;
|
|
47
|
+
const requestId = ++currentRequestId;
|
|
35
48
|
setLoading(true);
|
|
36
49
|
setFetchError(null);
|
|
37
50
|
const supported = props.quotaService.setActiveProvider(providerID);
|
|
38
51
|
setProviderSupported(supported);
|
|
39
52
|
if (!supported) {
|
|
40
|
-
setLoading(false);
|
|
41
53
|
setResult(null);
|
|
54
|
+
setLoading(false);
|
|
42
55
|
return;
|
|
43
56
|
}
|
|
44
57
|
props.quotaService.fetchQuota().then((data) => {
|
|
58
|
+
if (requestId !== currentRequestId)
|
|
59
|
+
return;
|
|
45
60
|
if (data && data.quota) {
|
|
46
61
|
setResult(data);
|
|
47
62
|
}
|
|
@@ -50,10 +65,15 @@ export function UsageView(props) {
|
|
|
50
65
|
}
|
|
51
66
|
setLoading(false);
|
|
52
67
|
}).catch((error) => {
|
|
68
|
+
if (requestId !== currentRequestId)
|
|
69
|
+
return;
|
|
70
|
+
console.error("[UsageView] Failed to fetch quota:", error);
|
|
53
71
|
setFetchError(String(error));
|
|
72
|
+
setResult(null);
|
|
54
73
|
setLoading(false);
|
|
55
74
|
});
|
|
56
75
|
};
|
|
76
|
+
// Effect 1: 检测 session 消息,提取 provider/model
|
|
57
77
|
createEffect(() => {
|
|
58
78
|
const sessionId = props.sessionId;
|
|
59
79
|
const messages = props.api.state.session.messages(sessionId);
|
|
@@ -64,9 +84,14 @@ export function UsageView(props) {
|
|
|
64
84
|
setProviderSupported(false);
|
|
65
85
|
return;
|
|
66
86
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
// 从后向前查找最后一个 assistant 消息
|
|
88
|
+
let lastAssistantMsg = null;
|
|
89
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
90
|
+
if (messages[i].role === "assistant") {
|
|
91
|
+
lastAssistantMsg = messages[i];
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
70
95
|
if (!lastAssistantMsg) {
|
|
71
96
|
setCurrentProvider(null);
|
|
72
97
|
setCurrentModel(null);
|
|
@@ -74,7 +99,7 @@ export function UsageView(props) {
|
|
|
74
99
|
setProviderSupported(false);
|
|
75
100
|
return;
|
|
76
101
|
}
|
|
77
|
-
if (!("providerID" in lastAssistantMsg)) {
|
|
102
|
+
if (!("providerID" in lastAssistantMsg) || typeof lastAssistantMsg.providerID !== "string") {
|
|
78
103
|
setCurrentProvider(null);
|
|
79
104
|
setCurrentModel(null);
|
|
80
105
|
setFetchError(null);
|
|
@@ -82,12 +107,16 @@ export function UsageView(props) {
|
|
|
82
107
|
return;
|
|
83
108
|
}
|
|
84
109
|
const providerID = lastAssistantMsg.providerID;
|
|
85
|
-
const modelID = "modelID" in lastAssistantMsg
|
|
110
|
+
const modelID = "modelID" in lastAssistantMsg && typeof lastAssistantMsg.modelID === "string"
|
|
86
111
|
? lastAssistantMsg.modelID
|
|
87
112
|
: "";
|
|
88
|
-
|
|
89
|
-
|
|
113
|
+
// 检查是否真的发生了变化
|
|
114
|
+
if (providerID !== currentProvider() || modelID !== currentModel()) {
|
|
115
|
+
setCurrentProvider(providerID);
|
|
116
|
+
setCurrentModel(modelID);
|
|
117
|
+
}
|
|
90
118
|
});
|
|
119
|
+
// Effect 2: 监听 provider 变化,触发额度获取
|
|
91
120
|
createEffect(() => {
|
|
92
121
|
const providerID = currentProvider();
|
|
93
122
|
if (!providerID) {
|
|
@@ -96,30 +125,9 @@ export function UsageView(props) {
|
|
|
96
125
|
setProviderSupported(false);
|
|
97
126
|
return;
|
|
98
127
|
}
|
|
99
|
-
|
|
100
|
-
setFetchError(null);
|
|
101
|
-
const supported = props.quotaService.setActiveProvider(providerID);
|
|
102
|
-
setProviderSupported(supported);
|
|
103
|
-
if (!supported) {
|
|
104
|
-
setResult(null);
|
|
105
|
-
setLoading(false);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
props.quotaService.fetchQuota().then((data) => {
|
|
109
|
-
if (data && data.quota) {
|
|
110
|
-
setResult(data);
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
setResult(null);
|
|
114
|
-
}
|
|
115
|
-
setLoading(false);
|
|
116
|
-
}).catch((error) => {
|
|
117
|
-
console.error("[UsageView] Failed to fetch quota:", error);
|
|
118
|
-
setFetchError(String(error));
|
|
119
|
-
setResult(null);
|
|
120
|
-
setLoading(false);
|
|
121
|
-
});
|
|
128
|
+
doRefresh();
|
|
122
129
|
});
|
|
130
|
+
// Effect 3: 倒计时定时器,归零时触发刷新
|
|
123
131
|
createEffect(() => {
|
|
124
132
|
setRefreshCountdown(REFRESH_INTERVAL);
|
|
125
133
|
const id = setInterval(() => {
|