dsh-ds-balance 1.0.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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/README_en.md +81 -0
  4. package/cordis.patch.yml +9 -0
  5. package/lib/adapters/console-logger.d.ts +25 -0
  6. package/lib/adapters/console-logger.js +35 -0
  7. package/lib/adapters/domain-core-store.d.ts +104 -0
  8. package/lib/adapters/domain-core-store.js +184 -0
  9. package/lib/adapters/http-deepseek-client.d.ts +36 -0
  10. package/lib/adapters/http-deepseek-client.js +101 -0
  11. package/lib/adapters/memory-metrics.d.ts +30 -0
  12. package/lib/adapters/memory-metrics.js +67 -0
  13. package/lib/adapters/salt-file.d.ts +32 -0
  14. package/lib/adapters/salt-file.js +40 -0
  15. package/lib/client.js +2120 -0
  16. package/lib/config.d.ts +127 -0
  17. package/lib/config.js +112 -0
  18. package/lib/domain/balance.d.ts +70 -0
  19. package/lib/domain/balance.js +8 -0
  20. package/lib/domain/errors.d.ts +84 -0
  21. package/lib/domain/errors.js +136 -0
  22. package/lib/domain/money.d.ts +39 -0
  23. package/lib/domain/money.js +74 -0
  24. package/lib/domain/normalize.d.ts +38 -0
  25. package/lib/domain/normalize.js +120 -0
  26. package/lib/domain/select.d.ts +26 -0
  27. package/lib/domain/select.js +51 -0
  28. package/lib/domain/severity.d.ts +34 -0
  29. package/lib/domain/severity.js +44 -0
  30. package/lib/http/handlers.d.ts +47 -0
  31. package/lib/http/handlers.js +261 -0
  32. package/lib/http/routes.d.ts +63 -0
  33. package/lib/http/routes.js +61 -0
  34. package/lib/http/wire.d.ts +82 -0
  35. package/lib/http/wire.js +68 -0
  36. package/lib/index.d.ts +36 -0
  37. package/lib/index.js +210 -0
  38. package/lib/ports/clock.d.ts +12 -0
  39. package/lib/ports/clock.js +6 -0
  40. package/lib/ports/core-store.d.ts +28 -0
  41. package/lib/ports/core-store.js +6 -0
  42. package/lib/ports/credentials.d.ts +30 -0
  43. package/lib/ports/credentials.js +9 -0
  44. package/lib/ports/deepseek-client.d.ts +47 -0
  45. package/lib/ports/deepseek-client.js +7 -0
  46. package/lib/ports/logger.d.ts +12 -0
  47. package/lib/ports/logger.js +6 -0
  48. package/lib/ports/metrics.d.ts +36 -0
  49. package/lib/ports/metrics.js +11 -0
  50. package/lib/services/account-tag.d.ts +24 -0
  51. package/lib/services/account-tag.js +29 -0
  52. package/lib/services/balance-service.d.ts +121 -0
  53. package/lib/services/balance-service.js +220 -0
  54. package/lib/services/config-service.d.ts +52 -0
  55. package/lib/services/config-service.js +51 -0
  56. package/lib/services/key-resolver.d.ts +42 -0
  57. package/lib/services/key-resolver.js +62 -0
  58. package/lib/services/scheduler.d.ts +93 -0
  59. package/lib/services/scheduler.js +142 -0
  60. package/lib/types/client/api-types.d.ts +54 -0
  61. package/lib/types/client/data.d.ts +103 -0
  62. package/lib/types/client/index.d.ts +18 -0
  63. package/lib/types/client/locales.d.ts +98 -0
  64. package/lib/types/client/mock/index.d.ts +36 -0
  65. package/lib/types/client/mock/scenarios.d.ts +51 -0
  66. package/lib/types/client/model.d.ts +93 -0
  67. package/lib/types/client/settings/BalanceSettingsCard.d.ts +22 -0
  68. package/lib/types/client/settings/fields.d.ts +191 -0
  69. package/lib/types/client/settings/use-config-form.d.ts +212 -0
  70. package/lib/types/client/settings/use-credential-state.d.ts +36 -0
  71. package/lib/types/client/sidebar/BalancePopover.d.ts +46 -0
  72. package/lib/types/client/sidebar/PercentRing.d.ts +36 -0
  73. package/lib/types/client/sidebar/SidebarBalance.d.ts +44 -0
  74. package/lib/version.d.ts +12 -0
  75. package/lib/version.js +12 -0
  76. package/package.json +112 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 视图模型:把后端契约机械映射成界面需要的形状。
3
+ * 这里不允许出现金额阈值判断 —— 颜色只由 `severity` 决定。
4
+ * @module dsh-ds-balance/client/model
5
+ */
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
+ /**
21
+ * severity → 环形态。
22
+ *
23
+ * **颜色只有四个色相可用**:官方 token 里 `error-primary` 与 `error-secondary`
24
+ * 在深色主题下同值,没有第五种颜色。所以两档「红」靠**形状**区分:
25
+ *
26
+ * - 颜色编码「数值严重度」:绿 → 琥珀 → 红。
27
+ * - 形状编码「账户可用性」:`unavailable` 是账户维度的事实,与余额高低无关,
28
+ * 它拿红弧再加一个中心叉号。色盲与低分辨率下依然能分开。
29
+ *
30
+ * `critical` 与 `unavailable` 都是红弧,这是有意的:**别再往回改成从红系里挑两个**。
31
+ * @param severity - 后端给的严重度。
32
+ * @returns 弧状态与中心符号。
33
+ */
34
+ export declare function ringSpecOf(severity: Severity): RingSpec;
35
+ /**
36
+ * 圆环的弧长比例:选中币种的余额占它 warn 阈值的几分之几,封顶 1。
37
+ *
38
+ * **阈值在这里只当刻度,不当判据**:颜色仍然完全来自 `severity`,
39
+ * 这个函数只回答「弧画多长」。阈值是用户自己设的,它天然就是「多少算少」的坐标轴,
40
+ * 不必再引入一个「满」的基准。`critical` 不参与这里 —— 它已经在后端决定了 `severity`,
41
+ * 再进一次弧长等于把同一件事算两遍。
42
+ *
43
+ * 金额比较走放大 1e8 的整数,不经过浮点数:`v = w` 必须**恰好**是满环。
44
+ * @param total - 选中币种的余额(定点小数字符串);`null` 表示没有可展示的币种。
45
+ * @param warnThreshold - 该币种的 warn 阈值;`undefined` 或非正数表示没配。
46
+ * @param severity - 后端给的严重度,仅在阈值不可用时用来定性。
47
+ * @returns 0~1 的弧长比例。
48
+ */
49
+ export declare function ringRatioOf(total: string | null, warnThreshold: number | undefined, severity: Severity): number;
50
+ /** 币种符号。未知币种回落到代码本身。 */
51
+ export declare function currencySymbol(currency: string): string;
52
+ /**
53
+ * 把后端的定点小数字符串裁成两位显示。
54
+ * 全程按字符串处理,不经过浮点数:金额相等比较与累加都在后端。
55
+ */
56
+ export declare function formatAmount(value: string): string;
57
+ /** 带币种符号的显示金额。 */
58
+ export declare function formatMoney(amount: string, currency: string): string;
59
+ /** 币种选择结果。 */
60
+ export interface CurrencySelection {
61
+ /** 实际用于展示的那条余额;`null` 表示后端没有给出可展示的币种。 */
62
+ shown: BalanceInfo | null;
63
+ /** 后端选定的币种是否就是设置里选的那个。 */
64
+ matchesPreference: boolean;
65
+ /** 是否处于「自动」模式。 */
66
+ auto: boolean;
67
+ /** 是否连一条可展示的余额都没有。 */
68
+ empty: boolean;
69
+ }
70
+ /**
71
+ * 从后端给出的 `selected` 读出展示币种。
72
+ *
73
+ * **前端不再自己挑币种**:挑选规则(偏好币种、CNY 优先、余额为 0 时跳过)是
74
+ * 后端的职责,前端只把结果映射成界面。设置里的显示币种作为查询参数传给后端。
75
+ *
76
+ * 读的字段只有两个:`selected.currency`(决定展示哪条)与 `selected` 是否为
77
+ * `null`(决定空态)。金额直接取 `balances` 里同币种那条 —— `selected` 只带
78
+ * `total`,浮层还要 `granted` 与 `toppedUp`。
79
+ *
80
+ * `balances` 里找不到 `selected.currency` 时返回 `shown: null` 而不是硬凑一条:
81
+ * 契约保证它一定在,真出现就是形状违约,宁可显示「暂无余额」也不要编一个金额。
82
+ * @param response - 后端响应。
83
+ * @param preference - 设置里的显示币种;`auto` 表示跟随账户。
84
+ * @returns 展示币种与三个布尔标记。
85
+ */
86
+ export declare function selectionOf(response: BalanceResponse, preference: string): CurrencySelection;
87
+ /** 浮层相对时间的档位。 */
88
+ export type AgeBucket = 'just-now' | 'seconds' | 'minutes' | 'hours' | 'days' | 'unknown';
89
+ /** 把毫秒差归到一档,具体文案交给词典。 */
90
+ export declare function ageBucket(ageMs: number): {
91
+ bucket: AgeBucket;
92
+ value: number;
93
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 「DeepSeek 余额」在设置页里的配置卡片。
3
+ * 只做配置:连接、展示、阈值、刷新四组;不展示任何额度信息,也不按阈值给任何东西上色。
4
+ * 分组按使用频率排序:刷新三项有合理默认值,放最后。
5
+ * @module dsh-ds-balance/client/settings/BalanceSettingsCard
6
+ */
7
+ import type { LocaleKey } from '../locales.ts';
8
+ import type { SettingsScope } from './use-config-form.ts';
9
+ export type { SettingsScope } from './use-config-form.ts';
10
+ /** 卡片要求的属性。t 由框架按注册时声明的字典命名空间注入。 */
11
+ export interface BalanceSettingsCardProps {
12
+ /** 词典读取器,键域来自本插件的命名空间。 */
13
+ t: (key: LocaleKey) => string;
14
+ /** 设置作用域;真实 ctx.settingsScope.bind() 的返回值结构上满足它。 */
15
+ scope: SettingsScope;
16
+ }
17
+ /**
18
+ * 渲染配置卡片。
19
+ * @param props - 词典读取器与设置作用域。
20
+ * @returns 卡片元素。
21
+ */
22
+ export declare function BalanceSettingsCard({ t, scope }: BalanceSettingsCardProps): import("react").JSX.Element;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * 手写字段控件:一行一个字段,控件只报告用户输入,写入统一由卡片的保存完成。
3
+ * 结构对齐官方 ui-settings-plugins/fields.tsx,取值全部走 --dsw-alias-* 语义令牌。
4
+ * @module dsh-ds-balance/client/settings/fields
5
+ */
6
+ import type { ReactNode } from 'react';
7
+ import type { TagTone } from '@deepseek-ai/dsh-client-ui-primitives';
8
+ /** 一个配置分组的属性。展开状态由调用方持有:DisclosureRow 是完全受控组件。 */
9
+ export interface FieldGroupProps {
10
+ /** 折叠头左侧的图标;展开后会被原语自己的 chevron 取代。 */
11
+ icon: ReactNode;
12
+ /** 折叠头标题,收起态也显示。 */
13
+ title: string;
14
+ /** 展开体开头的组级说明。 */
15
+ note?: ReactNode;
16
+ open: boolean;
17
+ onToggle: () => void;
18
+ /** 卡片里的最后一组:由它自己补上尾部 12px,卡片 body 不再单独贡献尾距。 */
19
+ last?: boolean;
20
+ children: ReactNode;
21
+ }
22
+ /**
23
+ * 一个可折叠的配置分组。
24
+ * 折叠头用原语 DisclosureRow(不自己画),展开体由原语在 open 时条件渲染、无动画。
25
+ * 各组独立展开,不做手风琴:官方 PluginCard 的注释写明多项同时展开是刻意的。
26
+ * @param props - 图标、标题、可选说明、受控展开状态与字段。
27
+ * @returns 分组元素。
28
+ */
29
+ export declare function FieldGroup(props: FieldGroupProps): import("react").JSX.Element;
30
+ /** 标签行右侧的状态胶囊:文案与色调都由调用方决定。 */
31
+ export interface FieldStatus {
32
+ readonly label: string;
33
+ readonly tone: TagTone;
34
+ }
35
+ /** 标签行右侧的徽标区:状态胶囊、未保存标记与撤销入口。 */
36
+ export interface FieldBadgesProps {
37
+ /** 常驻状态胶囊;为 null 或省略时不渲染。 */
38
+ status?: FieldStatus | null;
39
+ /** 存在未保存的草稿。 */
40
+ pending: boolean;
41
+ /** 存在可撤销的东西:草稿或已存的用户覆盖。 */
42
+ resettable: boolean;
43
+ /** 「未保存」文案。 */
44
+ pendingLabel: string;
45
+ /** 撤销入口文案。 */
46
+ resetLabel: string;
47
+ disabled: boolean;
48
+ /** 暂存一次清除。 */
49
+ onReset: () => void;
50
+ }
51
+ /**
52
+ * 渲染字段的徽标区。三者都不存在时不渲染任何东西。
53
+ * 状态胶囊排在最前,因为它描述的是当前生效值,而后面两个描述的是待保存的改动。
54
+ * @param props - 状态胶囊、两种改动状态与撤销回调。
55
+ * @returns 徽标区元素或 null。
56
+ */
57
+ export declare function FieldBadges(props: FieldBadgesProps): import("react").JSX.Element | null;
58
+ /** 字段容器:标签行、控件、提示行。 */
59
+ export interface FieldFrameProps extends FieldBadgesProps {
60
+ /** 关联 label 与控件的稳定 id。 */
61
+ id: string;
62
+ /** 可见标签。 */
63
+ label: string;
64
+ /** 是否为非法草稿;为真时提示行换成 invalidNote。 */
65
+ invalid: boolean;
66
+ /** 常驻说明;省略或为空串时不渲染提示行。 */
67
+ hint?: ReactNode;
68
+ /** 非法时替换说明的文案;省略时沿用 hint。 */
69
+ invalidNote?: ReactNode;
70
+ /** 控件本体。 */
71
+ children: ReactNode;
72
+ }
73
+ /**
74
+ * 渲染一个字段行。
75
+ * @param props - 字段文案、状态与控件。
76
+ * @returns 字段行元素。
77
+ */
78
+ export declare function FieldFrame(props: FieldFrameProps): import("react").JSX.Element;
79
+ /** 文本 / 数字输入。numeric 只提示数字键盘,不改变接受范围。 */
80
+ export interface TextControlProps {
81
+ id: string;
82
+ text: string;
83
+ /** 用 inputMode 的 numeric 值提示键盘;控件始终是 type 为 text。 */
84
+ numeric: boolean;
85
+ invalid: boolean;
86
+ disabled: boolean;
87
+ /** 失焦回调;阈值字段用它触发成对校验。 */
88
+ onBlur?: () => void;
89
+ onEdit: (text: string) => void;
90
+ }
91
+ /**
92
+ * 渲染单行文本框。
93
+ * @param props - 草稿文本与编辑回调。
94
+ * @returns 输入框元素。
95
+ */
96
+ export declare function TextControl(props: TextControlProps): import("react").JSX.Element;
97
+ /**
98
+ * 只读输入:字段照常渲染,但不可编辑,**框内不写任何文字**。
99
+ *
100
+ * 不从「隐藏字段」也不从「换一块只读文本」走:仍是 `disabled` 的真输入框,
101
+ * 与官方凭据字段同一形态。`readOnly` 与 `disabled` 同时给 ——
102
+ * 前者挡住程序化写入,后者给出官方的视觉与可访问语义。
103
+ *
104
+ * **为什么没有 placeholder**:灰字写在框里读起来像「这里该填但没填」。
105
+ * 只读的因由改由标签行右侧的状态徽章与它下方的说明行承担,那两处的措辞来自词典。
106
+ */
107
+ export interface ReadOnlyControlProps {
108
+ id: string;
109
+ }
110
+ /**
111
+ * 渲染只读输入。
112
+ * @param props - 控件 id。
113
+ * @returns 输入框元素。
114
+ */
115
+ export declare function ReadOnlyControl(props: ReadOnlyControlProps): import("react").JSX.Element;
116
+ /** 二级折叠:卡片里的「自定义设置」。 */
117
+ export interface DetailsGroupProps {
118
+ title: string;
119
+ /** 初始展开状态;省略即收起。 */
120
+ defaultOpen?: boolean;
121
+ children: ReactNode;
122
+ }
123
+ /**
124
+ * 渲染一个原生 details 折叠块。
125
+ *
126
+ * **受控但跟手**:`open` 由 state 持有,用户拨动时从 DOM 读回真实状态,
127
+ * 所以卡片每次重渲染(每敲一个字都会)不会把用户展开的块弹回去。
128
+ * @param props - 标题、初始状态与内容。
129
+ * @returns 折叠块元素。
130
+ */
131
+ export declare function DetailsGroup(props: DetailsGroupProps): import("react").JSX.Element;
132
+ /** 掩码输入:口令类型加一个显隐切换。 */
133
+ export interface SecretControlProps {
134
+ id: string;
135
+ text: string;
136
+ invalid: boolean;
137
+ disabled: boolean;
138
+ /** 当前是否明文显示。 */
139
+ revealed: boolean;
140
+ /** 切换显隐按钮的可访问名;语义状态走在 aria-pressed 上。 */
141
+ revealLabel: string;
142
+ onToggleReveal: () => void;
143
+ onEdit: (text: string) => void;
144
+ }
145
+ /**
146
+ * 渲染掩码输入与它的显隐切换。
147
+ * @param props - 草稿文本、显隐状态与回调。
148
+ * @returns 输入行元素。
149
+ */
150
+ export declare function SecretControl(props: SecretControlProps): import("react").JSX.Element;
151
+ /** 选择器的一个选项。 */
152
+ export interface SelectorOption {
153
+ readonly id: string;
154
+ readonly label: string;
155
+ }
156
+ /** 整行选择器:自绘 pill 触发按钮 + 原语 Menu 弹层。 */
157
+ export interface SelectorControlProps {
158
+ /** 触发按钮的 id,供行容器或测试锚点定位。 */
159
+ id: string;
160
+ /** 触发按钮的可访问名,也是弹层的 aria-label。 */
161
+ label: string;
162
+ options: readonly SelectorOption[];
163
+ selectedId: string;
164
+ disabled: boolean;
165
+ onSelect: (id: string) => void;
166
+ }
167
+ /**
168
+ * 渲染原生形态的整行选择器。
169
+ * 官方设置页的「整行选择型」都是这一套:pill 按钮带 aria-haspopup,弹层由 Menu 自绘并 portal 出去。
170
+ * 弹层本身(圆角、高程、勾选、键盘、外部点击关闭)全由 Menu 负责,这里只管触发器的外观。
171
+ * @param props - 当前选中项、可选项与回调。
172
+ * @returns 选择器元素。
173
+ */
174
+ export declare function SelectorControl(props: SelectorControlProps): import("react").JSX.Element;
175
+ /** 动作行与它的结果行。 */
176
+ export interface ActionRowProps {
177
+ label: string;
178
+ disabled: boolean;
179
+ /** 结果文案;无结果时为 null,此时不渲染结果行。 */
180
+ result: {
181
+ readonly ok: boolean;
182
+ readonly text: string;
183
+ } | null;
184
+ onClick: () => void;
185
+ }
186
+ /**
187
+ * 渲染一个动作按钮与它的状态结果行。
188
+ * @param props - 按钮文案、禁用态、结果与回调。
189
+ * @returns 动作行元素。
190
+ */
191
+ export declare function ActionRow(props: ActionRowProps): import("react").JSX.Element;
@@ -0,0 +1,212 @@
1
+ /**
2
+ * 配置表单的暂存与保存状态机。
3
+ * 编辑只落在本地草稿,保存是草稿变成设置的唯一出口。
4
+ * 快照三元组语义与官方 card-form 一致:value 是生效值,user 是用户覆盖层,键存在即「已覆盖」。
5
+ * @module dsh-ds-balance/client/settings/use-config-form
6
+ */
7
+ /** 作用域快照。真实 ctx.settingsScope 的快照还带 status/base/revision/mode,这里只取需要的三片。 */
8
+ export interface SettingsScopeSnapshotLike {
9
+ /** 合并后的生效配置。 */
10
+ value: Record<string, unknown>;
11
+ /** 用户覆盖层;键是否存在就是「已覆盖」的判据。 */
12
+ user: Record<string, unknown>;
13
+ /** 宿主文档是否接受写入。 */
14
+ writable: boolean;
15
+ }
16
+ /**
17
+ * 配置卡片依赖的最小作用域面。
18
+ * 真实的 ctx.settingsScope.bind() 返回值结构上满足它,但 set/unset 返回 Promise,父代理的适配层可以原样透传。
19
+ */
20
+ export interface SettingsScope {
21
+ /** 读当前快照。 */
22
+ getSnapshot(): SettingsScopeSnapshotLike;
23
+ /** 订阅快照变化。 */
24
+ subscribe(listener: () => void): () => void;
25
+ /** 写入一个字段。 */
26
+ set(field: string, value: unknown): void;
27
+ /** 清除一个字段的用户覆盖。 */
28
+ unset(field: string): void;
29
+ }
30
+ /** 一次字段写入。 */
31
+ export type FieldWrite = {
32
+ readonly kind: 'set';
33
+ readonly value: unknown;
34
+ } | {
35
+ readonly kind: 'clear';
36
+ };
37
+ /** 字段控件类型。 */
38
+ export type FieldKind = 'text' | 'secret' | 'number' | 'select';
39
+ /** 文本类字段规格。 */
40
+ export interface TextFieldSpec {
41
+ readonly field: string;
42
+ readonly kind: 'text' | 'secret' | 'number';
43
+ /** 把生效值渲染成草稿文本。 */
44
+ format(value: unknown): string;
45
+ /** 把草稿文本解析成写入;返回 undefined 表示这份草稿非法并阻止保存。 */
46
+ parse(text: string): FieldWrite | undefined;
47
+ }
48
+ /** 值类字段规格(下拉这类不经过文本草稿的控件)。 */
49
+ export interface ValueFieldSpec {
50
+ readonly field: string;
51
+ readonly kind: 'select';
52
+ }
53
+ /** 任一字段规格。 */
54
+ export type AnyFieldSpec = TextFieldSpec | ValueFieldSpec;
55
+ /**
56
+ * 判断规格是否文本类。
57
+ * @param spec - 字段规格。
58
+ * @returns 是否文本类。
59
+ */
60
+ export declare function isTextSpec(spec: AnyFieldSpec): spec is TextFieldSpec;
61
+ /** 自由文本:空串等于清除,与官方 textField 同规则。 */
62
+ export declare function textField(field: string, kind?: 'text' | 'secret'): TextFieldSpec;
63
+ /** 数字文本:空串等于清除,非有限数视为非法;上下界由宿主 schema 决定。 */
64
+ export declare function numberField(field: string): TextFieldSpec;
65
+ /** 下拉字段。 */
66
+ export declare function selectField(field: string): ValueFieldSpec;
67
+ /** 自动币种取值。 */
68
+ export declare const AUTO_CURRENCY = "auto";
69
+ /** 已知币种。阈值分组就是按这两个币种定义的,故它们是币种列表的最小闭集。 */
70
+ export declare const KNOWN_CURRENCIES: readonly string[];
71
+ /** 一对阈值:同一币种内的预警与告急。 */
72
+ export interface ThresholdPair {
73
+ readonly currency: string;
74
+ readonly warn: string;
75
+ readonly critical: string;
76
+ /**
77
+ * 两个字段的宿主默认值。
78
+ *
79
+ * **必须在这里存一份**:草稿清空之后生效的是默认值而不是旧值,判「空值算不算合法」就得知道它。
80
+ * 两个半体不许值导入,所以抄一份是没办法的事 —— 与宿主 schema 的一致性由
81
+ * `test/threshold-pairs.test.ts` 对着 [src/config.ts](../../config.ts) 的 `Config` 兜底。
82
+ */
83
+ readonly defaultWarn: number;
84
+ readonly defaultCritical: number;
85
+ }
86
+ /** 阈值成对的清单;字段名沿用宿主 schema 的「币种代码小写 + Warn / Critical」。 */
87
+ export declare const THRESHOLD_PAIRS: readonly ThresholdPair[];
88
+ /**
89
+ * 一对阈值是否满足「告急 **严格低于** 预警」。
90
+ *
91
+ * 相等也拒绝:那时余额恰好压线会被同时判成 warn 与 critical,「预警」这一档等于不存在。
92
+ * 任一侧的草稿不是数字时返回 `true` —— 那种情况由各自的 `parse` 报错,不在这里重复报。
93
+ * @param pair - 币种与它的两个字段。
94
+ * @param warn - 预警字段的状态。
95
+ * @param critical - 告急字段的状态。
96
+ * @returns 是否通过。
97
+ */
98
+ export declare function thresholdsOk(pair: ThresholdPair, warn: FieldState, critical: FieldState): boolean;
99
+ /**
100
+ * 下拉可选的币种代码。
101
+ * 已知币种打底;传入的取值里若是未知代码也一并保留,避免把用户已存或编辑中的取值弄丢。
102
+ * @param currents - 任意个候选取值,通常同时给草稿值与生效值。
103
+ * @returns 非自动的币种代码列表。
104
+ */
105
+ export declare function currencyCodes(...currents: readonly unknown[]): readonly string[];
106
+ /** 卡片配置的全部字段,顺序即渲染顺序的参照。 */
107
+ export declare const CONFIG_FIELDS: readonly AnyFieldSpec[];
108
+ /** 字段名到规格的索引。 */
109
+ export declare const SPEC_BY_FIELD: ReadonlyMap<string, AnyFieldSpec>;
110
+ /** 一个字段渲染所需的状态。 */
111
+ export interface FieldState {
112
+ /** 文本类字段的草稿文本。 */
113
+ readonly text: string;
114
+ /** 值类字段的草稿值;文本类字段为 undefined。 */
115
+ readonly value: unknown;
116
+ /** 快照里的生效值,与草稿无关。 */
117
+ readonly effective: unknown;
118
+ /** 用户层当前就存着这个字段(不看草稿)。状态徽标用它判「已覆盖」。 */
119
+ readonly stored: boolean;
120
+ /** 保存后该字段是否会留下用户层条目。 */
121
+ readonly overridden: boolean;
122
+ /** 这份草稿是否会产生一次写入。 */
123
+ readonly dirty: boolean;
124
+ /** 草稿是否不是该字段接受的值。 */
125
+ readonly invalid: boolean;
126
+ }
127
+ /** 表单整体状态。 */
128
+ export interface ConfigFormState {
129
+ /** 宿主文档是否接受写入。 */
130
+ readonly writable: boolean;
131
+ /** 是否存在会产生写入的草稿。 */
132
+ readonly dirty: boolean;
133
+ /** 是否存在非法草稿;为真时禁止保存。 */
134
+ readonly invalid: boolean;
135
+ /** 是否正在跨线写入。 */
136
+ readonly saving: boolean;
137
+ /** 上一次保存是否没有落定;下一次编辑或保存会清掉它。 */
138
+ readonly failed: boolean;
139
+ }
140
+ /** 测试连接的状态。 */
141
+ export interface TestState {
142
+ /** 是否正在测试。 */
143
+ readonly running: boolean;
144
+ /** 结果;未出结果时为 null。 */
145
+ readonly ok: boolean | null;
146
+ /** 失败原因;成功或未出结果时为空串。 */
147
+ readonly message: string;
148
+ }
149
+ /** 表单暴露给卡片的面。 */
150
+ export interface ConfigFormApi {
151
+ /** 整体状态。 */
152
+ readonly state: ConfigFormState;
153
+ /** 读一个字段的渲染状态。 */
154
+ field(field: string): FieldState;
155
+ /** 暂存文本类字段的编辑。 */
156
+ edit(field: string, text: string): void;
157
+ /** 暂存值类字段的编辑。 */
158
+ setValue(field: string, value: unknown): void;
159
+ /** 暂存一次清除,让该字段回到组合层。 */
160
+ resetField(field: string): void;
161
+ /** 丢弃全部草稿。 */
162
+ discard(): void;
163
+ /** 写入全部草稿,并从快照读回落定结果。 */
164
+ save(): Promise<void>;
165
+ /** 一个币种的阈值草稿是否满足「告急 < 预警」;空草稿按默认值算。 */
166
+ thresholdPairOk(currency: string): boolean;
167
+ /** 该字段是否已经失焦过;用来决定要不要显示成对校验提示。 */
168
+ touched(field: string): boolean;
169
+ /** 记一次失焦。 */
170
+ touch(field: string): void;
171
+ /** 测试连接状态。 */
172
+ readonly test: TestState;
173
+ /** 跑一次本地模拟的连接测试。 */
174
+ runTest(): void;
175
+ }
176
+ /** 本地模拟的连接测试耗时。 */
177
+ export declare const TEST_LATENCY_MS = 800;
178
+ /**
179
+ * 判定一次模拟连接测试是否失败。
180
+ * 规则:Base URL 必须以 http:// 或 https:// 开头;API Key 与引用名不能同时为空。
181
+ * 返回的是宿主诊断风格的英文短句,与官方卡片直接展示宿主诊断的做法一致。
182
+ * @param baseUrl - 当前草稿或生效的 Base URL。
183
+ * @param apiKey - 当前草稿或生效的 API Key。
184
+ * @param apiKeyRef - 当前草稿或生效的引用名。
185
+ * @returns 失败原因;可通过时为 null。
186
+ */
187
+ export declare function probeFailure(baseUrl: string, apiKey: string, apiKeyRef: string): string | null;
188
+ /** 计划中的一次写入;write 为 undefined 表示草稿非法。 */
189
+ interface PlannedWrite {
190
+ readonly field: string;
191
+ readonly write: FieldWrite | undefined;
192
+ }
193
+ /**
194
+ * 把写入计划里的成对字段排成「每一步合并后都合法」的顺序。
195
+ *
196
+ * 宿主的 `validate` 在**合并后的完整候选值**上跑,所以单字段写入会让中间态短暂非法:
197
+ * 把 (20, 15) 改成 (10, 5),先写 warn 会得到 (10, 15),宿主直接拒绝整次写入。
198
+ *
199
+ * 判据只看 warn:先写 warn 之后是 `(warn', critical)`,合法就保持原顺序;不合法就只能先写 critical ——
200
+ * 那时 `critical' < warn' ≤ critical < warn`,所以 `(warn, critical')` 必然合法。两者必有一个成立。
201
+ * @param writes - 待写入的编辑。
202
+ * @param current - 当前生效值,用来看「另一半现在是多少」。
203
+ * @returns 排好序的新数组。
204
+ */
205
+ export declare function orderPairWrites(writes: readonly PlannedWrite[], current: Record<string, unknown>): PlannedWrite[];
206
+ /**
207
+ * 绑定一个设置命名空间的配置表单。
208
+ * @param scope - 卡片拿到的设置作用域。
209
+ * @returns 表单状态与动作。
210
+ */
211
+ export declare function useConfigForm(scope: SettingsScope): ConfigFormApi;
212
+ export {};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 凭据状态:问后端要「配没配 / 可不可写」这三个事实。
3
+ *
4
+ * 官方的 provider 卡片用 `describeCredential()` 决定密钥字段是「可编辑」还是
5
+ * 「由启动环境提供(只读)」;浏览器半边拿不到凭据服务,所以走插件自己的
6
+ * `GET /api/v1/config`(**响应里没有密钥**,只有一个固定长度的掩码串)。
7
+ *
8
+ * 读不到就回 `null`,界面据此把字段当只读——宁可少给一个编辑入口,
9
+ * 也不要在不知道的情况下让用户以为能改。
10
+ * @module dsh-ds-balance/client/settings/use-credential-state
11
+ */
12
+ import { type CredentialInfo } from '../data.ts';
13
+ /** 只读凭据行要显示的状态。 */
14
+ export type CredentialView = 'env' | 'configured' | 'notConfigured' | 'overridden';
15
+ /**
16
+ * 决定只读凭据行显示哪一档。
17
+ *
18
+ * 它回答的是「**当前生效的值从哪来**」,不是「这个框能不能改」:
19
+ * - `overridden` 优先:折叠里存过 apiKey 或引用名时,生效的就是折叠里那一份。
20
+ * - 其次看宿主能不能写:写不了就是启动环境给的。
21
+ * - 再退到「配没配」。
22
+ * - 凭据信息读不到(`null`)一律当 `env`:这一行本来就是只读展示,编辑入口在下面的
23
+ * 「自定义设置」,所以「环境提供」在任何未知情况下都不会说错话,也不会因为字段缺失把卡片打挂。
24
+ *
25
+ * 优先级本身有测试兜底(`test/use-credential-state.test.ts`)—— 它是最容易被改错的一处。
26
+ * @param credential - 宿主的凭据描述;`null` 表示读不到。
27
+ * @param overridden - 用户层是否存着覆盖值(`apiKey` 或引用名)。
28
+ * @returns 四档之一。
29
+ */
30
+ export declare function credentialViewOf(credential: CredentialInfo | null, overridden: boolean): CredentialView;
31
+ /**
32
+ * 订阅凭据的只读描述。
33
+ * @param ref - 当前生效的凭据引用名;它一变就重读一次。
34
+ * @returns 三个事实,或 `null` 表示读不到。
35
+ */
36
+ export declare function useCredentialState(ref: string): CredentialInfo | null;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 余额浮层:点击条目后在条目的上方展开。
3
+ *
4
+ * 皮肤照抄官方「用量 / 用时」浮层(ui-chat 的 stat-dialog.module.css);
5
+ * 定位由父组件用 useAnchoredPosition 算好后经 style 传入,本组件只渲染表面。
6
+ * 面板由父组件 createPortal 挂到 document.body,所以 panelRef 指向面板自身。
7
+ * @module dsh-ds-balance/client/sidebar/BalancePopover
8
+ */
9
+ import type { CSSProperties, RefObject } from 'react';
10
+ import { type LocaleKey } from '../locales.ts';
11
+ import { type CurrencySelection } from '../model.ts';
12
+ /** 没有可显示金额时的占位符;条目与浮层共用同一个字面量。 */
13
+ export declare const BALANCE_PLACEHOLDER = "--";
14
+ /** 浮层属性。 */
15
+ export interface BalancePopoverProps {
16
+ /** 词典函数。 */
17
+ t: (key: LocaleKey) => string;
18
+ /** 币种选择结果。 */
19
+ selection: CurrencySelection;
20
+ /** 用户在设置里选的币种,用于不匹配文案。 */
21
+ displayCurrency: string;
22
+ /** 生效的抓取时刻(毫秒);模拟刷新会替换它。 */
23
+ fetchedAt: number;
24
+ /** 当前时刻(毫秒),由父组件按秒推进。 */
25
+ now: number;
26
+ /** 是否正在刷新。 */
27
+ refreshing: boolean;
28
+ /** 冷却剩余秒数;0 表示可以刷新。 */
29
+ cooldownSeconds: number;
30
+ /** 面板自身的引用:useAnchoredPosition 用它量尺寸,外部点击判定用它算「内部」。 */
31
+ panelRef: RefObject<HTMLElement>;
32
+ /** 由 useAnchoredPosition 给出的 fixed 坐标;首帧是 MEASURE_STYLE(隐藏待测)。 */
33
+ style: CSSProperties;
34
+ /** 刷新回调。 */
35
+ onRefresh: () => void;
36
+ /** 「改用 X」回调。 */
37
+ onUseShown: () => void;
38
+ /** 「去设置」回调;本阶段只做占位。 */
39
+ onOpenSettings: () => void;
40
+ }
41
+ /**
42
+ * 渲染余额浮层。
43
+ * @param props - 选择结果、时间、刷新状态、面板 ref/style 与三个动作回调。
44
+ * @returns 浮层元素。
45
+ */
46
+ export declare function BalancePopover(props: BalancePopoverProps): JSX.Element;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 状态圆环:条目在展开态与折叠态共用的唯一图标。
3
+ *
4
+ * 几何逐条照抄官方 ContextMeter:viewBox 14×14、r=5.5、stroke-width 2、
5
+ * 用 strokeDasharray 而不是 stroke-dashoffset,起点靠 rotate(-90 7 7) 挪到 12 点。
6
+ *
7
+ * 两件事分开编码:
8
+ * - **弧长**表达比例:余额占该币种 warn 阈值的几分之几,由 `model.ts` 的 `ringRatioOf` 算好传进来。
9
+ * 阈值是用户自己设的刻度,所以不必再定义「满」是多少;本组件不做任何金额判断。
10
+ * - **颜色**按 state 机械映射,取值与原生 StateDot 的 data-state 一一对应,来源只有 `severity`。
11
+ * 账户不可用(unavailable)在这条弧上再加一个中心叉号:颜色只有四个色相可用,
12
+ * 形状负责把「账户维度不可用」与「余额维度告急」分开。
13
+ * @module dsh-ds-balance/client/sidebar/PercentRing
14
+ */
15
+ import type { RingMarker } from '../model.ts';
16
+ /** 圆环能表达的状态;取值域与原生 StateDot 对齐,本插件用不到 ongoing。 */
17
+ export type RingState = 'done' | 'warning' | 'error' | 'idle';
18
+ /** 圆环属性。 */
19
+ export interface PercentRingProps {
20
+ /** 状态,决定环的颜色;语义与原生 StateDot 的同一套枚举一致。 */
21
+ state: RingState;
22
+ /** 中心符号;`null` 或省略时不画。 */
23
+ marker?: RingMarker | null;
24
+ /** 弧长比例 0~1;省略即满环。越界与非数都夹回 [0, 1]。 */
25
+ ratio?: number;
26
+ /** 渲染边长(px),默认 18。 */
27
+ size?: number;
28
+ /** 可选的原生悬停提示,由 SVG 的 <title> 子元素承载。 */
29
+ title?: string;
30
+ }
31
+ /**
32
+ * 渲染状态圆环。
33
+ * @param props - 状态、边长与可选提示。
34
+ * @returns 圆环 svg;自身 aria-hidden,语义名由调用方的 aria-label 提供。
35
+ */
36
+ export declare function PercentRing({ state, marker, ratio, size, title }: PercentRingProps): JSX.Element;