codexmate 0.0.28 → 0.0.30
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/cli/builtin-proxy.js +107 -2
- package/cli/config-bootstrap.js +30 -12
- package/cli/config-health.js +117 -1
- package/cli/local-bridge.js +324 -0
- package/cli/openai-bridge.js +195 -31
- package/cli.js +245 -28
- package/lib/cli-webhook.js +126 -0
- package/package.json +1 -1
- package/web-ui/app.js +28 -8
- package/web-ui/index.html +1 -0
- package/web-ui/logic.codex.mjs +13 -0
- package/web-ui/modules/app.computed.dashboard.mjs +25 -2
- package/web-ui/modules/app.computed.session.mjs +22 -17
- package/web-ui/modules/app.methods.claude-config.mjs +12 -2
- package/web-ui/modules/app.methods.codex-config.mjs +25 -0
- package/web-ui/modules/app.methods.index.mjs +2 -0
- package/web-ui/modules/app.methods.navigation.mjs +39 -8
- package/web-ui/modules/app.methods.providers.mjs +125 -8
- package/web-ui/modules/app.methods.session-actions.mjs +1 -1
- package/web-ui/modules/app.methods.session-browser.mjs +1 -1
- package/web-ui/modules/app.methods.session-trash.mjs +3 -4
- package/web-ui/modules/app.methods.startup-claude.mjs +1 -0
- package/web-ui/modules/app.methods.webhook.mjs +79 -0
- package/web-ui/modules/i18n.dict.mjs +1109 -72
- package/web-ui/modules/i18n.mjs +9 -3
- package/web-ui/modules/skills.methods.mjs +1 -0
- package/web-ui/partials/index/layout-header.html +25 -0
- package/web-ui/partials/index/modals-basic.html +0 -3
- package/web-ui/partials/index/panel-config-claude.html +8 -2
- package/web-ui/partials/index/panel-config-codex.html +28 -3
- package/web-ui/partials/index/panel-dashboard.html +33 -0
- package/web-ui/partials/index/panel-market.html +3 -3
- package/web-ui/partials/index/panel-plugins.html +2 -2
- package/web-ui/partials/index/panel-sessions.html +1 -9
- package/web-ui/partials/index/panel-settings.html +71 -134
- package/web-ui/partials/index/panel-trash.html +88 -0
- package/web-ui/session-helpers.mjs +20 -2
- package/web-ui/styles/dashboard.css +132 -0
- package/web-ui/styles/docs-panel.css +63 -39
- package/web-ui/styles/layout-shell.css +54 -34
- package/web-ui/styles/plugins-panel.css +121 -80
- package/web-ui/styles/sessions-list.css +41 -43
- package/web-ui/styles/sessions-preview.css +34 -38
- package/web-ui/styles/sessions-toolbar-trash.css +31 -27
- package/web-ui/styles/settings-panel.css +197 -33
- package/web-ui/styles/skills-list.css +12 -10
- package/web-ui/styles/skills-market.css +67 -44
- package/web-ui/styles/trash-panel.css +90 -0
- package/web-ui/styles/webhook.css +81 -0
- package/web-ui/styles.css +2 -0
package/cli/builtin-proxy.js
CHANGED
|
@@ -127,6 +127,42 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
function isTransientNetworkError(error) {
|
|
131
|
+
const text = String(error || '').trim();
|
|
132
|
+
if (!text) return false;
|
|
133
|
+
if (/socket hang up/i.test(text)) return true;
|
|
134
|
+
if (/ECONNRESET|ECONNREFUSED|EPIPE|EPROTO|ETIMEDOUT/i.test(text)) return true;
|
|
135
|
+
if (/EAI_AGAIN/i.test(text)) return true;
|
|
136
|
+
if (/UND_ERR_SOCKET/i.test(text)) return true;
|
|
137
|
+
if (/disconnected before|secure tls|tls handshake/i.test(text)) return true;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const TRANSIENT_RETRY_DELAYS_MS = [200, 600];
|
|
142
|
+
|
|
143
|
+
async function retryTransientRequest(executor) {
|
|
144
|
+
let lastResult = null;
|
|
145
|
+
for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
146
|
+
if (attempt > 0) {
|
|
147
|
+
const delay = TRANSIENT_RETRY_DELAYS_MS[attempt - 1];
|
|
148
|
+
// eslint-disable-next-line no-await-in-loop
|
|
149
|
+
await new Promise((r) => {
|
|
150
|
+
const t = setTimeout(r, delay);
|
|
151
|
+
if (typeof t.unref === 'function') t.unref();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// eslint-disable-next-line no-await-in-loop
|
|
155
|
+
const result = await executor(attempt);
|
|
156
|
+
lastResult = result;
|
|
157
|
+
if (!result) return result;
|
|
158
|
+
if (result.ok) return result;
|
|
159
|
+
if (result.retry) return result;
|
|
160
|
+
if (result.status && result.status > 0) return result;
|
|
161
|
+
if (!isTransientNetworkError(result.error)) return result;
|
|
162
|
+
}
|
|
163
|
+
return lastResult;
|
|
164
|
+
}
|
|
165
|
+
|
|
130
166
|
function proxyRequestJson(targetUrl, options = {}) {
|
|
131
167
|
const parsed = new URL(targetUrl);
|
|
132
168
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
@@ -206,7 +242,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
206
242
|
}
|
|
207
243
|
let lastResult = null;
|
|
208
244
|
for (let index = 0; index < urls.length; index += 1) {
|
|
209
|
-
const result = await proxyRequestJson(urls[index], options);
|
|
245
|
+
const result = await retryTransientRequest(() => proxyRequestJson(urls[index], options));
|
|
210
246
|
lastResult = result;
|
|
211
247
|
if (!result.ok) {
|
|
212
248
|
return result;
|
|
@@ -702,9 +738,34 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
702
738
|
}
|
|
703
739
|
}
|
|
704
740
|
|
|
741
|
+
function stopChatStreamHeartbeat(state) {
|
|
742
|
+
if (!state || !state.heartbeatTimer) return;
|
|
743
|
+
clearInterval(state.heartbeatTimer);
|
|
744
|
+
state.heartbeatTimer = null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function startChatStreamHeartbeat(state) {
|
|
748
|
+
if (!state || state.heartbeatTimer) return;
|
|
749
|
+
const timer = setInterval(() => {
|
|
750
|
+
if (state.finished) {
|
|
751
|
+
stopChatStreamHeartbeat(state);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const target = state.res;
|
|
755
|
+
if (!target || target.writableEnded || target.destroyed) {
|
|
756
|
+
stopChatStreamHeartbeat(state);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
try { target.write(': keepalive\n\n'); } catch (_) {}
|
|
760
|
+
}, 15000);
|
|
761
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
762
|
+
state.heartbeatTimer = timer;
|
|
763
|
+
}
|
|
764
|
+
|
|
705
765
|
function finishChatStreamResponsesSse(state) {
|
|
706
766
|
if (state.finished) return;
|
|
707
767
|
state.finished = true;
|
|
768
|
+
stopChatStreamHeartbeat(state);
|
|
708
769
|
|
|
709
770
|
if (state.messageItem) {
|
|
710
771
|
const outputIndex = state.output.indexOf(state.messageItem);
|
|
@@ -759,6 +820,22 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
759
820
|
state.res.end();
|
|
760
821
|
}
|
|
761
822
|
|
|
823
|
+
function failResponsesSseRaw(res, message) {
|
|
824
|
+
if (!res || res.writableEnded || res.destroyed) return;
|
|
825
|
+
try {
|
|
826
|
+
writeSse(res, 'response.failed', { type: 'response.failed', error: message || 'upstream stream failed' });
|
|
827
|
+
writeSse(res, 'done', '[DONE]');
|
|
828
|
+
res.end();
|
|
829
|
+
} catch (_) {}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function failChatStreamResponsesSse(state, message) {
|
|
833
|
+
if (!state || state.finished) return;
|
|
834
|
+
state.finished = true;
|
|
835
|
+
stopChatStreamHeartbeat(state);
|
|
836
|
+
failResponsesSseRaw(state.res, message);
|
|
837
|
+
}
|
|
838
|
+
|
|
762
839
|
function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
|
|
763
840
|
const parsed = new URL(targetUrl);
|
|
764
841
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
@@ -796,6 +873,29 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
796
873
|
const status = upstreamRes.statusCode || 0;
|
|
797
874
|
const chunks = [];
|
|
798
875
|
const contentType = String(upstreamRes.headers && upstreamRes.headers['content-type'] || '');
|
|
876
|
+
let streamState = null;
|
|
877
|
+
|
|
878
|
+
const handleAbort = (reason) => {
|
|
879
|
+
if (settled) return;
|
|
880
|
+
if (streamState) {
|
|
881
|
+
failChatStreamResponsesSse(streamState, reason);
|
|
882
|
+
finish({ ok: true });
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (res.headersSent) {
|
|
886
|
+
failResponsesSseRaw(res, reason);
|
|
887
|
+
finish({ ok: true });
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
finish({
|
|
891
|
+
ok: false,
|
|
892
|
+
status,
|
|
893
|
+
error: reason,
|
|
894
|
+
bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : ''
|
|
895
|
+
});
|
|
896
|
+
};
|
|
897
|
+
upstreamRes.on('error', (err) => handleAbort(err && err.message ? err.message : 'upstream stream failed'));
|
|
898
|
+
upstreamRes.on('aborted', () => handleAbort('upstream stream aborted'));
|
|
799
899
|
|
|
800
900
|
if (status === 404 || status === 405) {
|
|
801
901
|
upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
|
|
@@ -851,6 +951,11 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
851
951
|
return sequence;
|
|
852
952
|
}
|
|
853
953
|
};
|
|
954
|
+
streamState = state;
|
|
955
|
+
startChatStreamHeartbeat(state);
|
|
956
|
+
if (typeof res.on === 'function') {
|
|
957
|
+
res.on('close', () => stopChatStreamHeartbeat(state));
|
|
958
|
+
}
|
|
854
959
|
writeSse(res, 'response.created', {
|
|
855
960
|
type: 'response.created',
|
|
856
961
|
response: {
|
|
@@ -914,7 +1019,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
914
1019
|
}
|
|
915
1020
|
let lastResult = null;
|
|
916
1021
|
for (const url of urls) {
|
|
917
|
-
const result = await streamChatCompletionsAsResponsesSse(url, options);
|
|
1022
|
+
const result = await retryTransientRequest(() => streamChatCompletionsAsResponsesSse(url, options));
|
|
918
1023
|
lastResult = result;
|
|
919
1024
|
if (result && result.retry) continue;
|
|
920
1025
|
return result;
|
package/cli/config-bootstrap.js
CHANGED
|
@@ -30,6 +30,7 @@ function createConfigBootstrapController(deps = {}) {
|
|
|
30
30
|
DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
|
|
31
31
|
CODEXMATE_MANAGED_MARKER,
|
|
32
32
|
BUILTIN_PROXY_PROVIDER_NAME,
|
|
33
|
+
BUILTIN_LOCAL_PROVIDER_NAME,
|
|
33
34
|
EMPTY_CONFIG_FALLBACK_TEMPLATE
|
|
34
35
|
} = deps;
|
|
35
36
|
|
|
@@ -58,6 +59,7 @@ function createConfigBootstrapController(deps = {}) {
|
|
|
58
59
|
if (!Array.isArray(DEFAULT_MODELS)) throw new Error('createConfigBootstrapController 缺少 DEFAULT_MODELS');
|
|
59
60
|
if (!CODEXMATE_MANAGED_MARKER) throw new Error('createConfigBootstrapController 缺少 CODEXMATE_MANAGED_MARKER');
|
|
60
61
|
if (!BUILTIN_PROXY_PROVIDER_NAME) throw new Error('createConfigBootstrapController 缺少 BUILTIN_PROXY_PROVIDER_NAME');
|
|
62
|
+
if (!BUILTIN_LOCAL_PROVIDER_NAME) throw new Error('createConfigBootstrapController 缺少 BUILTIN_LOCAL_PROVIDER_NAME');
|
|
61
63
|
if (typeof EMPTY_CONFIG_FALLBACK_TEMPLATE !== 'string') throw new Error('createConfigBootstrapController 缺少 EMPTY_CONFIG_FALLBACK_TEMPLATE');
|
|
62
64
|
|
|
63
65
|
let initNotice = '';
|
|
@@ -118,17 +120,18 @@ function createConfigBootstrapController(deps = {}) {
|
|
|
118
120
|
return `${CODEXMATE_MANAGED_MARKER}
|
|
119
121
|
# codexmate-initialized-at: ${initializedAt}
|
|
120
122
|
|
|
121
|
-
model_provider = "
|
|
123
|
+
model_provider = "local"
|
|
122
124
|
model = "${defaultModel}"
|
|
123
125
|
model_context_window = ${DEFAULT_MODEL_CONTEXT_WINDOW}
|
|
124
126
|
model_auto_compact_token_limit = ${DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT}
|
|
125
127
|
|
|
126
|
-
[model_providers.
|
|
127
|
-
name = "
|
|
128
|
-
base_url = "
|
|
128
|
+
[model_providers.local]
|
|
129
|
+
name = "local"
|
|
130
|
+
base_url = "http://127.0.0.1:3737/bridge/local/v1"
|
|
129
131
|
wire_api = "responses"
|
|
130
|
-
requires_openai_auth =
|
|
131
|
-
preferred_auth_method = ""
|
|
132
|
+
requires_openai_auth = true
|
|
133
|
+
preferred_auth_method = "codexmate"
|
|
134
|
+
codexmate_bridge = "local"
|
|
132
135
|
request_max_retries = 4
|
|
133
136
|
stream_max_retries = 10
|
|
134
137
|
stream_idle_timeout_ms = 300000
|
|
@@ -145,9 +148,8 @@ stream_idle_timeout_ms = 300000
|
|
|
145
148
|
const currentProvider = typeof safeConfig.model_provider === 'string' ? safeConfig.model_provider.trim() : '';
|
|
146
149
|
const hasRemovedBuiltin = !!(providers && providers[BUILTIN_PROXY_PROVIDER_NAME]);
|
|
147
150
|
const currentIsRemovedBuiltin = currentProvider === BUILTIN_PROXY_PROVIDER_NAME;
|
|
148
|
-
const currentIsRemovedVirtualLocal = currentProvider === 'local' && !(providers && isPlainObject(providers.local));
|
|
149
151
|
|
|
150
|
-
if (!hasRemovedBuiltin && !currentIsRemovedBuiltin
|
|
152
|
+
if (!hasRemovedBuiltin && !currentIsRemovedBuiltin) {
|
|
151
153
|
return safeConfig;
|
|
152
154
|
}
|
|
153
155
|
|
|
@@ -163,11 +165,26 @@ stream_idle_timeout_ms = 300000
|
|
|
163
165
|
return {
|
|
164
166
|
...safeConfig,
|
|
165
167
|
model_providers: nextProviders,
|
|
166
|
-
model_provider:
|
|
167
|
-
model:
|
|
168
|
+
model_provider: currentIsRemovedBuiltin ? fallbackProvider : safeConfig.model_provider,
|
|
169
|
+
model: currentIsRemovedBuiltin ? fallbackModel : safeConfig.model
|
|
168
170
|
};
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
function ensureLocalProviderSection() {
|
|
174
|
+
if (!fs.existsSync(CONFIG_FILE)) return;
|
|
175
|
+
let content;
|
|
176
|
+
try {
|
|
177
|
+
content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Check if [model_providers.local] section already exists
|
|
182
|
+
if (/\[model_providers\.local\]/.test(content)) return;
|
|
183
|
+
|
|
184
|
+
const localSection = `\n[model_providers.local]\nname = "local"\nbase_url = "http://127.0.0.1:3737/bridge/local/v1"\nwire_api = "responses"\nrequires_openai_auth = true\npreferred_auth_method = "codexmate"\ncodexmate_bridge = "local"\nrequest_max_retries = 4\nstream_max_retries = 10\nstream_idle_timeout_ms = 300000\n`;
|
|
185
|
+
fs.appendFileSync(CONFIG_FILE, localSection, 'utf-8');
|
|
186
|
+
}
|
|
187
|
+
|
|
171
188
|
function readConfigOrVirtualDefault() {
|
|
172
189
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
173
190
|
try {
|
|
@@ -260,7 +277,7 @@ stream_idle_timeout_ms = 300000
|
|
|
260
277
|
ensureConfigDir();
|
|
261
278
|
|
|
262
279
|
const initializedAt = new Date().toISOString();
|
|
263
|
-
const defaultProvider = '
|
|
280
|
+
const defaultProvider = 'local';
|
|
264
281
|
const defaultModel = DEFAULT_MODELS[0] || 'gpt-4';
|
|
265
282
|
const forceResetExistingConfig = process.env.CODEXMATE_FORCE_RESET_EXISTING_CONFIG === '1';
|
|
266
283
|
const mark = readJsonFile(INIT_MARK_FILE, null);
|
|
@@ -273,6 +290,7 @@ stream_idle_timeout_ms = 300000
|
|
|
273
290
|
initNotice = '检测到配置缺失,已自动重建默认配置。';
|
|
274
291
|
return { notice: initNotice };
|
|
275
292
|
}
|
|
293
|
+
ensureLocalProviderSection();
|
|
276
294
|
ensureSupportFiles(defaultProvider, defaultModel);
|
|
277
295
|
return { notice: '' };
|
|
278
296
|
}
|
|
@@ -338,7 +356,7 @@ stream_idle_timeout_ms = 300000
|
|
|
338
356
|
function resetConfigToDefault() {
|
|
339
357
|
ensureConfigDir();
|
|
340
358
|
const initializedAt = new Date().toISOString();
|
|
341
|
-
const defaultProvider = '
|
|
359
|
+
const defaultProvider = 'local';
|
|
342
360
|
const defaultModel = DEFAULT_MODELS[0] || 'gpt-4';
|
|
343
361
|
|
|
344
362
|
let backupFile = '';
|
package/cli/config-health.js
CHANGED
|
@@ -332,7 +332,123 @@ async function buildConfigHealthReport(params = {}, deps = {}) {
|
|
|
332
332
|
};
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
async function buildAllProvidersHealthReport(params = {}, deps = {}) {
|
|
336
|
+
const {
|
|
337
|
+
readConfigOrVirtualDefault,
|
|
338
|
+
readCurrentModels,
|
|
339
|
+
probeJsonPost: probeJsonPostDep
|
|
340
|
+
} = deps;
|
|
341
|
+
|
|
342
|
+
if (typeof readConfigOrVirtualDefault !== 'function') {
|
|
343
|
+
throw new Error('buildAllProvidersHealthReport 缺少 readConfigOrVirtualDefault 依赖');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const status = readConfigOrVirtualDefault();
|
|
347
|
+
const config = status.config || {};
|
|
348
|
+
const providerEntries = config.model_providers && typeof config.model_providers === 'object'
|
|
349
|
+
? Object.entries(config.model_providers)
|
|
350
|
+
: [];
|
|
351
|
+
const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
352
|
+
const currentModels = typeof readCurrentModels === 'function' ? readCurrentModels() : {};
|
|
353
|
+
|
|
354
|
+
if (!providerEntries.length) {
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
currentProvider,
|
|
358
|
+
providers: [],
|
|
359
|
+
summary: { total: 0, green: 0, yellow: 0, red: 0 }
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const remote = !!(params && params.remote);
|
|
364
|
+
const timeoutMs = Number.isFinite(params && params.timeoutMs) ? Number(params.timeoutMs) : DEFAULT_TIMEOUT_MS;
|
|
365
|
+
|
|
366
|
+
const entries = providerEntries.map(([name, provider]) => {
|
|
367
|
+
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
368
|
+
const requiresAuth = provider.requires_openai_auth !== false;
|
|
369
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
370
|
+
? provider.preferred_auth_method.trim() : '';
|
|
371
|
+
const modelName = currentModels[name] || (name === currentProvider ? (config.model || '').trim() : '');
|
|
372
|
+
|
|
373
|
+
const checks = {
|
|
374
|
+
baseUrlValid: isValidHttpUrl(baseUrl),
|
|
375
|
+
apiKeyPresent: !requiresAuth || !!apiKey
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const issues = [];
|
|
379
|
+
if (!baseUrl) {
|
|
380
|
+
issues.push({ code: 'base-url-empty', message: `${name} 的 base_url 为空`, suggestion: '设置 base_url' });
|
|
381
|
+
} else if (!checks.baseUrlValid) {
|
|
382
|
+
issues.push({ code: 'base-url-invalid', message: `${name} 的 base_url 无效`, suggestion: '设置为 http/https 完整 URL' });
|
|
383
|
+
}
|
|
384
|
+
if (!checks.apiKeyPresent) {
|
|
385
|
+
issues.push({ code: 'api-key-missing', message: `${name} 未配置 API Key`, suggestion: '设置 preferred_auth_method' });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { name, provider, baseUrl, modelName, checks, issues };
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (remote) {
|
|
392
|
+
const remotePromises = entries.map(async (entry) => {
|
|
393
|
+
if (!entry.checks.baseUrlValid || !entry.modelName) {
|
|
394
|
+
entry.remote = null;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const report = await runRemoteHealthCheck(entry.name, entry.provider, entry.modelName, {
|
|
399
|
+
timeoutMs,
|
|
400
|
+
probeJsonPost: probeJsonPostDep
|
|
401
|
+
});
|
|
402
|
+
entry.remote = report.remote;
|
|
403
|
+
entry.issues.push(...report.issues);
|
|
404
|
+
} catch (e) {
|
|
405
|
+
entry.remote = null;
|
|
406
|
+
entry.issues.push({
|
|
407
|
+
code: 'remote-probe-error',
|
|
408
|
+
message: `${entry.name} 远程探测异常: ${e.message}`,
|
|
409
|
+
suggestion: '检查网络连接与 endpoint'
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
await Promise.allSettled(remotePromises);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const providers = entries.map((entry) => {
|
|
417
|
+
const hasConfigIssue = !entry.checks.baseUrlValid || !entry.checks.apiKeyPresent;
|
|
418
|
+
const remoteFailed = entry.remote && !entry.remote.ok;
|
|
419
|
+
let statusValue = 'green';
|
|
420
|
+
if (remoteFailed) {
|
|
421
|
+
statusValue = 'red';
|
|
422
|
+
} else if (hasConfigIssue) {
|
|
423
|
+
statusValue = 'yellow';
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
provider: entry.name,
|
|
427
|
+
isCurrent: entry.name === currentProvider,
|
|
428
|
+
status: statusValue,
|
|
429
|
+
checks: entry.checks,
|
|
430
|
+
remote: entry.remote || null,
|
|
431
|
+
issues: entry.issues
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const summary = {
|
|
436
|
+
total: providers.length,
|
|
437
|
+
green: providers.filter((p) => p.status === 'green').length,
|
|
438
|
+
yellow: providers.filter((p) => p.status === 'yellow').length,
|
|
439
|
+
red: providers.filter((p) => p.status === 'red').length
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
ok: summary.red === 0 && summary.yellow === 0,
|
|
444
|
+
currentProvider,
|
|
445
|
+
providers,
|
|
446
|
+
summary
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
335
450
|
module.exports = {
|
|
336
451
|
runRemoteHealthCheck,
|
|
337
|
-
buildConfigHealthReport
|
|
452
|
+
buildConfigHealthReport,
|
|
453
|
+
buildAllProvidersHealthReport
|
|
338
454
|
};
|