dsh-ds-balance 2.0.0-alpha.2 → 2.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.
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
11
11
  import { z } from 'zod';
12
- import { StorageError } from '../domain/errors.js';
12
+ import { StorageError, describeError } from '../domain/errors.js';
13
13
  import { formatMoney, parseMoney } from '../domain/money.js';
14
14
  /** 域里的表名。 */
15
15
  export const SNAPSHOT_TABLE = 'snapshots';
@@ -73,10 +73,6 @@ export function fromStored(record) {
73
73
  raw: undefined,
74
74
  };
75
75
  }
76
- /** 把未知异常压成一行可读文本。 */
77
- function describe(error) {
78
- return error instanceof Error ? error.message : String(error);
79
- }
80
76
  /**
81
77
  * 官方存储接缝上的快照存储。
82
78
  *
@@ -115,7 +111,7 @@ export class DomainCoreStore {
115
111
  this.ready = null;
116
112
  if (!this.openFailureLogged) {
117
113
  this.openFailureLogged = true;
118
- this.options.logger?.error('ds-balance: storage domain unavailable, running degraded', { error: describe(error) });
114
+ this.options.logger?.error('ds-balance: storage domain unavailable, running degraded', { error: describeError(error) });
119
115
  }
120
116
  });
121
117
  this.ready = attempt;
@@ -132,7 +128,7 @@ export class DomainCoreStore {
132
128
  async requireTable() {
133
129
  await this.ensureOpen();
134
130
  if (this.openError !== undefined) {
135
- throw new StorageError(`storage domain unavailable: ${describe(this.openError)}`, { cause: this.openError });
131
+ throw new StorageError(`storage domain unavailable: ${describeError(this.openError)}`, { cause: this.openError });
136
132
  }
137
133
  if (this.table === null)
138
134
  throw new StorageError('storage domain is not open');
@@ -164,7 +160,7 @@ export class DomainCoreStore {
164
160
  if (this.closed)
165
161
  return { ok: false, detail: 'closed' };
166
162
  await this.ensureOpen();
167
- return this.openError === undefined ? { ok: true } : { ok: false, detail: describe(this.openError) };
163
+ return this.openError === undefined ? { ok: true } : { ok: false, detail: describeError(this.openError) };
168
164
  }
169
165
  /** 幂等关闭。**必须挂在 `ctx.effect` 的 disposer 上。** */
170
166
  async close() {
@@ -2,7 +2,7 @@
2
2
  * DeepSeek 上游的 HTTP 实现。
3
3
  * @module dsh-ds-balance/adapters/http-deepseek-client
4
4
  */
5
- import { NetworkError, ParseError, TimeoutError, UpstreamError, classify } from '../domain/errors.js';
5
+ import { NetworkError, ParseError, TimeoutError, UpstreamError, classify, describeError } from '../domain/errors.js';
6
6
  import { parseErrorBody } from '../domain/normalize.js';
7
7
  /** 去掉尾部斜杠,避免拼出双斜杠路径。 */
8
8
  function joinUrl(baseUrl, path) {
@@ -82,7 +82,7 @@ export class HttpDeepSeekClient {
82
82
  if (error instanceof Error && error.name === 'AbortError') {
83
83
  throw new TimeoutError(`upstream aborted after ${options.timeoutMs}ms`, { cause: error });
84
84
  }
85
- throw new NetworkError(error instanceof Error ? error.message : String(error), { cause: error });
85
+ throw new NetworkError(describeError(error), { cause: error });
86
86
  }
87
87
  finally {
88
88
  clearTimeout(timer);
package/lib/client.js CHANGED
@@ -512,8 +512,11 @@ function probeFailure(baseUrl, apiKey, apiKeyRef) {
512
512
  if (apiKey.trim() === "" && apiKeyRef.trim() === "") return "no API key or credential reference configured";
513
513
  return null;
514
514
  }
515
+ function isRecord(input) {
516
+ return typeof input === "object" && input !== null && !Array.isArray(input);
517
+ }
515
518
  function asRecord(input) {
516
- return typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
519
+ return isRecord(input) ? input : {};
517
520
  }
518
521
  function shallowEqual(a, b) {
519
522
  const keys = Object.keys(a);
@@ -1319,7 +1322,7 @@ var scenarios = {
1319
1322
  selected: { currency: "CNY", total: "0.00000000" }
1320
1323
  }),
1321
1324
  /** 有旧值但本次刷新失败。 */
1322
- stale: make({ state: "stale", stale: true, severity: "unknown", ageMs: 36e5, fetchedAt: T0 - 36e5, error: { code: "NETWORK", message: "fetch failed" } }),
1325
+ stale: make({ state: "stale", stale: true, severity: "unknown", ageMs: 36e5, fetchedAt: T0 - 36e5, error: { code: "NO_NETWORK", message: "fetch failed" } }),
1323
1326
  /** 无值失败。 */
1324
1327
  error: make({
1325
1328
  state: "error",
@@ -1329,7 +1332,7 @@ var scenarios = {
1329
1332
  balances: [],
1330
1333
  selected: null,
1331
1334
  ageMs: 0,
1332
- error: { code: "HTTP_500", message: "upstream returned 500" }
1335
+ error: { code: "UPSTREAM_5XX", message: "upstream returned 500" }
1333
1336
  }),
1334
1337
  /** 未配置。 */
1335
1338
  empty: make({ state: "empty", severity: "unknown", isAvailable: false, balances: [], selected: null, ageMs: 0 }),
@@ -1984,8 +1987,17 @@ var DEFAULT_CONFIG = {
1984
1987
  clientPollSeconds: 30
1985
1988
  };
1986
1989
  var inject = ["slots", "locale"];
1990
+ function isTranslate(value) {
1991
+ return typeof value === "function";
1992
+ }
1993
+ function translateOf(value) {
1994
+ return isTranslate(value) ? value : (key) => key;
1995
+ }
1996
+ function isRecord2(value) {
1997
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1998
+ }
1987
1999
  function asRecord2(value) {
1988
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
2000
+ return isRecord2(value) ? value : {};
1989
2001
  }
1990
2002
  function useFormSnapshot(form) {
1991
2003
  const subscribe = (0, import_react6.useCallback)((listener) => form.subscribe(listener), [form]);
@@ -2030,7 +2042,7 @@ function SidebarSeatComponent(props) {
2030
2042
  SidebarBalance,
2031
2043
  {
2032
2044
  wide: props.seat.wide,
2033
- t: props.seat.t,
2045
+ t: translateOf(props.seat.t),
2034
2046
  configSlotProbe: props.configSlotProbe,
2035
2047
  pluginsNavigation: props.pluginsNavigation,
2036
2048
  onSelectCurrency: selectCurrency,
@@ -2046,7 +2058,7 @@ function SidebarSeatComponent(props) {
2046
2058
  );
2047
2059
  }
2048
2060
  function SettingsSeatComponent(props) {
2049
- const t = props.seat.t;
2061
+ const t = translateOf(props.seat.t);
2050
2062
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BalanceSettingsCard, { t, form: props.form });
2051
2063
  }
2052
2064
  var PLUGINS_PANEL_ID = "plugins";
@@ -2080,8 +2092,10 @@ function apply(ctx) {
2080
2092
  ctx.effect(() => () => configSlotProbe.dispose(), "ds-balance: config slot probe");
2081
2093
  const pluginsNavigation = createPluginsNavigation();
2082
2094
  ctx.inject(["layout"], (layoutCtx) => {
2095
+ if (!("layout" in layoutCtx)) return;
2083
2096
  const face = layoutCtx.layout;
2084
- if (face === void 0 || typeof face.selectPanel !== "function") return;
2097
+ if (typeof face !== "object" || face === null) return;
2098
+ if (!("selectPanel" in face) || typeof face.selectPanel !== "function") return;
2085
2099
  const selectPanel = face.selectPanel.bind(face);
2086
2100
  layoutCtx.effect(() => pluginsNavigation.attach(() => {
2087
2101
  try {
package/lib/config.d.ts CHANGED
@@ -173,7 +173,19 @@ export interface ThresholdPair {
173
173
  * 归成一对来判([use-config-form.ts](client/settings/use-config-form.ts) 的 `THRESHOLD_PAIRS`)。
174
174
  * 两边各存一份是因为**宿主与浏览器两个半体不许值导入**;约定本身由测试对着本表兜底。
175
175
  */
176
- export declare const THRESHOLD_PAIRS: readonly ThresholdPair[];
176
+ export declare const THRESHOLD_PAIRS: readonly [{
177
+ readonly currency: "CNY";
178
+ readonly warn: "cnyWarn";
179
+ readonly critical: "cnyCritical";
180
+ readonly defaultWarn: 10;
181
+ readonly defaultCritical: 5;
182
+ }, {
183
+ readonly currency: "USD";
184
+ readonly warn: "usdWarn";
185
+ readonly critical: "usdCritical";
186
+ readonly defaultWarn: 2;
187
+ readonly defaultCritical: 1;
188
+ }];
177
189
  /**
178
190
  * 跨字段校验:每个币种内 `critical` 必须**严格低于** `warn`。
179
191
  *
@@ -81,4 +81,13 @@ export declare function parseRetryAfter(headers: Headers | undefined, now?: numb
81
81
  * @returns 契约里的错误形状。
82
82
  */
83
83
  export declare function classify(error: unknown): ErrorInfo;
84
+ /**
85
+ * 把任意异常压成一行文本,供日志与错误消息使用。
86
+ *
87
+ * **绝不带凭据**:上游约定异常文本里不含密钥,所以它可以安全写进日志。
88
+ * 只取 `Error.message`;非 `Error` 的抛出值退回 `String()`。
89
+ * @param error - 任意抛出的值。
90
+ * @returns 单行错误文本。
91
+ */
92
+ export declare function describeError(error: unknown): string;
84
93
  //# sourceMappingURL=errors.d.ts.map
@@ -125,7 +125,7 @@ export function classify(error) {
125
125
  if (error.name === 'AbortError' || error.name === 'TimeoutError') {
126
126
  return { code: 'UPSTREAM_TIMEOUT', message: error.message, retryable: true };
127
127
  }
128
- const code = error.code;
128
+ const code = 'code' in error ? error.code : undefined;
129
129
  if (typeof code === 'string' && /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|UND_ERR)/.test(code)) {
130
130
  return { code: 'NO_NETWORK', message: error.message, retryable: true };
131
131
  }
@@ -133,4 +133,15 @@ export function classify(error) {
133
133
  }
134
134
  return { code: 'STORAGE_ERROR', message: String(error), retryable: true };
135
135
  }
136
+ /**
137
+ * 把任意异常压成一行文本,供日志与错误消息使用。
138
+ *
139
+ * **绝不带凭据**:上游约定异常文本里不含密钥,所以它可以安全写进日志。
140
+ * 只取 `Error.message`;非 `Error` 的抛出值退回 `String()`。
141
+ * @param error - 任意抛出的值。
142
+ * @returns 单行错误文本。
143
+ */
144
+ export function describeError(error) {
145
+ return error instanceof Error ? error.message : String(error);
146
+ }
136
147
  //# sourceMappingURL=errors.js.map
@@ -9,7 +9,7 @@
9
9
  * - **每次请求现读配置**,不许在闭包里捕获旧值。
10
10
  * @module dsh-ds-balance/http/handlers
11
11
  */
12
- import { classify } from '../domain/errors.js';
12
+ import { classify, describeError } from '../domain/errors.js';
13
13
  import { CONFIG_FIELDS, endpointOf, validateThresholds } from '../config.js';
14
14
  import { PLUGIN_VERSION, SCHEMA_VERSION } from '../version.js';
15
15
  import { newRequestId, toWireBalanceView, toWireError, } from './wire.js';
@@ -27,10 +27,6 @@ function json(body, status = 200) {
27
27
  function isPlainObject(value) {
28
28
  return typeof value === 'object' && value !== null && !Array.isArray(value);
29
29
  }
30
- /** 把未知异常压成一行。**绝不带凭据** —— 异常里不含密钥是上游的约定。 */
31
- function describe(error) {
32
- return error instanceof Error ? error.message : String(error);
33
- }
34
30
  /** 读一个查询参数;URL 坏掉时返回 `null`。 */
35
31
  function queryParam(request, name) {
36
32
  try {
@@ -100,7 +96,7 @@ export async function handleBalance(request, deps) {
100
96
  return json(toWireBalanceView(view, deps.service.accountTag8() ?? '', requestId));
101
97
  }
102
98
  catch (error) {
103
- deps.logger?.error('ds-balance: balance handler failed', { requestId, error: describe(error) });
99
+ deps.logger?.error('ds-balance: balance handler failed', { requestId, error: describeError(error) });
104
100
  return json(failureView(requestId, error));
105
101
  }
106
102
  }
@@ -115,7 +111,7 @@ export async function handleRefresh(request, deps) {
115
111
  return json({ requestId, schemaVersion: SCHEMA_VERSION, ...result, error: null });
116
112
  }
117
113
  catch (error) {
118
- deps.logger?.error('ds-balance: refresh handler failed', { requestId, error: describe(error) });
114
+ deps.logger?.error('ds-balance: refresh handler failed', { requestId, error: describeError(error) });
119
115
  return json({
120
116
  requestId,
121
117
  schemaVersion: SCHEMA_VERSION,
@@ -149,7 +145,7 @@ async function credentialInfo(deps, ref) {
149
145
  };
150
146
  }
151
147
  catch (error) {
152
- deps.logger?.debug('ds-balance: credential describe failed', { ref, error: describe(error) });
148
+ deps.logger?.debug('ds-balance: credential describe failed', { ref, error: describeError(error) });
153
149
  return null;
154
150
  }
155
151
  }
@@ -173,7 +169,7 @@ export async function handleConfigGet(_request, deps) {
173
169
  return json(await configBody(requestId, deps));
174
170
  }
175
171
  catch (error) {
176
- deps.logger?.error('ds-balance: config read handler failed', { requestId, error: describe(error) });
172
+ deps.logger?.error('ds-balance: config read handler failed', { requestId, error: describeError(error) });
177
173
  return json({ requestId, schemaVersion: SCHEMA_VERSION, error: toWireError(classify(error)) });
178
174
  }
179
175
  }
@@ -185,7 +181,7 @@ export async function handleConfigUpdate(request, deps) {
185
181
  const body = await readJsonObject(request);
186
182
  if (body === null)
187
183
  return json({ requestId, error: { code: 'VALIDATION', message: 'body must be a JSON object', retryable: false } }, 422);
188
- const unknown = Object.keys(body).filter((key) => !CONFIG_FIELDS.includes(key));
184
+ const unknown = Object.keys(body).filter((key) => !CONFIG_FIELDS.some((field) => field === key));
189
185
  if (unknown.length > 0) {
190
186
  return json({ requestId, error: { code: 'VALIDATION', message: `unknown config field: ${unknown.join(', ')}`, retryable: false } }, 422);
191
187
  }
@@ -201,7 +197,7 @@ export async function handleConfigUpdate(request, deps) {
201
197
  }
202
198
  }
203
199
  catch (error) {
204
- deps.logger?.warn('ds-balance: config write rejected', { requestId, error: describe(error) });
200
+ deps.logger?.warn('ds-balance: config write rejected', { requestId, error: describeError(error) });
205
201
  return json({ requestId, error: toWireError(classify(error)) }, 422);
206
202
  }
207
203
  // 写完立刻回读,让调用方看到落盘后的真实值(掩码同上)。
@@ -209,7 +205,7 @@ export async function handleConfigUpdate(request, deps) {
209
205
  return json(await configBody(requestId, deps));
210
206
  }
211
207
  catch (error) {
212
- deps.logger?.error('ds-balance: config readback failed', { requestId, error: describe(error) });
208
+ deps.logger?.error('ds-balance: config readback failed', { requestId, error: describeError(error) });
213
209
  return json({ requestId, schemaVersion: SCHEMA_VERSION, error: toWireError(classify(error)) });
214
210
  }
215
211
  }
@@ -235,7 +231,7 @@ export async function handleTestConnection(request, deps) {
235
231
  return json({ requestId, schemaVersion: SCHEMA_VERSION, ...result });
236
232
  }
237
233
  catch (error) {
238
- deps.logger?.warn('ds-balance: test-connection handler failed', { requestId, error: describe(error) });
234
+ deps.logger?.warn('ds-balance: test-connection handler failed', { requestId, error: describeError(error) });
239
235
  const info = classify(error);
240
236
  return json({ requestId, schemaVersion: SCHEMA_VERSION, ok: false, latencyMs: 0, code: info.code, message: info.message });
241
237
  }
@@ -250,7 +246,7 @@ export async function handleHealthz(_request, deps) {
250
246
  store = { ok: health.ok, detail: health.detail ?? null };
251
247
  }
252
248
  catch (error) {
253
- store = { ok: false, detail: describe(error) };
249
+ store = { ok: false, detail: describeError(error) };
254
250
  }
255
251
  return json({
256
252
  requestId,
@@ -6,7 +6,7 @@
6
6
  * @module dsh-ds-balance/http/wire
7
7
  */
8
8
  import type { BalanceView, CacheState, Currency, Severity } from '../domain/balance.js';
9
- import type { ErrorInfo } from '../domain/errors.js';
9
+ import type { ErrorCode, ErrorInfo } from '../domain/errors.js';
10
10
  /** 一个币种的余额,金额是八位小数字符串。 */
11
11
  export interface WireBalance {
12
12
  currency: Currency;
@@ -23,9 +23,10 @@ export interface WireThresholdPair {
23
23
  * 线上的错误结构。
24
24
  *
25
25
  * `details` **不出去**:它可能带上游原文,属于内部诊断面。
26
+ * `code` 取宿主闭集 {@link ErrorCode} —— 线上只出现这几种码。
26
27
  */
27
28
  export interface WireError {
28
- code: string;
29
+ code: ErrorCode;
29
30
  message: string;
30
31
  retryable: boolean;
31
32
  }
package/lib/index.js CHANGED
@@ -16,6 +16,7 @@ import { HttpDeepSeekClient } from './adapters/http-deepseek-client.js';
16
16
  import { SALT_BYTES, SALT_ENCODING, loadOrCreateSalt } from './adapters/salt-file.js';
17
17
  import { createConsoleLogger } from './adapters/console-logger.js';
18
18
  import { MemoryMetrics } from './adapters/memory-metrics.js';
19
+ import { describeError } from './domain/errors.js';
19
20
  import { registerHttpRoutes } from './http/routes.js';
20
21
  import { BalanceService } from './services/balance-service.js';
21
22
  import { ConfigService } from './services/config-service.js';
@@ -51,10 +52,6 @@ const systemClock = {
51
52
  now: () => Date.now(),
52
53
  timezone: () => 'Asia/Shanghai',
53
54
  };
54
- /** 把未知异常压成一行。 */
55
- function describe(error) {
56
- return error instanceof Error ? error.message : String(error);
57
- }
58
55
  /**
59
56
  * 读服务端盐;读不出来就退回一份进程内临时盐。
60
57
  *
@@ -68,7 +65,7 @@ async function resolveSalt(logger) {
68
65
  return await loadOrCreateSalt({ path: dshHomePath(SALT_FILE_NAME) });
69
66
  }
70
67
  catch (error) {
71
- logger.error('ds-balance: salt file unavailable, using an ephemeral salt', { error: describe(error) });
68
+ logger.error('ds-balance: salt file unavailable, using an ephemeral salt', { error: describeError(error) });
72
69
  return randomBytes(SALT_BYTES).toString(SALT_ENCODING);
73
70
  }
74
71
  }
@@ -217,7 +214,7 @@ export async function apply(ctx, refs) {
217
214
  // 排程只看这几项。事件给的是「真的变了的路径」,比原来的全量 diff 更准,
218
215
  // 所以改阈值、改展示币种都不会顺带打一次官方接口。
219
216
  const stopWatching = ctx.on('loader/volatile-update', (paths) => {
220
- if (paths.some((path) => path.length > 0 && SCHEDULE_FIELDS.includes(path[0]))) {
217
+ if (paths.some((path) => path.length > 0 && SCHEDULE_FIELDS.some((field) => field === path[0]))) {
221
218
  scheduler.reset();
222
219
  }
223
220
  });
@@ -5,7 +5,7 @@
5
5
  * **对外永不抛错**:一切失败都变成 `view.state` 与 `view.error`。
6
6
  * @module dsh-ds-balance/services/balance-service
7
7
  */
8
- import { classify, parseRetryAfter } from '../domain/errors.js';
8
+ import { classify, describeError, parseRetryAfter } from '../domain/errors.js';
9
9
  import { normalize } from '../domain/normalize.js';
10
10
  import { pickBalance } from '../domain/select.js';
11
11
  import { severityOf, thresholdsFor } from '../domain/severity.js';
@@ -67,7 +67,7 @@ export class BalanceService {
67
67
  this.publishGauge();
68
68
  }
69
69
  catch (error) {
70
- this.options.logger?.debug('ds-balance: no snapshot restored', { error: describe(error) });
70
+ this.options.logger?.debug('ds-balance: no snapshot restored', { error: describeError(error) });
71
71
  }
72
72
  }
73
73
  /**
@@ -165,7 +165,7 @@ export class BalanceService {
165
165
  if (this.persistWarned)
166
166
  return;
167
167
  this.persistWarned = true;
168
- this.options.logger?.warn('ds-balance: snapshot not persisted, continuing in memory', { error: describe(error) });
168
+ this.options.logger?.warn('ds-balance: snapshot not persisted, continuing in memory', { error: describeError(error) });
169
169
  }
170
170
  }
171
171
  /** 记录一次失败,并把状态推到 `stale` 或 `error`。 */
@@ -209,13 +209,11 @@ export class BalanceService {
209
209
  }
210
210
  /** 从上游错误里抽 `Retry-After`;抽不到返回 `null`。 */
211
211
  function retryAfterOf(error, now) {
212
+ if (typeof error !== 'object' || error === null || !('headers' in error))
213
+ return null;
212
214
  const headers = error.headers;
213
- if (headers === undefined)
215
+ if (!(headers instanceof Headers))
214
216
  return null;
215
217
  return parseRetryAfter(headers, now) ?? null;
216
218
  }
217
- /** 把未知异常压成一行。 */
218
- function describe(error) {
219
- return error instanceof Error ? error.message : String(error);
220
- }
221
219
  //# sourceMappingURL=balance-service.js.map
@@ -7,13 +7,9 @@
7
7
  * **无 credentials seam 时不抛错**:吞掉后继续往下走,让 env 层有机会接手。
8
8
  * @module dsh-ds-balance/services/key-resolver
9
9
  */
10
- import { NoKeyError } from '../domain/errors.js';
10
+ import { NoKeyError, describeError } from '../domain/errors.js';
11
11
  /** 契约要求的引用名形状。 */
12
12
  export const CREDENTIAL_REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
13
- /** 把未知异常压成一行。 */
14
- function describe(error) {
15
- return error instanceof Error ? error.message : String(error);
16
- }
17
13
  /** 解析当前可用的明文密钥。 */
18
14
  export class KeyResolver {
19
15
  options;
@@ -48,7 +44,7 @@ export class KeyResolver {
48
44
  // 没有 seam、引用名不存在、远程拒绝 —— 都只是「这一档没取到」,继续往下走。
49
45
  this.options.logger?.debug('ds-balance: credentials.resolve did not yield a key, falling back to env', {
50
46
  ref,
51
- error: describe(error),
47
+ error: describeError(error),
52
48
  });
53
49
  }
54
50
  }
@@ -5,6 +5,7 @@
5
5
  * `Retry-After` 都能生效,也不会堆积重叠的请求。
6
6
  * @module dsh-ds-balance/services/scheduler
7
7
  */
8
+ import { describeError } from '../domain/errors.js';
8
9
  /** 首拉延迟。 */
9
10
  export const INITIAL_DELAY_MS = 1000;
10
11
  /** 缺密钥时的快速重试间隔。 */
@@ -131,7 +132,7 @@ export class Scheduler {
131
132
  catch (error) {
132
133
  // 服务承诺永不抛错;这里是防御,不让调度链断掉。
133
134
  this.options.logger?.error('ds-balance: scheduler tick threw', {
134
- error: error instanceof Error ? error.message : String(error),
135
+ error: describeError(error),
135
136
  });
136
137
  }
137
138
  if (!this.running)
@@ -2,6 +2,8 @@
2
2
  * 后端契约类型。本阶段不实现后端,这些形状只用来约束 mock 数据。
3
3
  * @module dsh-ds-balance/client/api-types
4
4
  */
5
+ import type { ErrorCode } from '../domain/errors.ts';
6
+ import type { UNREACHABLE_CODE } from './data.ts';
5
7
  /** 余额数据的状态维度。 */
6
8
  export type BalanceState = 'empty' | 'ok' | 'stale' | 'error';
7
9
  /** 颜色严重度维度。前端只做机械映射,不比较金额。 */
@@ -24,9 +26,16 @@ export interface TodayUsage {
24
26
  needsReview: boolean;
25
27
  range: [string, string] | null;
26
28
  }
29
+ /**
30
+ * 客户端可见的错误码:宿主闭集 ∪ 客户端本地合成的「端点不可达」。
31
+ *
32
+ * `UNREACHABLE_CODE` 只在客户端产生(端点拿不到、或回的不是 JSON),宿主侧不认识它,
33
+ * 所以是**显式扩展**而不是往宿主闭集里塞。
34
+ */
35
+ export type BalanceErrorCode = ErrorCode | typeof UNREACHABLE_CODE;
27
36
  /** 后端错误。 */
28
37
  export interface BalanceError {
29
- code: string;
38
+ code: BalanceErrorCode;
30
39
  message: string;
31
40
  }
32
41
  /** `GET /api/v1/balance` 的响应。 */
@@ -9,7 +9,7 @@
9
9
  * 传过去、把失败翻成可展示的错误态。
10
10
  * @module dsh-ds-balance/client/data
11
11
  */
12
- import type { BalanceResponse } from './api-types.ts';
12
+ import type { BalanceResponse, BalanceState } from './api-types.ts';
13
13
  /** 余额端点。路径与宿主半边逐字一致,不带尾随斜杠。 */
14
14
  export declare const BALANCE_PATH = "/api/v1/balance";
15
15
  /** 手动刷新端点。 */
@@ -43,7 +43,7 @@ export interface RefreshResult {
43
43
  triggered: boolean;
44
44
  joined: boolean;
45
45
  cooldownMs: number;
46
- state: string;
46
+ state: BalanceState;
47
47
  }
48
48
  /** 端点拿不到、或回的不是 JSON 时用的错误码。 */
49
49
  export declare const UNREACHABLE_CODE = "PLUGIN_UNREACHABLE";
@@ -55,7 +55,7 @@ export declare function selectField(field: string): ValueFieldSpec;
55
55
  /** 自动币种取值。 */
56
56
  export declare const AUTO_CURRENCY = "auto";
57
57
  /** 已知币种。阈值分组就是按这两个币种定义的,故它们是币种列表的最小闭集。 */
58
- export declare const KNOWN_CURRENCIES: readonly string[];
58
+ export declare const KNOWN_CURRENCIES: readonly ["CNY", "USD"];
59
59
  /** 一对阈值:同一币种内的预警与告急。 */
60
60
  export interface ThresholdPair {
61
61
  readonly currency: string;
@@ -72,7 +72,19 @@ export interface ThresholdPair {
72
72
  readonly defaultCritical: number;
73
73
  }
74
74
  /** 阈值成对的清单;字段名沿用宿主 schema 的「币种代码小写 + Warn / Critical」。 */
75
- export declare const THRESHOLD_PAIRS: readonly ThresholdPair[];
75
+ export declare const THRESHOLD_PAIRS: readonly [{
76
+ readonly currency: "CNY";
77
+ readonly warn: "cnyWarn";
78
+ readonly critical: "cnyCritical";
79
+ readonly defaultWarn: 10;
80
+ readonly defaultCritical: 5;
81
+ }, {
82
+ readonly currency: "USD";
83
+ readonly warn: "usdWarn";
84
+ readonly critical: "usdCritical";
85
+ readonly defaultWarn: 2;
86
+ readonly defaultCritical: 1;
87
+ }];
76
88
  /**
77
89
  * 一对阈值是否满足「告急 **严格低于** 预警」。
78
90
  *
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 领域错误与错误分类。
3
+ *
4
+ * 宿主半边的对外错误一律映射成 {@link ErrorInfo}:合约规定余额错误走
5
+ * `200 + state: error`,HTTP 层不得把异常抛出去让宿主包成 500。
6
+ * @module dsh-ds-balance/domain/errors
7
+ */
8
+ /** 对外可见的错误码闭集。 */
9
+ export type ErrorCode = 'NO_KEY' | 'NO_NETWORK' | 'UPSTREAM_TIMEOUT' | 'UPSTREAM_401' | 'UPSTREAM_402' | 'UPSTREAM_422' | 'UPSTREAM_429' | 'UPSTREAM_4XX' | 'UPSTREAM_5XX' | 'UPSTREAM_503' | 'PARSE_ERROR' | 'SHAPE_ERROR' | 'COOLDOWN' | 'CONFLICT' | 'VALIDATION' | 'UNAUTHORIZED' | 'NOT_FOUND' | 'STORAGE_ERROR';
10
+ /** 跨线传给前端的错误形状。 */
11
+ export interface ErrorInfo {
12
+ code: ErrorCode;
13
+ message: string;
14
+ retryable: boolean;
15
+ details?: Record<string, unknown>;
16
+ }
17
+ /** 构造 {@link AppError} 的可选参数。 */
18
+ export interface AppErrorOptions {
19
+ retryable?: boolean;
20
+ details?: Record<string, unknown>;
21
+ cause?: unknown;
22
+ }
23
+ /** 所有本插件错误的基类:带错误码与可重试标记。 */
24
+ export declare class AppError extends Error {
25
+ readonly code: ErrorCode;
26
+ readonly retryable: boolean;
27
+ readonly details: Record<string, unknown> | undefined;
28
+ constructor(code: ErrorCode, message: string, options?: AppErrorOptions);
29
+ }
30
+ /** 没有可用的 API Key。解析链全空时抛这个。 */
31
+ export declare class NoKeyError extends AppError {
32
+ constructor(message?: string);
33
+ }
34
+ /** 请求超时。 */
35
+ export declare class TimeoutError extends AppError {
36
+ constructor(message?: string, options?: AppErrorOptions);
37
+ }
38
+ /** 网络不可达(DNS / 连接失败)。 */
39
+ export declare class NetworkError extends AppError {
40
+ constructor(message?: string, options?: AppErrorOptions);
41
+ }
42
+ /** 把 HTTP 状态码映射到错误码。 */
43
+ export declare function upstreamCodeOf(status: number): ErrorCode;
44
+ /** 上游返回了非 2xx。 */
45
+ export declare class UpstreamError extends AppError {
46
+ readonly status: number;
47
+ readonly headers: Headers | undefined;
48
+ constructor(status: number, message: string, options?: {
49
+ headers?: Headers;
50
+ cause?: unknown;
51
+ });
52
+ }
53
+ /** 金额或响应体解析失败。绝不静默归 0。 */
54
+ export declare class ParseError extends AppError {
55
+ constructor(message: string, options?: AppErrorOptions);
56
+ }
57
+ /** 响应结构与契约不符。 */
58
+ export declare class ShapeError extends AppError {
59
+ constructor(message?: string, options?: AppErrorOptions);
60
+ }
61
+ /** 入参或配置没通过校验。**不可重试** —— 同一份输入重放还是失败。 */
62
+ export declare class ValidationError extends AppError {
63
+ constructor(message?: string, options?: AppErrorOptions);
64
+ }
65
+ /** 存储层失败。可重试。 */
66
+ export declare class StorageError extends AppError {
67
+ constructor(message: string, options?: AppErrorOptions);
68
+ }
69
+ /**
70
+ * 解析 `Retry-After`。支持秒数与 HTTP-date 两种写法。
71
+ * @param headers - 上游响应头;缺失时返回 `undefined`。
72
+ * @param now - 当前时刻,HTTP-date 用它算差值;便于测试注入。
73
+ * @returns 毫秒数,或 `undefined` 表示没给出可用的提示。
74
+ */
75
+ export declare function parseRetryAfter(headers: Headers | undefined, now?: number): number | undefined;
76
+ /**
77
+ * 把任意异常映射成对外错误。
78
+ *
79
+ * 顺序重要:先认自己的类型,再认带 `status` 的外部错误,最后兜底。
80
+ * @param error - 任意抛出的值。
81
+ * @returns 契约里的错误形状。
82
+ */
83
+ export declare function classify(error: unknown): ErrorInfo;
84
+ /**
85
+ * 把任意异常压成一行文本,供日志与错误消息使用。
86
+ *
87
+ * **绝不带凭据**:上游约定异常文本里不含密钥,所以它可以安全写进日志。
88
+ * 只取 `Error.message`;非 `Error` 的抛出值退回 `String()`。
89
+ * @param error - 任意抛出的值。
90
+ * @returns 单行错误文本。
91
+ */
92
+ export declare function describeError(error: unknown): string;
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.0.0-alpha.2";
11
+ export declare const PLUGIN_VERSION = "2.0.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.0.0-alpha.2';
11
+ export const PLUGIN_VERSION = '2.0.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.0.0-alpha.2",
3
+ "version": "2.0.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": [