@yinxe/opencode-tui-usage 0.0.8 → 1.0.1

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.
Files changed (40) hide show
  1. package/README.md +7 -7
  2. package/dist/components.d.ts +12 -0
  3. package/dist/components.d.ts.map +1 -1
  4. package/dist/components.jsx +13 -0
  5. package/dist/context-usage.d.ts +9 -0
  6. package/dist/context-usage.d.ts.map +1 -1
  7. package/dist/context-usage.jsx +18 -1
  8. package/dist/formatters.d.ts +25 -0
  9. package/dist/formatters.d.ts.map +1 -1
  10. package/dist/formatters.js +33 -0
  11. package/dist/quota/config.d.ts +10 -0
  12. package/dist/quota/config.d.ts.map +1 -1
  13. package/dist/quota/config.js +11 -3
  14. package/dist/quota/index.d.ts +5 -0
  15. package/dist/quota/index.d.ts.map +1 -0
  16. package/dist/quota/index.js +4 -0
  17. package/dist/quota/provider.d.ts +20 -1
  18. package/dist/quota/provider.d.ts.map +1 -1
  19. package/dist/quota/provider.js +6 -0
  20. package/dist/quota/providers/minimax.d.ts +7 -2
  21. package/dist/quota/providers/minimax.d.ts.map +1 -1
  22. package/dist/quota/providers/minimax.js +17 -15
  23. package/dist/quota/providers/opencode-go.d.ts +4 -2
  24. package/dist/quota/providers/opencode-go.d.ts.map +1 -1
  25. package/dist/quota/providers/opencode-go.js +25 -17
  26. package/dist/quota/service.d.ts +29 -0
  27. package/dist/quota/service.d.ts.map +1 -1
  28. package/dist/quota/service.js +35 -1
  29. package/dist/quota/types.d.ts +26 -0
  30. package/dist/quota/types.d.ts.map +1 -1
  31. package/dist/session-info.d.ts +4 -0
  32. package/dist/session-info.d.ts.map +1 -1
  33. package/dist/session-info.jsx +12 -3
  34. package/dist/tokens-usage.d.ts +6 -0
  35. package/dist/tokens-usage.d.ts.map +1 -1
  36. package/dist/tokens-usage.jsx +10 -3
  37. package/dist/usage.d.ts +4 -0
  38. package/dist/usage.d.ts.map +1 -1
  39. package/dist/usage.jsx +43 -49
  40. 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 @yinx-in/opencode-tui-usage
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 @yinx-in:registry https://npm.pkg.github.com
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 @yinx-in/opencode-tui-usage
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": ["@yinx-in/opencode-tui-usage"]
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
- - 包名 `@yinx-in/opencode-tui-usage` 已在 package.json 中配置
273
+ - 包名 `@yinxe/opencode-tui-usage` 已在 package.json 中配置
274
274
  - `publishConfig.registry` 指定发布到哪个 registry
275
275
 
276
276
  ### 发布地址
277
277
 
278
278
  | 平台 | 包名 | 地址 |
279
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 |
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
 
@@ -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
@@ -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,CAchE"}
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"}
@@ -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';
@@ -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,CAiE1E"}
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"}
@@ -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
- const model = provider?.models[msg.modelID];
46
+ if (!provider) {
47
+ continue;
48
+ }
49
+ const model = provider.models[msg.modelID];
33
50
  limit = model?.limit?.context ?? 0;
34
51
  }
35
52
  }
@@ -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
@@ -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"}
@@ -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
+ }
@@ -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;AASnE,wBAAgB,kBAAkB,IAAI,gBAAgB,CAWrD;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAI5B"}
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"}
@@ -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"}
@@ -0,0 +1,4 @@
1
+ // Quota 模块统一导出
2
+ export { QuotaService } from "./service.js";
3
+ export { resolveEnvVar } from "./provider.js";
4
+ export { readProviderConfig, getProviderConfig } from "./config.js";
@@ -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;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"}
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"}
@@ -1,3 +1,9 @@
1
+ /**
2
+ * 解析环境变量引用
3
+ * 支持 ${ENV_VAR} 格式,从 process.env 读取实际值
4
+ * @param value 可能是 ${ENV_VAR} 格式的字符串
5
+ * @returns 解析后的值,如果未找到环境变量则返回 undefined
6
+ */
1
7
  export const resolveEnvVar = (value) => {
2
8
  if (!value)
3
9
  return undefined;
@@ -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
- resolveEnvVar(value: string | undefined): string | undefined;
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;AAwB9D,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;IA8C7C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS;IAI5D,OAAO,CAAC,sBAAsB;IA8C9B,OAAO,CAAC,cAAc;CAMvB;AAED,qBAAa,sBAAuB,SAAQ,oBAAoB;;CAI/D;AAED,qBAAa,sBAAuB,SAAQ,oBAAoB;;CAI/D"}
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) || resolveEnvVar(config.apiKeyEnvVar);
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
- resolveEnvVar(value) {
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: this.formatDuration(rollingResetSec),
91
+ reset: formatDurationCompact(rollingResetSec),
83
92
  },
84
93
  weekly: {
85
94
  usage: weeklyPercent,
86
- reset: this.formatDuration(weeklyResetSec),
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;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"}
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
- if (!rollingMatch || !weeklyMatch || !monthlyMatch) {
48
- console.error("[OpenCodeGoQuotaProvider] Failed to parse response:", text.substring(0, 200));
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: this.formatDuration(rollingUsage.resetInSec),
76
+ reset: formatDurationCompact(rollingUsage.resetInSec),
58
77
  },
59
78
  weekly: {
60
79
  usage: weeklyUsage.usagePercent,
61
- reset: this.formatDuration(weeklyUsage.resetInSec),
80
+ reset: formatDurationCompact(weeklyUsage.resetInSec),
62
81
  },
82
+ // 如果 monthly 状态是 "unlimited" 则不显示
63
83
  monthly: monthlyUsage.status !== "unlimited"
64
- ? { usage: monthlyUsage.usagePercent, reset: this.formatDuration(monthlyUsage.resetInSec) }
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
  }
@@ -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;;IAQzB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAI/C,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAmBhD,qBAAqB,IAAI,MAAM,GAAG,IAAI;IAItC,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAIlD,0BAA0B,IAAI,MAAM,EAAE;IAItC,0BAA0B,IAAI,MAAM,EAAE;IAIhC,UAAU,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;CAahD"}
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"}
@@ -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: quota,
87
+ quota,
54
88
  refreshCount: this.refreshCount,
55
89
  };
56
90
  }
@@ -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"}
@@ -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;AAcpC,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,GAAG,CAAC,OAAO,CA2Ed"}
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"}
@@ -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
- const lastAssistantMsg = [...messages]
12
- .reverse()
13
- .find((m) => m.role === "assistant");
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,
@@ -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;AAYD,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,GAAG,CAAC,OAAO,CAsIxE"}
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"}
@@ -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
@@ -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;AAIpD,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;AAoCD,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,GAAG,CAAC,OAAO,CA2N5D"}
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,CA0M5D"}
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,74 +37,57 @@ 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);
31
- const doRefresh = () => {
32
- const providerID = currentProvider();
33
- if (!providerID)
34
- return;
35
- setLoading(true);
36
- setFetchError(null);
37
- const supported = props.quotaService.setActiveProvider(providerID);
38
- setProviderSupported(supported);
39
- if (!supported) {
40
- setLoading(false);
41
- setResult(null);
42
- return;
43
- }
44
- props.quotaService.fetchQuota().then((data) => {
45
- if (data && data.quota) {
46
- setResult(data);
47
- }
48
- else {
49
- setResult(null);
50
- }
51
- setLoading(false);
52
- }).catch((error) => {
53
- setFetchError(String(error));
54
- setLoading(false);
55
- });
56
- };
40
+ // 请求 ID 计数器,用于处理竞态条件
41
+ let currentRequestId = 0;
42
+ // 主 effect:检测 provider 并获取额度数据
57
43
  createEffect(() => {
58
44
  const sessionId = props.sessionId;
59
45
  const messages = props.api.state.session.messages(sessionId);
46
+ const requestId = ++currentRequestId;
47
+ // 无消息时重置状态
60
48
  if (!messages || messages.length === 0) {
61
49
  setCurrentProvider(null);
62
50
  setCurrentModel(null);
63
51
  setFetchError(null);
64
52
  setProviderSupported(false);
53
+ setResult(null);
54
+ setLoading(false);
65
55
  return;
66
56
  }
67
- const lastAssistantMsg = [...messages]
68
- .reverse()
69
- .find((m) => m.role === "assistant");
57
+ // 从后向前查找最后一个 assistant 消息
58
+ let lastAssistantMsg = null;
59
+ for (let i = messages.length - 1; i >= 0; i--) {
60
+ if (messages[i].role === "assistant") {
61
+ lastAssistantMsg = messages[i];
62
+ break;
63
+ }
64
+ }
70
65
  if (!lastAssistantMsg) {
71
66
  setCurrentProvider(null);
72
67
  setCurrentModel(null);
73
68
  setFetchError(null);
74
69
  setProviderSupported(false);
70
+ setResult(null);
71
+ setLoading(false);
75
72
  return;
76
73
  }
77
- if (!("providerID" in lastAssistantMsg)) {
74
+ // 检查 providerID 类型是否正确
75
+ if (!("providerID" in lastAssistantMsg) || typeof lastAssistantMsg.providerID !== "string") {
78
76
  setCurrentProvider(null);
79
77
  setCurrentModel(null);
80
78
  setFetchError(null);
81
79
  setProviderSupported(false);
80
+ setResult(null);
81
+ setLoading(false);
82
82
  return;
83
83
  }
84
84
  const providerID = lastAssistantMsg.providerID;
85
- const modelID = "modelID" in lastAssistantMsg
85
+ const modelID = "modelID" in lastAssistantMsg && typeof lastAssistantMsg.modelID === "string"
86
86
  ? lastAssistantMsg.modelID
87
87
  : "";
88
88
  setCurrentProvider(providerID);
89
89
  setCurrentModel(modelID);
90
- });
91
- createEffect(() => {
92
- const providerID = currentProvider();
93
- if (!providerID) {
94
- setResult(null);
95
- setLoading(false);
96
- setProviderSupported(false);
97
- return;
98
- }
90
+ // 设置 Provider 并获取额度数据
99
91
  setLoading(true);
100
92
  setFetchError(null);
101
93
  const supported = props.quotaService.setActiveProvider(providerID);
@@ -105,7 +97,11 @@ export function UsageView(props) {
105
97
  setLoading(false);
106
98
  return;
107
99
  }
108
- props.quotaService.fetchQuota().then((data) => {
100
+ props.quotaService.fetchQuota()
101
+ .then((data) => {
102
+ // 忽略过期响应(provider 已切换)
103
+ if (requestId !== currentRequestId)
104
+ return;
109
105
  if (data && data.quota) {
110
106
  setResult(data);
111
107
  }
@@ -113,23 +109,21 @@ export function UsageView(props) {
113
109
  setResult(null);
114
110
  }
115
111
  setLoading(false);
116
- }).catch((error) => {
112
+ })
113
+ .catch((error) => {
114
+ if (requestId !== currentRequestId)
115
+ return;
117
116
  console.error("[UsageView] Failed to fetch quota:", error);
118
117
  setFetchError(String(error));
119
118
  setResult(null);
120
119
  setLoading(false);
121
120
  });
122
121
  });
122
+ // 倒计时定时器 effect
123
123
  createEffect(() => {
124
124
  setRefreshCountdown(REFRESH_INTERVAL);
125
125
  const id = setInterval(() => {
126
- setRefreshCountdown((r) => {
127
- if (r <= 1) {
128
- doRefresh();
129
- return REFRESH_INTERVAL;
130
- }
131
- return r - 1;
132
- });
126
+ setRefreshCountdown((r) => (r <= 1 ? REFRESH_INTERVAL : r - 1));
133
127
  }, 1000);
134
128
  onCleanup(() => clearInterval(id));
135
129
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yinxe/opencode-tui-usage",
3
- "version": "0.0.8",
3
+ "version": "1.0.1",
4
4
  "description": "OpenCode TUI 额度显示插件 - 在侧边栏显示用量和额度信息",
5
5
  "repository": "github:Yinxe/opencode-tui-usage",
6
6
  "type": "module",