dsh-ds-balance 2.1.2 → 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.
- package/README.md +78 -19
- package/README_en.md +81 -19
- package/lib/adapters/domain-core-store.d.ts +2 -2
- package/lib/adapters/domain-core-store.js +3 -1
- package/lib/adapters/host-account.d.ts +66 -0
- package/lib/adapters/host-account.js +68 -0
- package/lib/client.js +473 -114
- package/lib/domain/account.d.ts +40 -0
- package/lib/domain/account.js +74 -0
- package/lib/domain/balance.d.ts +20 -1
- package/lib/domain/normalize.d.ts +3 -2
- package/lib/domain/normalize.js +3 -2
- package/lib/domain/severity.d.ts +8 -2
- package/lib/domain/severity.js +11 -4
- package/lib/domain/situation.d.ts +104 -0
- package/lib/domain/situation.js +93 -0
- package/lib/http/handlers.d.ts +1 -1
- package/lib/http/handlers.js +29 -5
- package/lib/http/wire.d.ts +11 -1
- package/lib/http/wire.js +2 -0
- package/lib/index.js +122 -6
- package/lib/ports/account.d.ts +27 -0
- package/lib/ports/account.js +9 -0
- package/lib/services/balance-service.d.ts +66 -85
- package/lib/services/balance-service.js +132 -181
- package/lib/services/balance-source.d.ts +174 -0
- package/lib/services/balance-source.js +294 -0
- package/lib/services/scheduler.d.ts +31 -2
- package/lib/services/scheduler.js +29 -2
- package/lib/services/source-readers.d.ts +26 -0
- package/lib/services/source-readers.js +73 -0
- package/lib/services/source-selector.d.ts +50 -0
- package/lib/services/source-selector.js +64 -0
- package/lib/types/client/api-types.d.ts +16 -0
- package/lib/types/client/data.d.ts +12 -3
- package/lib/types/client/index.d.ts +6 -0
- package/lib/types/client/locales.d.ts +10 -18
- package/lib/types/client/mock/index.d.ts +12 -11
- package/lib/types/client/mock/scenarios.d.ts +13 -5
- package/lib/types/client/model.d.ts +49 -25
- package/lib/types/client/route-hint.d.ts +61 -0
- package/lib/types/client/sidebar/BalancePopover.d.ts +7 -0
- package/lib/types/client/sidebar/PercentRing.d.ts +13 -5
- package/lib/types/client/sidebar/SidebarBalance.d.ts +9 -1
- package/lib/types/client/situation.d.ts +164 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +25 -23
package/lib/index.js
CHANGED
|
@@ -22,6 +22,10 @@ import { BalanceService } from './services/balance-service.js';
|
|
|
22
22
|
import { ConfigService } from './services/config-service.js';
|
|
23
23
|
import { KeyResolver } from './services/key-resolver.js';
|
|
24
24
|
import { Scheduler } from './services/scheduler.js';
|
|
25
|
+
import { createAccountSource } from './adapters/host-account.js';
|
|
26
|
+
import { SourceLedger } from './services/balance-source.js';
|
|
27
|
+
import { accountReader, keyReader } from './services/source-readers.js';
|
|
28
|
+
import { PLUGIN_VERSION } from './version.js';
|
|
25
29
|
export { Config, ENTRY_ID } from './config.js';
|
|
26
30
|
export { CURRENCY_AUTO } from './config.js';
|
|
27
31
|
/** 插件名,loader 诊断用。 */
|
|
@@ -184,6 +188,81 @@ function connectStorage(ctx) {
|
|
|
184
188
|
return (await facility.open(spec));
|
|
185
189
|
};
|
|
186
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* 接上账号登录态这条可选接缝。
|
|
193
|
+
*
|
|
194
|
+
* 与 `connectStorage` 同一套把门方式:`deepseekAccount` 可能在插件装载之后才就绪,
|
|
195
|
+
* 所以句柄是可变的、缺席时读回 `undefined`(账号那条账本据此判「这条路此刻不可用」)。
|
|
196
|
+
*
|
|
197
|
+
* 服务面按鸭子类型收窄(见 [host-account.ts](adapters/host-account.ts) 的说明)。
|
|
198
|
+
* @param ctx - 宿主上下文。
|
|
199
|
+
* @param logger - 日志端口。
|
|
200
|
+
* @returns 取当前端口实现的函数。
|
|
201
|
+
*/
|
|
202
|
+
function connectAccount(ctx, logger) {
|
|
203
|
+
const handle = {};
|
|
204
|
+
const handlers = new Set();
|
|
205
|
+
ctx.inject(['deepseekAccount'], (accountCtx) => {
|
|
206
|
+
accountCtx.effect(() => {
|
|
207
|
+
const service = accountCtx
|
|
208
|
+
.deepseekAccount;
|
|
209
|
+
handle.source = createAccountSource({
|
|
210
|
+
service,
|
|
211
|
+
version: PLUGIN_VERSION,
|
|
212
|
+
// 只选 Platform 那边的服务端文案;界面文案由本插件自己的词典给。
|
|
213
|
+
locale: 'zh-CN',
|
|
214
|
+
timezoneOffsetSeconds: () => -new Date().getTimezoneOffset() * 60,
|
|
215
|
+
logger,
|
|
216
|
+
});
|
|
217
|
+
for (const handler of [...handlers])
|
|
218
|
+
handler();
|
|
219
|
+
return () => {
|
|
220
|
+
handle.source = undefined;
|
|
221
|
+
};
|
|
222
|
+
}, 'ds-balance: account source');
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
current: () => handle.source,
|
|
226
|
+
onReady: (handler) => {
|
|
227
|
+
handlers.add(handler);
|
|
228
|
+
// 服务可能早就到位(inject 回调同步跑过),那就当场通知一次。
|
|
229
|
+
if (handle.source !== undefined)
|
|
230
|
+
handler();
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 接上「当前默认模型走哪条路由」这条可选接缝。
|
|
236
|
+
*
|
|
237
|
+
* 只用来定**全局默认来源**;会话级的来源由客户端当提示带上来(见 `GetViewOptions.providerHint`)。
|
|
238
|
+
* 服务缺席时读回 `null`,选源就只按「Key 优先,账号兜底」走。
|
|
239
|
+
* @param ctx - 宿主上下文。
|
|
240
|
+
* @returns 读当前 provider id 的函数。
|
|
241
|
+
*/
|
|
242
|
+
function connectRoute(ctx) {
|
|
243
|
+
const handle = {};
|
|
244
|
+
const handlers = new Set();
|
|
245
|
+
ctx.inject(['agentDefaultModel'], (routeCtx) => {
|
|
246
|
+
routeCtx.effect(() => {
|
|
247
|
+
const model = routeCtx.agentDefaultModel;
|
|
248
|
+
// 现读:宿主的这个服务每次都从它自己的配置引用里取,不做缓存。
|
|
249
|
+
handle.read = () => model.currentSelection().provider ?? null;
|
|
250
|
+
for (const handler of [...handlers])
|
|
251
|
+
handler();
|
|
252
|
+
return () => {
|
|
253
|
+
handle.read = undefined;
|
|
254
|
+
};
|
|
255
|
+
}, 'ds-balance: model route');
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
current: () => handle.read?.() ?? null,
|
|
259
|
+
onReady: (handler) => {
|
|
260
|
+
handlers.add(handler);
|
|
261
|
+
if (handle.read !== undefined)
|
|
262
|
+
handler();
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
187
266
|
/**
|
|
188
267
|
* 组装并交出生命周期。
|
|
189
268
|
*
|
|
@@ -214,17 +293,54 @@ export async function apply(ctx, refs) {
|
|
|
214
293
|
// 默认组合没有指标 sink,所以用内存登记表把聚合值留下来,
|
|
215
294
|
// 由 healthz 的 metrics 段暴露(见 .agents/notes 的决策)。
|
|
216
295
|
const metrics = new MemoryMetrics();
|
|
296
|
+
// 两条取数路各持一份账本;选源由门面按四层回落决定。
|
|
297
|
+
const account = connectAccount(ctx, logger);
|
|
298
|
+
const route = connectRoute(ctx);
|
|
299
|
+
const ledgers = new Map([
|
|
300
|
+
[
|
|
301
|
+
'deepseek-http',
|
|
302
|
+
new SourceLedger({
|
|
303
|
+
source: 'deepseek-http',
|
|
304
|
+
reader: keyReader({ keys, client, config: configService, salt }),
|
|
305
|
+
store,
|
|
306
|
+
config: configService,
|
|
307
|
+
clock: systemClock,
|
|
308
|
+
logger,
|
|
309
|
+
metrics,
|
|
310
|
+
}),
|
|
311
|
+
],
|
|
312
|
+
[
|
|
313
|
+
'deepseek-account',
|
|
314
|
+
new SourceLedger({
|
|
315
|
+
source: 'deepseek-account',
|
|
316
|
+
reader: accountReader({ account: () => account.current(), salt }),
|
|
317
|
+
store,
|
|
318
|
+
config: configService,
|
|
319
|
+
clock: systemClock,
|
|
320
|
+
logger,
|
|
321
|
+
metrics,
|
|
322
|
+
}),
|
|
323
|
+
],
|
|
324
|
+
]);
|
|
217
325
|
const service = new BalanceService({
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
keys,
|
|
221
|
-
config: configService,
|
|
222
|
-
clock: systemClock,
|
|
223
|
-
salt,
|
|
326
|
+
ledgers,
|
|
327
|
+
routeProvider: () => route.current(),
|
|
224
328
|
logger,
|
|
225
329
|
metrics,
|
|
226
330
|
});
|
|
227
331
|
const scheduler = new Scheduler({ target: service, logger });
|
|
332
|
+
/**
|
|
333
|
+
* 可选服务到位时把来源判据重定一次。
|
|
334
|
+
*
|
|
335
|
+
* 宿主刚起来时账号与默认模型可能都还没就绪,那一次解析只能按兜底走;服务到齐后
|
|
336
|
+
* 作废「已定」并立刻重排一轮,免得首屏那一分钟一直在刷错的那条账本。
|
|
337
|
+
*/
|
|
338
|
+
const reselectSource = () => {
|
|
339
|
+
service.invalidateSource();
|
|
340
|
+
scheduler.reset();
|
|
341
|
+
};
|
|
342
|
+
account.onReady(reselectSource);
|
|
343
|
+
route.onReady(reselectSource);
|
|
228
344
|
ctx.effect(() => {
|
|
229
345
|
// 排程只看这几项。事件给的是「真的变了的路径」,比原来的全量 diff 更准,
|
|
230
346
|
// 所以改阈值、改展示币种都不会顺带打一次官方接口。
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 账号登录态端口。
|
|
3
|
+
*
|
|
4
|
+
* 宿主提供这条接缝(`ctx.deepseekAccount`)时才装配;**缺席时账号那条取数路整条不存在**,
|
|
5
|
+
* 插件退回只看 API Key —— 与没有这条接缝之前逐字相同。
|
|
6
|
+
* @module dsh-ds-balance/ports/account
|
|
7
|
+
*/
|
|
8
|
+
import type { AccountBalance } from '../domain/account.js';
|
|
9
|
+
/** 账号来源的只读面。没有写入口:余额只是读。 */
|
|
10
|
+
export interface AccountSource {
|
|
11
|
+
/** 本地存着登录凭据(宿主原文:存储态,不代表服务端验过)。 */
|
|
12
|
+
signedIn(): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* 稳定账号 id:作为账号账本的派生输入。
|
|
15
|
+
*
|
|
16
|
+
* 拿不到时回 `null`(宿主本次进程还没读过 profile),调用方按「未知账号」处理。
|
|
17
|
+
*/
|
|
18
|
+
accountId(): Promise<string | null>;
|
|
19
|
+
/**
|
|
20
|
+
* 读一次钱包。
|
|
21
|
+
*
|
|
22
|
+
* `null` 表示**此刻没有可用账号**(未登录,或凭据在查询途中被换掉)——
|
|
23
|
+
* 调用方据此回落另一条路,而不是把它当成零余额。
|
|
24
|
+
*/
|
|
25
|
+
readBalance(): Promise<AccountBalance | null>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=account.d.ts.map
|
|
@@ -1,71 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 余额服务门面:**决定这一轮走哪条取数路**,然后把活交给那条路的账本。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 门面自己不持状态、不抓数据 —— 两份状态在 {@link SourceLedger} 里,一个来源一份。
|
|
5
|
+
* 选源规则是四层回落(会话路由 → 全局默认路由 → Key 优先 → 都没有),判据是纯函数
|
|
6
|
+
* [source-selector.ts](source-selector.ts);两条路的读取策略在
|
|
7
|
+
* [source-readers.ts](source-readers.ts)。
|
|
6
8
|
* @module dsh-ds-balance/services/balance-service
|
|
7
9
|
*/
|
|
8
|
-
import type {
|
|
9
|
-
import { type ErrorCode } from '../domain/errors.js';
|
|
10
|
-
import type { Clock } from '../ports/clock.js';
|
|
11
|
-
import type { CoreStore } from '../ports/core-store.js';
|
|
12
|
-
import type { DeepSeekClient } from '../ports/deepseek-client.js';
|
|
10
|
+
import type { BalanceSource, BalanceView } from '../domain/balance.js';
|
|
13
11
|
import type { Logger } from '../ports/logger.js';
|
|
14
|
-
import {
|
|
15
|
-
import type {
|
|
16
|
-
|
|
17
|
-
/** `forceRefresh` 的返回值,形状对齐契约 §8.4。 */
|
|
18
|
-
export interface RefreshResult {
|
|
19
|
-
triggered: boolean;
|
|
20
|
-
joined: boolean;
|
|
21
|
-
cooldownMs: number;
|
|
22
|
-
state: CacheState;
|
|
23
|
-
}
|
|
24
|
-
/** 调度器需要的状态切片。 */
|
|
25
|
-
export interface BalanceStatus {
|
|
26
|
-
state: CacheState;
|
|
27
|
-
errorCode: ErrorCode | null;
|
|
28
|
-
consecutiveFailures: number;
|
|
29
|
-
hasSnapshot: boolean;
|
|
30
|
-
serverRefreshSeconds: number;
|
|
31
|
-
/** 上游给的 `Retry-After`,优先于自算退避。 */
|
|
32
|
-
retryAfterMs: number | null;
|
|
33
|
-
/** 最近一次成功抓取的时刻;从未成功过是 `null`。健康检查要用。 */
|
|
34
|
-
lastSuccessAt: number | null;
|
|
35
|
-
}
|
|
12
|
+
import type { Metrics } from '../ports/metrics.js';
|
|
13
|
+
import type { BalanceStatus, LedgerViewOptions, RefreshResult, SourceLedger } from './balance-source.js';
|
|
14
|
+
export type { BalanceStatus, RefreshResult, SourceReader } from './balance-source.js';
|
|
36
15
|
/** `getView` 的可选参数。 */
|
|
37
|
-
export interface GetViewOptions {
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
16
|
+
export interface GetViewOptions extends LedgerViewOptions {
|
|
17
|
+
/**
|
|
18
|
+
* 客户端给的**当前会话路由 id**(宿主 provider 名)。
|
|
19
|
+
*
|
|
20
|
+
* 只当提示用:认不出来或没给,就回落到全局默认路由 —— 客户端读不到会话时,
|
|
21
|
+
* 界面不该因此失去来源。
|
|
22
|
+
*/
|
|
23
|
+
providerHint?: string | null;
|
|
42
24
|
}
|
|
43
25
|
/** 构造参数。 */
|
|
44
26
|
export interface BalanceServiceOptions {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
clock: Clock;
|
|
50
|
-
/** 服务端盐,用来算 `accountTag`。 */
|
|
51
|
-
salt: string;
|
|
27
|
+
/** 两条账本;两个 id 都要在。 */
|
|
28
|
+
ledgers: ReadonlyMap<BalanceSource, SourceLedger>;
|
|
29
|
+
/** 全局默认路由 id(宿主 `agentDefaultModel.currentSelection().provider`);读不到回 `null`。 */
|
|
30
|
+
routeProvider: () => string | null;
|
|
52
31
|
logger?: Logger | undefined;
|
|
32
|
+
/** 指标;来源切换计数用它(界面刻意不给提示,这是唯一观测出口)。 */
|
|
53
33
|
metrics?: Metrics | undefined;
|
|
54
34
|
}
|
|
55
|
-
/**
|
|
35
|
+
/** 余额服务门面。 */
|
|
56
36
|
export declare class BalanceService {
|
|
57
37
|
private readonly options;
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
38
|
+
/** 当前活跃来源;由最近一次解析决定,调度与健康检查读它。 */
|
|
39
|
+
private active;
|
|
40
|
+
/**
|
|
41
|
+
* 来源判据是否已经定下来过。
|
|
42
|
+
*
|
|
43
|
+
* 宿主刚起来时两个可选服务(账号、默认模型)可能还没到,那一次解析只能按兜底走;
|
|
44
|
+
* 服务到齐后由装配处调 {@link invalidateSource} + `scheduler.reset()` 重新定一次。
|
|
45
|
+
* 一旦有请求解析过就以请求为准 —— 调度不拿全局默认去顶会话级的来源。
|
|
46
|
+
*/
|
|
47
|
+
private resolved;
|
|
48
|
+
/** 每条路只恢复一次(存储扫描不便宜,来回切来源不该重复扫)。 */
|
|
49
|
+
private readonly restores;
|
|
67
50
|
constructor(options: BalanceServiceOptions);
|
|
68
|
-
/**
|
|
51
|
+
/** 当前活跃来源。诊断与测试用。 */
|
|
52
|
+
activeSource(): BalanceSource;
|
|
53
|
+
/** 当前活跃账本的状态切片,供调度与健康检查使用。 */
|
|
69
54
|
status(): BalanceStatus;
|
|
70
55
|
/**
|
|
71
56
|
* 当前账本标识的前 8 位;还没有快照时是 `null`。
|
|
@@ -73,49 +58,45 @@ export declare class BalanceService {
|
|
|
73
58
|
* **只回前 8 位**:完整 tag 是账本作用域标识,没有对外的理由。
|
|
74
59
|
*/
|
|
75
60
|
accountTag8(): string | null;
|
|
76
|
-
/**
|
|
77
|
-
* 从存储恢复最近快照(挂载时调一次)。
|
|
78
|
-
*
|
|
79
|
-
* **按 `accountTag` 过滤**:凭据轮换后 tag 变了,旧快照视为不存在,不混用。
|
|
80
|
-
* 没有密钥可算 tag 时静默跳过。
|
|
81
|
-
*/
|
|
61
|
+
/** 从存储恢复最近快照(挂载时调一次,恢复的是**当前活跃来源**那条账本)。 */
|
|
82
62
|
restore(): Promise<void>;
|
|
83
63
|
/**
|
|
84
64
|
* 取当前视图。
|
|
85
65
|
*
|
|
86
|
-
*
|
|
87
|
-
*/
|
|
88
|
-
getView(options?: GetViewOptions): Promise<BalanceView>;
|
|
89
|
-
/**
|
|
90
|
-
* 手动刷新。
|
|
66
|
+
* `providerHint` 一变(用户切了会话、切了模型)就重新解析来源 —— 这是「跟着会话走」的入口。
|
|
91
67
|
*
|
|
92
|
-
*
|
|
68
|
+
* **本插件只记官方的那一个数字**,所以首选那条路拿不出数字时**退到另一条官方路**:
|
|
69
|
+
* 叉(空态 / 失败态)只在「官方两条路都拿不到数据」时才出现 —— 那才是「没接入官方」。
|
|
93
70
|
*/
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
private
|
|
71
|
+
getView(options?: GetViewOptions): Promise<BalanceView>;
|
|
72
|
+
/** 取一条路的视图(先确保它恢复过)。 */
|
|
73
|
+
private viewOf;
|
|
97
74
|
/**
|
|
98
|
-
*
|
|
75
|
+
* 定时刷新:刷**用户实际看到的那条**。
|
|
99
76
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
77
|
+
* 来源还没定过(宿主刚起来、可选服务未到齐)时先解析一次;定过之后就认 `active` ——
|
|
78
|
+
* 而 `active` 是**上一次真正把数字交出去的那条**(`getView` 兜底出去的也算),
|
|
79
|
+
* 所以兜底展示的账号数字才会被继续刷新,不会永远停在一张旧快照上。
|
|
103
80
|
*/
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
|
|
81
|
+
refreshActive(): Promise<BalanceView>;
|
|
82
|
+
/** 作废「来源已定」这件事,让下一次调度重新解析。 */
|
|
83
|
+
invalidateSource(): void;
|
|
107
84
|
/**
|
|
108
|
-
*
|
|
85
|
+
* 手动刷新:**用户看哪条就刷哪条**(与 {@link getView} 同一条判定)。
|
|
109
86
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
87
|
+
* 首选那条一条数字都没有时,界面上实际是兜底那条在撑着 —— 那就刷兜底那条,
|
|
88
|
+
* 否则用户点了刷新、数字却永远不动。冷却仍由那条自己的账本管。
|
|
112
89
|
*/
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
private
|
|
116
|
-
/**
|
|
117
|
-
private
|
|
118
|
-
/**
|
|
119
|
-
private
|
|
90
|
+
forceRefresh(reason: string, providerHint?: string | null): Promise<RefreshResult>;
|
|
91
|
+
/** 找一条**有快照**的官方路;都没有就退回默认那条(那时才画叉)。 */
|
|
92
|
+
private firstWithSnapshot;
|
|
93
|
+
/** 记下「这条刚刚把数字交出去了」:展示、刷新、标签从此都认它。 */
|
|
94
|
+
private mark;
|
|
95
|
+
/** 解析这一轮**首选**哪条路。 */
|
|
96
|
+
private resolve;
|
|
97
|
+
/** 取账本;缺一条属于装配错误,直接抛(静默降级会让界面永远停在空态)。 */
|
|
98
|
+
private ledger;
|
|
99
|
+
/** 首次用到某条路时才恢复它的快照;并发调用合并成一次。 */
|
|
100
|
+
private restoreLedger;
|
|
120
101
|
}
|
|
121
102
|
//# sourceMappingURL=balance-service.d.ts.map
|