mslxdff 0.1.65 → 0.1.67
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/chat/cooling.js +99 -0
- package/src/chat/direct.js +57 -0
- package/src/chat/gateway.js +148 -0
- package/src/chat/orchestrator.js +218 -0
- package/src/chat/sse.js +69 -0
- package/src/chat/upstream.js +59 -501
- package/src/cli/bootstrap.js +1 -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/provider-row.js +73 -0
- package/src/cli/status.js +283 -0
- package/src/cli/util.js +24 -0
- package/src/providers/base.js +159 -0
- package/src/providers/dispatcher.js +18 -6
- package/src/providers/generic.js +28 -166
- package/src/providers/openrouter.js +19 -205
- package/src/providers/workbuddy/auth.js +175 -0
- package/src/providers/workbuddy/balance.js +84 -0
- package/src/providers/workbuddy/chat.js +310 -0
- package/src/providers/workbuddy/index.js +263 -0
- package/src/providers/workbuddy/models.js +111 -0
- package/src/providers/workbuddy/rotation-log.js +54 -0
- package/src/providers/workbuddy.js +2 -677
- package/src/routes/chat/broadband-handler.js +25 -46
- package/src/routes/chat/exhausted-handler.js +2 -2
- package/src/routes/chat/gateway.js +301 -0
- package/src/routes/chat/hedge-handler.js +65 -83
- package/src/routes/chat/index.js +1 -384
- package/src/routes/chat/local-handler.js +32 -61
- package/src/routes/chat/peer-handler.js +32 -23
- package/src/routes/chat/relay-pipeline.js +151 -0
- package/src/runtime/bootstrap.js +408 -0
- package/src/state/facade.js +57 -0
- package/src/state/memory.js +161 -0
- package/src/state/merge.js +26 -0
- package/src/state/persist.js +42 -0
- package/src/state/provider-config.js +143 -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 +204 -0
- package/src/state/schemas/token.js +43 -0
- package/src/state/store.js +176 -0
- package/src/state.js +1 -711
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { defaultStateFile, readState, writeStateImmediate } from "../store.js";
|
|
2
|
+
export {
|
|
3
|
+
WORKBUDDY_DEFAULT_BASE_URL,
|
|
4
|
+
normalizeBaseUrl,
|
|
5
|
+
normalizeAuths,
|
|
6
|
+
normalizeAllowedModel,
|
|
7
|
+
normalizeEndpointPath,
|
|
8
|
+
defaultModelsPath,
|
|
9
|
+
defaultChatPath,
|
|
10
|
+
providerKeyEnv,
|
|
11
|
+
providerBaseUrlEnv,
|
|
12
|
+
providerShareEnv,
|
|
13
|
+
} from "../provider-config.js";
|
|
14
|
+
import {
|
|
15
|
+
normalizeBaseUrl as _normalizeBaseUrl,
|
|
16
|
+
normalizeAuths as _normalizeAuths,
|
|
17
|
+
normalizeAllowedModel as _normalizeAllowedModel,
|
|
18
|
+
normalizeEndpointPath as _normalizeEndpointPath,
|
|
19
|
+
defaultModelsPath as _defaultModelsPath,
|
|
20
|
+
defaultChatPath as _defaultChatPath,
|
|
21
|
+
providerKeyEnv as _providerKeyEnv,
|
|
22
|
+
providerBaseUrlEnv as _providerBaseUrlEnv,
|
|
23
|
+
providerShareEnv as _providerShareEnv,
|
|
24
|
+
WORKBUDDY_DEFAULT_BASE_URL as _WORKBUDDY,
|
|
25
|
+
} from "../provider-config.js";
|
|
26
|
+
|
|
27
|
+
// 薄门面:所有 load/save 仍在此,但归一化逻辑已收至 provider-config 单点
|
|
28
|
+
export function loadProviderModelsPath(id, { file = defaultStateFile() } = {}) {
|
|
29
|
+
const cfg = readState(file).providerConfigs?.[id];
|
|
30
|
+
if (cfg && typeof cfg.modelsPath === "string" && cfg.modelsPath.trim()) return _normalizeEndpointPath(cfg.modelsPath);
|
|
31
|
+
return _defaultModelsPath(id);
|
|
32
|
+
}
|
|
33
|
+
export function loadProviderChatPath(id, { file = defaultStateFile() } = {}) {
|
|
34
|
+
const cfg = readState(file).providerConfigs?.[id];
|
|
35
|
+
if (cfg && typeof cfg.chatPath === "string" && cfg.chatPath.trim()) return _normalizeEndpointPath(cfg.chatPath);
|
|
36
|
+
return _defaultChatPath(id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function loadProviderKeys(id, { file = defaultStateFile() } = {}) {
|
|
40
|
+
const env = (process.env[_providerKeyEnv(id)] || "").trim();
|
|
41
|
+
if (env) return [env];
|
|
42
|
+
const configs = readState(file).providerConfigs;
|
|
43
|
+
const cfg = configs && typeof configs === "object" ? configs[id] : undefined;
|
|
44
|
+
if (cfg && typeof cfg === "object" && Array.isArray(cfg.keys)) {
|
|
45
|
+
const list = [...new Set(cfg.keys.filter((x) => typeof x === "string" && x.trim().length))];
|
|
46
|
+
if (list.length) return list;
|
|
47
|
+
}
|
|
48
|
+
const keys = readState(file).providerKeys;
|
|
49
|
+
const v = keys && typeof keys === "object" ? keys[id] : undefined;
|
|
50
|
+
if (typeof v === "string") return v.trim() ? [v.trim()] : [];
|
|
51
|
+
if (Array.isArray(v)) return [...new Set(v.filter((x) => typeof x === "string" && x.trim().length))];
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
export function loadProviderKey(id, opts = {}) { return loadProviderKeys(id, opts)[0] || ""; }
|
|
55
|
+
export function saveProviderKeys(id, list, { file = defaultStateFile() } = {}) {
|
|
56
|
+
const keys = { ...(readState(file).providerKeys || {}) };
|
|
57
|
+
const clean = [...new Set((Array.isArray(list) ? list : []).map((k) => String(k || "").trim()).filter(Boolean))];
|
|
58
|
+
if (clean.length) keys[id] = clean; else delete keys[id];
|
|
59
|
+
writeStateImmediate(file, { providerKeys: keys });
|
|
60
|
+
return clean;
|
|
61
|
+
}
|
|
62
|
+
export function saveProviderKey(id, key, opts = {}) {
|
|
63
|
+
const list = [...loadProviderKeys(id, opts)];
|
|
64
|
+
const clean = String(key || "").trim();
|
|
65
|
+
if (clean && !list.includes(clean)) list.push(clean);
|
|
66
|
+
return saveProviderKeys(id, clean ? list : [], opts);
|
|
67
|
+
}
|
|
68
|
+
export function addProviderKey(id, key, opts = {}) { return saveProviderKey(id, key, opts); }
|
|
69
|
+
export function removeProviderKey(id, key, opts = {}) { return removeProviderKeys(id, [key], opts); }
|
|
70
|
+
export function removeProviderKeys(id, targets = [], opts = {}) {
|
|
71
|
+
const set = new Set((Array.isArray(targets) ? targets : [targets]).map((k) => String(k || "").trim()).filter(Boolean));
|
|
72
|
+
const list = loadProviderKeys(id, opts).filter((k) => !set.has(k));
|
|
73
|
+
return saveProviderKeys(id, list, opts);
|
|
74
|
+
}
|
|
75
|
+
export function loadProviderShareKeys(id, { file = defaultStateFile() } = {}) {
|
|
76
|
+
const env = process.env[_providerShareEnv(id)] || "";
|
|
77
|
+
if (env) return ["1", "true", "on", "yes"].includes(String(env).trim().toLowerCase());
|
|
78
|
+
const map = readState(file).providerShareKeys;
|
|
79
|
+
return !!(map && typeof map === "object" && map[id]);
|
|
80
|
+
}
|
|
81
|
+
export function saveProviderShareKeys(id, on, { file = defaultStateFile() } = {}) {
|
|
82
|
+
const map = { ...(readState(file).providerShareKeys || {}) };
|
|
83
|
+
if (on) map[id] = true; else delete map[id];
|
|
84
|
+
writeStateImmediate(file, { providerShareKeys: map });
|
|
85
|
+
return !!on;
|
|
86
|
+
}
|
|
87
|
+
export function loadProviderConfigs({ file = defaultStateFile() } = {}) {
|
|
88
|
+
const v = readState(file).providerConfigs;
|
|
89
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
export function loadProviderConfig(id, { file = defaultStateFile() } = {}) {
|
|
93
|
+
const envUrl = (process.env[_providerBaseUrlEnv(id)] || "").trim();
|
|
94
|
+
const envKeys = loadProviderKeys(id, { file });
|
|
95
|
+
if (envUrl || (process.env[_providerKeyEnv(id)] || "").trim()) {
|
|
96
|
+
const baseUrl = envUrl || loadProviderConfigs({ file })[id]?.baseUrl || (String(id).toLowerCase() === "workbuddy" ? _WORKBUDDY : "");
|
|
97
|
+
const cfg = loadProviderConfigs({ file })[id];
|
|
98
|
+
const allowedModels = cfg && Array.isArray(cfg.allowedModels) ? [...new Set(cfg.allowedModels.map((m) => _normalizeAllowedModel(m, id)).filter(Boolean))] : [];
|
|
99
|
+
const auths = cfg && Array.isArray(cfg.auths) ? _normalizeAuths(cfg.auths) : [];
|
|
100
|
+
const modelsPath = cfg && typeof cfg.modelsPath === "string" && cfg.modelsPath.trim() ? _normalizeEndpointPath(cfg.modelsPath) : _defaultModelsPath(id);
|
|
101
|
+
const chatPath = cfg && typeof cfg.chatPath === "string" && cfg.chatPath.trim() ? _normalizeEndpointPath(cfg.chatPath) : _defaultChatPath(id);
|
|
102
|
+
if (baseUrl || envKeys.length || allowedModels.length || auths.length) return { baseUrl: _normalizeBaseUrl(baseUrl), keys: envKeys, auths, allowedModels, modelsPath, chatPath };
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const configs = loadProviderConfigs({ file });
|
|
106
|
+
const cfg = configs[id];
|
|
107
|
+
if (cfg && typeof cfg === "object") {
|
|
108
|
+
return {
|
|
109
|
+
baseUrl: _normalizeBaseUrl(cfg.baseUrl || (String(id).toLowerCase() === "workbuddy" ? _WORKBUDDY : "")),
|
|
110
|
+
keys: Array.isArray(cfg.keys) ? [...new Set(cfg.keys.filter((x) => typeof x === "string" && x.trim().length))] : [],
|
|
111
|
+
auths: _normalizeAuths(cfg.auths),
|
|
112
|
+
allowedModels: Array.isArray(cfg.allowedModels) ? [...new Set(cfg.allowedModels.map((m) => _normalizeAllowedModel(m, id)).filter(Boolean))] : [],
|
|
113
|
+
modelsPath: typeof cfg.modelsPath === "string" && cfg.modelsPath.trim() ? _normalizeEndpointPath(cfg.modelsPath) : _defaultModelsPath(id),
|
|
114
|
+
chatPath: typeof cfg.chatPath === "string" && cfg.chatPath.trim() ? _normalizeEndpointPath(cfg.chatPath) : _defaultChatPath(id),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const keys = loadProviderKeys(id, { file });
|
|
118
|
+
if (keys.length) return { baseUrl: String(id).toLowerCase() === "workbuddy" ? _WORKBUDDY : "", keys, auths: [], allowedModels: [], modelsPath: _defaultModelsPath(id), chatPath: _defaultChatPath(id) };
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
export function loadProviderBaseUrl(id, opts = {}) {
|
|
122
|
+
const env = (process.env[_providerBaseUrlEnv(id)] || "").trim();
|
|
123
|
+
if (env) return _normalizeBaseUrl(env);
|
|
124
|
+
const cfg = loadProviderConfigs(opts)[id];
|
|
125
|
+
if (cfg && typeof cfg.baseUrl === "string" && _normalizeBaseUrl(cfg.baseUrl)) return _normalizeBaseUrl(cfg.baseUrl);
|
|
126
|
+
if (String(id || "").toLowerCase() === "workbuddy") return _WORKBUDDY;
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
export function loadProviderAuths(id, { file = defaultStateFile() } = {}) {
|
|
130
|
+
const cfg = loadProviderConfigs({ file })[id];
|
|
131
|
+
if (cfg && Array.isArray(cfg.auths)) return _normalizeAuths(cfg.auths);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
export function saveProviderAuths(id, list, { file = defaultStateFile() } = {}) {
|
|
135
|
+
const clean = _normalizeAuths(list);
|
|
136
|
+
const configs = { ...loadProviderConfigs({ file }) };
|
|
137
|
+
const cur = configs[id] && typeof configs[id] === "object" ? configs[id] : {};
|
|
138
|
+
const baseUrl = _normalizeBaseUrl(cur.baseUrl || loadProviderBaseUrl(id, { file }) || "");
|
|
139
|
+
const keys = Array.isArray(cur.keys) ? [...new Set(cur.keys.filter((x) => typeof x === "string" && x.trim().length))] : loadProviderKeys(id, { file });
|
|
140
|
+
const allowedModels = Array.isArray(cur.allowedModels) ? [...new Set(cur.allowedModels.map((m) => _normalizeAllowedModel(m, id)).filter(Boolean))] : [];
|
|
141
|
+
const modelsPath = typeof cur.modelsPath === "string" ? _normalizeEndpointPath(cur.modelsPath) : "";
|
|
142
|
+
const chatPath = typeof cur.chatPath === "string" ? _normalizeEndpointPath(cur.chatPath) : "";
|
|
143
|
+
if (!baseUrl && !keys.length && !clean.length && !allowedModels.length && !modelsPath && !chatPath) delete configs[id];
|
|
144
|
+
else {
|
|
145
|
+
configs[id] = { baseUrl, keys };
|
|
146
|
+
if (clean.length) configs[id].auths = clean;
|
|
147
|
+
if (allowedModels.length) configs[id].allowedModels = allowedModels;
|
|
148
|
+
if (modelsPath) configs[id].modelsPath = modelsPath;
|
|
149
|
+
if (chatPath) configs[id].chatPath = chatPath;
|
|
150
|
+
}
|
|
151
|
+
writeStateImmediate(file, { providerConfigs: configs });
|
|
152
|
+
return clean;
|
|
153
|
+
}
|
|
154
|
+
export function saveProviderBaseUrl(id, baseUrl, { file = defaultStateFile() } = {}) {
|
|
155
|
+
const clean = _normalizeBaseUrl(baseUrl);
|
|
156
|
+
const configs = { ...loadProviderConfigs({ file }) };
|
|
157
|
+
const cur = configs[id] && typeof configs[id] === "object" ? configs[id] : {};
|
|
158
|
+
const keys = Array.isArray(cur.keys) ? cur.keys : loadProviderKeys(id, { file });
|
|
159
|
+
const auths = _normalizeAuths(cur.auths);
|
|
160
|
+
const allowedModels = Array.isArray(cur.allowedModels) ? [...new Set(cur.allowedModels.map((m) => _normalizeAllowedModel(m, id)).filter(Boolean))] : [];
|
|
161
|
+
const modelsPath = typeof cur.modelsPath === "string" ? _normalizeEndpointPath(cur.modelsPath) : "";
|
|
162
|
+
const chatPath = typeof cur.chatPath === "string" ? _normalizeEndpointPath(cur.chatPath) : "";
|
|
163
|
+
if (!clean && !keys.length && !auths.length && !allowedModels.length && !modelsPath && !chatPath) delete configs[id];
|
|
164
|
+
else {
|
|
165
|
+
configs[id] = { baseUrl: clean, keys };
|
|
166
|
+
if (auths.length) configs[id].auths = auths;
|
|
167
|
+
if (allowedModels.length) configs[id].allowedModels = allowedModels;
|
|
168
|
+
if (modelsPath) configs[id].modelsPath = modelsPath;
|
|
169
|
+
if (chatPath) configs[id].chatPath = chatPath;
|
|
170
|
+
}
|
|
171
|
+
writeStateImmediate(file, { providerConfigs: configs });
|
|
172
|
+
return clean;
|
|
173
|
+
}
|
|
174
|
+
export function saveProviderConfig(id, { baseUrl, keys, auths, allowedModels, modelsPath, chatPath }, { file = defaultStateFile() } = {}) {
|
|
175
|
+
const cleanUrl = _normalizeBaseUrl(baseUrl);
|
|
176
|
+
const cleanKeys = [...new Set((Array.isArray(keys) ? keys : []).map((k) => String(k || "").trim()).filter(Boolean))];
|
|
177
|
+
const cleanAuths = auths === undefined ? undefined : _normalizeAuths(auths);
|
|
178
|
+
const cleanAllowed = [...new Set((Array.isArray(allowedModels) ? allowedModels : []).map((m) => _normalizeAllowedModel(m, id)).filter(Boolean))];
|
|
179
|
+
const cleanModelsPath = modelsPath === undefined ? undefined : (String(modelsPath).trim() ? _normalizeEndpointPath(modelsPath) : "");
|
|
180
|
+
const cleanChatPath = chatPath === undefined ? undefined : (String(chatPath).trim() ? _normalizeEndpointPath(chatPath) : "");
|
|
181
|
+
const configs = { ...loadProviderConfigs({ file }) };
|
|
182
|
+
const cur = configs[id] && typeof configs[id] === "object" ? configs[id] : {};
|
|
183
|
+
const finalAllowed = allowedModels === undefined ? (Array.isArray(cur.allowedModels) ? [...new Set(cur.allowedModels.map((m) => _normalizeAllowedModel(m, id)).filter(Boolean))] : []) : cleanAllowed;
|
|
184
|
+
const finalAuths = cleanAuths === undefined ? _normalizeAuths(cur.auths) : cleanAuths;
|
|
185
|
+
const finalModelsPath = cleanModelsPath === undefined ? (typeof cur.modelsPath === "string" ? _normalizeEndpointPath(cur.modelsPath) : "") : cleanModelsPath;
|
|
186
|
+
const finalChatPath = cleanChatPath === undefined ? (typeof cur.chatPath === "string" ? _normalizeEndpointPath(cur.chatPath) : "") : cleanChatPath;
|
|
187
|
+
if (!cleanUrl && !cleanKeys.length && !finalAllowed.length && !finalAuths.length && !finalModelsPath && !finalChatPath) delete configs[id];
|
|
188
|
+
else {
|
|
189
|
+
configs[id] = { baseUrl: cleanUrl, keys: cleanKeys };
|
|
190
|
+
if (finalAuths.length) configs[id].auths = finalAuths;
|
|
191
|
+
if (finalAllowed.length) configs[id].allowedModels = finalAllowed;
|
|
192
|
+
if (finalModelsPath) configs[id].modelsPath = finalModelsPath;
|
|
193
|
+
if (finalChatPath) configs[id].chatPath = finalChatPath;
|
|
194
|
+
}
|
|
195
|
+
const oldKeys = readState(file).providerKeys;
|
|
196
|
+
if (oldKeys && typeof oldKeys === "object" && oldKeys[id] !== undefined) {
|
|
197
|
+
const nextKeys = { ...oldKeys };
|
|
198
|
+
delete nextKeys[id];
|
|
199
|
+
writeStateImmediate(file, { providerKeys: nextKeys, providerConfigs: configs });
|
|
200
|
+
return { baseUrl: cleanUrl, keys: cleanKeys, auths: finalAuths, allowedModels: finalAllowed, modelsPath: finalModelsPath, chatPath: finalChatPath };
|
|
201
|
+
}
|
|
202
|
+
writeStateImmediate(file, { providerConfigs: configs });
|
|
203
|
+
return { baseUrl: cleanUrl, keys: cleanKeys, auths: finalAuths, allowedModels: finalAllowed, modelsPath: finalModelsPath, chatPath: finalChatPath };
|
|
204
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { defaultStateFile, tokenFile, generateToken, readState, writeStateImmediate, getEntry } from "../store.js";
|
|
4
|
+
|
|
5
|
+
function syncTokenFile(token, file) {
|
|
6
|
+
try {
|
|
7
|
+
const tf = tokenFile(file);
|
|
8
|
+
mkdirSync(dirname(tf), { recursive: true });
|
|
9
|
+
writeFileSync(tf, String(token || "").trim() + "\n", "utf8");
|
|
10
|
+
} catch {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function loadToken({ file = defaultStateFile() } = {}) {
|
|
14
|
+
const state = readState(file);
|
|
15
|
+
if (typeof state.token === "string" && state.token.length > 0) {
|
|
16
|
+
syncTokenFile(state.token, file);
|
|
17
|
+
return { token: state.token, created: false };
|
|
18
|
+
}
|
|
19
|
+
if (existsSync(file)) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
22
|
+
if (typeof raw.token === "string" && raw.token.length > 0) {
|
|
23
|
+
syncTokenFile(raw.token, file);
|
|
24
|
+
const e = getEntry(file);
|
|
25
|
+
e.data = raw;
|
|
26
|
+
try { e.mtimeMs = statSync(file).mtimeMs; } catch {}
|
|
27
|
+
return { token: raw.token, created: false };
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
const tok = generateToken();
|
|
32
|
+
const saved = writeStateImmediate(file, { token: tok, createdAt: new Date().toISOString() }).token;
|
|
33
|
+
syncTokenFile(saved, file);
|
|
34
|
+
return { token: saved, created: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function refreshToken({ file = defaultStateFile() } = {}) {
|
|
38
|
+
const tok = writeStateImmediate(file, { token: generateToken(), createdAt: new Date().toISOString() }).token;
|
|
39
|
+
syncTokenFile(tok, file);
|
|
40
|
+
return tok;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { generateToken, tokenFile };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { loadFromDisk, atomicWriteSync, getMtimeMs, ensureDir } from "./persist.js";
|
|
7
|
+
import { mergeState, COLD_WINS } from "./merge.js";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_PORT = 8989;
|
|
10
|
+
|
|
11
|
+
function isTestEnv() {
|
|
12
|
+
if (process.env.NODE_ENV === "test") return true;
|
|
13
|
+
if (process.env.MSLXDFF_STATE_FILE && String(process.env.MSLXDFF_STATE_FILE).includes("mslxdff-test")) return true;
|
|
14
|
+
if (process.argv.some((a) => String(a).includes("--test") || String(a).endsWith(".test.js"))) return true;
|
|
15
|
+
if (Array.isArray(process.execArgv) && process.execArgv.some((a) => String(a).includes("--test"))) return true;
|
|
16
|
+
if (process.env.NODE_TEST_CONTEXT) return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function defaultStateFile() {
|
|
21
|
+
if (process.env.MSLXDFF_STATE_FILE) return process.env.MSLXDFF_STATE_FILE;
|
|
22
|
+
if (isTestEnv()) return join(os.tmpdir(), "mslxdff-test-state.json");
|
|
23
|
+
return join(os.homedir(), ".config", "mslxdff", "state.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function tokenFile(file) {
|
|
27
|
+
if (process.env.MSLXDFF_TOKEN_FILE) return process.env.MSLXDFF_TOKEN_FILE;
|
|
28
|
+
const sf = file || defaultStateFile();
|
|
29
|
+
const realDefault = join(os.homedir(), ".config", "mslxdff", "state.json");
|
|
30
|
+
if (sf !== realDefault) return join(dirname(sf), "token");
|
|
31
|
+
if (isTestEnv()) return join(os.tmpdir(), "mslxdff-test-token");
|
|
32
|
+
return join(os.homedir(), ".config", "mslxdff", "token");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function generateToken() {
|
|
36
|
+
return randomBytes(32).toString("hex");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---- 缓存层:读走内存,写分“热/冷”两档,热数据 500ms 批量刷盘 ----
|
|
40
|
+
const stateCache = new Map(); // file -> { data, dirty, timer, mtimeMs }
|
|
41
|
+
const FLUSH_MS = (() => {
|
|
42
|
+
const n = Number(process.env.MSLXDFF_STATE_FLUSH_MS);
|
|
43
|
+
return Number.isInteger(n) && n >= 0 ? n : 500;
|
|
44
|
+
})();
|
|
45
|
+
|
|
46
|
+
function getEntry(file) {
|
|
47
|
+
let e = stateCache.get(file);
|
|
48
|
+
if (!e) {
|
|
49
|
+
e = { data: null, dirty: false, timer: null, mtimeMs: 0 };
|
|
50
|
+
stateCache.set(file, e);
|
|
51
|
+
}
|
|
52
|
+
return e;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 合并规则已抽至 merge.js 单一真相
|
|
56
|
+
|
|
57
|
+
function readState(file) {
|
|
58
|
+
const e = getEntry(file);
|
|
59
|
+
if (e.data !== null) {
|
|
60
|
+
try {
|
|
61
|
+
const st = statSync(file);
|
|
62
|
+
if (st.mtimeMs === e.mtimeMs) return e.data;
|
|
63
|
+
if (e.dirty) {
|
|
64
|
+
const disk = loadFromDisk(file);
|
|
65
|
+
const diskObj = typeof disk === "object" && disk !== null ? disk : {};
|
|
66
|
+
const merged = mergeState(diskObj, e.data);
|
|
67
|
+
e.data = merged;
|
|
68
|
+
e.mtimeMs = st.mtimeMs;
|
|
69
|
+
return e.data;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
if (e.data) return e.data;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const disk = loadFromDisk(file);
|
|
76
|
+
const obj = typeof disk === "object" && disk !== null ? disk : {};
|
|
77
|
+
e.data = obj;
|
|
78
|
+
try {
|
|
79
|
+
const st = statSync(file);
|
|
80
|
+
e.mtimeMs = st.mtimeMs;
|
|
81
|
+
} catch {
|
|
82
|
+
e.mtimeMs = Date.now();
|
|
83
|
+
}
|
|
84
|
+
e.dirty = false;
|
|
85
|
+
return e.data;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeStateImmediate(file, patch) {
|
|
89
|
+
const e = getEntry(file);
|
|
90
|
+
const base = e.data !== null ? e.data : readState(file);
|
|
91
|
+
const merged = { ...base, ...patch };
|
|
92
|
+
e.data = merged;
|
|
93
|
+
e.dirty = false;
|
|
94
|
+
if (e.timer) {
|
|
95
|
+
clearTimeout(e.timer);
|
|
96
|
+
e.timer = null;
|
|
97
|
+
}
|
|
98
|
+
ensureDir(file);
|
|
99
|
+
const mtime = atomicWriteSync(file, merged);
|
|
100
|
+
e.mtimeMs = mtime;
|
|
101
|
+
return merged;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function scheduleFlush(file) {
|
|
105
|
+
const e = getEntry(file);
|
|
106
|
+
if (e.timer) return;
|
|
107
|
+
if (FLUSH_MS === 0) {
|
|
108
|
+
flushStateSync(file);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
e.timer = setTimeout(() => {
|
|
112
|
+
e.timer = null;
|
|
113
|
+
void flushState(file);
|
|
114
|
+
}, FLUSH_MS);
|
|
115
|
+
e.timer.unref?.();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function flushState(file) {
|
|
119
|
+
const e = stateCache.get(file);
|
|
120
|
+
if (!e || !e.dirty || !e.data) return;
|
|
121
|
+
const snapshot = e.data;
|
|
122
|
+
try {
|
|
123
|
+
ensureDir(file);
|
|
124
|
+
const mtime = await atomicWriteAsync(file, snapshot);
|
|
125
|
+
e.dirty = false;
|
|
126
|
+
e.mtimeMs = mtime;
|
|
127
|
+
} catch {
|
|
128
|
+
// retain dirty
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeStateDeferred(file, patch) {
|
|
133
|
+
const e = getEntry(file);
|
|
134
|
+
const base = e.data !== null ? e.data : readState(file);
|
|
135
|
+
const merged = { ...base, ...patch };
|
|
136
|
+
e.data = merged;
|
|
137
|
+
e.dirty = true;
|
|
138
|
+
scheduleFlush(file);
|
|
139
|
+
return merged;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function flushStateSync(file = defaultStateFile()) {
|
|
143
|
+
const e = stateCache.get(file);
|
|
144
|
+
if (!e || !e.dirty || !e.data) return;
|
|
145
|
+
try {
|
|
146
|
+
ensureDir(file);
|
|
147
|
+
const mtime = atomicWriteSync(file, e.data);
|
|
148
|
+
e.dirty = false;
|
|
149
|
+
e.mtimeMs = mtime;
|
|
150
|
+
if (e.timer) {
|
|
151
|
+
clearTimeout(e.timer);
|
|
152
|
+
e.timer = null;
|
|
153
|
+
}
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function clearStateCache(file) {
|
|
158
|
+
if (file) stateCache.delete(file);
|
|
159
|
+
else stateCache.clear();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// internal helper for facade compat
|
|
163
|
+
function writeState(file, patch) {
|
|
164
|
+
return writeStateImmediate(file, patch);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export {
|
|
168
|
+
readState,
|
|
169
|
+
writeStateImmediate,
|
|
170
|
+
writeStateDeferred,
|
|
171
|
+
scheduleFlush,
|
|
172
|
+
flushState,
|
|
173
|
+
writeState,
|
|
174
|
+
getEntry,
|
|
175
|
+
stateCache,
|
|
176
|
+
};
|