@kenz1117/dsh-ui-usage-billing 1.5.1 → 1.6.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 (43) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/lib/client.js +5 -5
  4. package/lib/index.js +728 -420
  5. package/lib/types/client/TokenPanel.d.ts +1 -1
  6. package/lib/types/client/UsageBilling.d.ts +7 -347
  7. package/lib/types/client/api/balance.d.ts +24 -0
  8. package/lib/types/client/api/pricing.d.ts +26 -0
  9. package/lib/types/client/api/relay.d.ts +13 -0
  10. package/lib/types/client/api/subscriptions.d.ts +13 -0
  11. package/lib/types/client/api/usage-stats.d.ts +11 -0
  12. package/lib/types/client/api/usage-tool.d.ts +16 -0
  13. package/lib/types/client/budget-store.d.ts +6 -0
  14. package/lib/types/client/constants.d.ts +28 -0
  15. package/lib/types/client/domain/forecast.d.ts +24 -0
  16. package/lib/types/client/domain/host-version.d.ts +18 -0
  17. package/lib/types/client/domain/icons.d.ts +9 -0
  18. package/lib/types/client/domain/kpi.d.ts +34 -0
  19. package/lib/types/client/domain/model-budget.d.ts +13 -0
  20. package/lib/types/client/domain/notify.d.ts +14 -0
  21. package/lib/types/client/domain/provider-aliases.d.ts +28 -0
  22. package/lib/types/client/domain/provider-merge.d.ts +26 -0
  23. package/lib/types/client/domain/recharge.d.ts +15 -0
  24. package/lib/types/client/domain/recost.d.ts +10 -0
  25. package/lib/types/client/domain/session-viewer.d.ts +13 -0
  26. package/lib/types/client/domain/streak.d.ts +25 -0
  27. package/lib/types/client/domain/subscription-vendors.d.ts +11 -0
  28. package/lib/types/client/domain/types.d.ts +312 -0
  29. package/lib/types/client/domain/week.d.ts +35 -0
  30. package/lib/types/client/locales.d.ts +1 -1
  31. package/lib/types/client/settings-search.d.ts +12 -0
  32. package/lib/types/config.d.ts +79 -0
  33. package/lib/types/guard.d.ts +49 -0
  34. package/lib/types/index.d.ts +3 -100
  35. package/lib/types/routes/balance.d.ts +37 -0
  36. package/lib/types/routes/notify-claim.d.ts +24 -0
  37. package/lib/types/routes/pricing.d.ts +26 -0
  38. package/lib/types/routes/relay-quotas.d.ts +27 -0
  39. package/lib/types/routes/subscriptions.d.ts +28 -0
  40. package/lib/types/routes/usage-stats.d.ts +44 -0
  41. package/lib/types/routes/usage-tool.d.ts +27 -0
  42. package/lib/types/silent.d.ts +21 -0
  43. package/package.json +14 -7
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 共享 loopback 守卫(v1.5.2 抽自 src/index.ts)。
3
+ *
4
+ * 端点只供本机浏览器用,局域网/远端请求一律拒绝,避免面板数据(含中转站
5
+ * origin 与余额)外泄。三层防御:peer socket(最可信)+ Host 头精确字面量
6
+ * (防 `127.0.0.1.evil.com` 这类 DNS rebinding 域名)+ trustedHosts 白名单
7
+ * (反向代理场景的精确主机名放行)。Origin 头独立校验(写操作用)。
8
+ *
9
+ * 设计要点:精确字面量匹配(`startsWith('127.')` 会被 `127.0.0.1.evil.com`
10
+ * 穿透,必须正则)。IPv6 字面量带方括号(`[::1]:3080`)单独剥离。
11
+ */
12
+ import type { IncomingMessage, ServerResponse } from 'node:http';
13
+ /** Peer 地址是否为回环(本地)。回环防护:本插件的端点只供本机浏览器用,
14
+ * 局域网/远端请求一律拒绝,避免面板数据(含中转站 origin 与余额)外泄。 */
15
+ export declare function isLoopbackPeer(req: IncomingMessage): boolean;
16
+ /** 校验 Host 头是本机回环(精确 127.0.0.0/8 / ::1 / localhost 或空,供 curl 不带 Host 的极简请求)。
17
+ * 拒绝 `127.0.0.1.attacker.com` 这类以 `127.` 开头但解析到外部的 DNS rebinding 域名:
18
+ * 只用 `startsWith('127.')` 会被它穿透,必须精确匹配回环 IP 的字面量。 */
19
+ /**
20
+ * Host 头/名单条目的主机名部分:IPv6 字面量带方括号(`[::1]:3080`),整体去括号;
21
+ * 裸 IPv6(多个冒号、无方括号)整体是字面量;其余按首个冒号去端口。统一小写。
22
+ * (此前一律 `split(':')[0]`:`[::1]:3080` 截成 `[`,IPv6 回环请求被误拒,
23
+ * trustedHosts 也配不进去——fail-closed 无安全暴露,但本机 IPv6 场景功能失效。)
24
+ */
25
+ export declare function hostNameOf(host: string): string;
26
+ /**
27
+ * 信任主机名归一化:去空白、去端口、转小写,丢弃空项(IPv6 经 {@link hostNameOf}
28
+ * 去方括号)。与请求侧 Host 头解析同口径,因此匹配忽略大小写与端口。
29
+ * @param hosts - 配置里的原始名单。
30
+ * @returns 归一化后的主机名集合(空集 = 与历史版本行为一致)。
31
+ */
32
+ export declare function normalizeTrustedHosts(hosts: readonly string[] | undefined): ReadonlySet<string>;
33
+ /** 校验 Host 头是本机回环(精确 127.0.0.0/8 / ::1 / localhost 或空,供 curl 不带 Host 的极简请求)。
34
+ * 拒绝 `127.0.0.1.attacker.com` 这类以 `127.` 开头但解析到外部的 DNS rebinding 域名:
35
+ * 只用 `startsWith('127.')` 会被它穿透,必须精确匹配回环 IP 的字面量。 */
36
+ export declare function isLoopbackHost(req: IncomingMessage, trustedHosts?: ReadonlySet<string>): boolean;
37
+ /** 校验 Origin 头是否回环(写操作用,防止跨站表单/fetch 改写设置)。Origin 缺失
38
+ * (curl 或同源 fetch 不带)视为放行,交给下方的 Content-Type 校验兜底;Origin 存在
39
+ * 但主机非回环则拒绝——跨站脚本发起的写请求必带攻击者域名的 Origin。 */
40
+ export declare function isLoopbackOrigin(origin: string | undefined): boolean;
41
+ /**
42
+ * 回环防护守卫:仅接受回环 GET 请求(peer socket 地址 + Host 头同时校验)。
43
+ * 不满足时返回 403 并结束响应;调用方在 handler 顶部调用,返回 false 即已拒绝。
44
+ * @param req - 当前请求。
45
+ * @param res - 当前响应。
46
+ * @returns 是否放行;false = 已拒绝并结束响应。
47
+ */
48
+ export declare function guardLoopback(req: IncomingMessage, res: ServerResponse, trustedHosts?: ReadonlySet<string>): boolean;
49
+ //# sourceMappingURL=guard.d.ts.map
@@ -12,13 +12,12 @@
12
12
  * file is served as a fallback, and a missing file answers `{ error }` so the
13
13
  * dashboard shows zeros, never fabricated samples.
14
14
  */
15
- import type { IncomingMessage, ServerResponse } from 'node:http';
16
15
  import type { Context } from '@deepseek-ai/cordis';
17
16
  import type { CredentialProvider } from '@deepseek-ai/dsh-credentials';
18
- import z from '@deepseek-ai/schemastery';
19
17
  import { type UsageLedgerStore, type UsagePersistence } from './aggregate.ts';
20
- import type { CustomBalanceConfig, DeclaredEndpointConfig, SubscriptionPlanConfig } from './pricing-shared.ts';
18
+ import type { UsageBillingConfig } from './config.ts';
21
19
  import { type IdentifiedSubscriptionPlan, type SubscriptionKeys } from './subscriptions.ts';
20
+ export { guardLoopback, isLoopbackOrigin, isLoopbackHost, isLoopbackPeer, hostNameOf, normalizeTrustedHosts } from './guard.ts';
22
21
  /** 设置服务的读取面:两代宿主运行时都提供 describe(条目键为 profile entry id)。 */
23
22
  interface SettingsReader {
24
23
  describe(options?: {
@@ -28,102 +27,7 @@ interface SettingsReader {
28
27
  value: unknown;
29
28
  }[];
30
29
  }
31
- /**
32
- * 信任主机名归一化:去空白、去端口、转小写,丢弃空项(IPv6 经 {@link hostNameOf}
33
- * 去方括号)。与请求侧 Host 头解析同口径,因此匹配忽略大小写与端口。
34
- * @param hosts - 配置里的原始名单。
35
- * @returns 归一化后的主机名集合(空集 = 与历史版本行为一致)。
36
- */
37
- export declare function normalizeTrustedHosts(hosts: readonly string[] | undefined): ReadonlySet<string>;
38
- /**
39
- * 回环防护守卫:仅接受回环 GET 请求(peer socket 地址 + Host 头同时校验)。
40
- * 不满足时返回 403 并结束响应;调用方在 handler 顶部调用,返回 false 即已拒绝。
41
- * @param req - 当前请求。
42
- * @param res - 当前响应。
43
- * @returns 是否放行;false = 已拒绝并结束响应。
44
- */
45
- export declare function guardLoopback(req: IncomingMessage, res: ServerResponse, trustedHosts?: ReadonlySet<string>): boolean;
46
- /** 宿主 0.1.7+ 经 Config schema 解析后,volatile 字段在 config 中的稳定引用形态
47
- * (cosmokit `Volatile<T>` 的最小结构镜像,避免为类型引入直接依赖)。 */
48
- export interface VolatileToggle {
49
- /** @returns 当前生效的布尔快照(设置写入后原位更新,无需重载插件)。 */
50
- get(): boolean;
51
- }
52
- /**
53
- * Loader 挂载时对条目 config 做校验的 Config 声明:`enableUsageStatsTool` 标记
54
- * volatile,宿主 0.1.7+ 设置系统据此把它投影为可实时编辑的开关(describe/update)。
55
- * schemastery 非严格 object 透传未声明键,其余 config 字段不受影响;未导出 Config
56
- * 的旧宿主直接把原始 config 交给 apply,普通布尔形态由读取端兼容。
57
- */
58
- export declare const Config: z<Schemastery.ObjectS<NoInfer<{
59
- enableUsageStatsTool: z<boolean, boolean, "volatile-defined">;
60
- }>>, Schemastery.ObjectT<NoInfer<{
61
- enableUsageStatsTool: z<boolean, boolean, "volatile-defined">;
62
- }>>, "plain">;
63
- /** Plugin configuration. */
64
- export interface UsageBillingConfig {
65
- /** Absolute path to a `.dsh-usage-stats.json` fallback file. */
66
- statsPath?: string;
67
- /** 统计快照的持久化路径;默认 `<harness home>/.dsh-usage-stats.json`
68
- * (harness home = `DSH_HOME` 环境变量或 `~/.dsh`,见 resolveDshHome)。
69
- * 测试注入临时目录以隔离真实家目录(聚合失败回退与快照落盘都走此路径)。 */
70
- snapshotPath?: string;
71
- /** 独立持久用量账本的绝对路径;默认 `<harness home>/.dsh-usage-ledger.json`。
72
- * 账本与会话日志解耦,因此永久删除会话不会抹掉已经观测到的用量。 */
73
- ledgerPath?: string;
74
- /** 余额差对账基准的持久化路径;默认 `<harness home>/.dsh-usage-reconcile.json`。 */
75
- reconcilePath?: string;
76
- /** 订阅制(coding / token / agent plan)provider id 列表;默认 kimi-coding、xiaomi-token-plan-cn。 */
77
- subscriptionProviders?: string[];
78
- /** 订阅套餐额度适配器(kimi / zai / opencode-go);默认全部内置。 */
79
- subscriptionPlans?: readonly SubscriptionPlanConfig[];
80
- /** 余额查询用的 DeepSeek 凭据引用(环境变量名);默认 DEEPSEEK_API_KEY。 */
81
- balanceApiKeyEnv?: string;
82
- /** 月度预算(人民币元);设置后随 usage-stats 下发,仪表盘显示预算进度条。 */
83
- monthlyBudget?: number;
84
- /** 余额不足告警阈值(人民币元):余额低于此值时仪表盘每天提醒一次;
85
- 不设置则客户端按默认阈值(50 元)兜底。 */
86
- lowBalanceThreshold?: number;
87
- /** 自定义 Provider 余额查询(任意 HTTP 端点 + extract 规则,适配 NewApi/LiteLLM 等)。 */
88
- customBalances?: readonly CustomBalanceConfig[];
89
- /**
90
- * 声明端点(declarative endpoints):为内置表没有的供应商自声明余额/额度接口。
91
- * 绑定到某条已配置 provider 的同源地址,`fields` / `windows` 写响应里的取值路径;
92
- * 安全边界(origin 绑定、单斜杠 path、只 GET、跨源重定向失败、凭据取自 apiKeyEnv)
93
- * 写死在 declarative.ts。缺省空。
94
- */
95
- declaredEndpoints?: readonly DeclaredEndpointConfig[];
96
- /**
97
- * 允许通过 Host 头校验的额外主机名(反向代理场景);缺省空,行为与历史版本完全一致。
98
- * 仅在 peer socket 已通过回环校验后才参考:socket 校验仍为强制且不受本字段影响,
99
- * 本名单只放宽「纵深防御」的第二层。匹配为**精确主机名**,忽略大小写与端口;
100
- * 不支持后缀 / 通配符,因此 `evil.com` 永远无法满足 `trusted.com`。
101
- * 例:`['llm.example.com']`。
102
- */
103
- trustedHosts?: string[];
104
- /** `usage_stats` 工具注入开关(默认 false:不注入)。宿主 0.1.7+ 经 Config
105
- * 解析后是 volatile 引用(设置写入即时反映),直连 apply(测试)是普通布尔;
106
- * 读取统一走 readUsageStatsToggle。该工具占用每次请求的上下文,coding 场景多在仪表盘查看。 */
107
- enableUsageStatsTool?: boolean | VolatileToggle;
108
- /**
109
- * 联网搜索请求(`web/deepseek-search-llm-request`,日志只有请求、无用量事件)
110
- * 的单次费用估算(人民币元)。这类调用直连官方 api.deepseek.com,开放平台照常
111
- * 计费但本地日志无 usage,按次估值计入今日/本月费用;默认 0.02 元/次,设 0 关闭。
112
- */
113
- searchCallEstimateCny?: number;
114
- /**
115
- * 历史路由别名(旧 provider 路由名 → 当前路由名):改过名/删除过的路由,其
116
- * 历史用量原会落「未知路由」桶且站点/订阅/官方判定全部失效;配置别名后按
117
- * 目标路由归位。例:`{ "deepseek-official": "tencent", "tencent-cloud": "tencent" }`。
118
- */
119
- routeAliases?: Record<string, string>;
120
- /**
121
- * 用户自定义模型别名(真实日志 model id → 计费目录键):目录外的新模型无需
122
- * 等发版,配置一条别名即完成识别与计价(值必须是 MODEL_CATALOG 的既有 key,
123
- * 如 `{ "hy4-preview": "hunyuan" }`)。聚合折叠与客户端渲染共用同一映射。
124
- */
125
- modelKeyAliases?: Record<string, string>;
126
- }
30
+ export type { UsageBillingConfig } from './config.ts';
127
31
  /**
128
32
  * Create the atomic file-backed durable-ledger store. The previous complete file
129
33
  * is retained as `.bak`; a malformed/missing main file falls back to that backup.
@@ -199,5 +103,4 @@ export declare function adaptSessionPersistence(raw: unknown): UsagePersistence;
199
103
  * @param config - optional statsPath override.
200
104
  */
201
105
  export declare function apply(ctx: Context, config?: UsageBillingConfig): void;
202
- export {};
203
106
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 路由:GET /api/billing/balance
3
+ *
4
+ * 多平台余额(DeepSeek / Kimi / StepFun / SiliconFlow / xAI / 智谱 / 腾讯云
5
+ * TokenHub 等)+ 用户自定义余额 + 声明端点 + 余额差对账。5 分钟缓存(避免
6
+ * 每 30s 后台轮询打上游),in-flight 去重(并发请求共享同一次进行中的刷新)。
7
+ *
8
+ * 路由文件只关心 HTTP 缓存逻辑:5 分钟过期判定 + 失败时仍发旧缓存(不挂起
9
+ * 已 writeHead 的响应)。上游查询(queryBalances / queryCustomBalances /
10
+ * queryDeclaredEndpoints / reconcileBalanceDelta)在装配器里跑,路由只通过
11
+ * `cached()` 与 `refreshOnce()` 拿到结果。
12
+ */
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ import type { ProviderBalance } from '../pricing-shared.ts';
15
+ import type { ReconcileEvent } from '../reconcile.ts';
16
+ /** 余额响应体形状(来自装配器 `balanceCache.doc`)。 */
17
+ export interface BalanceDoc {
18
+ balances: readonly ProviderBalance[];
19
+ reconcile?: ReconcileEvent;
20
+ }
21
+ interface CachedBalance {
22
+ doc: BalanceDoc;
23
+ at: number;
24
+ }
25
+ /** 路由依赖。 */
26
+ export interface BalanceDeps {
27
+ trustedHosts: ReadonlySet<string>;
28
+ /** 装配器内缓存 + 时间戳(`Date.now()`)。 */
29
+ cached: () => CachedBalance;
30
+ /** in-flight 去重的刷新入口。 */
31
+ refreshOnce: () => Promise<void>;
32
+ /** 5 分钟缓存间隔(毫秒)。 */
33
+ cacheMs: number;
34
+ }
35
+ export declare function registerBalanceRoute(ctx: Context, deps: BalanceDeps): void;
36
+ export {};
37
+ //# sourceMappingURL=balance.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 路由:POST /api/billing/notify-claim
3
+ *
4
+ * 跨实例提醒认领(issue #44):多实例(CLI + 桌面等)合法共存,各自轮询
5
+ * 会重复弹通知。两级去重——进程内 Set 挡同进程的并发/重渲染(trigger 与仪表盘
6
+ * 是两个组件实例,effect 同周期并发时本地 store 标记读到的还是旧值);跨进程走
7
+ * 认领端点(先到先得),另一实例刚发过则跳过。端点不可达(旧版本/网络故障)
8
+ * 时放行,退化为原有 per-客户端去重。通知构造失败(平台限制)静默。
9
+ *
10
+ * 路由文件职责:handler + 模块级 `notifyClaims` 单例(与 `loadNotifications`
11
+ * 共享内存状态——先到先得认领)。文件持久化(`writeFileAtomic`)由 index.ts 装配
12
+ * 器通过 `claimsPath` 注入;handler 拿到路径后自行管理读写。
13
+ */
14
+ import type { Context } from '@deepseek-ai/cordis';
15
+ /** 路由依赖:装配器注入,无需直接读 ctx。 */
16
+ export interface NotifyClaimDeps {
17
+ /** 反向代理白名单(来自 `config.trustedHosts` 经 normalizeTrustedHosts 归一化)。 */
18
+ trustedHosts: ReadonlySet<string>;
19
+ /** 认领表文件路径:`<snapshotPath>.notify-claims.json`。 */
20
+ claimsPath: string;
21
+ }
22
+ /** 注册路由到 cordis webServer effect;handler 闭包外只读 deps。 */
23
+ export declare function registerNotifyClaimRoute(ctx: Context, deps: NotifyClaimDeps): void;
24
+ //# sourceMappingURL=notify-claim.d.ts.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 路由:GET /api/billing/pricing + POST /api/billing/pricing/refresh
3
+ *
4
+ * 实时定价(汇率 + OpenRouter 模型价 + models.dev 目录外补充)的 GET/POST 同
5
+ * concern,合一个文件:
6
+ * - GET 由前端 30s 轮询 + 弹窗初始拉取触发,返回完整价目表文档(live + 内置
7
+ * 目录 + 别名表 + syncedAt)。
8
+ * - POST 是费率条「立即同步」按钮触发,强制服务端重新拉取一次;写操作额外
9
+ * 校验 Origin 与 Content-Type。
10
+ *
11
+ * 路由文件只关心 HTTP 层:响应体序列化与缓存(pricingBodyCache)由 index.ts
12
+ * 装配器通过 `body()` 闭包注入;服务端重拉逻辑(refreshPricing + 价格目录代际
13
+ * 变化触发 reprice)也在装配器里,路由只通过 `refresh()` 触发。
14
+ */
15
+ import type { Context } from '@deepseek-ai/cordis';
16
+ /** 路由依赖:装配器注入。 */
17
+ export interface PricingDeps {
18
+ trustedHosts: ReadonlySet<string>;
19
+ /** 装配器内缓存的序列化响应体(含 live + 内置目录 + 别名 + syncedAt)。 */
20
+ body: () => string;
21
+ /** 触发服务端重新拉取 models.dev / 汇率。 */
22
+ refresh: () => Promise<void>;
23
+ }
24
+ /** 一次性注册两条同 concern 路由。 */
25
+ export declare function registerPricingRoutes(ctx: Context, deps: PricingDeps): void;
26
+ //# sourceMappingURL=pricing.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 路由:GET /api/billing/relay-quotas
3
+ *
4
+ * 中转站额度(New API / Sub2API 余额 + 滚动窗口)。5 分钟缓存 + in-flight
5
+ * 去重。诊断(每条路由的 origin / kind)随响应下发,供「我的中转站为什么不显
6
+ * 示」自查。
7
+ *
8
+ * 路由文件只关心 HTTP 缓存 + 诊断序列化。上游查询(queryRelayQuotas +
9
+ * 路由筛选 + isOfficialBaseUrl 排除)在装配器里跑。
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ import type { RelayQuota } from '../pricing-shared.ts';
13
+ interface CachedRelay {
14
+ quotas: readonly RelayQuota[];
15
+ at: number;
16
+ }
17
+ /** 路由依赖。 */
18
+ export interface RelayQuotasDeps {
19
+ trustedHosts: ReadonlySet<string>;
20
+ cached: () => CachedRelay;
21
+ refreshOnce: () => Promise<void>;
22
+ /** 5 分钟缓存间隔(毫秒)。 */
23
+ cacheMs: number;
24
+ }
25
+ export declare function registerRelayQuotasRoute(ctx: Context, deps: RelayQuotasDeps): void;
26
+ export {};
27
+ //# sourceMappingURL=relay-quotas.d.ts.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 路由:GET /api/billing/subscriptions
3
+ *
4
+ * 订阅套餐额度(Kimi / Z.ai Coding / OpenCode / MiniMax / OpenRouter / Claude /
5
+ * CommandCode / 腾讯云 TokenHub 等)。5 分钟缓存 + in-flight 去重(与 balance
6
+ * 同模式——并发请求共享同一次进行中的刷新)。
7
+ *
8
+ * 路由文件只关心 HTTP 缓存逻辑:超时或未命中时触发 refreshOnce,失败时仍用
9
+ * 旧缓存(不挂起已 writeHead 的响应)。上游查询(collectSubscriptions +
10
+ * resolveSubscriptionKeys + 适配器 + 套餐知识库)在装配器里跑。
11
+ */
12
+ import type { Context } from '@deepseek-ai/cordis';
13
+ import type { SubscriptionQuota } from '../pricing-shared.ts';
14
+ interface CachedQuotas {
15
+ quotas: readonly SubscriptionQuota[];
16
+ at: number;
17
+ }
18
+ /** 路由依赖。 */
19
+ export interface SubscriptionsDeps {
20
+ trustedHosts: ReadonlySet<string>;
21
+ cached: () => CachedQuotas;
22
+ refreshOnce: () => Promise<void>;
23
+ /** 5 分钟缓存间隔(毫秒)。 */
24
+ cacheMs: number;
25
+ }
26
+ export declare function registerSubscriptionsRoute(ctx: Context, deps: SubscriptionsDeps): void;
27
+ export {};
28
+ //# sourceMappingURL=subscriptions.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 路由:GET /api/billing/usage-stats
3
+ *
4
+ * 实时用量聚合文档(byDay / byModel / bySession / bySite / byTurn / perf 等)。
5
+ * stale-while-revalidate:聚合是全量扫描(重度用户上万条会话的冷启动折叠以
6
+ * 分钟计),HTTP 请求绝不全程等它——只给 RESPONSE_BUDGET_MS 的等待预算,超
7
+ * 预算立即回最近一次快照;后台折叠完成后快照随之刷新(挂在聚合 promise 上,
8
+ * 与响应路径解耦)。宿主是单进程,慢响应期间事件循环被折叠占住,会话/模型
9
+ * 列表 RPC 会一起饿死(用户反馈的启动卡顿)。
10
+ *
11
+ * 路由文件只关心 HTTP 拼装:
12
+ * - decorate(注入 PACKAGE_VERSION / 预算 / 阈值)
13
+ * - 聚合预算(Promise.race + setTimeout.unref)
14
+ * - 响应体缓存(`statsBodyCache`:装饰 + 序列化结果按 doc 引用 + config 字段
15
+ * 缓存,30s 轮询期间复用)
16
+ * - 失败路径(聚合失败 → 读快照候选 → 仍无则 `{ error }`)
17
+ *
18
+ * 聚合器(`aggregator.aggregate()` / `sessionUsageOf()`)+ 快照读写 + 配置在
19
+ * 装配器里跑,路由只通过 Deps 拿到回调。
20
+ */
21
+ import type { Context } from '@deepseek-ai/cordis';
22
+ import type { UsageAggregator } from '../aggregate.ts';
23
+ import type { UsageBillingConfig } from '../config.ts';
24
+ /** 路由依赖。 */
25
+ export interface UsageStatsDeps {
26
+ trustedHosts: ReadonlySet<string>;
27
+ /** 聚合器(仅用 .aggregate() 与 .sessionUsageOf())。 */
28
+ aggregator: Pick<UsageAggregator, 'aggregate' | 'sessionUsageOf'>;
29
+ config: UsageBillingConfig;
30
+ /** 快照候选路径列表(主文件 + 约定路径 + config.snapshotPath)。 */
31
+ candidates: readonly string[];
32
+ /** 聚合成功后异步落盘节流(30s 内多次触发只写一次)。 */
33
+ persistSnapshot: (doc: Record<string, unknown>) => void;
34
+ /** 读一个快照候选:主文件优先,主文件损坏时回退 `.bak`,都失败返回 null。 */
35
+ readSnapshot: (candidate: string) => Promise<Record<string, unknown> | null>;
36
+ /** 插件版本(来自 package.json)。 */
37
+ packageVersion: string;
38
+ /** 等待预算(毫秒):聚合在此预算内完成就回实时数据,超预算立即回最近快照。 */
39
+ responseBudgetMs: number;
40
+ }
41
+ /** 校验一份聚合文档;返回漂移原因(null = 形状正常)。纯只读,不改 doc。 */
42
+ export declare function usageStatsShapeError(doc: unknown): string | null;
43
+ export declare function registerUsageStatsRoute(ctx: Context, deps: UsageStatsDeps): void;
44
+ //# sourceMappingURL=usage-stats.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 路由:GET/POST /api/billing/usage-tool
3
+ *
4
+ * usage_stats 工具开关:插件自带的读/写接口(不依赖宿主浏览器设置白名单)。
5
+ * GET 返回当前是否注入;POST 写设置命名空间(重启应用后工具注入随新值生效)。
6
+ *
7
+ * 双世代宿主(issue #19 + #28 + v1.4.19)的接线在装配器里完成:
8
+ * - 旧宿主 ≤0.1.6 走 `settings.register` 命名空间 scope。
9
+ * - 宿主 0.1.7+ 注册面已移除,走 volatile config 投影。
10
+ * 装配器提供 `read` / `write` 闭包,路由文件不感知双世代——`read()` 已内部分支
11
+ * (scope 存在时读 scope,否则读 volatile + config base),`write(next)` 同理。
12
+ * 工具注入由装配器的 `syncUsageTool` 驱动:写回后路由通过 `syncTool()` 显式调用一次
13
+ * (幂等),覆盖「写回成功但引用未即时可见」的组合(测试替身不提交)。
14
+ */
15
+ import type { Context } from '@deepseek-ai/cordis';
16
+ /** 路由依赖。 */
17
+ export interface UsageToolDeps {
18
+ trustedHosts: ReadonlySet<string>;
19
+ /** 当前 enableUsageStatsTool 值(装配器内已做双世代分支)。 */
20
+ read: () => boolean;
21
+ /** 写回 + 立即同步工具注册(幂等)。 */
22
+ write: (next: boolean) => Promise<void>;
23
+ /** 写回后显式调用一次的工具注册同步(幂等)。 */
24
+ syncTool: () => void;
25
+ }
26
+ export declare function registerUsageToolRoute(ctx: Context, deps: UsageToolDeps): void;
27
+ //# sourceMappingURL=usage-tool.d.ts.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 静默吞错的结构化记录(S-6):「缺失=未配置」类 catch 的统一出口。
3
+ *
4
+ * 本仓 78 处 server 端 catch 多数是有意的静默(文件缺失=首次运行、凭据未
5
+ * 配置=跳过该 provider),此前唯一载体是注释——用户报「X 不工作」时只能靠
6
+ * 猜。本工具把这些静默升级为**节流的结构化日志**:同 reason 5 分钟最多一条
7
+ * info,宿主日志可 grep `swallowed:`,诊断时看得到「哪个环节吞了什么」,
8
+ * 而又不把已知常态刷成告警噪音。
9
+ *
10
+ * 不取代 console.warn——已知异常需要用户行动的(账本损坏、刷新失败)继续
11
+ * warn;这里只收「确认无害的静默」。节流状态导出重置仅供测试。
12
+ */
13
+ /**
14
+ * 记录一次有意的静默吞错。
15
+ * @param reason - 稳定的短横线 reason 键(如 `reconcile-ref-missing`),节流以此为粒度。
16
+ * @param error - 可选的原始异常(Error 打 message,其他值原样)。
17
+ */
18
+ export declare function swallowed(reason: string, error?: unknown): void;
19
+ /** 清空节流状态(仅供测试:模块级 Map 会跨用例泄漏)。 */
20
+ export declare function resetSwallowedForTest(): void;
21
+ //# sourceMappingURL=silent.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kenz1117/dsh-ui-usage-billing",
3
3
  "description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
4
- "version": "1.5.1",
4
+ "version": "1.6.1",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "deepseek",
@@ -78,6 +78,9 @@
78
78
  "0.2.1-alpha.1": "compatible",
79
79
  "0.2.1-alpha.2": "compatible",
80
80
  "0.2.1-alpha.3": "compatible",
81
+ "0.2.1-alpha.4": "compatible",
82
+ "0.2.1-alpha.5": "compatible",
83
+ "0.2.1-rc.1": "compatible",
81
84
  "0.2.1-rc.2": "compatible"
82
85
  }
83
86
  }
@@ -86,7 +89,10 @@
86
89
  "build": "tsc -b tsconfig.json && tsdown",
87
90
  "bundle": "tsdown",
88
91
  "watch": "tsdown --watch",
89
- "test": "vitest run"
92
+ "lint": "node scripts/lint.mjs && node scripts/check-clientbundle-sync.mjs",
93
+ "test": "vitest run",
94
+ "test:e2e": "playwright test",
95
+ "test:e2e:install": "playwright install chromium"
90
96
  },
91
97
  "license": "MIT",
92
98
  "engines": {
@@ -105,8 +111,8 @@
105
111
  "@deepseek-ai/dsh-client-ui-sidebar": "*",
106
112
  "@deepseek-ai/dsh-client-ui-slots": "*",
107
113
  "@deepseek-ai/dsh-credentials": "*",
108
- "@deepseek-ai/dsh-host-webserver": "*",
109
114
  "@deepseek-ai/dsh-home-paths": "*",
115
+ "@deepseek-ai/dsh-host-webserver": "*",
110
116
  "@deepseek-ai/dsh-invariants": "*",
111
117
  "@deepseek-ai/dsh-llm": "*",
112
118
  "@deepseek-ai/dsh-session": "*",
@@ -123,11 +129,11 @@
123
129
  "@deepseek-ai/cordis": "*",
124
130
  "@deepseek-ai/cordis-plugin-include": "^1.0.7",
125
131
  "@deepseek-ai/cordis-plugin-loader": "^1.0.5",
132
+ "@deepseek-ai/dsh-agent": "0.2.1-rc.2",
126
133
  "@deepseek-ai/dsh-api-gateway": "0.2.1-rc.2",
127
134
  "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.2",
128
135
  "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.2",
129
136
  "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.2",
130
- "@deepseek-ai/dsh-agent": "0.2.1-rc.2",
131
137
  "@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
132
138
  "@deepseek-ai/dsh-client-store": "0.2.1-rc.2",
133
139
  "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.2",
@@ -136,10 +142,9 @@
136
142
  "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.2",
137
143
  "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
138
144
  "@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
139
- "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.2",
140
145
  "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
146
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.2",
141
147
  "@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
142
- "@deepseek-ai/dsh-util-code-language": "0.2.1-rc.2",
143
148
  "@deepseek-ai/dsh-llm": "0.2.1-rc.2",
144
149
  "@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.2",
145
150
  "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
@@ -152,17 +157,19 @@
152
157
  "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
153
158
  "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
154
159
  "@deepseek-ai/dsh-user-approval": "0.2.1-rc.2",
160
+ "@deepseek-ai/dsh-util-code-language": "0.2.1-rc.2",
155
161
  "@deepseek-ai/dsh-util-workspace-path": "0.2.1-rc.2",
156
162
  "@deepseek-ai/schemastery": "^3.18.4",
163
+ "@playwright/test": "^1.64.1",
157
164
  "@shikijs/langs": "^4.3.1",
158
165
  "@testing-library/dom": "^10.4.1",
159
166
  "@testing-library/react": "^16.1.0",
160
167
  "@types/node": "^22.20.0",
161
168
  "@types/react": "~18.3.1",
162
169
  "@types/react-dom": "^19.2.7",
163
- "immer": "^10.1.1",
164
170
  "anser": "^2.3.5",
165
171
  "diff": "^9.0.0",
172
+ "immer": "^10.1.1",
166
173
  "jsdom": "^26.1.0",
167
174
  "katex": "^0.16.47",
168
175
  "lightningcss": "^1.32.0",