dsh-ds-balance 2.1.1 → 2.1.3
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 +14 -1
- package/README_en.md +13 -1
- package/lib/adapters/console-logger.js +12 -4
- package/lib/adapters/domain-core-store.js +9 -3
- package/lib/adapters/http-deepseek-client.js +14 -4
- package/lib/adapters/salt-file.js +6 -2
- package/lib/client.js +372 -169
- package/lib/config.js +7 -1
- package/lib/domain/errors.js +10 -5
- package/lib/domain/select.js +5 -5
- package/lib/http/handlers.js +55 -13
- package/lib/index.js +34 -9
- package/lib/services/balance-service.d.ts +9 -0
- package/lib/services/balance-service.js +32 -9
- package/lib/services/scheduler.d.ts +23 -0
- package/lib/services/scheduler.js +34 -3
- package/lib/types/client/model.d.ts +25 -0
- package/lib/types/client/settings/fields.d.ts +3 -3
- package/lib/types/client/sidebar/PercentRing.d.ts +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="icon.svg" alt="dsh-ds-balance" width="80" height="80">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
<p align="center">
|
|
2
6
|
<h1 align="center">dsh-ds-balance</h1>
|
|
3
7
|
</p>
|
|
@@ -132,7 +136,8 @@ dsh plugin --profile web add "$PWD"
|
|
|
132
136
|
但**手改配置文件写成的非法组合不会再报错** —— 宿主那一侧已经不再强制它,插件会在读到的那一刻把这一对**回落成默认值**并在日志里记一次。
|
|
133
137
|
|
|
134
138
|
> **升级提示**:设置项存在插件那一行的配置里,键名跟着宿主升级变过一次,**旧值不会自动迁移** —— 升级后请照上表重填一次。
|
|
135
|
-
-
|
|
139
|
+
- **刷新**:服务端刷新周期、界面轮询周期与手动刷新冷却。冷却从**刷完那一刻**起算,
|
|
140
|
+
浮层上的秒数就是真实剩余;后端说这次没触发,界面按它给的剩余时间收尾,不假装刷过。
|
|
136
141
|
|
|
137
142
|
保存即生效,不必重启 DSH。
|
|
138
143
|
|
|
@@ -148,6 +153,14 @@ dsh plugin --profile web add "$PWD"
|
|
|
148
153
|
密钥只经 DSH 的凭据通道解析,卡片里的 `API Key` 一栏默认继承官方模型页配好的那一份,且只读不可改。
|
|
149
154
|
由启动环境(环境变量)提供时,字段只读,徽标写明来源。
|
|
150
155
|
|
|
156
|
+
**换了 key 多久生效,跟着 DSH 的层走 —— 本插件不额外追:**
|
|
157
|
+
|
|
158
|
+
- **启动环境(环境变量)给的 key:改了要重启 DSH。** 进程的环境在它启动那一刻就定死了,之后再改外面那个变量,跑着的插件看不到。
|
|
159
|
+
- **在官方模型页 / 凭据库换的 key:不必重启**,最迟下一个服务端刷新周期生效(连续失败退避期间会更久);想立刻看到就点浮层里的手动刷新(它带冷却)。
|
|
160
|
+
- 生效之前,界面显示的还是**旧账本**的数字 —— 账本标识按 key 派生,新旧不会混,但内存里那份要等下一次抓取成功才被换掉。
|
|
161
|
+
|
|
162
|
+
为什么这么定 → [关键决策](docs/ARCHITECTURE.md#关键决策)。
|
|
163
|
+
|
|
151
164
|
## 安全与边界
|
|
152
165
|
|
|
153
166
|
- **API Key 永不返回界面**:配置接口只回一个固定长度的掩码串,连末几位也不给。
|
package/README_en.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="icon.svg" alt="dsh-ds-balance" width="80" height="80">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
<p align="center">
|
|
2
6
|
<h1 align="center">dsh-ds-balance</h1>
|
|
3
7
|
</p>
|
|
@@ -132,7 +136,7 @@ Open **Plugins → Installed** and step into the **DeepSeek Balance** (shown as
|
|
|
132
136
|
but a hand-edited config file with an illegal pair **no longer errors** — the Host side stopped enforcing it, so the plugin **falls that pair back to its defaults** the moment it reads it, and logs one line.
|
|
133
137
|
|
|
134
138
|
> **Upgrade note**: the settings live in this plugin's own entry config and the key it is filed under changed once; **old values are not migrated** — please fill the table above in once after upgrading.
|
|
135
|
-
- **Refresh**: the server refresh interval
|
|
139
|
+
- **Refresh**: the server refresh interval, the UI poll interval and the manual refresh cooldown. The cooldown counts from **the moment the refresh finishes**, so the seconds shown in the popover are the real ones left; if the backend says the refresh did not fire, the UI takes its remaining time instead of pretending otherwise.
|
|
136
140
|
|
|
137
141
|
Saving applies immediately; there is no need to restart DSH.
|
|
138
142
|
|
|
@@ -148,6 +152,14 @@ Saving applies immediately; there is no need to restart DSH.
|
|
|
148
152
|
The key is resolved only through DSH's credential channel; the card's "API key" inherits the one already configured on the official model settings page.
|
|
149
153
|
When it comes from the launch environment (an environment variable) the field is read-only and the badge says where it came from.
|
|
150
154
|
|
|
155
|
+
**How long a key change takes to land follows DSH's own layers — this plugin deliberately adds nothing on top:**
|
|
156
|
+
|
|
157
|
+
- **A key from the launch environment (an environment variable): restart DSH to change it.** A process's environment is fixed the moment it starts; a later change to that variable outside is invisible to the running plugin.
|
|
158
|
+
- **A key changed on the official model settings page / in the credential store: no restart needed** — it lands by the next server refresh cycle at the latest (later while failure backoff is active); to see it at once, use the manual refresh in the popover (it is rate-limited).
|
|
159
|
+
- Until then the UI still shows the **previous ledger**: the ledger identifier is derived from the key, so the two never mix, but the in-memory copy is only replaced once the next fetch succeeds.
|
|
160
|
+
|
|
161
|
+
Why it is this way → [Key decisions](docs/ARCHITECTURE.md#关键决策).
|
|
162
|
+
|
|
151
163
|
## Security and boundaries
|
|
152
164
|
|
|
153
165
|
- **The API key is never returned to the UI**: the config endpoint returns only a fixed-length mask, not even the last few characters.
|
|
@@ -26,10 +26,18 @@ function render(fields) {
|
|
|
26
26
|
*/
|
|
27
27
|
export function createConsoleLogger(sink = console) {
|
|
28
28
|
return {
|
|
29
|
-
debug: (message, fields) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
debug: (message, fields) => {
|
|
30
|
+
sink.debug(`${LOG_PREFIX} ${message}${render(fields)}`);
|
|
31
|
+
},
|
|
32
|
+
info: (message, fields) => {
|
|
33
|
+
sink.info(`${LOG_PREFIX} ${message}${render(fields)}`);
|
|
34
|
+
},
|
|
35
|
+
warn: (message, fields) => {
|
|
36
|
+
sink.warn(`${LOG_PREFIX} ${message}${render(fields)}`);
|
|
37
|
+
},
|
|
38
|
+
error: (message, fields) => {
|
|
39
|
+
sink.error(`${LOG_PREFIX} ${message}${render(fields)}`);
|
|
40
|
+
},
|
|
33
41
|
};
|
|
34
42
|
}
|
|
35
43
|
//# sourceMappingURL=console-logger.js.map
|
|
@@ -111,7 +111,9 @@ export class DomainCoreStore {
|
|
|
111
111
|
this.ready = null;
|
|
112
112
|
if (!this.openFailureLogged) {
|
|
113
113
|
this.openFailureLogged = true;
|
|
114
|
-
this.options.logger?.error('ds-balance: storage domain unavailable, running degraded', {
|
|
114
|
+
this.options.logger?.error('ds-balance: storage domain unavailable, running degraded', {
|
|
115
|
+
error: describeError(error),
|
|
116
|
+
});
|
|
115
117
|
}
|
|
116
118
|
});
|
|
117
119
|
this.ready = attempt;
|
|
@@ -128,7 +130,9 @@ export class DomainCoreStore {
|
|
|
128
130
|
async requireTable() {
|
|
129
131
|
await this.ensureOpen();
|
|
130
132
|
if (this.openError !== undefined) {
|
|
131
|
-
throw new StorageError(`storage domain unavailable: ${describeError(this.openError)}`, {
|
|
133
|
+
throw new StorageError(`storage domain unavailable: ${describeError(this.openError)}`, {
|
|
134
|
+
cause: this.openError,
|
|
135
|
+
});
|
|
132
136
|
}
|
|
133
137
|
if (this.table === null)
|
|
134
138
|
throw new StorageError('storage domain is not open');
|
|
@@ -160,7 +164,9 @@ export class DomainCoreStore {
|
|
|
160
164
|
if (this.closed)
|
|
161
165
|
return { ok: false, detail: 'closed' };
|
|
162
166
|
await this.ensureOpen();
|
|
163
|
-
return this.openError === undefined
|
|
167
|
+
return this.openError === undefined
|
|
168
|
+
? { ok: true }
|
|
169
|
+
: { ok: false, detail: describeError(this.openError) };
|
|
164
170
|
}
|
|
165
171
|
/** 幂等关闭。**必须挂在 `ctx.effect` 的 disposer 上。** */
|
|
166
172
|
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, describeError } 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) {
|
|
@@ -39,13 +39,21 @@ export class HttpDeepSeekClient {
|
|
|
39
39
|
const raw = await this.fetchBalance(options);
|
|
40
40
|
const latencyMs = this.now() - startedAt;
|
|
41
41
|
const balances = Array.isArray(raw?.balance_infos)
|
|
42
|
-
? raw.balance_infos.map((item) => ({
|
|
42
|
+
? raw.balance_infos.map((item) => ({
|
|
43
|
+
currency: String(item.currency),
|
|
44
|
+
total: String(item.total_balance),
|
|
45
|
+
}))
|
|
43
46
|
: [];
|
|
44
47
|
return { ok: true, latencyMs, isAvailable: raw?.is_available === true, balances };
|
|
45
48
|
}
|
|
46
49
|
catch (error) {
|
|
47
50
|
const info = classify(error);
|
|
48
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
latencyMs: this.now() - startedAt,
|
|
54
|
+
code: info.code,
|
|
55
|
+
message: info.message,
|
|
56
|
+
};
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
/**
|
|
@@ -61,7 +69,9 @@ export class HttpDeepSeekClient {
|
|
|
61
69
|
timedOut = true;
|
|
62
70
|
controller.abort();
|
|
63
71
|
}, options.timeoutMs);
|
|
64
|
-
const forwardAbort = () => {
|
|
72
|
+
const forwardAbort = () => {
|
|
73
|
+
controller.abort();
|
|
74
|
+
};
|
|
65
75
|
options.signal?.addEventListener('abort', forwardAbort, { once: true });
|
|
66
76
|
let response;
|
|
67
77
|
try {
|
|
@@ -20,8 +20,12 @@ export const SALT_ENCODING = 'hex';
|
|
|
20
20
|
*/
|
|
21
21
|
export async function loadOrCreateSalt(options) {
|
|
22
22
|
const read = options.read ?? (async (path) => readFile(path, 'utf8'));
|
|
23
|
-
const write = options.write ??
|
|
24
|
-
|
|
23
|
+
const write = options.write ??
|
|
24
|
+
(async (path, data, mode) => writeFile(path, data, { mode }));
|
|
25
|
+
const makeDir = options.makeDir ??
|
|
26
|
+
(async (path) => {
|
|
27
|
+
await mkdir(path, { recursive: true });
|
|
28
|
+
});
|
|
25
29
|
const random = options.random ?? ((bytes) => randomBytes(bytes).toString(SALT_ENCODING));
|
|
26
30
|
try {
|
|
27
31
|
const existing = (await read(options.path)).trim();
|