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.
- package/lib/adapters/domain-core-store.js +4 -8
- package/lib/adapters/http-deepseek-client.js +2 -2
- package/lib/client.js +21 -7
- package/lib/config.d.ts +13 -1
- package/lib/domain/errors.d.ts +9 -0
- package/lib/domain/errors.js +12 -1
- package/lib/http/handlers.js +10 -14
- package/lib/http/wire.d.ts +3 -2
- package/lib/index.js +3 -6
- package/lib/services/balance-service.js +6 -8
- package/lib/services/key-resolver.js +2 -6
- package/lib/services/scheduler.js +2 -1
- package/lib/types/client/api-types.d.ts +10 -1
- package/lib/types/client/data.d.ts +2 -2
- package/lib/types/client/settings/use-config-form.d.ts +14 -2
- package/lib/types/domain/errors.d.ts +92 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
|
@@ -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:
|
|
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: ${
|
|
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:
|
|
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(
|
|
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
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
*
|
package/lib/domain/errors.d.ts
CHANGED
|
@@ -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
|
package/lib/domain/errors.js
CHANGED
|
@@ -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
|
package/lib/http/handlers.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
249
|
+
store = { ok: false, detail: describeError(error) };
|
|
254
250
|
}
|
|
255
251
|
return json({
|
|
256
252
|
requestId,
|
package/lib/http/wire.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
11
|
+
export declare const PLUGIN_VERSION = "2.0.0";
|
|
12
12
|
//# sourceMappingURL=version.d.ts.map
|
package/lib/version.js
CHANGED
package/package.json
CHANGED