codexmate 0.0.22 → 0.0.24

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.
Files changed (66) hide show
  1. package/README.md +5 -3
  2. package/README.zh.md +8 -5
  3. package/cli/auth-profiles.js +23 -7
  4. package/cli/doctor-core.js +903 -0
  5. package/cli/import-skills-url.js +334 -0
  6. package/cli.js +304 -208
  7. package/lib/cli-models-utils.js +0 -40
  8. package/lib/cli-network-utils.js +28 -2
  9. package/package.json +5 -2
  10. package/plugins/README.md +20 -0
  11. package/plugins/README.zh-CN.md +20 -0
  12. package/plugins/prompt-templates/comment-polish/index.mjs +25 -0
  13. package/plugins/prompt-templates/computed.mjs +253 -0
  14. package/plugins/prompt-templates/index.mjs +8 -0
  15. package/plugins/prompt-templates/manifest.mjs +15 -0
  16. package/plugins/prompt-templates/methods.mjs +619 -0
  17. package/plugins/prompt-templates/overview.mjs +90 -0
  18. package/plugins/prompt-templates/ownership.mjs +19 -0
  19. package/plugins/prompt-templates/rule-ack/index.mjs +21 -0
  20. package/plugins/prompt-templates/storage.mjs +64 -0
  21. package/plugins/registry.mjs +16 -0
  22. package/res/logo-pack.webp +0 -0
  23. package/web-ui/app.js +68 -34
  24. package/web-ui/index.html +4 -3
  25. package/web-ui/modules/app.computed.dashboard.mjs +22 -22
  26. package/web-ui/modules/app.computed.main-tabs.mjs +9 -2
  27. package/web-ui/modules/app.methods.agents.mjs +91 -3
  28. package/web-ui/modules/app.methods.codex-config.mjs +153 -164
  29. package/web-ui/modules/app.methods.install.mjs +16 -0
  30. package/web-ui/modules/app.methods.navigation.mjs +76 -0
  31. package/web-ui/modules/app.methods.runtime.mjs +24 -2
  32. package/web-ui/modules/app.methods.session-browser.mjs +73 -1
  33. package/web-ui/modules/app.methods.startup-claude.mjs +12 -0
  34. package/web-ui/modules/app.methods.task-orchestration.mjs +96 -11
  35. package/web-ui/modules/config-mode.computed.mjs +1 -3
  36. package/web-ui/modules/i18n.dict.mjs +2039 -0
  37. package/web-ui/modules/i18n.mjs +2 -1555
  38. package/web-ui/modules/plugins.computed.mjs +2 -219
  39. package/web-ui/modules/plugins.methods.mjs +2 -619
  40. package/web-ui/modules/plugins.storage.mjs +11 -37
  41. package/web-ui/modules/sessions-filters-url.mjs +85 -0
  42. package/web-ui/partials/index/layout-header.html +38 -34
  43. package/web-ui/partials/index/modal-config-template-agents.html +3 -4
  44. package/web-ui/partials/index/modal-health-check.html +33 -60
  45. package/web-ui/partials/index/panel-config-claude.html +56 -15
  46. package/web-ui/partials/index/panel-config-codex.html +68 -19
  47. package/web-ui/partials/index/panel-config-openclaw.html +8 -3
  48. package/web-ui/partials/index/panel-dashboard.html +186 -0
  49. package/web-ui/partials/index/panel-docs.html +1 -1
  50. package/web-ui/partials/index/panel-market.html +3 -0
  51. package/web-ui/partials/index/panel-orchestration.html +105 -111
  52. package/web-ui/partials/index/panel-plugins.html +48 -12
  53. package/web-ui/partials/index/panel-sessions.html +12 -3
  54. package/web-ui/partials/index/panel-settings.html +1 -1
  55. package/web-ui/partials/index/panel-usage.html +7 -6
  56. package/web-ui/styles/controls-forms.css +16 -2
  57. package/web-ui/styles/dashboard.css +274 -0
  58. package/web-ui/styles/layout-shell.css +11 -5
  59. package/web-ui/styles/navigation-panels.css +8 -0
  60. package/web-ui/styles/plugins-panel.css +5 -0
  61. package/web-ui/styles/sessions-list.css +3 -3
  62. package/web-ui/styles/sessions-usage.css +37 -0
  63. package/web-ui/styles/skills-market.css +12 -2
  64. package/web-ui/styles/task-orchestration.css +57 -11
  65. package/web-ui/styles.css +1 -0
  66. package/res/logo.png +0 -0
package/cli.js CHANGED
@@ -42,8 +42,9 @@ const {
42
42
  extractModelNames,
43
43
  hasModelsListPayload,
44
44
  buildModelsCacheKey,
45
+ buildApiProbeUrlCandidates,
45
46
  buildModelProbeSpec,
46
- buildModelConversationSpecs,
47
+ buildModelProbeSpecs,
47
48
  extractModelResponseText,
48
49
  normalizeWireApi,
49
50
  getSupplementalModelsForBaseUrl,
@@ -74,6 +75,7 @@ const {
74
75
  executeTaskPlan
75
76
  } = require('./lib/task-orchestrator');
76
77
  const { buildConfigHealthReport: buildConfigHealthReportCore } = require('./cli/config-health');
78
+ const { buildDoctorReport, buildDoctorLegacyPayload, renderDoctorMarkdown } = require('./cli/doctor-core');
77
79
  const {
78
80
  createAuthProfileController
79
81
  } = require('./cli/auth-profiles');
@@ -125,6 +127,7 @@ const {
125
127
  deleteSkills,
126
128
  deleteCodexSkills
127
129
  } = require('./cli/skills');
130
+ const { cmdImportSkills: cmdImportSkillsFromUrl } = require('./cli/import-skills-url');
128
131
  const {
129
132
  getFileStatSafe,
130
133
  isBootstrapLikeText,
@@ -4155,12 +4158,14 @@ async function listAllSessions(params = {}) {
4155
4158
  const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
4156
4159
  const hasQuery = queryTokens.length > 0;
4157
4160
  const browseLightweight = params.browseLightweight === true && !hasQuery && !hasPathFilter;
4158
- const cacheKey = hasQuery ? '' : `${browseLightweight ? 'browse' : 'default'}:${source}:${limit}:${normalizedPathFilter}`;
4159
- if (!hasQuery) {
4160
- const cached = getSessionListCache(cacheKey, forceRefresh);
4161
- if (cached) {
4162
- return cached;
4163
- }
4161
+ const queryKeyRaw = typeof params.query === 'string' ? params.query.trim() : '';
4162
+ const queryKey = queryKeyRaw.length > 240 ? queryKeyRaw.slice(0, 240) : queryKeyRaw;
4163
+ const cacheKey = hasQuery
4164
+ ? `query:${source}:${limit}:${normalizedPathFilter}:${params.queryMode || ''}:${params.queryScope || ''}:${params.roleFilter || ''}:${Number(params.contentScanLimit) || ''}:${Number(params.contentScanBytes) || ''}:${queryKey}`
4165
+ : `${browseLightweight ? 'browse' : 'default'}:${source}:${limit}:${normalizedPathFilter}`;
4166
+ const cached = getSessionListCache(cacheKey, forceRefresh);
4167
+ if (cached) {
4168
+ return cached;
4164
4169
  }
4165
4170
 
4166
4171
  const scanOptions = hasPathFilter
@@ -4201,9 +4206,7 @@ async function listAllSessions(params = {}) {
4201
4206
  });
4202
4207
  }
4203
4208
  result = mergeAndLimitSessions(result, limit);
4204
- if (!hasQuery) {
4205
- setSessionListCache(cacheKey, result);
4206
- }
4209
+ setSessionListCache(cacheKey, result);
4207
4210
  return result;
4208
4211
  }
4209
4212
 
@@ -6003,6 +6006,60 @@ function importConfigData(payload, options = {}) {
6003
6006
  function resolveSpeedTestTarget(params) {
6004
6007
  if (!params) return { error: 'Missing params' };
6005
6008
 
6009
+ if (typeof params.kind === 'string' && params.kind.trim() === 'claude') {
6010
+ const baseUrl = typeof params.url === 'string' ? params.url.trim() : '';
6011
+ const apiKey = typeof params.apiKey === 'string' ? params.apiKey.trim() : '';
6012
+ const model = typeof params.model === 'string' ? params.model.trim() : '';
6013
+ if (!baseUrl) {
6014
+ return { error: 'Missing url' };
6015
+ }
6016
+ if (!apiKey) {
6017
+ return { error: 'Missing apiKey' };
6018
+ }
6019
+ if (!model) {
6020
+ return { error: 'Missing model' };
6021
+ }
6022
+ const normalizedBase = baseUrl.replace(/\/+$/, '');
6023
+ let parsed = null;
6024
+ try {
6025
+ parsed = new URL(normalizedBase);
6026
+ } catch (_) {
6027
+ return { error: 'Invalid URL' };
6028
+ }
6029
+ const pathname = typeof parsed.pathname === 'string' ? parsed.pathname : '';
6030
+ const trimmedPath = pathname.replace(/\/+$/, '');
6031
+ const isRootPath = !trimmedPath || trimmedPath === '/';
6032
+ const endsWithV1 = trimmedPath.endsWith('/v1');
6033
+ const makeCandidate = (url) => ({
6034
+ method: 'POST',
6035
+ url,
6036
+ body: {
6037
+ model,
6038
+ max_tokens: 16,
6039
+ messages: [{ role: 'user', content: 'ping' }]
6040
+ }
6041
+ });
6042
+ const candidates = [];
6043
+ if (endsWithV1) {
6044
+ candidates.push(makeCandidate(`${normalizedBase}/messages`));
6045
+ } else if (isRootPath) {
6046
+ candidates.push(makeCandidate(`${normalizedBase}/v1/messages`));
6047
+ candidates.push(makeCandidate(`${normalizedBase}/messages`));
6048
+ } else {
6049
+ candidates.push(makeCandidate(`${normalizedBase}/messages`));
6050
+ candidates.push(makeCandidate(`${normalizedBase}/v1/messages`));
6051
+ }
6052
+ return {
6053
+ kind: 'claude',
6054
+ candidates,
6055
+ apiKey,
6056
+ apiKeyHeader: 'x-api-key',
6057
+ headers: {
6058
+ 'anthropic-version': '2023-06-01'
6059
+ }
6060
+ };
6061
+ }
6062
+
6006
6063
  if (params.name) {
6007
6064
  const { config } = readConfigOrVirtualDefault();
6008
6065
  const providers = config.model_providers || {};
@@ -6013,20 +6070,32 @@ function resolveSpeedTestTarget(params) {
6013
6070
  if (!provider.base_url) {
6014
6071
  return { error: 'Provider missing URL' };
6015
6072
  }
6016
- const currentModel = typeof config.model === 'string' ? config.model.trim() : '';
6017
- const probeSpec = buildModelProbeSpec(provider, currentModel, provider.base_url);
6018
- if (probeSpec && probeSpec.url) {
6019
- return {
6020
- method: 'POST',
6021
- url: probeSpec.url,
6022
- body: probeSpec.body,
6023
- apiKey: provider.preferred_auth_method || ''
6024
- };
6073
+ const providerName = String(params.name).trim();
6074
+ const currentModels = readCurrentModels();
6075
+ const selectedModel = typeof currentModels[providerName] === 'string' && currentModels[providerName].trim()
6076
+ ? currentModels[providerName].trim()
6077
+ : (typeof config.model === 'string' ? config.model.trim() : '');
6078
+
6079
+ const apiKey = typeof provider.preferred_auth_method === 'string'
6080
+ ? provider.preferred_auth_method.trim()
6081
+ : '';
6082
+
6083
+ const candidates = [];
6084
+ for (const spec of buildModelProbeSpecs(provider, selectedModel, provider.base_url)) {
6085
+ if (!spec || !spec.url) continue;
6086
+ candidates.push({ method: 'POST', url: spec.url, body: spec.body });
6087
+ }
6088
+ for (const url of buildApiProbeUrlCandidates(provider.base_url, 'models')) {
6089
+ candidates.push({ method: 'GET', url });
6025
6090
  }
6091
+ if (candidates.length === 0) {
6092
+ candidates.push({ method: 'GET', url: provider.base_url });
6093
+ }
6094
+
6026
6095
  return {
6027
- method: 'GET',
6028
- url: provider.base_url,
6029
- apiKey: provider.preferred_auth_method || ''
6096
+ kind: 'provider',
6097
+ candidates,
6098
+ apiKey
6030
6099
  };
6031
6100
  }
6032
6101
 
@@ -6041,155 +6110,6 @@ function resolveSpeedTestTarget(params) {
6041
6110
  return { error: 'Missing name or url' };
6042
6111
  }
6043
6112
 
6044
- function extractApiPayloadErrorMessage(payload) {
6045
- if (!payload || typeof payload !== 'object') {
6046
- return '';
6047
- }
6048
- if (typeof payload.error === 'string' && payload.error.trim()) {
6049
- return payload.error.trim();
6050
- }
6051
- if (!payload.error || typeof payload.error !== 'object') {
6052
- return '';
6053
- }
6054
- if (typeof payload.error.message === 'string' && payload.error.message.trim()) {
6055
- return payload.error.message.trim();
6056
- }
6057
- if (typeof payload.error.code === 'string' && payload.error.code.trim()) {
6058
- return payload.error.code.trim();
6059
- }
6060
- return '';
6061
- }
6062
-
6063
- function resolveProviderChatTarget(params) {
6064
- const providerName = typeof (params && params.name) === 'string' ? params.name.trim() : '';
6065
- const prompt = typeof (params && params.prompt) === 'string' ? params.prompt.trim() : '';
6066
- if (!providerName) {
6067
- return { error: 'Provider name is required' };
6068
- }
6069
- if (!prompt) {
6070
- return { error: 'Prompt is required' };
6071
- }
6072
-
6073
- const { config } = readConfigOrVirtualDefault();
6074
- const providers = config.model_providers || {};
6075
- const provider = providers[providerName];
6076
- if (!provider || typeof provider !== 'object') {
6077
- return { error: `Provider not found: ${providerName}` };
6078
- }
6079
-
6080
- const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
6081
- if (!baseUrl) {
6082
- return { error: `Provider ${providerName} missing URL` };
6083
- }
6084
-
6085
- const currentModels = readCurrentModels();
6086
- const savedModel = currentModels && typeof currentModels[providerName] === 'string'
6087
- ? currentModels[providerName].trim()
6088
- : '';
6089
- const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
6090
- const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
6091
- const model = savedModel || (activeProvider === providerName ? activeModel : '');
6092
- if (!model) {
6093
- return { error: `Provider ${providerName} missing current model` };
6094
- }
6095
-
6096
- const specs = buildModelConversationSpecs(provider, model, baseUrl, prompt, {
6097
- maxOutputTokens: 256
6098
- });
6099
- if (!specs.length) {
6100
- return { error: `Provider ${providerName} missing available conversation endpoint` };
6101
- }
6102
-
6103
- return {
6104
- providerName,
6105
- provider,
6106
- model,
6107
- prompt,
6108
- specs,
6109
- apiKey: typeof provider.preferred_auth_method === 'string'
6110
- ? provider.preferred_auth_method.trim()
6111
- : ''
6112
- };
6113
- }
6114
-
6115
- async function runProviderChatCheck(params = {}) {
6116
- const target = resolveProviderChatTarget(params);
6117
- if (target.error) {
6118
- return { ok: false, error: target.error };
6119
- }
6120
-
6121
- const timeoutMs = Number.isFinite(params.timeoutMs)
6122
- ? Math.max(1000, Number(params.timeoutMs))
6123
- : 30000;
6124
- let finalSpec = target.specs[0];
6125
- let result = null;
6126
-
6127
- for (let index = 0; index < target.specs.length; index += 1) {
6128
- const candidate = target.specs[index];
6129
- const probeResult = await probeJsonPost(candidate.url, candidate.body, {
6130
- apiKey: target.apiKey,
6131
- timeoutMs,
6132
- maxBytes: 512 * 1024
6133
- });
6134
- finalSpec = candidate;
6135
- result = probeResult;
6136
- const shouldTryNextCandidate = index < target.specs.length - 1
6137
- && (!probeResult.ok || probeResult.status === 404);
6138
- if (!shouldTryNextCandidate) {
6139
- break;
6140
- }
6141
- }
6142
-
6143
- if (!result || !result.ok) {
6144
- return {
6145
- ok: false,
6146
- provider: target.providerName,
6147
- model: target.model,
6148
- url: finalSpec.url,
6149
- status: Number.isFinite(result && result.status) ? result.status : 0,
6150
- durationMs: Number.isFinite(result && result.durationMs) ? result.durationMs : 0,
6151
- reply: '',
6152
- rawPreview: '',
6153
- error: result && result.error ? result.error : 'request failed'
6154
- };
6155
- }
6156
-
6157
- let payload = null;
6158
- try {
6159
- payload = result.body ? JSON.parse(result.body) : null;
6160
- } catch (e) {
6161
- payload = null;
6162
- }
6163
-
6164
- const payloadError = extractApiPayloadErrorMessage(payload);
6165
- if (result.status >= 400 || payloadError) {
6166
- return {
6167
- ok: false,
6168
- provider: target.providerName,
6169
- model: target.model,
6170
- url: finalSpec.url,
6171
- status: Number.isFinite(result.status) ? result.status : 0,
6172
- durationMs: Number.isFinite(result.durationMs) ? result.durationMs : 0,
6173
- reply: '',
6174
- rawPreview: result.body ? truncateText(result.body, 600) : '',
6175
- error: payloadError || `HTTP ${result.status}`
6176
- };
6177
- }
6178
-
6179
- const reply = extractModelResponseText(payload);
6180
- return {
6181
- ok: true,
6182
- provider: target.providerName,
6183
- model: target.model,
6184
- url: finalSpec.url,
6185
- status: Number.isFinite(result.status) ? result.status : 0,
6186
- durationMs: Number.isFinite(result.durationMs) ? result.durationMs : 0,
6187
- reply,
6188
- rawPreview: reply ? '' : (result.body ? truncateText(result.body, 600) : ''),
6189
- error: ''
6190
- };
6191
- }
6192
-
6193
6113
  function runSpeedTest(targetUrl, apiKey, options = {}) {
6194
6114
  const timeoutMs = Number.isFinite(options.timeoutMs)
6195
6115
  ? Math.max(1000, Number(options.timeoutMs))
@@ -6198,6 +6118,8 @@ function runSpeedTest(targetUrl, apiKey, options = {}) {
6198
6118
  if (method === 'POST') {
6199
6119
  return probeJsonPost(targetUrl, options.body || {}, {
6200
6120
  apiKey,
6121
+ apiKeyHeader: typeof options.apiKeyHeader === 'string' ? options.apiKeyHeader : '',
6122
+ headers: options.headers && typeof options.headers === 'object' ? options.headers : null,
6201
6123
  timeoutMs,
6202
6124
  maxBytes: 256 * 1024
6203
6125
  }).then((result) => ({
@@ -6209,6 +6131,8 @@ function runSpeedTest(targetUrl, apiKey, options = {}) {
6209
6131
  }
6210
6132
  return probeUrl(targetUrl, {
6211
6133
  apiKey,
6134
+ apiKeyHeader: typeof options.apiKeyHeader === 'string' ? options.apiKeyHeader : '',
6135
+ headers: options.headers && typeof options.headers === 'object' ? options.headers : null,
6212
6136
  timeoutMs,
6213
6137
  maxBytes: 256 * 1024
6214
6138
  }).then((result) => ({
@@ -6492,6 +6416,123 @@ function cmdStatus() {
6492
6416
  console.log();
6493
6417
  }
6494
6418
 
6419
+ function parseDoctorCommandArgs(argv = []) {
6420
+ const options = {
6421
+ format: 'json',
6422
+ lang: '',
6423
+ range: '7d',
6424
+ targetApp: 'codex',
6425
+ remote: true,
6426
+ includeInstall: true,
6427
+ includeUsage: true,
6428
+ includeTasks: true,
6429
+ includeSkills: true,
6430
+ output: ''
6431
+ };
6432
+ let cursor = 0;
6433
+ while (cursor < argv.length) {
6434
+ const token = String(argv[cursor] || '');
6435
+ if (token === '--json') {
6436
+ options.format = 'json';
6437
+ cursor += 1;
6438
+ continue;
6439
+ }
6440
+ if (token === '--format') {
6441
+ const value = String(argv[cursor + 1] || '').trim().toLowerCase();
6442
+ if (!value || value.startsWith('--')) {
6443
+ throw new Error('错误: --format 需要一个值(json/md)');
6444
+ }
6445
+ options.format = value === 'md' || value === 'markdown' ? 'md' : 'json';
6446
+ cursor += 2;
6447
+ continue;
6448
+ }
6449
+ if (token === '--output') {
6450
+ const value = String(argv[cursor + 1] || '').trim();
6451
+ if (!value || value.startsWith('--')) {
6452
+ throw new Error('错误: --output 需要一个值(文件路径)');
6453
+ }
6454
+ options.output = value;
6455
+ cursor += 2;
6456
+ continue;
6457
+ }
6458
+ if (token === '--lang') {
6459
+ const value = String(argv[cursor + 1] || '').trim().toLowerCase();
6460
+ if (!value || value.startsWith('--')) {
6461
+ throw new Error('错误: --lang 需要一个值(zh/en)');
6462
+ }
6463
+ options.lang = value === 'en' ? 'en' : 'zh';
6464
+ cursor += 2;
6465
+ continue;
6466
+ }
6467
+ if (token === '--range') {
6468
+ const value = String(argv[cursor + 1] || '').trim().toLowerCase();
6469
+ if (!value || value.startsWith('--')) {
6470
+ throw new Error('错误: --range 需要一个值(7d/30d/all)');
6471
+ }
6472
+ options.range = value === 'all' ? 'all' : (value === '30d' ? '30d' : '7d');
6473
+ cursor += 2;
6474
+ continue;
6475
+ }
6476
+ if (token === '--target-app') {
6477
+ const value = String(argv[cursor + 1] || '').trim().toLowerCase();
6478
+ if (!value || value.startsWith('--')) {
6479
+ throw new Error('错误: --target-app 需要一个值(codex/claude)');
6480
+ }
6481
+ options.targetApp = value === 'claude' ? 'claude' : 'codex';
6482
+ cursor += 2;
6483
+ continue;
6484
+ }
6485
+ if (token === '--no-remote') {
6486
+ options.remote = false;
6487
+ cursor += 1;
6488
+ continue;
6489
+ }
6490
+ if (token === '--no-install') {
6491
+ options.includeInstall = false;
6492
+ cursor += 1;
6493
+ continue;
6494
+ }
6495
+ cursor += 1;
6496
+ }
6497
+ return options;
6498
+ }
6499
+
6500
+ async function cmdDoctor(argv = []) {
6501
+ try {
6502
+ const options = parseDoctorCommandArgs(argv);
6503
+ const report = await buildDoctorReport(options, {
6504
+ getStatusPayload: buildMcpStatusPayload,
6505
+ buildInstallStatusReport,
6506
+ buildConfigHealthReport,
6507
+ listSessionUsage,
6508
+ buildTaskOverviewPayload,
6509
+ listSkills
6510
+ });
6511
+ const format = options.format === 'md' ? 'md' : 'json';
6512
+ const text = format === 'md'
6513
+ ? renderDoctorMarkdown(report)
6514
+ : JSON.stringify(report, null, 2);
6515
+ if (options.output) {
6516
+ ensureDir(path.dirname(options.output));
6517
+ fs.writeFileSync(options.output, text);
6518
+ } else {
6519
+ process.stdout.write(text + '\n');
6520
+ }
6521
+ } catch (e) {
6522
+ console.error('错误:', e && e.message ? e.message : e);
6523
+ process.exitCode = 1;
6524
+ }
6525
+ }
6526
+
6527
+ async function cmdImportSkills(argv = []) {
6528
+ try {
6529
+ await cmdImportSkillsFromUrl(argv);
6530
+ } catch (e) {
6531
+ console.error('错误:', e && e.message ? e.message : e);
6532
+ process.exitCode = 1;
6533
+ }
6534
+ }
6535
+
6495
6536
  // 列出所有提供商
6496
6537
  function cmdList() {
6497
6538
  const configResult = readConfigOrVirtualDefault();
@@ -8339,6 +8380,21 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8339
8380
  case 'config-health-check':
8340
8381
  result = await buildConfigHealthReport(params || {});
8341
8382
  break;
8383
+ case 'doctor':
8384
+ {
8385
+ const doctorParams = isPlainObject(params) ? params : {};
8386
+ const report = await buildDoctorReport(doctorParams, {
8387
+ getStatusPayload: buildMcpStatusPayload,
8388
+ buildInstallStatusReport,
8389
+ buildConfigHealthReport,
8390
+ listSessionUsage,
8391
+ buildTaskOverviewPayload,
8392
+ listSkills
8393
+ });
8394
+ result = buildDoctorLegacyPayload(report);
8395
+ result.markdown = renderDoctorMarkdown(report);
8396
+ }
8397
+ break;
8342
8398
  case 'get-agents-file':
8343
8399
  result = readAgentsFile(params || {});
8344
8400
  break;
@@ -8446,11 +8502,41 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8446
8502
  result = { error: target.error };
8447
8503
  break;
8448
8504
  }
8449
- result = await runSpeedTest(target.url, target.apiKey, target);
8450
- break;
8451
- }
8452
- case 'provider-chat-check': {
8453
- result = await runProviderChatCheck(params || {});
8505
+ const timeoutMs = Number.isFinite(params && params.timeoutMs)
8506
+ ? Math.max(1000, Number(params.timeoutMs))
8507
+ : 0;
8508
+ if (Array.isArray(target.candidates) && target.candidates.length > 0) {
8509
+ let finalCandidate = target.candidates[0];
8510
+ let finalResult = null;
8511
+ for (let index = 0; index < target.candidates.length; index += 1) {
8512
+ const candidate = target.candidates[index];
8513
+ const probeResult = await runSpeedTest(candidate.url, target.apiKey, {
8514
+ ...candidate,
8515
+ apiKeyHeader: target.apiKeyHeader,
8516
+ headers: target.headers,
8517
+ timeoutMs: timeoutMs || undefined
8518
+ });
8519
+ finalCandidate = candidate;
8520
+ finalResult = probeResult;
8521
+ const status = Number.isFinite(probeResult && probeResult.status) ? probeResult.status : 0;
8522
+ const shouldTryNext = index < target.candidates.length - 1 && status === 404;
8523
+ if (!shouldTryNext) {
8524
+ break;
8525
+ }
8526
+ }
8527
+ result = {
8528
+ ok: !!(finalResult && finalResult.ok),
8529
+ status: Number.isFinite(finalResult && finalResult.status) ? finalResult.status : 0,
8530
+ durationMs: Number.isFinite(finalResult && finalResult.durationMs) ? finalResult.durationMs : 0,
8531
+ error: finalResult && finalResult.ok ? '' : (finalResult && finalResult.error ? finalResult.error : ''),
8532
+ url: finalCandidate && finalCandidate.url ? finalCandidate.url : ''
8533
+ };
8534
+ break;
8535
+ }
8536
+ result = await runSpeedTest(target.url, target.apiKey, {
8537
+ ...target,
8538
+ timeoutMs: timeoutMs || undefined
8539
+ });
8454
8540
  break;
8455
8541
  }
8456
8542
  case 'openai-bridge-get-provider': {
@@ -8913,7 +8999,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8913
8999
  const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
8914
9000
  server.listen(port, host, () => {
8915
9001
  console.log('\n✓ Web UI 已启动');
8916
- console.log(` 待访问: ${openUrl}`);
9002
+ const willOpenBrowser = !!openBrowser && !process.env.CODEXMATE_NO_BROWSER;
9003
+ console.log(` ${willOpenBrowser ? '已打开' : '待访问'}: ${openUrl}`);
8917
9004
  if (host && host !== openHost) {
8918
9005
  console.log(' 监听地址:', host);
8919
9006
  }
@@ -8923,9 +9010,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8923
9010
  console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
8924
9011
  }
8925
9012
 
8926
- if (!process.env.CODEXMATE_NO_BROWSER && openBrowser) {
8927
- const url = openUrl;
8928
- openBrowserAfterReady(url);
9013
+ if (willOpenBrowser) {
9014
+ openBrowserAfterReady(openUrl);
8929
9015
  }
8930
9016
  });
8931
9017
 
@@ -9036,14 +9122,15 @@ function cmdStart(options = {}) {
9036
9122
  || process.env.CODEXMATE_DEV === '1'
9037
9123
  || process.env.CODEXMATE_DEV === 'true';
9038
9124
 
9039
- // 禁止自动打开浏览器:仅输出 URL,交由用户自行点击/打开。
9125
+ const shouldOpenBrowser = !options.noBrowser && !process.env.CODEXMATE_NO_BROWSER;
9126
+
9040
9127
  let serverHandle = createWebServer({
9041
9128
  htmlPath,
9042
9129
  assetsDir,
9043
9130
  webDir,
9044
9131
  host,
9045
9132
  port,
9046
- openBrowser: false
9133
+ openBrowser: shouldOpenBrowser
9047
9134
  });
9048
9135
 
9049
9136
  // 禁止前端变更侦测与自动重启:避免终端输出噪音与访问时短暂 Connection Refused。
@@ -13076,6 +13163,37 @@ async function cmdMcp(args = []) {
13076
13163
  });
13077
13164
  }
13078
13165
 
13166
+ function printMainHelp() {
13167
+ console.log('\nCodex Mate - Codex 提供商管理工具');
13168
+ console.log('\n用法:');
13169
+ console.log(' codexmate status 显示当前状态');
13170
+ console.log(' codexmate doctor [--format json|md] [--lang zh|en] [--output <PATH>] 输出诊断报告');
13171
+ console.log(' codexmate import-skills <URL> [--target-app codex|claude] [--name <NAME>] [--timeout-ms <MS>] 从 URL 导入 skills');
13172
+ console.log(' codexmate setup 交互式配置向导');
13173
+ console.log(' codexmate list 列出所有提供商');
13174
+ console.log(' codexmate models 列出所有模型');
13175
+ console.log(' codexmate switch <名称> 切换提供商');
13176
+ console.log(' codexmate use <模型> 切换模型');
13177
+ console.log(' codexmate add <名称> <URL> [密钥] [--bridge <openai>]');
13178
+ console.log(' codexmate delete <名称> 删除提供商');
13179
+ console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
13180
+ console.log(' codexmate auth <list|import|switch|delete|status> 认证管理');
13181
+ console.log(' codexmate add-model <模型> 添加模型');
13182
+ console.log(' codexmate delete-model <模型> 删除模型');
13183
+ console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
13184
+ console.log(' codexmate task <plan|run|runs|queue|retry|cancel|logs> 本地任务编排');
13185
+ console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
13186
+ console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo');
13187
+ console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
13188
+ console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
13189
+ console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
13190
+ console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
13191
+ console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
13192
+ console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
13193
+ console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');
13194
+ console.log('');
13195
+ }
13196
+
13079
13197
  // ============================================================================
13080
13198
  // 主程序
13081
13199
  // ============================================================================
@@ -13091,32 +13209,8 @@ async function main() {
13091
13209
  }
13092
13210
  }
13093
13211
 
13094
- if (args.length === 0) {
13095
- console.log('\nCodex Mate - Codex 提供商管理工具');
13096
- console.log('\n用法:');
13097
- console.log(' codexmate status 显示当前状态');
13098
- console.log(' codexmate setup 交互式配置向导');
13099
- console.log(' codexmate list 列出所有提供商');
13100
- console.log(' codexmate models 列出所有模型');
13101
- console.log(' codexmate switch <名称> 切换提供商');
13102
- console.log(' codexmate use <模型> 切换模型');
13103
- console.log(' codexmate add <名称> <URL> [密钥] [--bridge <openai>]');
13104
- console.log(' codexmate delete <名称> 删除提供商');
13105
- console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
13106
- console.log(' codexmate add-model <模型> 添加模型');
13107
- console.log(' codexmate delete-model <模型> 删除模型');
13108
- console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
13109
- console.log(' codexmate task <plan|run|runs|queue|retry|cancel|logs> 本地任务编排');
13110
- console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
13111
- console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo');
13112
- console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
13113
- console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
13114
- console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
13115
- console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
13116
- console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
13117
- console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
13118
- console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');
13119
- console.log('');
13212
+ if (args.length === 0 || command === '--help' || command === '-h' || command === 'help') {
13213
+ printMainHelp();
13120
13214
  process.exit(0);
13121
13215
  }
13122
13216
 
@@ -13155,6 +13249,8 @@ async function main() {
13155
13249
  switch (command) {
13156
13250
  case '__task-worker': await cmdTaskWorker(args.slice(1)); break;
13157
13251
  case 'status': cmdStatus(); break;
13252
+ case 'doctor': await cmdDoctor(args.slice(1)); break;
13253
+ case 'import-skills': await cmdImportSkills(args.slice(1)); break;
13158
13254
  case 'setup': await cmdSetup(); break;
13159
13255
  case 'list': cmdList(); break;
13160
13256
  case 'models': await cmdModels(); break;