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.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/README_en.md +81 -0
- package/cordis.patch.yml +9 -0
- package/lib/adapters/console-logger.d.ts +25 -0
- package/lib/adapters/console-logger.js +35 -0
- package/lib/adapters/domain-core-store.d.ts +104 -0
- package/lib/adapters/domain-core-store.js +184 -0
- package/lib/adapters/http-deepseek-client.d.ts +36 -0
- package/lib/adapters/http-deepseek-client.js +101 -0
- package/lib/adapters/memory-metrics.d.ts +30 -0
- package/lib/adapters/memory-metrics.js +67 -0
- package/lib/adapters/salt-file.d.ts +32 -0
- package/lib/adapters/salt-file.js +40 -0
- package/lib/client.js +2120 -0
- package/lib/config.d.ts +127 -0
- package/lib/config.js +112 -0
- package/lib/domain/balance.d.ts +70 -0
- package/lib/domain/balance.js +8 -0
- package/lib/domain/errors.d.ts +84 -0
- package/lib/domain/errors.js +136 -0
- package/lib/domain/money.d.ts +39 -0
- package/lib/domain/money.js +74 -0
- package/lib/domain/normalize.d.ts +38 -0
- package/lib/domain/normalize.js +120 -0
- package/lib/domain/select.d.ts +26 -0
- package/lib/domain/select.js +51 -0
- package/lib/domain/severity.d.ts +34 -0
- package/lib/domain/severity.js +44 -0
- package/lib/http/handlers.d.ts +47 -0
- package/lib/http/handlers.js +261 -0
- package/lib/http/routes.d.ts +63 -0
- package/lib/http/routes.js +61 -0
- package/lib/http/wire.d.ts +82 -0
- package/lib/http/wire.js +68 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +210 -0
- package/lib/ports/clock.d.ts +12 -0
- package/lib/ports/clock.js +6 -0
- package/lib/ports/core-store.d.ts +28 -0
- package/lib/ports/core-store.js +6 -0
- package/lib/ports/credentials.d.ts +30 -0
- package/lib/ports/credentials.js +9 -0
- package/lib/ports/deepseek-client.d.ts +47 -0
- package/lib/ports/deepseek-client.js +7 -0
- package/lib/ports/logger.d.ts +12 -0
- package/lib/ports/logger.js +6 -0
- package/lib/ports/metrics.d.ts +36 -0
- package/lib/ports/metrics.js +11 -0
- package/lib/services/account-tag.d.ts +24 -0
- package/lib/services/account-tag.js +29 -0
- package/lib/services/balance-service.d.ts +121 -0
- package/lib/services/balance-service.js +220 -0
- package/lib/services/config-service.d.ts +52 -0
- package/lib/services/config-service.js +51 -0
- package/lib/services/key-resolver.d.ts +42 -0
- package/lib/services/key-resolver.js +62 -0
- package/lib/services/scheduler.d.ts +93 -0
- package/lib/services/scheduler.js +142 -0
- package/lib/types/client/api-types.d.ts +54 -0
- package/lib/types/client/data.d.ts +103 -0
- package/lib/types/client/index.d.ts +18 -0
- package/lib/types/client/locales.d.ts +98 -0
- package/lib/types/client/mock/index.d.ts +36 -0
- package/lib/types/client/mock/scenarios.d.ts +51 -0
- package/lib/types/client/model.d.ts +93 -0
- package/lib/types/client/settings/BalanceSettingsCard.d.ts +22 -0
- package/lib/types/client/settings/fields.d.ts +191 -0
- package/lib/types/client/settings/use-config-form.d.ts +212 -0
- package/lib/types/client/settings/use-credential-state.d.ts +36 -0
- package/lib/types/client/sidebar/BalancePopover.d.ts +46 -0
- package/lib/types/client/sidebar/PercentRing.d.ts +36 -0
- package/lib/types/client/sidebar/SidebarBalance.d.ts +44 -0
- package/lib/version.d.ts +12 -0
- package/lib/version.js +12 -0
- package/package.json +112 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 刷新调度。
|
|
3
|
+
*
|
|
4
|
+
* `setTimeout` 链而不是 `setInterval`:每一轮跑完才排下一轮,这样退避与
|
|
5
|
+
* `Retry-After` 都能生效,也不会堆积重叠的请求。
|
|
6
|
+
* @module dsh-ds-balance/services/scheduler
|
|
7
|
+
*/
|
|
8
|
+
import type { BalanceView } from '../domain/balance.js';
|
|
9
|
+
import type { Logger } from '../ports/logger.js';
|
|
10
|
+
import type { BalanceStatus, GetViewOptions } from './balance-service.js';
|
|
11
|
+
/** 首拉延迟。 */
|
|
12
|
+
export declare const INITIAL_DELAY_MS = 1000;
|
|
13
|
+
/** 缺密钥时的快速重试间隔。 */
|
|
14
|
+
export declare const NO_KEY_RETRY_MS = 5000;
|
|
15
|
+
/** 指数退避的基数与上限。 */
|
|
16
|
+
export declare const BASE_BACKOFF_MS = 5000;
|
|
17
|
+
export declare const MAX_BACKOFF_MS = 300000;
|
|
18
|
+
/** 抖动比例。 */
|
|
19
|
+
export declare const JITTER_RATIO = 0.2;
|
|
20
|
+
/** 延迟下限,避免忙转。 */
|
|
21
|
+
export declare const MIN_DELAY_MS = 1000;
|
|
22
|
+
/** 定时器抽象,测试可注入。 */
|
|
23
|
+
export interface SchedulerTimers {
|
|
24
|
+
set(callback: () => void, ms: number): unknown;
|
|
25
|
+
clear(handle: unknown): void;
|
|
26
|
+
}
|
|
27
|
+
/** 调度器需要的最小服务面。 */
|
|
28
|
+
export interface SchedulerTarget {
|
|
29
|
+
getView(options?: GetViewOptions): Promise<BalanceView>;
|
|
30
|
+
status(): BalanceStatus;
|
|
31
|
+
}
|
|
32
|
+
/** 构造参数。 */
|
|
33
|
+
export interface SchedulerOptions {
|
|
34
|
+
target: SchedulerTarget;
|
|
35
|
+
timers?: SchedulerTimers;
|
|
36
|
+
/** 随机源,抖动用。 */
|
|
37
|
+
random?: () => number;
|
|
38
|
+
/** 时刻函数,`nextRunAt` 用。 */
|
|
39
|
+
now?: () => number;
|
|
40
|
+
/** 首拉延迟覆盖,测试用。 */
|
|
41
|
+
initialDelayMs?: number;
|
|
42
|
+
logger?: Logger | undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 加抖动:`base * (1 ± ratio)`。
|
|
46
|
+
* @param base - 基准毫秒数。
|
|
47
|
+
* @param ratio - 抖动比例。
|
|
48
|
+
* @param random - 随机源,返回 `[0, 1)`。
|
|
49
|
+
* @returns 取整后的毫秒数。
|
|
50
|
+
*/
|
|
51
|
+
export declare function jitter(base: number, ratio: number, random: () => number): number;
|
|
52
|
+
/**
|
|
53
|
+
* 算下一轮该等多久。
|
|
54
|
+
*
|
|
55
|
+
* 优先级:上游 `Retry-After` → 缺密钥快速重试 → 失败指数退避 → 配置的刷新频率。
|
|
56
|
+
* @param status - 服务的状态切片。
|
|
57
|
+
* @param options - 随机源。
|
|
58
|
+
* @returns 毫秒数。
|
|
59
|
+
*/
|
|
60
|
+
export declare function nextDelayMs(status: BalanceStatus, options?: {
|
|
61
|
+
random?: () => number;
|
|
62
|
+
}): number;
|
|
63
|
+
/**
|
|
64
|
+
* 刷新调度器。
|
|
65
|
+
*
|
|
66
|
+
* `start` / `stop` 幂等;`stop` 之后不再排程。**必须挂在 `ctx.effect` 的 disposer 上。**
|
|
67
|
+
*/
|
|
68
|
+
export declare class Scheduler {
|
|
69
|
+
private readonly options;
|
|
70
|
+
private readonly timers;
|
|
71
|
+
private readonly random;
|
|
72
|
+
private readonly now;
|
|
73
|
+
private readonly initialDelayMs;
|
|
74
|
+
private handle;
|
|
75
|
+
private running;
|
|
76
|
+
private nextAt;
|
|
77
|
+
constructor(options: SchedulerOptions);
|
|
78
|
+
/** 下一轮的计划时刻;没排程时为 `null`。 */
|
|
79
|
+
nextRunAt(): number | null;
|
|
80
|
+
/** 是否在运行。 */
|
|
81
|
+
isRunning(): boolean;
|
|
82
|
+
/** 开始调度。重复调用无效。 */
|
|
83
|
+
start(): void;
|
|
84
|
+
/** 停止调度并清掉已排的那一轮。 */
|
|
85
|
+
stop(): void;
|
|
86
|
+
/** 配置或阈值变了之后重排:立刻跑一轮再按新配置继续。 */
|
|
87
|
+
reset(): void;
|
|
88
|
+
/** 排一轮。 */
|
|
89
|
+
private schedule;
|
|
90
|
+
/** 跑一轮并按状态排下一轮。 */
|
|
91
|
+
private tick;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 刷新调度。
|
|
3
|
+
*
|
|
4
|
+
* `setTimeout` 链而不是 `setInterval`:每一轮跑完才排下一轮,这样退避与
|
|
5
|
+
* `Retry-After` 都能生效,也不会堆积重叠的请求。
|
|
6
|
+
* @module dsh-ds-balance/services/scheduler
|
|
7
|
+
*/
|
|
8
|
+
/** 首拉延迟。 */
|
|
9
|
+
export const INITIAL_DELAY_MS = 1000;
|
|
10
|
+
/** 缺密钥时的快速重试间隔。 */
|
|
11
|
+
export const NO_KEY_RETRY_MS = 5000;
|
|
12
|
+
/** 指数退避的基数与上限。 */
|
|
13
|
+
export const BASE_BACKOFF_MS = 5000;
|
|
14
|
+
export const MAX_BACKOFF_MS = 300_000;
|
|
15
|
+
/** 抖动比例。 */
|
|
16
|
+
export const JITTER_RATIO = 0.2;
|
|
17
|
+
/** 退避指数的上限,防止 `2 ** n` 溢出。 */
|
|
18
|
+
const MAX_BACKOFF_EXPONENT = 10;
|
|
19
|
+
/** 延迟下限,避免忙转。 */
|
|
20
|
+
export const MIN_DELAY_MS = 1000;
|
|
21
|
+
/** 把值夹到区间内。 */
|
|
22
|
+
function clamp(value, min, max) {
|
|
23
|
+
return Math.min(max, Math.max(min, value));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 加抖动:`base * (1 ± ratio)`。
|
|
27
|
+
* @param base - 基准毫秒数。
|
|
28
|
+
* @param ratio - 抖动比例。
|
|
29
|
+
* @param random - 随机源,返回 `[0, 1)`。
|
|
30
|
+
* @returns 取整后的毫秒数。
|
|
31
|
+
*/
|
|
32
|
+
export function jitter(base, ratio, random) {
|
|
33
|
+
const min = base * (1 - ratio);
|
|
34
|
+
const span = base * ratio * 2;
|
|
35
|
+
return Math.floor(min + random() * span);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 算下一轮该等多久。
|
|
39
|
+
*
|
|
40
|
+
* 优先级:上游 `Retry-After` → 缺密钥快速重试 → 失败指数退避 → 配置的刷新频率。
|
|
41
|
+
* @param status - 服务的状态切片。
|
|
42
|
+
* @param options - 随机源。
|
|
43
|
+
* @returns 毫秒数。
|
|
44
|
+
*/
|
|
45
|
+
export function nextDelayMs(status, options = {}) {
|
|
46
|
+
const random = options.random ?? Math.random;
|
|
47
|
+
if (status.retryAfterMs !== null && status.retryAfterMs > 0) {
|
|
48
|
+
return clamp(status.retryAfterMs, MIN_DELAY_MS, MAX_BACKOFF_MS);
|
|
49
|
+
}
|
|
50
|
+
if (status.errorCode === 'NO_KEY' && !status.hasSnapshot) {
|
|
51
|
+
return jitter(NO_KEY_RETRY_MS, JITTER_RATIO, random);
|
|
52
|
+
}
|
|
53
|
+
if (status.state === 'error' || status.state === 'stale') {
|
|
54
|
+
const exponent = Math.min(Math.max(0, status.consecutiveFailures - 1), MAX_BACKOFF_EXPONENT);
|
|
55
|
+
return jitter(Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** exponent), JITTER_RATIO, random);
|
|
56
|
+
}
|
|
57
|
+
return jitter(Math.max(MIN_DELAY_MS, status.serverRefreshSeconds * 1000), JITTER_RATIO, random);
|
|
58
|
+
}
|
|
59
|
+
/** 真正的默认定时器。 */
|
|
60
|
+
const defaultTimers = {
|
|
61
|
+
set: (callback, ms) => setTimeout(callback, ms),
|
|
62
|
+
clear: (handle) => { clearTimeout(handle); },
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* 刷新调度器。
|
|
66
|
+
*
|
|
67
|
+
* `start` / `stop` 幂等;`stop` 之后不再排程。**必须挂在 `ctx.effect` 的 disposer 上。**
|
|
68
|
+
*/
|
|
69
|
+
export class Scheduler {
|
|
70
|
+
options;
|
|
71
|
+
timers;
|
|
72
|
+
random;
|
|
73
|
+
now;
|
|
74
|
+
initialDelayMs;
|
|
75
|
+
handle = null;
|
|
76
|
+
running = false;
|
|
77
|
+
nextAt = null;
|
|
78
|
+
constructor(options) {
|
|
79
|
+
this.options = options;
|
|
80
|
+
this.timers = options.timers ?? defaultTimers;
|
|
81
|
+
this.random = options.random ?? Math.random;
|
|
82
|
+
this.now = options.now ?? (() => Date.now());
|
|
83
|
+
this.initialDelayMs = options.initialDelayMs ?? INITIAL_DELAY_MS;
|
|
84
|
+
}
|
|
85
|
+
/** 下一轮的计划时刻;没排程时为 `null`。 */
|
|
86
|
+
nextRunAt() {
|
|
87
|
+
return this.nextAt;
|
|
88
|
+
}
|
|
89
|
+
/** 是否在运行。 */
|
|
90
|
+
isRunning() {
|
|
91
|
+
return this.running;
|
|
92
|
+
}
|
|
93
|
+
/** 开始调度。重复调用无效。 */
|
|
94
|
+
start() {
|
|
95
|
+
if (this.running)
|
|
96
|
+
return;
|
|
97
|
+
this.running = true;
|
|
98
|
+
this.schedule(this.initialDelayMs);
|
|
99
|
+
}
|
|
100
|
+
/** 停止调度并清掉已排的那一轮。 */
|
|
101
|
+
stop() {
|
|
102
|
+
this.running = false;
|
|
103
|
+
if (this.handle !== null) {
|
|
104
|
+
this.timers.clear(this.handle);
|
|
105
|
+
this.handle = null;
|
|
106
|
+
}
|
|
107
|
+
this.nextAt = null;
|
|
108
|
+
}
|
|
109
|
+
/** 配置或阈值变了之后重排:立刻跑一轮再按新配置继续。 */
|
|
110
|
+
reset() {
|
|
111
|
+
this.stop();
|
|
112
|
+
this.running = true;
|
|
113
|
+
this.schedule(0);
|
|
114
|
+
}
|
|
115
|
+
/** 排一轮。 */
|
|
116
|
+
schedule(delay) {
|
|
117
|
+
if (!this.running)
|
|
118
|
+
return;
|
|
119
|
+
this.nextAt = this.now() + delay;
|
|
120
|
+
this.handle = this.timers.set(() => { void this.tick(); }, delay);
|
|
121
|
+
}
|
|
122
|
+
/** 跑一轮并按状态排下一轮。 */
|
|
123
|
+
async tick() {
|
|
124
|
+
this.handle = null;
|
|
125
|
+
this.nextAt = null;
|
|
126
|
+
if (!this.running)
|
|
127
|
+
return;
|
|
128
|
+
try {
|
|
129
|
+
await this.options.target.getView({ force: true });
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// 服务承诺永不抛错;这里是防御,不让调度链断掉。
|
|
133
|
+
this.options.logger?.error('ds-balance: scheduler tick threw', {
|
|
134
|
+
error: error instanceof Error ? error.message : String(error),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (!this.running)
|
|
138
|
+
return;
|
|
139
|
+
this.schedule(nextDelayMs(this.options.target.status(), { random: this.random }));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 后端契约类型。本阶段不实现后端,这些形状只用来约束 mock 数据。
|
|
3
|
+
* @module dsh-ds-balance/client/api-types
|
|
4
|
+
*/
|
|
5
|
+
/** 余额数据的状态维度。 */
|
|
6
|
+
export type BalanceState = 'empty' | 'ok' | 'stale' | 'error';
|
|
7
|
+
/** 颜色严重度维度。前端只做机械映射,不比较金额。 */
|
|
8
|
+
export type Severity = 'ok' | 'warn' | 'critical' | 'unavailable' | 'unknown';
|
|
9
|
+
/** 估算来源。第一版 UI 不消费。 */
|
|
10
|
+
export type UsageSource = 'blended' | 'balance-observed' | 'projection';
|
|
11
|
+
/** 单个币种的余额。金额一律是字符串。 */
|
|
12
|
+
export interface BalanceInfo {
|
|
13
|
+
currency: string;
|
|
14
|
+
total: string;
|
|
15
|
+
granted: string;
|
|
16
|
+
toppedUp: string;
|
|
17
|
+
}
|
|
18
|
+
/** 今日用量。第一版 UI 不消费,保留以维持契约完整。 */
|
|
19
|
+
export interface TodayUsage {
|
|
20
|
+
value: string;
|
|
21
|
+
currency: string;
|
|
22
|
+
source: UsageSource;
|
|
23
|
+
confidence: string;
|
|
24
|
+
needsReview: boolean;
|
|
25
|
+
range: [string, string] | null;
|
|
26
|
+
}
|
|
27
|
+
/** 后端错误。 */
|
|
28
|
+
export interface BalanceError {
|
|
29
|
+
code: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
/** `GET /api/v1/balance` 的响应。 */
|
|
33
|
+
export interface BalanceResponse {
|
|
34
|
+
requestId: string;
|
|
35
|
+
schemaVersion: number;
|
|
36
|
+
state: BalanceState;
|
|
37
|
+
stale: boolean;
|
|
38
|
+
fetchedAt: number;
|
|
39
|
+
ageMs: number;
|
|
40
|
+
isAvailable: boolean;
|
|
41
|
+
accountTag8: string;
|
|
42
|
+
balances: BalanceInfo[];
|
|
43
|
+
selected: {
|
|
44
|
+
currency: string;
|
|
45
|
+
total: string;
|
|
46
|
+
} | null;
|
|
47
|
+
severity: Severity;
|
|
48
|
+
thresholds: Record<string, {
|
|
49
|
+
warn: string;
|
|
50
|
+
critical: string;
|
|
51
|
+
}>;
|
|
52
|
+
todayUsage: TodayUsage | null;
|
|
53
|
+
error: BalanceError | null;
|
|
54
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据层:向后端要余额。
|
|
3
|
+
*
|
|
4
|
+
* 端点由宿主半边的 `ctx.connection.fetch` 注册,物理载体**已做完信任与浏览器
|
|
5
|
+
* 鉴权**,所以浏览器这边只是同源 `fetch`,不带任何自己的凭据。
|
|
6
|
+
*
|
|
7
|
+
* **本模块不缓存、不合并、不排程**:节奏归调用方(`SidebarBalance` 的轮询与
|
|
8
|
+
* 手动刷新),这里只负责一次往返与两件事 —— 把 `displayCurrency` 作为查询参数
|
|
9
|
+
* 传过去、把失败翻成可展示的错误态。
|
|
10
|
+
* @module dsh-ds-balance/client/data
|
|
11
|
+
*/
|
|
12
|
+
import type { BalanceResponse } from './api-types.ts';
|
|
13
|
+
/** 余额端点。路径与宿主半边逐字一致,不带尾随斜杠。 */
|
|
14
|
+
export declare const BALANCE_PATH = "/api/v1/balance";
|
|
15
|
+
/** 手动刷新端点。 */
|
|
16
|
+
export declare const REFRESH_PATH = "/api/v1/balance/refresh";
|
|
17
|
+
/** 配置读端点。界面只从这里取「凭据可不可写」这一件事。 */
|
|
18
|
+
export declare const CONFIG_PATH = "/api/v1/config";
|
|
19
|
+
/** 可替换的 fetch,便于测试注入。 */
|
|
20
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
21
|
+
/**
|
|
22
|
+
* 凭据的只读描述。
|
|
23
|
+
*
|
|
24
|
+
* 与宿主的线上形状同构,也逐字对齐官方 `describe()`:**没有装值的槽**。
|
|
25
|
+
*/
|
|
26
|
+
export interface CredentialInfo {
|
|
27
|
+
ref: string;
|
|
28
|
+
configured: boolean;
|
|
29
|
+
source: string | null;
|
|
30
|
+
writable: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** `GET /api/v1/config` 里本插件消费的那部分。 */
|
|
33
|
+
export interface ConfigResponse {
|
|
34
|
+
config: Record<string, unknown>;
|
|
35
|
+
/** 已配置时是固定长度的星号串;**不含密钥的任何片段**。 */
|
|
36
|
+
apiKeyMasked: string;
|
|
37
|
+
/** 凭据端口缺席或读失败时是 `null`。 */
|
|
38
|
+
credential: CredentialInfo | null;
|
|
39
|
+
timeoutMs: number;
|
|
40
|
+
}
|
|
41
|
+
/** `POST /api/v1/balance/refresh` 的响应。 */
|
|
42
|
+
export interface RefreshResult {
|
|
43
|
+
triggered: boolean;
|
|
44
|
+
joined: boolean;
|
|
45
|
+
cooldownMs: number;
|
|
46
|
+
state: string;
|
|
47
|
+
}
|
|
48
|
+
/** 端点拿不到、或回的不是 JSON 时用的错误码。 */
|
|
49
|
+
export declare const UNREACHABLE_CODE = "PLUGIN_UNREACHABLE";
|
|
50
|
+
/**
|
|
51
|
+
* 「还没拿到数据」的占位视图。
|
|
52
|
+
*
|
|
53
|
+
* 空态而不是错误态:首拉还没回来时不该先闪一下「读取失败」。
|
|
54
|
+
* @returns 契约形状的响应。
|
|
55
|
+
*/
|
|
56
|
+
export declare function pendingView(): BalanceResponse;
|
|
57
|
+
/**
|
|
58
|
+
* 「拿不到数据」的视图。
|
|
59
|
+
*
|
|
60
|
+
* 只在**一次都没成功过**的时候用;已经有数据时宁可继续显示旧值 ——
|
|
61
|
+
* 后端自己的 `state: stale` 才是「数据过期」的权威表达。
|
|
62
|
+
* @param message - 面向用户的一句话。
|
|
63
|
+
* @returns 契约形状的响应。
|
|
64
|
+
*/
|
|
65
|
+
export declare function unreachableView(message: string): BalanceResponse;
|
|
66
|
+
/**
|
|
67
|
+
* 读一次余额。
|
|
68
|
+
*
|
|
69
|
+
* `displayCurrency` **总是**作为查询参数传出去,包括 `auto` —— 后端的挑选规则
|
|
70
|
+
* 把 `auto` 当作「不指定」,所以传它不等于替后端做决定。
|
|
71
|
+
* @param options - 显示币种、可注入的 fetch 与取消信号。
|
|
72
|
+
* @returns 后端响应。
|
|
73
|
+
* @throws 端点不可达、非 2xx 或响应不是 JSON。
|
|
74
|
+
*/
|
|
75
|
+
export declare function requestBalance(options: {
|
|
76
|
+
currency: string;
|
|
77
|
+
fetchImpl?: FetchLike;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}): Promise<BalanceResponse>;
|
|
80
|
+
/**
|
|
81
|
+
* 读一次插件配置。
|
|
82
|
+
*
|
|
83
|
+
* 只为拿 `credential`:界面靠 `writable` 决定凭据字段是「可编辑」还是
|
|
84
|
+
* 「由启动环境提供(只读)」。**响应里没有密钥**,掩码是固定长度的星号串。
|
|
85
|
+
* @param options - 可注入的 fetch。
|
|
86
|
+
* @returns 配置响应里本插件消费的那部分。
|
|
87
|
+
* @throws 端点不可达、非 2xx 或响应不是 JSON。
|
|
88
|
+
*/
|
|
89
|
+
export declare function requestConfig(options?: {
|
|
90
|
+
fetchImpl?: FetchLike;
|
|
91
|
+
}): Promise<ConfigResponse>;
|
|
92
|
+
/**
|
|
93
|
+
* 触发一次手动刷新。
|
|
94
|
+
*
|
|
95
|
+
* 后端的冷却还没过时它回 `triggered: false`,那是正常结果而不是错误。
|
|
96
|
+
* @param options - 刷新原因与可注入的 fetch。
|
|
97
|
+
* @returns 后端的刷新结果。
|
|
98
|
+
* @throws 端点不可达、非 2xx 或响应不是 JSON。
|
|
99
|
+
*/
|
|
100
|
+
export declare function requestRefresh(options?: {
|
|
101
|
+
reason?: string;
|
|
102
|
+
fetchImpl?: FetchLike;
|
|
103
|
+
}): Promise<RefreshResult>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器半边入口:注册词典、设置卡片与左下角条目。
|
|
3
|
+
*
|
|
4
|
+
* 这两半各占一个 slot,互不依赖;设置卡片的位置由宿主 `installSection` 与
|
|
5
|
+
* 本文件注册的 `settings.plugin.item` 用同一个命名空间配对决定。
|
|
6
|
+
* @module dsh-ds-balance/client
|
|
7
|
+
*/
|
|
8
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
9
|
+
import './sidebar/footer-stack.module.css';
|
|
10
|
+
/** 设置命名空间:与宿主 `SETTINGS_NAMESPACE` 逐字一致。 */
|
|
11
|
+
export declare const SETTINGS_NAMESPACE = "ds-balance";
|
|
12
|
+
/**
|
|
13
|
+
* 运行时服务门禁:删任何一项都会让 `apply` 静默不跑。
|
|
14
|
+
* 这里只需要 slot、词典与设置作用域三个服务。
|
|
15
|
+
*/
|
|
16
|
+
export declare const inject: string[];
|
|
17
|
+
/** 挂载两半。 */
|
|
18
|
+
export declare function apply(ctx: ClientContext): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地化词典。`zh` 是键集真源,`en` 用 `Record<LocaleKey, string>` 做编译期完整性检查。
|
|
3
|
+
* @module dsh-ds-balance/client/locales
|
|
4
|
+
*/
|
|
5
|
+
/** 词典命名空间,必须与 slot 注册项的 `locale` 字段一致。 */
|
|
6
|
+
export declare const NS = "ds-balance";
|
|
7
|
+
/** 中文词典(键集真源)。 */
|
|
8
|
+
export declare const zh: {
|
|
9
|
+
readonly 'sidebar.label': "DeepSeek 余额";
|
|
10
|
+
readonly 'sidebar.aria.balance': "DeepSeek 余额";
|
|
11
|
+
readonly 'sidebar.aria.ring': "DeepSeek 余额状态";
|
|
12
|
+
readonly 'sidebar.aria.refresh': "刷新余额";
|
|
13
|
+
readonly 'sidebar.aria.mismatch': "显示的币种与你选择的不一致";
|
|
14
|
+
readonly 'popover.title': "账户余额";
|
|
15
|
+
readonly 'popover.total': "余额";
|
|
16
|
+
readonly 'popover.granted': "赠送";
|
|
17
|
+
readonly 'popover.toppedUp': "充值";
|
|
18
|
+
readonly 'popover.updated': "{value}前";
|
|
19
|
+
readonly 'popover.updated.justNow': "刚刚更新";
|
|
20
|
+
readonly 'popover.refreshing': "刷新中";
|
|
21
|
+
readonly 'popover.cooldown': "{value} 秒后可再次刷新";
|
|
22
|
+
readonly 'popover.mismatch': "未找到 {wanted} 余额,当前显示 {shown}";
|
|
23
|
+
readonly 'popover.action.useShown': "改用 {shown}";
|
|
24
|
+
readonly 'popover.action.openSettings': "去设置";
|
|
25
|
+
readonly 'state.unavailable': "账户不可用";
|
|
26
|
+
readonly 'state.stale': "数据已过期";
|
|
27
|
+
readonly 'state.error': "读取失败";
|
|
28
|
+
readonly 'state.empty': "尚未配置";
|
|
29
|
+
readonly 'state.noKey': "尚未填写 API Key";
|
|
30
|
+
readonly 'state.noBalance': "暂无余额";
|
|
31
|
+
readonly 'settings.title': "DeepSeek 余额";
|
|
32
|
+
readonly 'settings.description': "配置账户连接、刷新节奏与预警阈值。";
|
|
33
|
+
readonly 'settings.group.connection': "连接";
|
|
34
|
+
readonly 'settings.group.refresh': "刷新";
|
|
35
|
+
readonly 'settings.group.display': "展示";
|
|
36
|
+
readonly 'settings.group.thresholds': "阈值";
|
|
37
|
+
readonly 'settings.field.apiKey': "API 密钥";
|
|
38
|
+
readonly 'settings.field.apiKeyRef': "API 密钥引用名";
|
|
39
|
+
readonly 'settings.field.baseUrl': "API 地址";
|
|
40
|
+
readonly 'settings.field.serverRefreshSeconds': "服务端刷新频率(秒)";
|
|
41
|
+
readonly 'settings.field.clientPollSeconds': "客户端轮询频率(秒)";
|
|
42
|
+
readonly 'settings.field.manualRefreshCooldownSeconds': "手动刷新冷却(秒)";
|
|
43
|
+
readonly 'settings.field.displayCurrency': "显示币种";
|
|
44
|
+
readonly 'settings.field.cnyWarn': "CNY 预警";
|
|
45
|
+
readonly 'settings.field.cnyCritical': "CNY 告急";
|
|
46
|
+
readonly 'settings.field.usdWarn': "USD 预警";
|
|
47
|
+
readonly 'settings.field.usdCritical': "USD 告急";
|
|
48
|
+
readonly 'settings.hint.apiKey': "留空则改用引用名从凭据存储读取。";
|
|
49
|
+
readonly 'settings.hint.apiKeyRef': "凭据存储里的条目名;非空时优先于 API Key。";
|
|
50
|
+
readonly 'settings.hint.baseUrl': "官方端点;使用代理或镜像时修改。";
|
|
51
|
+
readonly 'settings.hint.displayCurrency': "所选币种不存在时,回落到账户实际持有的币种。";
|
|
52
|
+
readonly 'settings.hint.number': "请填数字;留空表示使用默认值。";
|
|
53
|
+
readonly 'settings.hint.threshold': "后端判定;界面颜色跟随返回的严重度。";
|
|
54
|
+
readonly 'settings.hint.thresholdPair': "告急值必须低于预警值";
|
|
55
|
+
readonly 'settings.hint.serverRefreshSeconds': "访问官方接口的间隔。";
|
|
56
|
+
readonly 'settings.hint.clientPollSeconds': "刷新显示的间隔;只读本地缓存,不访问上游。";
|
|
57
|
+
readonly 'settings.hint.manualRefreshCooldownSeconds': "两次手动刷新之间的最短间隔。";
|
|
58
|
+
readonly 'settings.currency.auto': "自动(跟随账户)";
|
|
59
|
+
readonly 'settings.credential.envLocked': "已由启动环境提供";
|
|
60
|
+
readonly 'settings.hint.credential.env': "启动环境提供,只读。";
|
|
61
|
+
readonly 'settings.hint.credential.configured': "不写入设置文件。留空表示保持当前密钥。";
|
|
62
|
+
readonly 'settings.hint.credential.notConfigured': "尚未配置。";
|
|
63
|
+
readonly 'settings.hint.credential.overridden': "使用自定义设置里的值,忽略继承。";
|
|
64
|
+
readonly 'settings.group.customized': "自定义设置";
|
|
65
|
+
readonly 'settings.test': "测试连接";
|
|
66
|
+
readonly 'settings.testing': "测试中…";
|
|
67
|
+
readonly 'settings.test.ok': "连接正常。";
|
|
68
|
+
readonly 'settings.test.fail': "连接失败:{message}";
|
|
69
|
+
readonly 'settings.save': "保存";
|
|
70
|
+
readonly 'settings.saving': "保存中…";
|
|
71
|
+
readonly 'settings.discard': "放弃修改";
|
|
72
|
+
readonly 'settings.failed': "保存失败,请重试。";
|
|
73
|
+
readonly 'settings.readOnly': "当前配置只读,无法保存修改。";
|
|
74
|
+
readonly 'settings.unsaved': "未保存";
|
|
75
|
+
readonly 'settings.invalid': "请检查标红的字段。";
|
|
76
|
+
readonly 'settings.configured': "已配置";
|
|
77
|
+
readonly 'settings.notConfigured': "未配置";
|
|
78
|
+
readonly 'settings.overridden': "已覆盖";
|
|
79
|
+
readonly 'settings.reset': "重置";
|
|
80
|
+
readonly 'settings.invalidNumber': "请填数字;留空表示使用默认值。";
|
|
81
|
+
readonly 'settings.hint.refreshAdvanced': "不常改:默认值适用于大多数情况。";
|
|
82
|
+
readonly 'settings.currencyMismatch': "当前账户无 {wanted} 余额,实际显示 {shown}";
|
|
83
|
+
readonly 'dev.title': "开发场景";
|
|
84
|
+
readonly 'dev.hint': "仅用于 UI 开发;不影响真实配置。";
|
|
85
|
+
};
|
|
86
|
+
/** 词典键。 */
|
|
87
|
+
export type LocaleKey = keyof typeof zh;
|
|
88
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
89
|
+
interface LocaleNamespaceMap {
|
|
90
|
+
'ds-balance': LocaleKey;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** 英文词典。缺键会在编译期报错。 */
|
|
94
|
+
export declare const en: Record<LocaleKey, string>;
|
|
95
|
+
/** 取词典。未知语言回落英文。 */
|
|
96
|
+
export declare function dictionaryFor(language: string): Record<LocaleKey, string>;
|
|
97
|
+
/** 极简插值:把 `{name}` 替换成实参。 */
|
|
98
|
+
export declare function interpolate(template: string, params: Record<string, string>): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mock 场景解析与切换。
|
|
3
|
+
*
|
|
4
|
+
* **默认走真实端点**:只有 URL 参数或 localStorage 明确选过场景时才使用 mock,
|
|
5
|
+
* 否则插件在真机上会永远显示 demo 数据。带 `?dsb=live` 打开会清掉已存的选择。
|
|
6
|
+
*
|
|
7
|
+
* 任何一步失败都静默回落到下一档:mock 层不允许把页面搞崩。
|
|
8
|
+
* @module dsh-ds-balance/client/mock
|
|
9
|
+
*/
|
|
10
|
+
import type { BalanceResponse } from '../api-types.ts';
|
|
11
|
+
import { type ScenarioKey } from './scenarios.ts';
|
|
12
|
+
/** URL 查询参数名。 */
|
|
13
|
+
export declare const SCENARIO_PARAM = "dsb";
|
|
14
|
+
/** 开发模式参数名:出现即显示场景切换器。 */
|
|
15
|
+
export declare const DEV_PARAM = "dsb-dev";
|
|
16
|
+
/** `?dsb=` 的这个取值表示「回到真实端点」,并顺手清掉已存的场景。 */
|
|
17
|
+
export declare const LIVE_PARAM_VALUE = "live";
|
|
18
|
+
/**
|
|
19
|
+
* 当前生效的 mock 场景键;**该走真实端点时返回 `null`**。
|
|
20
|
+
*
|
|
21
|
+
* 这是唯一会写 localStorage 的读入口:`?dsb=live` 会顺手把已存的选择清掉。
|
|
22
|
+
* @returns 场景键,或 `null` 表示用真实端点。
|
|
23
|
+
*/
|
|
24
|
+
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
|
+
/** 订阅场景变化。回调立即收到一次当前值,返回退订函数。 */
|
|
32
|
+
export declare function subscribeScenario(listener: (key: ScenarioKey) => void): () => void;
|
|
33
|
+
/** 通知所有订阅者场景已变。 */
|
|
34
|
+
export declare function notifyScenario(key: ScenarioKey): void;
|
|
35
|
+
/** 取当前场景的数据快照。 */
|
|
36
|
+
export declare function currentBalance(): BalanceResponse;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 勘察与开发用的 mock 场景。覆盖后端契约里所有状态组合。
|
|
3
|
+
*
|
|
4
|
+
* **不变量**:`selected` 由后端权威给出,所以每个场景里它要么是 `null`,
|
|
5
|
+
* 要么必须能在 `balances` 里找到同币种同金额的一条。覆写 `balances` 的场景
|
|
6
|
+
* 必须同时覆写 `selected`。这条由 `test/mock-scenarios.test.ts` 兜底。
|
|
7
|
+
* @module dsh-ds-balance/client/mock/scenarios
|
|
8
|
+
*/
|
|
9
|
+
import type { BalanceResponse } from '../api-types.ts';
|
|
10
|
+
/** 全部场景。键即 URL 参数 `?dsb=<key>` 的取值。 */
|
|
11
|
+
export declare const scenarios: {
|
|
12
|
+
/** 正常。 */
|
|
13
|
+
ok: BalanceResponse;
|
|
14
|
+
/** 余额偏低但可用。 */
|
|
15
|
+
warn: BalanceResponse;
|
|
16
|
+
/** 余额告急。 */
|
|
17
|
+
critical: BalanceResponse;
|
|
18
|
+
/** 账户不可用(余额耗尽 / 欠费)。 */
|
|
19
|
+
unavailable: BalanceResponse;
|
|
20
|
+
/** 有旧值但本次刷新失败。 */
|
|
21
|
+
stale: BalanceResponse;
|
|
22
|
+
/** 无值失败。 */
|
|
23
|
+
error: BalanceResponse;
|
|
24
|
+
/** 未配置。 */
|
|
25
|
+
empty: BalanceResponse;
|
|
26
|
+
/** 未配置且带 NO_KEY 错误。 */
|
|
27
|
+
noKey: BalanceResponse;
|
|
28
|
+
/** 多币种。`selected` 显式写出,不靠 `make()` 的默认值兜。 */
|
|
29
|
+
multiCurrency: BalanceResponse;
|
|
30
|
+
/**
|
|
31
|
+
* 选定了账户里没有的币种(USD),账户只有 CNY。
|
|
32
|
+
*
|
|
33
|
+
* 不匹配由「`selected.currency` 与设置里的显示币种不等」判定 —— 所以场景本身
|
|
34
|
+
* 只需要保证 `selected` 是账户里真实存在的那个币种,剩下的交给设置值。
|
|
35
|
+
*/
|
|
36
|
+
currencyMismatch: BalanceResponse;
|
|
37
|
+
/** 账户完全没有余额。 */
|
|
38
|
+
noBalanceAtAll: BalanceResponse;
|
|
39
|
+
/** 今日用量缺失(第一版 UI 不消费,用于契约回归)。 */
|
|
40
|
+
usageMissing: BalanceResponse;
|
|
41
|
+
/** 今日用量需复核(第一版 UI 不消费,用于契约回归)。 */
|
|
42
|
+
usageNeedsReview: BalanceResponse;
|
|
43
|
+
};
|
|
44
|
+
/** 场景键。 */
|
|
45
|
+
export type ScenarioKey = keyof typeof scenarios;
|
|
46
|
+
/** 场景键清单,供开发切换器使用。 */
|
|
47
|
+
export declare const scenarioKeys: ScenarioKey[];
|
|
48
|
+
/** 默认场景。 */
|
|
49
|
+
export declare const defaultScenario: ScenarioKey;
|
|
50
|
+
/** 运行时判断一个字符串是不是已知场景键。 */
|
|
51
|
+
export declare function isScenarioKey(value: string): value is ScenarioKey;
|