dsh-ds-balance 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +68 -18
  2. package/README_en.md +72 -18
  3. package/lib/adapters/domain-core-store.d.ts +2 -2
  4. package/lib/adapters/domain-core-store.js +3 -1
  5. package/lib/adapters/host-account.d.ts +66 -0
  6. package/lib/adapters/host-account.js +68 -0
  7. package/lib/client.js +437 -95
  8. package/lib/domain/account.d.ts +40 -0
  9. package/lib/domain/account.js +74 -0
  10. package/lib/domain/balance.d.ts +20 -1
  11. package/lib/domain/normalize.d.ts +3 -2
  12. package/lib/domain/normalize.js +3 -2
  13. package/lib/domain/severity.d.ts +8 -2
  14. package/lib/domain/severity.js +11 -4
  15. package/lib/domain/situation.d.ts +104 -0
  16. package/lib/domain/situation.js +93 -0
  17. package/lib/http/handlers.d.ts +1 -1
  18. package/lib/http/handlers.js +29 -5
  19. package/lib/http/wire.d.ts +11 -1
  20. package/lib/http/wire.js +2 -0
  21. package/lib/index.js +122 -6
  22. package/lib/ports/account.d.ts +27 -0
  23. package/lib/ports/account.js +9 -0
  24. package/lib/services/balance-service.d.ts +66 -94
  25. package/lib/services/balance-service.js +132 -196
  26. package/lib/services/balance-source.d.ts +174 -0
  27. package/lib/services/balance-source.js +294 -0
  28. package/lib/services/scheduler.d.ts +8 -2
  29. package/lib/services/scheduler.js +1 -1
  30. package/lib/services/source-readers.d.ts +26 -0
  31. package/lib/services/source-readers.js +73 -0
  32. package/lib/services/source-selector.d.ts +50 -0
  33. package/lib/services/source-selector.js +64 -0
  34. package/lib/types/client/api-types.d.ts +16 -0
  35. package/lib/types/client/data.d.ts +12 -3
  36. package/lib/types/client/index.d.ts +6 -0
  37. package/lib/types/client/locales.d.ts +10 -18
  38. package/lib/types/client/mock/index.d.ts +12 -11
  39. package/lib/types/client/mock/scenarios.d.ts +13 -5
  40. package/lib/types/client/model.d.ts +24 -25
  41. package/lib/types/client/route-hint.d.ts +61 -0
  42. package/lib/types/client/sidebar/BalancePopover.d.ts +7 -0
  43. package/lib/types/client/sidebar/PercentRing.d.ts +13 -5
  44. package/lib/types/client/sidebar/SidebarBalance.d.ts +9 -1
  45. package/lib/types/client/situation.d.ts +164 -0
  46. package/lib/version.d.ts +1 -1
  47. package/lib/version.js +1 -1
  48. package/package.json +25 -23
@@ -7,11 +7,12 @@ export declare const NS = "ds-balance";
7
7
  /** 中文词典(键集真源)。 */
8
8
  export declare const zh: {
9
9
  readonly 'sidebar.label': "DeepSeek 余额";
10
- readonly 'sidebar.aria.balance': "DeepSeek 余额";
11
10
  readonly 'sidebar.aria.ring': "DeepSeek 余额状态";
12
11
  readonly 'sidebar.aria.refresh': "刷新余额";
13
12
  readonly 'sidebar.aria.mismatch': "显示的币种与你选择的不一致";
14
13
  readonly 'popover.title': "账户余额";
14
+ readonly 'popover.source.key': "(API Key)";
15
+ readonly 'popover.source.account': "(账号登录)";
15
16
  readonly 'popover.total': "余额";
16
17
  readonly 'popover.granted': "赠送";
17
18
  readonly 'popover.toppedUp': "充值";
@@ -23,12 +24,13 @@ export declare const zh: {
23
24
  readonly 'popover.action.useShown': "改用 {shown}";
24
25
  readonly 'popover.action.openPlugins': "打开插件页";
25
26
  readonly 'popover.action.openPluginConfig': "打开插件配置页";
26
- readonly 'state.unavailable': "账户不可用";
27
- readonly 'state.stale': "数据已过期";
28
- readonly 'state.error': "读取失败";
29
- readonly 'state.empty': "尚未配置";
30
- readonly 'state.noKey': "尚未填写 API Key";
31
- readonly 'state.noBalance': "暂无余额";
27
+ readonly 'situation.checking': "正在获取";
28
+ readonly 'situation.unavailable': "读不到余额";
29
+ readonly 'situation.internalError': "服务异常";
30
+ readonly 'situation.noCredential': "尚未配置凭据";
31
+ readonly 'situation.accountUnavailable': "账户不可用";
32
+ readonly 'situation.stale': "数据已过期";
33
+ readonly 'situation.emptyWallet': "暂无余额";
32
34
  readonly 'settings.group.connection': "连接";
33
35
  readonly 'settings.group.refresh': "刷新";
34
36
  readonly 'settings.group.display': "展示";
@@ -44,15 +46,13 @@ export declare const zh: {
44
46
  readonly 'settings.field.cnyCritical': "CNY 告急";
45
47
  readonly 'settings.field.usdWarn': "USD 预警";
46
48
  readonly 'settings.field.usdCritical': "USD 告急";
47
- readonly 'settings.hint.apiKey': "留空则改用引用名从凭据存储读取。";
48
49
  readonly 'settings.hint.apiKeyRef': "凭据存储里的条目名;非空时优先于 API Key。";
49
50
  readonly 'settings.hint.baseUrl': "留空则使用提供方默认地址。";
50
51
  readonly 'settings.hint.displayCurrency': "所选币种不存在时,回落到账户实际持有的币种。";
51
- readonly 'settings.hint.number': "请填数字;留空表示使用默认值。";
52
52
  readonly 'settings.hint.threshold': "后端判定;界面颜色跟随返回的严重度。";
53
53
  readonly 'settings.hint.thresholdPair': "告急值必须低于预警值";
54
54
  readonly 'settings.hint.serverRefreshSeconds': "访问官方接口的间隔。";
55
- readonly 'settings.hint.clientPollSeconds': "刷新显示的间隔;只读本地缓存,不访问上游。";
55
+ readonly 'settings.hint.clientPollSeconds': "刷新显示的间隔;正常时只读缓存,读不到时才访问上游。";
56
56
  readonly 'settings.hint.manualRefreshCooldownSeconds': "两次手动刷新之间的最短间隔。";
57
57
  readonly 'settings.currency.auto': "自动(跟随账户)";
58
58
  readonly 'settings.credential.configured': "已配置密钥。";
@@ -75,17 +75,9 @@ export declare const zh: {
75
75
  readonly 'settings.reset': "重置";
76
76
  readonly 'settings.invalidNumber': "请填数字;留空表示使用默认值。";
77
77
  readonly 'settings.hint.refreshAdvanced': "不常改:默认值适用于大多数情况。";
78
- readonly 'settings.currencyMismatch': "当前账户无 {wanted} 余额,实际显示 {shown}";
79
- readonly 'dev.title': "开发场景";
80
- readonly 'dev.hint': "仅用于 UI 开发;不影响真实配置。";
81
78
  };
82
79
  /** 词典键。 */
83
80
  export type LocaleKey = keyof typeof zh;
84
- declare module '@deepseek-ai/dsh-client-ui-slots' {
85
- interface LocaleNamespaceMap {
86
- 'ds-balance': LocaleKey;
87
- }
88
- }
89
81
  /** 英文词典。缺键会在编译期报错。 */
90
82
  export declare const en: Record<LocaleKey, string>;
91
83
  /** 取词典。未知语言回落英文。 */
@@ -11,8 +11,6 @@ import type { BalanceResponse } from '../api-types.ts';
11
11
  import { type ScenarioKey } from './scenarios.ts';
12
12
  /** URL 查询参数名。 */
13
13
  export declare const SCENARIO_PARAM = "dsb";
14
- /** 开发模式参数名:出现即显示场景切换器。 */
15
- export declare const DEV_PARAM = "dsb-dev";
16
14
  /** `?dsb=` 的这个取值表示「回到真实端点」,并顺手清掉已存的场景。 */
17
15
  export declare const LIVE_PARAM_VALUE = "live";
18
16
  /**
@@ -22,15 +20,18 @@ export declare const LIVE_PARAM_VALUE = "live";
22
20
  * @returns 场景键,或 `null` 表示用真实端点。
23
21
  */
24
22
  export declare function resolveScenario(): ScenarioKey | null;
25
- /** 当前生效的场景键;走真实端点时回落到默认场景,供切换器显示。 */
26
- export declare function currentScenario(): ScenarioKey;
27
- /** 切换场景并持久化;刷新页面后仍生效。 */
28
- export declare function setScenario(key: ScenarioKey): void;
29
- /** 是否处于开发模式(显示场景切换器)。 */
30
- export declare function isDevMode(): boolean;
31
- /** 订阅场景变化。回调立即收到一次当前值,返回退订函数。 */
23
+ /**
24
+ * 订阅场景变化。回调**立即收到一次当前值**,返回退订函数。
25
+ *
26
+ * 现有调用方(`SidebarBalance`)只用它那一次立即回调来对账「现在走不走 mock」——
27
+ * 因为**场景在运行期不会变**:唯一的入口是 URL 参数(刷新页面才重新解析)。
28
+ *
29
+ * **想加回场景切换器**(曾经的 `?dsb-dev` + `setScenario()` + `notifyScenario()` 三件套,
30
+ * 见 [README.md](README.md))时:把那两个函数加回来、在这里调 `notifyScenario(key)` 即可 ——
31
+ * 注册表与订阅语义都还在下面。本轮把它们删掉是因为它们**全仓零引用**
32
+ * (`isDevMode` / `DEV_PARAM` / `setScenario` / `notifyScenario`),
33
+ * 而配套的两条词典键也是孤儿键。
34
+ */
32
35
  export declare function subscribeScenario(listener: (key: ScenarioKey) => void): () => void;
33
- /** 通知所有订阅者场景已变。 */
34
- export declare function notifyScenario(key: ScenarioKey): void;
35
36
  /** 取当前场景的数据快照。 */
36
37
  export declare function currentBalance(): BalanceResponse;
@@ -11,20 +11,28 @@ import type { BalanceResponse } from '../api-types.ts';
11
11
  export declare const scenarios: {
12
12
  /** 正常。 */
13
13
  ok: BalanceResponse;
14
+ /** 正常,但数字来自账号登录那条路(用来眼看「来源标签」)。 */
15
+ account: BalanceResponse;
14
16
  /** 余额偏低但可用。 */
15
17
  warn: BalanceResponse;
16
18
  /** 余额告急。 */
17
19
  critical: BalanceResponse;
18
- /** 账户不可用(余额耗尽 / 欠费)。 */
20
+ /** 账户不可用(余额耗尽 / 欠费)。读到了,但上游说这个账户停用。 */
19
21
  unavailable: BalanceResponse;
20
- /** 有旧值但本次刷新失败。 */
22
+ /** 有旧值但本次刷新失败:数字照常显示,文案说它旧。 */
21
23
  stale: BalanceResponse;
22
- /** 无值失败。 */
24
+ /** 无值失败:接入了却读不到(没有旧快照)。环是叉。 */
23
25
  error: BalanceResponse;
24
- /** 未配置。 */
26
+ /** 插件自己的端点不可达(旧契约里没有这个信号,靠客户端本地合成)。 */
27
+ offline: BalanceResponse;
28
+ /** 插件自己出错了(装配错误之类):与「上游坏了」分开说。 */
29
+ internalError: BalanceResponse;
30
+ /** 未配置:尚无任何解释。 */
25
31
  empty: BalanceResponse;
26
- /** 未配置且带 NO_KEY 错误。 */
32
+ /** 未配置且带 NO_KEY 错误 —— 需要用户去配置的那一族。 */
27
33
  noKey: BalanceResponse;
34
+ /** 首帧:还没问到后端。 */
35
+ checking: BalanceResponse;
28
36
  /** 多币种。`selected` 显式写出,不靠 `make()` 的默认值兜。 */
29
37
  multiCurrency: BalanceResponse;
30
38
  /**
@@ -4,34 +4,17 @@
4
4
  * @module dsh-ds-balance/client/model
5
5
  */
6
6
  import type { BalanceInfo, BalanceResponse, Severity } from './api-types.ts';
7
- /** StateDot 的五个状态(原生原语取值)。 */
8
- export type DotState = 'done' | 'warning' | 'ongoing' | 'error' | 'idle';
9
- /** severity → StateDot 状态。见 .agents/notes 的映射决策。 */
10
- export declare function dotStateOf(severity: Severity): DotState;
11
- /** 圆环中心可以画的符号。目前只有「账户不可用」用得到。 */
12
- export type RingMarker = 'cross';
13
- /** 一个 severity 对应的环形态。 */
14
- export interface RingSpec {
15
- /** 弧的状态;决定弧色。 */
16
- state: DotState;
17
- /** 中心符号;`null` 表示不画。 */
18
- marker: RingMarker | null;
19
- }
20
7
  /**
21
- * severity → 环形态。
8
+ * StateDot 的五个状态(原生原语取值)。
22
9
  *
23
- * **颜色只有四个色相可用**:官方 token 里 `error-primary` 与 `error-secondary`
24
- * 在深色主题下同值,没有第五种颜色。所以两档「红」靠**形状**区分:
25
- *
26
- * - 颜色编码「数值严重度」:绿 → 琥珀 → 红。
27
- * - 形状编码「账户可用性」:`unavailable` 是账户维度的事实,与余额高低无关,
28
- * 它拿红弧再加一个中心叉号。色盲与低分辨率下依然能分开。
29
- *
30
- * `critical` 与 `unavailable` 都是红弧,这是有意的:**别再往回改成从红系里挑两个**。
31
- * @param severity - 后端给的严重度。
32
- * @returns 弧状态与中心符号。
10
+ * **这是圆环状态的真源**:`PercentRing` 的 `RingState` 就是它(类型别名,不是另抄一份)——
11
+ * 抄一份的代价是两处各自增删取值都能编译通过,而 CSS 的 `data-state` 规则只认其中一个。
33
12
  */
34
- export declare function ringSpecOf(severity: Severity): RingSpec;
13
+ export type DotState = 'done' | 'warning' | 'ongoing' | 'error' | 'idle';
14
+ /** severity → StateDot 状态。见 .agents/notes 的映射决策。 */
15
+ export declare function dotStateOf(severity: Severity): DotState;
16
+ /** 圆环中心可以画的符号。两者几何同源(同一个 `MARK_ARM`),只差朝向。 */
17
+ export type RingMarker = 'cross' | 'plus';
35
18
  /**
36
19
  * 圆环的弧长比例:选中币种的余额占它 warn 阈值的几分之几,封顶 1。
37
20
  *
@@ -103,6 +86,22 @@ export declare function ageBucket(ageMs: number): {
103
86
  * @returns 毫秒计的当前年龄;基准缺失时按 0 算。
104
87
  */
105
88
  export declare function currentAgeMs(seenAt: number, seenAgeMs: number, now: number): number;
89
+ /**
90
+ * 来源标签的词典键。
91
+ *
92
+ * 只认两条官方取数路:认不出来就回 `null`,界面不标 —— 标错来源比不标更糟。
93
+ * 括号与词本身都在词典里(中文全角、英文半角带空格),这里只回键。
94
+ *
95
+ * 键的形状写在这里而不是从词典模块取 `LocaleKey`:视图模型不该依赖词典模块,
96
+ * 而这两个键是它自己要产出的东西(词典侧由 `Record<LocaleKey, string>` 兜完整性)。
97
+ */
98
+ export type SourceLabelKey = 'popover.source.key' | 'popover.source.account';
99
+ /**
100
+ * 把来源折成标签键。
101
+ * @param source - 响应里的来源;老宿主或占位视图可能没有。
102
+ * @returns 词典键,或 `null` 表示不标。
103
+ */
104
+ export declare function sourceLabelKeyOf(source: string | undefined | null): SourceLabelKey | null;
106
105
  /**
107
106
  * 一次手动刷新之后的冷却截止时刻。
108
107
  *
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 「当前会话在用哪条路由」这条提示。
3
+ *
4
+ * 侧栏是**壳层**的座位,不在会话作用域里,所以它拿不到会话级模型选择 —— 这里自己去宿主
5
+ * 的客户端服务上读一次。
6
+ *
7
+ * 三条更新路径,缺一不可:
8
+ * 1. **切会话**:订阅 `uiSession` 的主绑定;
9
+ * 2. **同一会话内换模型**:订阅那个会话的 `modelSelection` 投影(换会话时重绑);
10
+ * 3. 读不到就什么都不说 —— 后端回落到全局默认路由,界面照常,只是少了「跟着会话走」。
11
+ *
12
+ * **服务由调用方经 `ctx.inject` 递进来**(构造参数是一个 getter),本模块绝不碰 `ctx`:
13
+ * cordis 不许读没 `inject` 过的服务,直接访问会抛 —— 而这个读发生在**渲染期**,
14
+ * 抛出去等于整条左下角条目被停用(真机踩过:条目注册了、但 `active: false`)。
15
+ *
16
+ * 形状**全部鸭子类型**:`uiSession` 与 `sessions` 是宿主自己的客户端服务,跨版本会动,
17
+ * 而本仓不为它们新增依赖(见 [AGENTS.md](AGENTS.md) 的跨插件服务约定)。读不出来只降级,不抛错。
18
+ * @module dsh-ds-balance/client/route-hint
19
+ */
20
+ /** 宿主 `HostObservable` 的最小面。 */
21
+ interface Observable {
22
+ getSnapshot?: () => unknown;
23
+ subscribe?: (listener: () => void) => () => void;
24
+ }
25
+ /** 会话绑定里我们要走到的那条链。 */
26
+ interface SessionBindingLike {
27
+ session?: {
28
+ projections?: {
29
+ faceOf?: (key: string) => Observable | undefined;
30
+ };
31
+ };
32
+ }
33
+ /**
34
+ * 这条提示要读的宿主面。
35
+ *
36
+ * 两个字段都可缺席:宿主没装相应插件、或服务还没到位时,整条提示退化成 `undefined`。
37
+ */
38
+ export interface RouteHintContext {
39
+ uiSession?: {
40
+ adapter?: {
41
+ current?: Observable;
42
+ };
43
+ };
44
+ sessions?: {
45
+ binding?: (id: unknown) => SessionBindingLike | undefined;
46
+ };
47
+ }
48
+ /** 可订阅的路由提示。 */
49
+ export interface RouteHint {
50
+ /** 当前会话在用的 provider id;读不到回 `undefined`。 */
51
+ getSnapshot(): string | undefined;
52
+ /** 订阅会话切换与模型切换;返回退订函数。 */
53
+ subscribe(listener: () => void): () => void;
54
+ }
55
+ /**
56
+ * 造一个路由提示。
57
+ * @param services - 取当前宿主服务面的函数(由 `ctx.inject` 填的那个可变句柄)。
58
+ * @returns 提示对象;宿主没有这些服务时永远回 `undefined`。
59
+ */
60
+ export declare function createRouteHint(services: () => RouteHintContext): RouteHint;
61
+ export {};
@@ -21,6 +21,13 @@ export interface BalancePopoverProps {
21
21
  displayCurrency: string;
22
22
  /** 数据现在的年龄(毫秒)= 收到那份响应时后端报的年龄 + 此后流逝的时间。 */
23
23
  ageMs: number;
24
+ /**
25
+ * 标题里的来源标签(含括号,来自词典);`null` 表示不标。
26
+ *
27
+ * 由父组件算好再传进来:本组件不做来源判断,也不拼括号 —— 中英文的括号不同形,
28
+ * 拼在代码里必然出错。
29
+ */
30
+ sourceLabel: string | null;
24
31
  /** 是否正在刷新。 */
25
32
  refreshing: boolean;
26
33
  /** 冷却剩余秒数;0 表示可以刷新。 */
@@ -13,13 +13,21 @@
13
13
  * - **弧长**表达比例:余额占该币种 warn 阈值的几分之几,由 `model.ts` 的 `ringRatioOf` 算好传进来。
14
14
  * 阈值是用户自己设的刻度,所以不必再定义「满」是多少;本组件不做任何金额判断。
15
15
  * - **颜色**按 state 机械映射,取值与原生 StateDot 的 data-state 一一对应,来源只有 `severity`。
16
- * 账户不可用(unavailable)在这条弧上再加一个中心叉号:颜色只有四个色相可用,
17
- * 形状负责把「账户维度不可用」与「余额维度告急」分开。
16
+ * 中心记号用**形状**补颜色表达不了的那两件事,两者几何同源、只差朝向:
17
+ * - `cross`(斜交 45°):账号维度读不到 —— 否定;
18
+ * - `plus`(正交 0°/90°):没有接入凭据,需要用户去配置 —— 肯定/添加。
19
+ * 颜色只有四个色相可用,形状负责把「读不到」「待配置」「余额告急」三者分开。
18
20
  * @module dsh-ds-balance/client/sidebar/PercentRing
19
21
  */
20
- import type { RingMarker } from '../model.ts';
21
- /** 圆环能表达的状态;取值域与原生 StateDot 对齐,本插件用不到 ongoing。 */
22
- export type RingState = 'done' | 'warning' | 'error' | 'idle';
22
+ import type { DotState, RingMarker } from '../model.ts';
23
+ /**
24
+ * 圆环能表达的状态。
25
+ *
26
+ * **是 `DotState` 的别名,不是另抄一份**:抄一份的代价是两处各自增删取值都能编译通过,
27
+ * 而 CSS 的 `data-state` 规则只认其中一个 —— 那时会出现「组件传了一个没有对应规则的值」,
28
+ * 颜色静默回落到基色。取值域与原生 StateDot 的 `data-state` 逐值对齐(含 `ongoing`)。
29
+ */
30
+ export type RingState = DotState;
23
31
  /** 圆环属性。 */
24
32
  export interface PercentRingProps {
25
33
  /** 状态,决定环的颜色;语义与原生 StateDot 的同一套枚举一致。 */
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { type LocaleKey } from '../locales.ts';
15
15
  import { type ConfigSlotProbe } from '../config-slot.ts';
16
+ import type { RouteHint } from '../route-hint.ts';
16
17
  import { type PluginsAction } from './BalancePopover.tsx';
17
18
  /**
18
19
  * 「打开本插件配置」这个**可选**入口。宿主没提供 layout 服务时整条链不存在,
@@ -47,6 +48,13 @@ export interface SidebarBalanceProps {
47
48
  * 宿主缺 layout 服务时整条链不存在,图标就不出现。
48
49
  */
49
50
  pluginsNavigation?: PluginsNavigation;
51
+ /**
52
+ * 「当前会话在用哪条路由」这条提示;宿主没有相应客户端服务时给 `undefined`。
53
+ *
54
+ * 它只影响**取哪条数据来源**(后端据此选 Key 那条还是账号那条),不影响别的行为;
55
+ * 缺席时后端回落到全局默认路由,界面照常。
56
+ */
57
+ routeHint?: RouteHint;
50
58
  /**
51
59
  * 「改用 X」:把后端实际给的那个币种写进本插件那一行的设置命名空间(`displayCurrency`)。
52
60
  *
@@ -85,4 +93,4 @@ export interface SidebarBalanceProps {
85
93
  * @param props - 展开态、词典与配置切片。
86
94
  * @returns 条目元素。
87
95
  */
88
- export declare function SidebarBalance({ wide, t, config, configSlotProbe, onSelectCurrency, pluginsNavigation, }: SidebarBalanceProps): JSX.Element | null;
96
+ export declare function SidebarBalance({ wide, t, config, configSlotProbe, onSelectCurrency, pluginsNavigation, routeHint, }: SidebarBalanceProps): JSX.Element | null;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * 处境 → 界面形态的唯一映射表。
3
+ *
4
+ * **为什么要有它**:环、文案、来源标签以前**各拼各的** —— 环只吃 `severity`,文案吃
5
+ * `state + error.code + isAvailable + selection`,标签吃 `state + source`。三套输入集不同、
6
+ * 优先级也不同,于是必然出现「环说 A、文案说 B」与「一个空环代表三种处境」。
7
+ * 现在三者都从同一个处境派生,组件里不再有第二个分支入口。
8
+ *
9
+ * **分层是刻意的**(细节见 [docs/ARCHITECTURE.md](../../../docs/ARCHITECTURE.md)):
10
+ * - **处境**(内部,11 个取值):诊断要细,日志与指标都用它;
11
+ * - **视觉族**(收起态,5 个):视觉要粗 —— 收起态只有一个圆环,塞不下 11 种区别;
12
+ * - **通道值**(色 / 弧 / 符号):组件只认这一层。
13
+ *
14
+ * **每一处合并都是有意的**(见 `SHAPES` 的注释),不是默认掉进去的。
15
+ * 反向也成立:**每一处拆分都是为了消掉一次同形** —— 同形的两组在屏幕上没法区分,
16
+ * 而它们要用户做的事往往不同。这条不变量由 `test/render-matrix.test.ts` 按
17
+ * **真组件的渲染结果**钉住(不是读这张表)。
18
+ * @module dsh-ds-balance/client/situation
19
+ */
20
+ import type { BalanceInfo, BalanceResponse, Severity } from './api-types.ts';
21
+ import type { LocaleKey } from './locales.ts';
22
+ import { type DotState, type RingMarker } from './model.ts';
23
+ /**
24
+ * 处境闭集 —— 与宿主 `domain/situation.ts` **逐字同形**。
25
+ *
26
+ * 两个半体不共享值,所以这里是自己抄的一份字面量,由 `test/situation.test.ts` 对账。
27
+ */
28
+ export type Situation = 'checking' | 'offline' | 'internal-error' | 'no-credential' | 'fetch-failed' | 'account-unavailable' | 'stale' | 'empty-wallet' | 'ok' | 'low' | 'critical';
29
+ /** 全部处境;测试用它断言形态表完备。 */
30
+ export declare const SITUATIONS: readonly ["checking", "offline", "internal-error", "no-credential", "fetch-failed", "account-unavailable", "stale", "empty-wallet", "ok", "low", "critical"];
31
+ /**
32
+ * 收起态的视觉族 = **圆环能表达的几种形态**。
33
+ *
34
+ * 五族与「屏幕上看得见的东西」一一对应,**没有两族同形**(`test/render-matrix.test.ts`
35
+ * 按真组件的渲染结果钉住这条):
36
+ * - `gauge`:有数字,颜色是余额高低的编码 —— 看数字(ok / low / critical / stale);
37
+ * - `unreadable`:读不到,或读到了但用不了 —— 红环 + 叉。等 / 检查 / 去查账户
38
+ * (端点不通 / 抓取失败 / 我们坏了 / 账户停用);
39
+ * - `needs-credential`:没接入,需要**用户去配置** —— 灰环 + +(唯一需要动手的一族);
40
+ * - `pending`:正在取,还没有答案 —— 灰环 + 一条不完整的转弧(首帧);
41
+ * - `empty`:连上了、账户可用,就是没有余额 —— 灰空环,无需动作。
42
+ *
43
+ * **为什么 `checking` 与 `empty-wallet` 拆成两族**:两者都是灰环,曾经同族 ——
44
+ * 于是「加载中」与「账户没钱」在屏幕上**逐像素相同**,而用户要做的事完全不同
45
+ * (等 vs 什么都不用做)。转弧把「还在动」画出来,两者才分得开。
46
+ *
47
+ * **为什么 `account-unavailable` 归 `unreadable`**:它和另外三个一样,都是
48
+ * 「这个数字现在拿不到 / 不可信」,用户要做的也是去查(账户 / 网络 / 我们)。
49
+ * 它曾经挂在 `gauge` 的红端,靠「红环无叉」与「余额告急」区分 —— 但那区分**不成立**:
50
+ * 余额恰好为 0 的 `critical` 同样是红环、无叉、无弧,两者逐像素相同。
51
+ * 加叉之后与 `critical` 分开了;叉的语义扩成「读不到**或用不了**」,同属
52
+ * 「否定:这里没有可用的数字」。
53
+ *
54
+ * **`unreadable` 与 `pending` 不合并**:合并只能二选一 ——
55
+ * 全红会让每次加载都闪一下叉(冤枉),全灰会让真失败看起来像「没事」。
56
+ * 两者的颜色差就是「试过且失败」与「还没有答案」的分界,值得留。
57
+ */
58
+ export type SituationFamily = 'gauge' | 'unreadable' | 'needs-credential' | 'pending' | 'empty';
59
+ /** 弧长来源:按余额比例画,还是不画。 */
60
+ export type ArcSource = 'gauge' | 'none';
61
+ /** 一个处境对应的界面形态。 */
62
+ export interface Presentation {
63
+ /** 视觉族(收起态的粒度)。 */
64
+ family: SituationFamily;
65
+ /** 环的颜色状态。 */
66
+ ring: DotState;
67
+ /** 环的中心符号;`null` 表示不画。 */
68
+ marker: RingMarker | null;
69
+ /** 弧长从哪来。 */
70
+ arc: ArcSource;
71
+ /** 浮层 / 悬停的状态文案键;`null` 表示「一切正常,不必说」。 */
72
+ textKey: LocaleKey | null;
73
+ }
74
+ /**
75
+ * 取一个处境的形态。
76
+ *
77
+ * `gauge` 族的环色**必须**由 `severity` 定(绿 / 琥珀 / 红就是余额高低的编码),
78
+ * 所以这一族的颜色在这里现算 —— 这是全仓**唯一**读 `severity` 配色的地方。
79
+ * @param situation - 处境。
80
+ * @param severity - 后端给的严重度;只对 `gauge` 族有意义。
81
+ * @returns 完整形态。
82
+ */
83
+ export declare function presentationOf(situation: Situation, severity: Severity): Presentation;
84
+ /**
85
+ * 一个处境的视觉族。
86
+ *
87
+ * **它没有生产消费方**(`grep family src/` 只剩声明与这里)—— 这是有意的,不是遗漏:
88
+ * 组件只需要 `ring` / `marker` / `arc` 那三个通道值,多知道一层「族」就是多耦合一层。
89
+ *
90
+ * **但它不是死代码**,和退役的 `ringSpecOf` 有本质区别(那条教训见
91
+ * [test/model.test.ts](../../../test/model.test.ts) 的「形态不变量」):
92
+ * - `ringSpecOf` 是个**映射函数**,输出没有任何消费方,测试断言的又是它自己的映射 ——
93
+ * 那些断言**自我满足**,守着一个再也画不到屏幕上的东西;
94
+ * - `family` 是一条**独立的声明**:「我宣称这些处境在屏幕上应当同形」。它被
95
+ * [test/render-matrix.test.ts](../../../test/render-matrix.test.ts) 当作**假设**,
96
+ * 拿去和**真组件的渲染结果**(另一份独立观测)对账 —— 假设与证据来自两处,
97
+ * 对不上才红。删掉它,那条不变量就失去「应该」这一侧,只剩「实际」。
98
+ *
99
+ * 数学上也确实推不出来:`(ring, marker, arc)` 这组通道值**定不了族** ——
100
+ * `error|cross|none` 收了 unreadable 四个,而 gauge 四个处境分散在三种通道值上
101
+ * (颜色与弧长本身就是它们的读数)。所以族是一层额外的、必须显式声明的信息。
102
+ * @param situation - 处境。
103
+ * @returns 视觉族。
104
+ */
105
+ export declare function familyOf(situation: Situation): SituationFamily;
106
+ /**
107
+ * 悬停文案 —— **收起态与展开态共用这一条**。
108
+ *
109
+ * 规则:**数字可信才显数字;这个处境有话说时,先说话**。
110
+ *
111
+ * 判据写成 `stateText ?? 金额`,而不是「有 `shown` 就显金额」,因为这两件事
112
+ * **不是一回事**:
113
+ * - `account-unavailable`(账户停用)上游**可能给余额行**(mock 的 `unavailable`
114
+ * 场景就带着一条 ¥0),`stale` 更是必然带着一份旧数字。它们都有 `shown`,
115
+ * 但那份数字**不可信或不可用** —— 显它反而误导(「我有 ¥0」/「我有 ¥110」)。
116
+ * - 环已经把问题画出来了(红叉 / 旧色),悬停该做的是**解释问题**,不是再报一个数字。
117
+ *
118
+ * 而「有没有文案」正好就是「数字可不可信」:`textKey` 只在 `ok` / `low` / `critical`
119
+ * 三个处境上是 `null` —— 那三个才是「没什么要说的,看数字就行」。
120
+ * 这不是巧合:文案表与形态表是同一份声明(见 `SHAPES`)。
121
+ *
122
+ * **为什么必须是同一个函数**:两态此前各算各的 —— 收起态用 `ringTitle`
123
+ * (只在有状态文案时才设),展开态用 `hoverLabel`(有数字就给数字)。于是有三种不一致:
124
+ * 1. 收起态**有数字时什么都不显示**:`ok` / `low` / `critical` 的 `stateText` 是 `null`,
125
+ * 于是 `ringTitle` 是 `undefined`,那个 `<svg>` 里连 `<title>` 都没有;
126
+ * 2. 收起态遇到 `stale`(有数字也有文案)显的是「数据已过期」,展开态却显旧金额 ——
127
+ * 两态**各说各的**;
128
+ * 3. 收起态还多一个「DeepSeek 余额状态 」前缀,同一个处境两态文字不同。
129
+ *
130
+ * 收敛到一个返回值之后,两态**不可能**再分叉 —— 这是结构保证,不是靠两处对齐。
131
+ * 交付通道仍分两条(收起态走 SVG 原生 `<title>`、展开态走官方 `Tooltip` 原语),
132
+ * 但那是「怎么显示」的差别,与「说什么」无关。
133
+ * @param shown - 实际展示的那条余额;`null` 表示没有数字。
134
+ * @param stateText - 处境的文案;`null` 表示这个处境没什么要说的(数字可信)。
135
+ * @returns 悬停要显示的那一句;`null` 表示两态都不挂悬停。
136
+ */
137
+ export declare function hoverTextOf(shown: BalanceInfo | null, stateText: string | null): string | null;
138
+ /** 是不是处境闭集里的取值。 */
139
+ export declare function isSituation(value: unknown): value is Situation;
140
+ /**
141
+ * 从**旧宿主**的响应推导处境。
142
+ *
143
+ * 客户端半边由 HMR 立刻换新,宿主半边要重启才换(见
144
+ * [事故复盘](../../../docs/postmortem/2026-09-17-client-host-version-skew.md))——
145
+ * 所以「新客户端 + 旧宿主」是**合法的中间态**,那时响应里没有 `situation`,
146
+ * 只能照旧字段推。这条路径是**兼容层**,新宿主上了就走不到。
147
+ *
148
+ * **优先级必须与宿主 `situationOf` 逐条对齐**,包括那三条修正。这里曾经漏搬了第三条:
149
+ * `state === 'stale'` 判在 `selected === null` **之前**,于是「快照在、里面没有可展示币种、
150
+ * 这轮又没读到」会被推成 `stale` —— 用户手里一个数字都没有,界面上却说「数据已过期」。
151
+ * 兼容层走的是同一条渲染路径,所以那边的缺陷在这边同样成立,修一条不算修完。
152
+ *
153
+ * 推不出来的边角(`internal-error` 与 `account-unavailable` 在旧契约里没有各自的信号)
154
+ * 退到最接近的那个处境。
155
+ * @param response - 后端响应。
156
+ * @returns 处境。
157
+ */
158
+ export declare function legacySituationOf(response: BalanceResponse): Situation;
159
+ /**
160
+ * 取一份响应的处境:新宿主给什么用什么,旧宿主现推。
161
+ * @param response - 后端响应。
162
+ * @returns 处境。
163
+ */
164
+ export declare function situationOfResponse(response: BalanceResponse): Situation;
package/lib/version.d.ts CHANGED
@@ -8,5 +8,5 @@
8
8
  /** 线上 schema 版本。响应里逐条回传,消费方据此判断兼容性。 */
9
9
  export declare const SCHEMA_VERSION = 1;
10
10
  /** 插件版本。与 `package.json` 的 `version` 一致(有测试兜底)。 */
11
- export declare const PLUGIN_VERSION = "2.1.3";
11
+ export declare const PLUGIN_VERSION = "2.2.0";
12
12
  //# sourceMappingURL=version.d.ts.map
package/lib/version.js CHANGED
@@ -8,5 +8,5 @@
8
8
  /** 线上 schema 版本。响应里逐条回传,消费方据此判断兼容性。 */
9
9
  export const SCHEMA_VERSION = 1;
10
10
  /** 插件版本。与 `package.json` 的 `version` 一致(有测试兜底)。 */
11
- export const PLUGIN_VERSION = '2.1.3';
11
+ export const PLUGIN_VERSION = '2.2.0';
12
12
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ds-balance",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "icon": "./icon.svg",
5
5
  "description": "DSH plugin: shows the DeepSeek account balance at the bottom of the sidebar, with a configuration card on the Plugins page.",
6
6
  "keywords": [
@@ -47,7 +47,7 @@
47
47
  "LICENSE"
48
48
  ],
49
49
  "engines": {
50
- "dsh": ">=0.1.7-alpha.1",
50
+ "dsh": ">=0.2.0-rc.1",
51
51
  "node": ">=20"
52
52
  },
53
53
  "dsh": {
@@ -75,15 +75,16 @@
75
75
  },
76
76
  "peerDependencies": {
77
77
  "@deepseek-ai/cordis": "^4.0.3",
78
- "@deepseek-ai/dsh-client-connection": ">=0.1.7-alpha.1",
79
- "@deepseek-ai/dsh-client-locale": ">=0.1.7-alpha.1",
80
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-alpha.1",
81
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-alpha.1",
82
- "@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-alpha.1",
83
- "@deepseek-ai/dsh-credentials": ">=0.1.7-alpha.1",
84
- "@deepseek-ai/dsh-home-paths": ">=0.1.7-alpha.1",
85
- "@deepseek-ai/dsh-settings": ">=0.1.7-alpha.1",
86
- "@deepseek-ai/dsh-storage-domain": ">=0.1.7-alpha.1",
78
+ "@deepseek-ai/dsh-client-connection": ">=0.2.0-rc.1",
79
+ "@deepseek-ai/dsh-client-locale": ">=0.2.0-rc.1",
80
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.0-rc.1",
81
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.2.0-rc.1",
82
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.2.0-rc.1",
83
+ "@deepseek-ai/dsh-credentials": ">=0.2.0-rc.1",
84
+ "@deepseek-ai/dsh-deepseek-account-platform": ">=0.2.0-rc.1",
85
+ "@deepseek-ai/dsh-home-paths": ">=0.2.0-rc.1",
86
+ "@deepseek-ai/dsh-settings": ">=0.2.0-rc.1",
87
+ "@deepseek-ai/dsh-storage-domain": ">=0.2.0-rc.1",
87
88
  "@deepseek-ai/schemastery": "^3.18.3",
88
89
  "react": "^18.3.1",
89
90
  "react-dom": "^18.3.1"
@@ -91,18 +92,19 @@
91
92
  "devDependencies": {
92
93
  "@deepseek-ai/cordis": "^4.0.3",
93
94
  "@deepseek-ai/cordis-plugin-loader": "^1.0.4",
94
- "@deepseek-ai/dsh-client-connection": ">=0.1.7-alpha.1",
95
- "@deepseek-ai/dsh-client-locale": ">=0.1.7-alpha.1",
96
- "@deepseek-ai/dsh-client-ui-plugin-manager": ">=0.1.7-alpha.1",
97
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-alpha.1",
98
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-alpha.1",
99
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-alpha.1",
100
- "@deepseek-ai/dsh-client-ui-sidebar": ">=0.1.7-alpha.1",
101
- "@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-alpha.1",
102
- "@deepseek-ai/dsh-credentials": ">=0.1.7-alpha.1",
103
- "@deepseek-ai/dsh-home-paths": ">=0.1.7-alpha.1",
104
- "@deepseek-ai/dsh-settings": ">=0.1.7-alpha.1",
105
- "@deepseek-ai/dsh-storage-domain": ">=0.1.7-alpha.1",
95
+ "@deepseek-ai/dsh-client-connection": ">=0.2.0-rc.1",
96
+ "@deepseek-ai/dsh-client-locale": ">=0.2.0-rc.1",
97
+ "@deepseek-ai/dsh-client-ui-plugin-manager": ">=0.2.0-rc.1",
98
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.0-rc.1",
99
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.2.0-rc.1",
100
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.2.0-rc.1",
101
+ "@deepseek-ai/dsh-client-ui-sidebar": ">=0.2.0-rc.1",
102
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.2.0-rc.1",
103
+ "@deepseek-ai/dsh-credentials": ">=0.2.0-rc.1",
104
+ "@deepseek-ai/dsh-deepseek-account-platform": ">=0.2.0-rc.1",
105
+ "@deepseek-ai/dsh-home-paths": ">=0.2.0-rc.1",
106
+ "@deepseek-ai/dsh-settings": ">=0.2.0-rc.1",
107
+ "@deepseek-ai/dsh-storage-domain": ">=0.2.0-rc.1",
106
108
  "@deepseek-ai/schemastery": "^3.18.3",
107
109
  "@eslint/js": "^10.0.1",
108
110
  "@types/node": "^20.0.0",