mslxdff 0.1.65 → 0.1.66
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/bin/mslxdff.js +3 -3159
- package/package.json +1 -1
- package/src/cli/bootstrap.js +408 -0
- package/src/cli/commands/daemon.js +151 -0
- package/src/cli/commands/group.js +272 -0
- package/src/cli/commands/model.js +405 -0
- package/src/cli/commands/provider/add.js +106 -0
- package/src/cli/commands/provider/allowlist.js +99 -0
- package/src/cli/commands/provider/config.js +82 -0
- package/src/cli/commands/provider/index.js +193 -0
- package/src/cli/commands/provider/keys.js +135 -0
- package/src/cli/commands/provider/models.js +99 -0
- package/src/cli/commands/provider.js +1 -0
- package/src/cli/commands/sync.js +143 -0
- package/src/cli/commands/system.js +236 -0
- package/src/cli/commands/workbuddy.js +91 -0
- package/src/cli/format.js +119 -0
- package/src/cli/group-helpers.js +69 -0
- package/src/cli/help.js +66 -0
- package/src/cli/index.js +59 -0
- package/src/cli/interactive.js +84 -0
- package/src/cli/policy.js +119 -0
- package/src/cli/status.js +332 -0
- package/src/cli/util.js +24 -0
- package/src/providers/base.js +159 -0
- package/src/providers/generic.js +26 -166
- package/src/providers/openrouter.js +32 -162
- package/src/providers/workbuddy.js +7 -32
- package/src/routes/chat/gateway.js +301 -0
- package/src/routes/chat/index.js +1 -384
- package/src/state/facade.js +57 -0
- package/src/state/memory.js +180 -0
- package/src/state/persist.js +42 -0
- package/src/state/schemas/allowlist.js +87 -0
- package/src/state/schemas/group.js +31 -0
- package/src/state/schemas/model.js +53 -0
- package/src/state/schemas/peer.js +31 -0
- package/src/state/schemas/port.js +10 -0
- package/src/state/schemas/provider.js +254 -0
- package/src/state/schemas/token.js +43 -0
- package/src/state/store.js +202 -0
- package/src/state.js +1 -711
package/src/state.js
CHANGED
|
@@ -1,711 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync, statSync, renameSync, unlinkSync, existsSync } from "node:fs";
|
|
3
|
-
import { writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
|
|
7
|
-
export const DEFAULT_PORT = 8989;
|
|
8
|
-
|
|
9
|
-
function isTestEnv() {
|
|
10
|
-
if (process.env.NODE_ENV === "test") return true;
|
|
11
|
-
if (process.env.MSLXDFF_STATE_FILE && String(process.env.MSLXDFF_STATE_FILE).includes("mslxdff-test")) return true;
|
|
12
|
-
if (process.argv.some((a) => String(a).includes("--test") || String(a).endsWith(".test.js"))) return true;
|
|
13
|
-
if (Array.isArray(process.execArgv) && process.execArgv.some((a) => String(a).includes("--test"))) return true;
|
|
14
|
-
// Node test runner sets this
|
|
15
|
-
if (process.env.NODE_TEST_CONTEXT) return true;
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function defaultStateFile() {
|
|
20
|
-
if (process.env.MSLXDFF_STATE_FILE) return process.env.MSLXDFF_STATE_FILE;
|
|
21
|
-
if (isTestEnv()) {
|
|
22
|
-
return join(os.tmpdir(), "mslxdff-test-state.json");
|
|
23
|
-
}
|
|
24
|
-
return join(os.homedir(), ".config", "mslxdff", "state.json");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function tokenFile(file) {
|
|
28
|
-
if (process.env.MSLXDFF_TOKEN_FILE) return process.env.MSLXDFF_TOKEN_FILE;
|
|
29
|
-
const sf = file || defaultStateFile();
|
|
30
|
-
const realDefault = join(os.homedir(), ".config", "mslxdff", "state.json");
|
|
31
|
-
if (sf !== realDefault) return join(dirname(sf), "token");
|
|
32
|
-
if (isTestEnv()) {
|
|
33
|
-
return join(os.tmpdir(), "mslxdff-test-token");
|
|
34
|
-
}
|
|
35
|
-
return join(os.homedir(), ".config", "mslxdff", "token");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function syncTokenFile(token, file) {
|
|
39
|
-
try {
|
|
40
|
-
const tf = tokenFile(file);
|
|
41
|
-
mkdirSync(dirname(tf), { recursive: true });
|
|
42
|
-
writeFileSync(tf, String(token || "").trim() + "\n", "utf8");
|
|
43
|
-
} catch {}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function generateToken() {
|
|
47
|
-
return randomBytes(32).toString("hex");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ---- 缓存层:读走内存,写分“热/冷”两档,热数据 500ms 批量刷盘 ----
|
|
51
|
-
const stateCache = new Map(); // file -> { data, dirty, timer, mtimeMs }
|
|
52
|
-
const FLUSH_MS = (() => {
|
|
53
|
-
const n = Number(process.env.MSLXDFF_STATE_FLUSH_MS);
|
|
54
|
-
return Number.isInteger(n) && n >= 0 ? n : 500;
|
|
55
|
-
})();
|
|
56
|
-
|
|
57
|
-
function getEntry(file) {
|
|
58
|
-
let e = stateCache.get(file);
|
|
59
|
-
if (!e) {
|
|
60
|
-
e = { data: null, dirty: false, timer: null, mtimeMs: 0 };
|
|
61
|
-
stateCache.set(file, e);
|
|
62
|
-
}
|
|
63
|
-
return e;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function loadFromDisk(file) {
|
|
67
|
-
try {
|
|
68
|
-
return JSON.parse(readFileSync(file, "utf8"));
|
|
69
|
-
} catch {
|
|
70
|
-
return {};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function readState(file) {
|
|
75
|
-
const e = getEntry(file);
|
|
76
|
-
// 即使脏数据,也要检查 mtime 是否被外部(CLI)改过;若改过则合并,避免 CLI 的 providerConfigs 白名单等被 daemon 的 modelErrors 刷盘覆盖
|
|
77
|
-
if (e.data !== null) {
|
|
78
|
-
try {
|
|
79
|
-
const st = statSync(file);
|
|
80
|
-
if (st.mtimeMs === e.mtimeMs) return e.data;
|
|
81
|
-
// mtime 变了,说明外部改过文件
|
|
82
|
-
if (e.dirty) {
|
|
83
|
-
const disk = loadFromDisk(file);
|
|
84
|
-
const diskObj = typeof disk === "object" && disk !== null ? disk : {};
|
|
85
|
-
// 合并:disk 为新基准,e.data 的热数据(modelErrors/modelLatencies/peerStats 等)覆盖回去,但 providerConfigs 等配置以 disk 为准
|
|
86
|
-
const merged = { ...diskObj, ...e.data };
|
|
87
|
-
// 配置类字段以 disk 为准(CLI 改的白名单、keys、port、token 等)
|
|
88
|
-
if (diskObj.providerConfigs !== undefined) merged.providerConfigs = diskObj.providerConfigs;
|
|
89
|
-
if (diskObj.providerKeys !== undefined) merged.providerKeys = diskObj.providerKeys;
|
|
90
|
-
if (diskObj.providerShareKeys !== undefined) merged.providerShareKeys = diskObj.providerShareKeys;
|
|
91
|
-
if (diskObj.port !== undefined) merged.port = diskObj.port;
|
|
92
|
-
if (diskObj.token !== undefined) merged.token = diskObj.token;
|
|
93
|
-
if (diskObj.preferredModel !== undefined) merged.preferredModel = diskObj.preferredModel;
|
|
94
|
-
if (diskObj.modelPicks !== undefined) merged.modelPicks = diskObj.modelPicks;
|
|
95
|
-
e.data = merged;
|
|
96
|
-
e.mtimeMs = st.mtimeMs;
|
|
97
|
-
// 保持 dirty,让热数据下次刷盘时带上合并后的配置
|
|
98
|
-
return e.data;
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
if (e.data) return e.data;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const disk = loadFromDisk(file);
|
|
105
|
-
const obj = typeof disk === "object" && disk !== null ? disk : {};
|
|
106
|
-
e.data = obj;
|
|
107
|
-
try {
|
|
108
|
-
const st = statSync(file);
|
|
109
|
-
e.mtimeMs = st.mtimeMs;
|
|
110
|
-
} catch {
|
|
111
|
-
e.mtimeMs = Date.now();
|
|
112
|
-
}
|
|
113
|
-
e.dirty = false;
|
|
114
|
-
return e.data;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function writeStateImmediate(file, patch) {
|
|
118
|
-
const e = getEntry(file);
|
|
119
|
-
const base = e.data !== null ? e.data : readState(file);
|
|
120
|
-
const merged = { ...base, ...patch };
|
|
121
|
-
e.data = merged;
|
|
122
|
-
e.dirty = false;
|
|
123
|
-
if (e.timer) {
|
|
124
|
-
clearTimeout(e.timer);
|
|
125
|
-
e.timer = null;
|
|
126
|
-
}
|
|
127
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
128
|
-
const tmp = `${file}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
129
|
-
writeFileSync(tmp, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
130
|
-
try {
|
|
131
|
-
renameSync(tmp, file);
|
|
132
|
-
} catch {
|
|
133
|
-
try { writeFileSync(file, JSON.stringify(merged, null, 2), { mode: 0o600 }); } catch {}
|
|
134
|
-
try { unlinkSync(tmp); } catch {}
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
e.mtimeMs = statSync(file).mtimeMs;
|
|
138
|
-
} catch {
|
|
139
|
-
e.mtimeMs = Date.now();
|
|
140
|
-
}
|
|
141
|
-
return merged;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function scheduleFlush(file) {
|
|
145
|
-
const e = getEntry(file);
|
|
146
|
-
if (e.timer) return;
|
|
147
|
-
if (FLUSH_MS === 0) {
|
|
148
|
-
// 同步刷(测试用)
|
|
149
|
-
flushStateSync(file);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
e.timer = setTimeout(() => {
|
|
153
|
-
e.timer = null;
|
|
154
|
-
void flushState(file);
|
|
155
|
-
}, FLUSH_MS);
|
|
156
|
-
e.timer.unref?.();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function flushState(file) {
|
|
160
|
-
const e = stateCache.get(file);
|
|
161
|
-
if (!e || !e.dirty || !e.data) return;
|
|
162
|
-
const snapshot = e.data;
|
|
163
|
-
try {
|
|
164
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
165
|
-
await writeFile(file, JSON.stringify(snapshot, null, 2), { mode: 0o600 });
|
|
166
|
-
e.dirty = false;
|
|
167
|
-
try {
|
|
168
|
-
e.mtimeMs = statSync(file).mtimeMs;
|
|
169
|
-
} catch {
|
|
170
|
-
e.mtimeMs = Date.now();
|
|
171
|
-
}
|
|
172
|
-
} catch {
|
|
173
|
-
// 保留 dirty,下次重试
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function writeStateDeferred(file, patch) {
|
|
178
|
-
const e = getEntry(file);
|
|
179
|
-
const base = e.data !== null ? e.data : readState(file);
|
|
180
|
-
const merged = { ...base, ...patch };
|
|
181
|
-
e.data = merged;
|
|
182
|
-
e.dirty = true;
|
|
183
|
-
scheduleFlush(file);
|
|
184
|
-
return merged;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function flushStateSync(file = defaultStateFile()) {
|
|
188
|
-
const e = stateCache.get(file);
|
|
189
|
-
if (!e || !e.dirty || !e.data) return;
|
|
190
|
-
try {
|
|
191
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
192
|
-
writeFileSync(file, JSON.stringify(e.data, null, 2), { mode: 0o600 });
|
|
193
|
-
e.dirty = false;
|
|
194
|
-
try {
|
|
195
|
-
e.mtimeMs = statSync(file).mtimeMs;
|
|
196
|
-
} catch {
|
|
197
|
-
e.mtimeMs = Date.now();
|
|
198
|
-
}
|
|
199
|
-
if (e.timer) {
|
|
200
|
-
clearTimeout(e.timer);
|
|
201
|
-
e.timer = null;
|
|
202
|
-
}
|
|
203
|
-
} catch {}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function clearStateCache(file) {
|
|
207
|
-
if (file) stateCache.delete(file);
|
|
208
|
-
else stateCache.clear();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ---- 对外 API:冷数据立即落盘,热数据延迟批量 ----
|
|
212
|
-
|
|
213
|
-
export async function loadToken({ file = defaultStateFile() } = {}) {
|
|
214
|
-
const state = readState(file);
|
|
215
|
-
if (typeof state.token === "string" && state.token.length > 0) {
|
|
216
|
-
syncTokenFile(state.token, file);
|
|
217
|
-
return { token: state.token, created: false };
|
|
218
|
-
}
|
|
219
|
-
if (existsSync(file)) {
|
|
220
|
-
try {
|
|
221
|
-
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
222
|
-
if (typeof raw.token === "string" && raw.token.length > 0) {
|
|
223
|
-
syncTokenFile(raw.token, file);
|
|
224
|
-
const e = getEntry(file);
|
|
225
|
-
e.data = raw;
|
|
226
|
-
try { e.mtimeMs = statSync(file).mtimeMs; } catch {}
|
|
227
|
-
return { token: raw.token, created: false };
|
|
228
|
-
}
|
|
229
|
-
} catch {}
|
|
230
|
-
}
|
|
231
|
-
const tok = generateToken();
|
|
232
|
-
const saved = writeStateImmediate(file, { token: tok, createdAt: new Date().toISOString() }).token;
|
|
233
|
-
syncTokenFile(saved, file);
|
|
234
|
-
return { token: saved, created: true };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export async function refreshToken({ file = defaultStateFile() } = {}) {
|
|
238
|
-
const tok = writeStateImmediate(file, { token: generateToken(), createdAt: new Date().toISOString() }).token;
|
|
239
|
-
syncTokenFile(tok, file);
|
|
240
|
-
return tok;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function setPort(port, { file = defaultStateFile() } = {}) {
|
|
244
|
-
writeStateImmediate(file, { port: Number(port) });
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export function getPort({ file = defaultStateFile() } = {}) {
|
|
248
|
-
const port = readState(file).port;
|
|
249
|
-
return typeof port === "number" && Number.isInteger(port) && port > 0 ? port : null;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function loadModelErrors({ file = defaultStateFile() } = {}) {
|
|
253
|
-
const errors = readState(file).modelErrors;
|
|
254
|
-
return errors && typeof errors === "object" && !Array.isArray(errors) ? errors : {};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function saveModelErrors(errors, { file = defaultStateFile() } = {}) {
|
|
258
|
-
writeStateDeferred(file, { modelErrors: errors });
|
|
259
|
-
return errors;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export function loadModelLatencies({ file = defaultStateFile() } = {}) {
|
|
263
|
-
const lat = readState(file).modelLatencies;
|
|
264
|
-
return lat && typeof lat === "object" && !Array.isArray(lat) ? lat : {};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function saveModelLatencies(latencies, { file = defaultStateFile() } = {}) {
|
|
268
|
-
writeStateDeferred(file, { modelLatencies: latencies });
|
|
269
|
-
return latencies;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export function loadModelStats({ file = defaultStateFile() } = {}) {
|
|
273
|
-
const s = readState(file).modelStats;
|
|
274
|
-
return s && typeof s === "object" && !Array.isArray(s) ? s : {};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function saveModelStats(stats, { file = defaultStateFile() } = {}) {
|
|
278
|
-
writeStateDeferred(file, { modelStats: stats });
|
|
279
|
-
return stats;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export function loadPreferredModel({ file = defaultStateFile() } = {}) {
|
|
283
|
-
const v = readState(file).preferredModel;
|
|
284
|
-
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// 供应商 API key:state 存 `providerKeys: { openrouter: ["sk-1", "sk-2"] }`
|
|
288
|
-
// 兼容旧版单字符串 `openrouter: "sk-1"`(读到自动转数组)。
|
|
289
|
-
// env `MSLXDFF_<ID>_KEY`(大写)优先,state 兜底;key 为空视为未配置
|
|
290
|
-
export const providerKeyEnv = (id) => `MSLXDFF_${String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_")}_KEY`;
|
|
291
|
-
export const providerBaseUrlEnv = (id) => `MSLXDFF_${String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_")}_BASE_URL`;
|
|
292
|
-
|
|
293
|
-
export function loadProviderKeys(id, { file = defaultStateFile() } = {}) {
|
|
294
|
-
const env = (process.env[providerKeyEnv(id)] || "").trim();
|
|
295
|
-
if (env) return [env];
|
|
296
|
-
// 通用供应商的 keys 优先从 providerConfigs 读取
|
|
297
|
-
const configs = readState(file).providerConfigs;
|
|
298
|
-
const cfg = configs && typeof configs === "object" ? configs[id] : undefined;
|
|
299
|
-
if (cfg && typeof cfg === "object" && Array.isArray(cfg.keys)) {
|
|
300
|
-
const list = [...new Set(cfg.keys.filter((x) => typeof x === "string" && x.trim().length))];
|
|
301
|
-
if (list.length) return list;
|
|
302
|
-
}
|
|
303
|
-
const keys = readState(file).providerKeys;
|
|
304
|
-
const v = keys && typeof keys === "object" ? keys[id] : undefined;
|
|
305
|
-
if (typeof v === "string") return v.trim() ? [v.trim()] : [];
|
|
306
|
-
if (Array.isArray(v)) return [...new Set(v.filter((x) => typeof x === "string" && x.trim().length))];
|
|
307
|
-
return [];
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export function loadProviderKey(id, opts = {}) {
|
|
311
|
-
return loadProviderKeys(id, opts)[0] || "";
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export function saveProviderKeys(id, list, { file = defaultStateFile() } = {}) {
|
|
315
|
-
const keys = { ...(readState(file).providerKeys || {}) };
|
|
316
|
-
const clean = [...new Set((Array.isArray(list) ? list : []).map((k) => String(k || "").trim()).filter(Boolean))];
|
|
317
|
-
if (clean.length) keys[id] = clean;
|
|
318
|
-
else delete keys[id];
|
|
319
|
-
writeStateImmediate(file, { providerKeys: keys });
|
|
320
|
-
return clean;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export function saveProviderKey(id, key, opts = {}) {
|
|
324
|
-
const list = [...loadProviderKeys(id, opts)];
|
|
325
|
-
const clean = String(key || "").trim();
|
|
326
|
-
if (clean && !list.includes(clean)) list.push(clean);
|
|
327
|
-
return saveProviderKeys(id, clean ? list : [], opts);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
export function addProviderKey(id, key, opts = {}) {
|
|
331
|
-
return saveProviderKey(id, key, opts);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export function removeProviderKey(id, key, opts = {}) {
|
|
335
|
-
return removeProviderKeys(id, [key], opts);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export function removeProviderKeys(id, targets = [], opts = {}) {
|
|
339
|
-
const set = new Set((Array.isArray(targets) ? targets : [targets]).map((k) => String(k || "").trim()).filter(Boolean));
|
|
340
|
-
const list = loadProviderKeys(id, opts).filter((k) => !set.has(k));
|
|
341
|
-
return saveProviderKeys(id, list, opts);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// 供应商 key 瞬时共享开关(ADR-0008):默认 false;true = 本节点在 outgoing 转发时附带
|
|
345
|
-
// 该供应商 key 给组员(瞬时借用,不落盘)。env `MSLXDFF_<ID>_SHARE_KEYS` 可覆盖 state。
|
|
346
|
-
export const providerShareEnv = (id) => `MSLXDFF_${String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_")}_SHARE_KEYS`;
|
|
347
|
-
|
|
348
|
-
export function loadProviderShareKeys(id, { file = defaultStateFile() } = {}) {
|
|
349
|
-
const env = process.env[providerShareEnv(id)] || "";
|
|
350
|
-
if (env) return ["1", "true", "on", "yes"].includes(String(env).trim().toLowerCase());
|
|
351
|
-
const map = readState(file).providerShareKeys;
|
|
352
|
-
return !!(map && typeof map === "object" && map[id]);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export function saveProviderShareKeys(id, on, { file = defaultStateFile() } = {}) {
|
|
356
|
-
const map = { ...(readState(file).providerShareKeys || {}) };
|
|
357
|
-
if (on) map[id] = true;
|
|
358
|
-
else delete map[id];
|
|
359
|
-
writeStateImmediate(file, { providerShareKeys: map });
|
|
360
|
-
return !!on;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// ---- 通用 OpenAI 兼容供应商配置:providerConfigs: { [id]: { baseUrl, keys, auths, allowedModels } } ----
|
|
364
|
-
export const WORKBUDDY_DEFAULT_BASE_URL = "https://copilot.tencent.com";
|
|
365
|
-
function normalizeBaseUrl(url) {
|
|
366
|
-
const s = String(url || "").trim();
|
|
367
|
-
if (!s) return "";
|
|
368
|
-
return s.replace(/\/+$/, "");
|
|
369
|
-
}
|
|
370
|
-
function normalizeAuths(list) {
|
|
371
|
-
if (!Array.isArray(list)) return [];
|
|
372
|
-
const out = [];
|
|
373
|
-
for (const a of list) {
|
|
374
|
-
if (!a || typeof a !== "object") continue;
|
|
375
|
-
const uid = String(a.uid || a.userId || "").trim();
|
|
376
|
-
if (!uid) continue;
|
|
377
|
-
out.push({
|
|
378
|
-
uid,
|
|
379
|
-
domain: String(a.domain || "www.codebuddy.cn").trim() || "www.codebuddy.cn",
|
|
380
|
-
enterpriseId: String(a.enterpriseId || a.enterprise_id || "").trim(),
|
|
381
|
-
refreshToken: String(a.refreshToken || a.refresh_token || "").trim(),
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
return out;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function normalizeAllowedModel(model, providerId) {
|
|
388
|
-
let s = String(model || "").trim();
|
|
389
|
-
if (!s) return "";
|
|
390
|
-
// 支持带前缀的输入:bai/glm-5.3-flash -> glm-5.3-flash(仅当 provider 匹配时剥前缀)
|
|
391
|
-
const idx = s.indexOf("/");
|
|
392
|
-
if (idx > 0) {
|
|
393
|
-
const head = s.slice(0, idx).toLowerCase();
|
|
394
|
-
if (head === String(providerId || "").toLowerCase()) s = s.slice(idx + 1);
|
|
395
|
-
}
|
|
396
|
-
return s;
|
|
397
|
-
}
|
|
398
|
-
function normalizeEndpointPath(v) {
|
|
399
|
-
const s = String(v || "").trim();
|
|
400
|
-
if (!s) return "";
|
|
401
|
-
return s.startsWith("/") ? s : `/${s}`;
|
|
402
|
-
}
|
|
403
|
-
function defaultModelsPath(id) {
|
|
404
|
-
if (String(id).toLowerCase() === "workbuddy") return "/console/enterprises/personal/models";
|
|
405
|
-
return "/models";
|
|
406
|
-
}
|
|
407
|
-
function defaultChatPath(id) {
|
|
408
|
-
if (String(id).toLowerCase() === "workbuddy") return "/v2/chat/completions";
|
|
409
|
-
return "/chat/completions";
|
|
410
|
-
}
|
|
411
|
-
export function loadProviderModelsPath(id, { file = defaultStateFile() } = {}) {
|
|
412
|
-
const cfg = loadProviderConfigs({ file })[id];
|
|
413
|
-
if (cfg && typeof cfg.modelsPath === "string" && cfg.modelsPath.trim()) return normalizeEndpointPath(cfg.modelsPath);
|
|
414
|
-
return defaultModelsPath(id);
|
|
415
|
-
}
|
|
416
|
-
export function loadProviderChatPath(id, { file = defaultStateFile() } = {}) {
|
|
417
|
-
const cfg = loadProviderConfigs({ file })[id];
|
|
418
|
-
if (cfg && typeof cfg.chatPath === "string" && cfg.chatPath.trim()) return normalizeEndpointPath(cfg.chatPath);
|
|
419
|
-
return defaultChatPath(id);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
export function loadProviderConfigs({ file = defaultStateFile() } = {}) {
|
|
423
|
-
const v = readState(file).providerConfigs;
|
|
424
|
-
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
425
|
-
return {};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export function loadProviderConfig(id, { file = defaultStateFile() } = {}) {
|
|
429
|
-
const envUrl = (process.env[providerBaseUrlEnv(id)] || "").trim();
|
|
430
|
-
const envKeys = loadProviderKeys(id, { file });
|
|
431
|
-
// env 覆盖时以 env 为准
|
|
432
|
-
if (envUrl || (process.env[providerKeyEnv(id)] || "").trim()) {
|
|
433
|
-
const baseUrl = envUrl || loadProviderConfigs({ file })[id]?.baseUrl || (String(id).toLowerCase() === "workbuddy" ? WORKBUDDY_DEFAULT_BASE_URL : "");
|
|
434
|
-
const cfg = loadProviderConfigs({ file })[id];
|
|
435
|
-
const allowedModels = cfg && Array.isArray(cfg.allowedModels) ? [...new Set(cfg.allowedModels.map((m) => normalizeAllowedModel(m, id)).filter(Boolean))] : [];
|
|
436
|
-
const auths = cfg && Array.isArray(cfg.auths) ? normalizeAuths(cfg.auths) : [];
|
|
437
|
-
const modelsPath = cfg && typeof cfg.modelsPath === "string" && cfg.modelsPath.trim() ? normalizeEndpointPath(cfg.modelsPath) : defaultModelsPath(id);
|
|
438
|
-
const chatPath = cfg && typeof cfg.chatPath === "string" && cfg.chatPath.trim() ? normalizeEndpointPath(cfg.chatPath) : defaultChatPath(id);
|
|
439
|
-
if (baseUrl || envKeys.length || allowedModels.length || auths.length) return { baseUrl: normalizeBaseUrl(baseUrl), keys: envKeys, auths, allowedModels, modelsPath, chatPath };
|
|
440
|
-
return null;
|
|
441
|
-
}
|
|
442
|
-
const configs = loadProviderConfigs({ file });
|
|
443
|
-
const cfg = configs[id];
|
|
444
|
-
if (cfg && typeof cfg === "object") {
|
|
445
|
-
return {
|
|
446
|
-
baseUrl: normalizeBaseUrl(cfg.baseUrl || (String(id).toLowerCase() === "workbuddy" ? WORKBUDDY_DEFAULT_BASE_URL : "")),
|
|
447
|
-
keys: Array.isArray(cfg.keys) ? [...new Set(cfg.keys.filter((x) => typeof x === "string" && x.trim().length))] : [],
|
|
448
|
-
auths: normalizeAuths(cfg.auths),
|
|
449
|
-
allowedModels: Array.isArray(cfg.allowedModels) ? [...new Set(cfg.allowedModels.map((m) => normalizeAllowedModel(m, id)).filter(Boolean))] : [],
|
|
450
|
-
modelsPath: typeof cfg.modelsPath === "string" && cfg.modelsPath.trim() ? normalizeEndpointPath(cfg.modelsPath) : defaultModelsPath(id),
|
|
451
|
-
chatPath: typeof cfg.chatPath === "string" && cfg.chatPath.trim() ? normalizeEndpointPath(cfg.chatPath) : defaultChatPath(id),
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
// 兼容旧 providerKeys 形态:有 key 但无 configs 时视为通用供应商(baseUrl 为空,需后补)
|
|
455
|
-
const keys = loadProviderKeys(id, { file });
|
|
456
|
-
if (keys.length) return { baseUrl: String(id).toLowerCase() === "workbuddy" ? WORKBUDDY_DEFAULT_BASE_URL : "", keys, auths: [], allowedModels: [], modelsPath: defaultModelsPath(id), chatPath: defaultChatPath(id) };
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
export function loadProviderBaseUrl(id, opts = {}) {
|
|
461
|
-
const env = (process.env[providerBaseUrlEnv(id)] || "").trim();
|
|
462
|
-
if (env) return normalizeBaseUrl(env);
|
|
463
|
-
const cfg = loadProviderConfigs(opts)[id];
|
|
464
|
-
if (cfg && typeof cfg.baseUrl === "string" && normalizeBaseUrl(cfg.baseUrl)) return normalizeBaseUrl(cfg.baseUrl);
|
|
465
|
-
if (String(id || "").toLowerCase() === "workbuddy") return WORKBUDDY_DEFAULT_BASE_URL;
|
|
466
|
-
return "";
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
export function loadProviderAuths(id, { file = defaultStateFile() } = {}) {
|
|
470
|
-
const cfg = loadProviderConfigs({ file })[id];
|
|
471
|
-
if (cfg && Array.isArray(cfg.auths)) return normalizeAuths(cfg.auths);
|
|
472
|
-
return [];
|
|
473
|
-
}
|
|
474
|
-
export function saveProviderAuths(id, list, { file = defaultStateFile() } = {}) {
|
|
475
|
-
const clean = normalizeAuths(list);
|
|
476
|
-
const configs = { ...loadProviderConfigs({ file }) };
|
|
477
|
-
const cur = configs[id] && typeof configs[id] === "object" ? configs[id] : {};
|
|
478
|
-
const baseUrl = normalizeBaseUrl(cur.baseUrl || loadProviderBaseUrl(id, { file }) || "");
|
|
479
|
-
const keys = Array.isArray(cur.keys) ? [...new Set(cur.keys.filter((x) => typeof x === "string" && x.trim().length))] : loadProviderKeys(id, { file });
|
|
480
|
-
const allowedModels = Array.isArray(cur.allowedModels) ? [...new Set(cur.allowedModels.map((m) => normalizeAllowedModel(m, id)).filter(Boolean))] : [];
|
|
481
|
-
const modelsPath = typeof cur.modelsPath === "string" ? normalizeEndpointPath(cur.modelsPath) : "";
|
|
482
|
-
const chatPath = typeof cur.chatPath === "string" ? normalizeEndpointPath(cur.chatPath) : "";
|
|
483
|
-
if (!baseUrl && !keys.length && !clean.length && !allowedModels.length && !modelsPath && !chatPath) {
|
|
484
|
-
delete configs[id];
|
|
485
|
-
} else {
|
|
486
|
-
configs[id] = { baseUrl, keys };
|
|
487
|
-
if (clean.length) configs[id].auths = clean;
|
|
488
|
-
if (allowedModels.length) configs[id].allowedModels = allowedModels;
|
|
489
|
-
if (modelsPath) configs[id].modelsPath = modelsPath;
|
|
490
|
-
if (chatPath) configs[id].chatPath = chatPath;
|
|
491
|
-
}
|
|
492
|
-
writeStateImmediate(file, { providerConfigs: configs });
|
|
493
|
-
return clean;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
export function saveProviderBaseUrl(id, baseUrl, { file = defaultStateFile() } = {}) {
|
|
497
|
-
const clean = normalizeBaseUrl(baseUrl);
|
|
498
|
-
const configs = { ...loadProviderConfigs({ file }) };
|
|
499
|
-
const cur = configs[id] && typeof configs[id] === "object" ? configs[id] : {};
|
|
500
|
-
const keys = Array.isArray(cur.keys) ? cur.keys : loadProviderKeys(id, { file });
|
|
501
|
-
const auths = normalizeAuths(cur.auths);
|
|
502
|
-
const allowedModels = Array.isArray(cur.allowedModels) ? [...new Set(cur.allowedModels.map((m) => normalizeAllowedModel(m, id)).filter(Boolean))] : [];
|
|
503
|
-
const modelsPath = typeof cur.modelsPath === "string" ? normalizeEndpointPath(cur.modelsPath) : "";
|
|
504
|
-
const chatPath = typeof cur.chatPath === "string" ? normalizeEndpointPath(cur.chatPath) : "";
|
|
505
|
-
if (!clean && !keys.length && !auths.length && !allowedModels.length && !modelsPath && !chatPath) {
|
|
506
|
-
delete configs[id];
|
|
507
|
-
} else {
|
|
508
|
-
configs[id] = { baseUrl: clean, keys };
|
|
509
|
-
if (auths.length) configs[id].auths = auths;
|
|
510
|
-
if (allowedModels.length) configs[id].allowedModels = allowedModels;
|
|
511
|
-
if (modelsPath) configs[id].modelsPath = modelsPath;
|
|
512
|
-
if (chatPath) configs[id].chatPath = chatPath;
|
|
513
|
-
}
|
|
514
|
-
writeStateImmediate(file, { providerConfigs: configs });
|
|
515
|
-
return clean;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
export function saveProviderConfig(id, { baseUrl, keys, auths, allowedModels, modelsPath, chatPath }, { file = defaultStateFile() } = {}) {
|
|
519
|
-
const cleanUrl = normalizeBaseUrl(baseUrl);
|
|
520
|
-
const cleanKeys = [...new Set((Array.isArray(keys) ? keys : []).map((k) => String(k || "").trim()).filter(Boolean))];
|
|
521
|
-
const cleanAuths = auths === undefined ? undefined : normalizeAuths(auths);
|
|
522
|
-
const cleanAllowed = [...new Set((Array.isArray(allowedModels) ? allowedModels : []).map((m) => normalizeAllowedModel(m, id)).filter(Boolean))];
|
|
523
|
-
const cleanModelsPath = modelsPath === undefined ? undefined : (String(modelsPath).trim() ? normalizeEndpointPath(modelsPath) : "");
|
|
524
|
-
const cleanChatPath = chatPath === undefined ? undefined : (String(chatPath).trim() ? normalizeEndpointPath(chatPath) : "");
|
|
525
|
-
const configs = { ...loadProviderConfigs({ file }) };
|
|
526
|
-
const cur = configs[id] && typeof configs[id] === "object" ? configs[id] : {};
|
|
527
|
-
// 保留已有的 allowedModels / auths / paths 若本次未传入
|
|
528
|
-
const finalAllowed = allowedModels === undefined ? (Array.isArray(cur.allowedModels) ? [...new Set(cur.allowedModels.map((m) => normalizeAllowedModel(m, id)).filter(Boolean))] : []) : cleanAllowed;
|
|
529
|
-
const finalAuths = cleanAuths === undefined ? normalizeAuths(cur.auths) : cleanAuths;
|
|
530
|
-
const finalModelsPath = cleanModelsPath === undefined ? (typeof cur.modelsPath === "string" ? normalizeEndpointPath(cur.modelsPath) : "") : cleanModelsPath;
|
|
531
|
-
const finalChatPath = cleanChatPath === undefined ? (typeof cur.chatPath === "string" ? normalizeEndpointPath(cur.chatPath) : "") : cleanChatPath;
|
|
532
|
-
if (!cleanUrl && !cleanKeys.length && !finalAllowed.length && !finalAuths.length && !finalModelsPath && !finalChatPath) {
|
|
533
|
-
delete configs[id];
|
|
534
|
-
} else {
|
|
535
|
-
configs[id] = { baseUrl: cleanUrl, keys: cleanKeys };
|
|
536
|
-
if (finalAuths.length) configs[id].auths = finalAuths;
|
|
537
|
-
if (finalAllowed.length) configs[id].allowedModels = finalAllowed;
|
|
538
|
-
if (finalModelsPath) configs[id].modelsPath = finalModelsPath;
|
|
539
|
-
if (finalChatPath) configs[id].chatPath = finalChatPath;
|
|
540
|
-
}
|
|
541
|
-
// 同步清理旧 providerKeys 中同 id 的残留,避免双写
|
|
542
|
-
const oldKeys = readState(file).providerKeys;
|
|
543
|
-
if (oldKeys && typeof oldKeys === "object" && oldKeys[id] !== undefined) {
|
|
544
|
-
const nextKeys = { ...oldKeys };
|
|
545
|
-
delete nextKeys[id];
|
|
546
|
-
writeStateImmediate(file, { providerKeys: nextKeys, providerConfigs: configs });
|
|
547
|
-
return { baseUrl: cleanUrl, keys: cleanKeys, auths: finalAuths, allowedModels: finalAllowed, modelsPath: finalModelsPath, chatPath: finalChatPath };
|
|
548
|
-
}
|
|
549
|
-
writeStateImmediate(file, { providerConfigs: configs });
|
|
550
|
-
return { baseUrl: cleanUrl, keys: cleanKeys, auths: finalAuths, allowedModels: finalAllowed, modelsPath: finalModelsPath, chatPath: finalChatPath };
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// ---- 供应商模型白名单:providerConfigs.<id>.allowedModels(空 = 不限) ----
|
|
554
|
-
export function loadProviderAllowedModels(id, { file = defaultStateFile() } = {}) {
|
|
555
|
-
const cfg = loadProviderConfig(id, { file });
|
|
556
|
-
if (cfg && Array.isArray(cfg.allowedModels)) return [...cfg.allowedModels];
|
|
557
|
-
const raw = readState(file).providerConfigs?.[id];
|
|
558
|
-
if (raw && Array.isArray(raw.allowedModels)) return [...new Set(raw.allowedModels.map((m) => normalizeAllowedModel(m, id)).filter(Boolean))];
|
|
559
|
-
return [];
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
export function saveProviderAllowedModels(id, list, { file = defaultStateFile() } = {}) {
|
|
563
|
-
const clean = [...new Set((Array.isArray(list) ? list : []).map((m) => normalizeAllowedModel(m, id)).filter(Boolean))];
|
|
564
|
-
const configs = { ...loadProviderConfigs({ file }) };
|
|
565
|
-
const cur = configs[id] && typeof configs[id] === "object" ? configs[id] : {};
|
|
566
|
-
const baseUrl = normalizeBaseUrl(cur.baseUrl || loadProviderBaseUrl(id, { file }) || "");
|
|
567
|
-
const keys = Array.isArray(cur.keys) ? cur.keys : loadProviderKeys(id, { file });
|
|
568
|
-
const auths = normalizeAuths(cur.auths);
|
|
569
|
-
const modelsPath = typeof cur.modelsPath === "string" ? normalizeEndpointPath(cur.modelsPath) : "";
|
|
570
|
-
const chatPath = typeof cur.chatPath === "string" ? normalizeEndpointPath(cur.chatPath) : "";
|
|
571
|
-
if (!baseUrl && !keys.length && !auths.length && !clean.length && !modelsPath && !chatPath) {
|
|
572
|
-
delete configs[id];
|
|
573
|
-
} else {
|
|
574
|
-
configs[id] = { baseUrl, keys };
|
|
575
|
-
if (auths.length) configs[id].auths = auths;
|
|
576
|
-
if (clean.length) configs[id].allowedModels = clean;
|
|
577
|
-
if (modelsPath) configs[id].modelsPath = modelsPath;
|
|
578
|
-
if (chatPath) configs[id].chatPath = chatPath;
|
|
579
|
-
}
|
|
580
|
-
writeStateImmediate(file, { providerConfigs: configs });
|
|
581
|
-
return clean;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
export function addProviderAllowedModel(id, model, opts = {}) {
|
|
585
|
-
const cur = loadProviderAllowedModels(id, opts);
|
|
586
|
-
const norm = normalizeAllowedModel(model, id);
|
|
587
|
-
if (!norm || cur.includes(norm)) return cur;
|
|
588
|
-
return saveProviderAllowedModels(id, [...cur, norm], opts);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
export function removeProviderAllowedModel(id, model, opts = {}) {
|
|
592
|
-
return removeProviderAllowedModels(id, [model], opts);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
export function removeProviderAllowedModels(id, targets = [], opts = {}) {
|
|
596
|
-
const set = new Set((Array.isArray(targets) ? targets : [targets]).map((m) => normalizeAllowedModel(m, id)).filter(Boolean));
|
|
597
|
-
const cur = loadProviderAllowedModels(id, opts);
|
|
598
|
-
const next = cur.filter((m) => !set.has(m));
|
|
599
|
-
return saveProviderAllowedModels(id, next, opts);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function normalizeAllowAny(v, id) {
|
|
603
|
-
if (typeof v === "boolean") return v;
|
|
604
|
-
if (String(id || "").toLowerCase() === "opencode") return true;
|
|
605
|
-
return false;
|
|
606
|
-
}
|
|
607
|
-
export function loadProviderAllowAnyModels(id, { file = defaultStateFile() } = {}) {
|
|
608
|
-
const cfg = readState(file).providerConfigs?.[id];
|
|
609
|
-
if (cfg && typeof cfg.allowAnyModels === "boolean") return cfg.allowAnyModels;
|
|
610
|
-
return normalizeAllowAny(undefined, id);
|
|
611
|
-
}
|
|
612
|
-
export function saveProviderAllowAnyModels(id, allowAny, { file = defaultStateFile() } = {}) {
|
|
613
|
-
const state = readState(file);
|
|
614
|
-
const cfgs = { ...(state.providerConfigs || {}) };
|
|
615
|
-
const cur = cfgs[id] || {};
|
|
616
|
-
cfgs[id] = { ...cur, allowAnyModels: normalizeAllowAny(allowAny, id) };
|
|
617
|
-
writeStateImmediate(file, { providerConfigs: cfgs });
|
|
618
|
-
return cfgs[id].allowAnyModels;
|
|
619
|
-
}
|
|
620
|
-
export function isModelAllowed(id, rawModel, { file = defaultStateFile() } = {}) {
|
|
621
|
-
const raw = String(rawModel || "").trim();
|
|
622
|
-
if (!raw) return true;
|
|
623
|
-
const allowed = loadProviderAllowedModels(id, { file });
|
|
624
|
-
const allowAny = loadProviderAllowAnyModels(id, { file });
|
|
625
|
-
if (!allowed.length) return allowAny;
|
|
626
|
-
const norm = normalizeAllowedModel(raw, id);
|
|
627
|
-
return allowed.includes(norm);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// 常用模型勾选集(auto 候选池白名单):空数组 = 不启用筛选(全量 auto)
|
|
631
|
-
export function loadModelPicks({ file = defaultStateFile() } = {}) {
|
|
632
|
-
const picks = readState(file).modelPicks;
|
|
633
|
-
if (!Array.isArray(picks)) return [];
|
|
634
|
-
return [...new Set(picks.filter((x) => typeof x === "string" && x.trim().length))];
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
export function saveModelPicks(picks, { file = defaultStateFile() } = {}) {
|
|
638
|
-
const list = [...new Set((Array.isArray(picks) ? picks : []).filter((x) => typeof x === "string" && x.trim().length))];
|
|
639
|
-
writeStateImmediate(file, { modelPicks: list });
|
|
640
|
-
return list;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
export function savePreferredModel(id, { file = defaultStateFile() } = {}) {
|
|
644
|
-
writeStateImmediate(file, { preferredModel: String(id || "").trim() });
|
|
645
|
-
return String(id || "").trim();
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
export function loadPeers({ file = defaultStateFile() } = {}) {
|
|
649
|
-
const peers = readState(file).peers;
|
|
650
|
-
return Array.isArray(peers) ? peers : [];
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
export function savePeers(peers, { file = defaultStateFile() } = {}) {
|
|
654
|
-
writeStateImmediate(file, { peers });
|
|
655
|
-
return peers;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
export function loadPeerErrors({ file = defaultStateFile() } = {}) {
|
|
659
|
-
const errors = readState(file).peerErrors;
|
|
660
|
-
return errors && typeof errors === "object" && !Array.isArray(errors) ? errors : {};
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
export function savePeerErrors(errors, { file = defaultStateFile() } = {}) {
|
|
664
|
-
writeStateDeferred(file, { peerErrors: errors });
|
|
665
|
-
return errors;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
export function loadPeerStats({ file = defaultStateFile() } = {}) {
|
|
669
|
-
const stats = readState(file).peerStats;
|
|
670
|
-
return stats && typeof stats === "object" && !Array.isArray(stats) ? stats : {};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
export function savePeerStats(stats, { file = defaultStateFile() } = {}) {
|
|
674
|
-
writeStateDeferred(file, { peerStats: stats });
|
|
675
|
-
return stats;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
export function loadGroups({ file = defaultStateFile() } = {}) {
|
|
679
|
-
const groups = readState(file).groups;
|
|
680
|
-
return groups && typeof groups === "object" && !Array.isArray(groups) ? groups : {};
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
export function loadGroupsJoined({ file = defaultStateFile() } = {}) {
|
|
684
|
-
const joined = readState(file).groupsJoined;
|
|
685
|
-
return Array.isArray(joined) ? joined : [];
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
export function saveGroupsJoined(joined, { file = defaultStateFile() } = {}) {
|
|
689
|
-
writeStateImmediate(file, { groupsJoined: joined });
|
|
690
|
-
return joined;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
export function loadBans({ file = defaultStateFile() } = {}) {
|
|
694
|
-
const bans = readState(file).bans;
|
|
695
|
-
return bans && typeof bans === "object" && !Array.isArray(bans) ? bans : {};
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
export function saveBans(bans, { file = defaultStateFile() } = {}) {
|
|
699
|
-
writeStateImmediate(file, { bans });
|
|
700
|
-
return bans;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
export function saveGroups(groups, { file = defaultStateFile() } = {}) {
|
|
704
|
-
writeStateImmediate(file, { groups });
|
|
705
|
-
return groups;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function writeState(file, patch) {
|
|
709
|
-
// 兼容旧调用:默认走立即落盘
|
|
710
|
-
return writeStateImmediate(file, patch);
|
|
711
|
-
}
|
|
1
|
+
export * from "./state/facade.js";
|