codexmate 0.0.43 → 0.0.45

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 (42) hide show
  1. package/README.md +2 -0
  2. package/README.zh.md +2 -0
  3. package/cli/claude-proxy.js +611 -14
  4. package/cli/update.js +77 -7
  5. package/cli.js +188 -21
  6. package/package.json +1 -1
  7. package/web-ui/app.js +36 -3
  8. package/web-ui/index.html +1 -0
  9. package/web-ui/logic.claude.mjs +65 -2
  10. package/web-ui/logic.runtime.mjs +0 -7
  11. package/web-ui/modules/app.computed.index.mjs +3 -1
  12. package/web-ui/modules/app.computed.main-tabs.mjs +3 -0
  13. package/web-ui/modules/app.computed.prompts.mjs +28 -0
  14. package/web-ui/modules/app.computed.session.mjs +23 -1
  15. package/web-ui/modules/app.methods.agents.mjs +50 -4
  16. package/web-ui/modules/app.methods.claude-config.mjs +28 -12
  17. package/web-ui/modules/app.methods.index.mjs +1 -1
  18. package/web-ui/modules/app.methods.install.mjs +129 -1
  19. package/web-ui/modules/app.methods.navigation.mjs +2 -1
  20. package/web-ui/modules/app.methods.session-actions.mjs +17 -2
  21. package/web-ui/modules/app.methods.session-timeline.mjs +0 -1
  22. package/web-ui/modules/app.methods.startup-claude.mjs +26 -3
  23. package/web-ui/modules/i18n/locales/en.mjs +42 -5
  24. package/web-ui/modules/i18n/locales/ja.mjs +42 -5
  25. package/web-ui/modules/i18n/locales/vi.mjs +51 -0
  26. package/web-ui/modules/i18n/locales/zh.mjs +42 -5
  27. package/web-ui/partials/index/layout-footer.html +1 -1
  28. package/web-ui/partials/index/layout-header.html +64 -0
  29. package/web-ui/partials/index/modal-config-template-agents.html +12 -13
  30. package/web-ui/partials/index/modals-basic.html +18 -1
  31. package/web-ui/partials/index/panel-config-claude.html +4 -7
  32. package/web-ui/partials/index/panel-config-codex.html +2 -6
  33. package/web-ui/partials/index/panel-prompts.html +100 -0
  34. package/web-ui/partials/index/panel-sessions.html +30 -10
  35. package/web-ui/partials/index/panel-usage.html +34 -18
  36. package/web-ui/res/web-ui-render.precompiled.js +579 -149
  37. package/web-ui/styles/controls-forms.css +5 -5
  38. package/web-ui/styles/layout-shell.css +145 -0
  39. package/web-ui/styles/modals-core.css +162 -0
  40. package/web-ui/styles/responsive.css +77 -5
  41. package/web-ui/styles/sessions-toolbar-trash.css +45 -10
  42. package/web-ui/styles/sessions-usage.css +31 -2
package/cli/update.js CHANGED
@@ -64,26 +64,92 @@ async function cmdToolUpdate(args = []) {
64
64
  }
65
65
  }
66
66
 
67
- async function fetchLatestVersion() {
67
+ async function fetchLatestVersion(options = {}) {
68
68
  return new Promise((resolve, reject) => {
69
+ const timeoutMs = Number.isFinite(Number(options.timeoutMs))
70
+ ? Math.max(0, Number(options.timeoutMs))
71
+ : 5000;
69
72
  const url = 'https://registry.npmjs.org/codexmate/latest';
70
- https.get(url, (res) => {
73
+ let settled = false;
74
+ const finish = (fn, value) => {
75
+ if (settled) return;
76
+ settled = true;
77
+ fn(value);
78
+ };
79
+ const req = https.get(url, (res) => {
71
80
  let data = '';
72
81
  res.on('data', (chunk) => { data += chunk; });
73
82
  res.on('end', () => {
74
83
  try {
84
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
85
+ finish(reject, new Error(`NPM registry returned ${res.statusCode}`));
86
+ return;
87
+ }
75
88
  const json = JSON.parse(data);
76
- resolve(json.version || '');
89
+ finish(resolve, json.version || '');
77
90
  } catch (e) {
78
- reject(new Error('解析 NPM 响应失败'));
91
+ finish(reject, new Error('解析 NPM 响应失败'));
79
92
  }
80
93
  });
81
- }).on('error', (err) => {
82
- reject(err);
94
+ });
95
+ if (timeoutMs > 0) {
96
+ req.setTimeout(timeoutMs, () => {
97
+ req.destroy(new Error('获取 NPM 最新版本超时'));
98
+ });
99
+ }
100
+ req.on('error', (err) => {
101
+ finish(reject, err);
83
102
  });
84
103
  });
85
104
  }
86
105
 
106
+ function normalizePackageVersion(value) {
107
+ const normalized = typeof value === 'string' ? value.trim().replace(/^v/i, '') : '';
108
+ return /^\d+(?:\.\d+){0,2}(?:[-+][0-9A-Za-z.-]+)?$/.test(normalized) ? normalized : '';
109
+ }
110
+
111
+ function comparePackageVersions(left, right) {
112
+ const normalizeParts = (value) => {
113
+ const normalized = normalizePackageVersion(value);
114
+ if (!normalized) return null;
115
+ return normalized.split(/[+-]/)[0].split('.').map((part) => Number.parseInt(part, 10) || 0);
116
+ };
117
+ const a = normalizeParts(left);
118
+ const b = normalizeParts(right);
119
+ if (!a || !b) return 0;
120
+ for (let i = 0; i < 3; i += 1) {
121
+ const diff = (a[i] || 0) - (b[i] || 0);
122
+ if (diff < 0) return -1;
123
+ if (diff > 0) return 1;
124
+ }
125
+ return 0;
126
+ }
127
+
128
+ let latestVersionStatusCache = null;
129
+
130
+ async function fetchLatestVersionStatus(options = {}) {
131
+ const currentVersion = normalizePackageVersion(options.currentVersion) || String(options.currentVersion || '');
132
+ const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 5000;
133
+ const cacheTtlMs = Number.isFinite(Number(options.cacheTtlMs)) ? Math.max(0, Number(options.cacheTtlMs)) : 10 * 60 * 1000;
134
+ const now = typeof options.now === 'function' ? options.now() : Date.now();
135
+ if (latestVersionStatusCache && cacheTtlMs > 0 && now - latestVersionStatusCache.checkedAtMs < cacheTtlMs) {
136
+ return { ...latestVersionStatusCache.payload, cached: true };
137
+ }
138
+
139
+ const latestVersionRaw = await fetchLatestVersion({ timeoutMs });
140
+ const latestVersion = normalizePackageVersion(latestVersionRaw) || String(latestVersionRaw || '');
141
+ const payload = {
142
+ currentVersion,
143
+ latestVersion,
144
+ updateAvailable: !!currentVersion && !!latestVersion && comparePackageVersions(currentVersion, latestVersion) < 0,
145
+ source: 'npm',
146
+ checkedAt: new Date(now).toISOString(),
147
+ cached: false
148
+ };
149
+ latestVersionStatusCache = { checkedAtMs: now, payload };
150
+ return payload;
151
+ }
152
+
87
153
  function detectInstallMethod() {
88
154
  const cliPath = path.resolve(__dirname, '..');
89
155
 
@@ -167,5 +233,9 @@ function updateViaStandalone(version) {
167
233
  }
168
234
 
169
235
  module.exports = {
170
- cmdToolUpdate
236
+ cmdToolUpdate,
237
+ fetchLatestVersion,
238
+ fetchLatestVersionStatus,
239
+ normalizePackageVersion,
240
+ comparePackageVersions
171
241
  };
package/cli.js CHANGED
@@ -148,7 +148,7 @@ const {
148
148
  deleteCodexSkills
149
149
  } = require('./cli/skills');
150
150
  const { cmdImportSkills: cmdImportSkillsFromUrl } = require('./cli/import-skills-url');
151
- const { cmdToolUpdate } = require('./cli/update');
151
+ const { cmdToolUpdate, fetchLatestVersionStatus } = require('./cli/update');
152
152
  const {
153
153
  getFileStatSafe,
154
154
  isBootstrapLikeText,
@@ -291,7 +291,11 @@ const DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS = Object.freeze({
291
291
  host: '127.0.0.1',
292
292
  port: 8328,
293
293
  provider: '',
294
+ upstreamProviderName: '',
295
+ upstreamBaseUrl: '',
296
+ upstreamApiKey: '',
294
297
  authSource: 'provider',
298
+ targetApi: 'responses',
295
299
  timeoutMs: 30000
296
300
  });
297
301
  const CLI_INSTALL_TARGETS = Object.freeze([
@@ -5740,7 +5744,9 @@ const {
5740
5744
  HTTPS_KEEP_ALIVE_AGENT,
5741
5745
  readConfigOrVirtualDefault,
5742
5746
  resolveBuiltinProxyProviderName,
5743
- resolveAuthTokenFromCurrentProfile
5747
+ resolveAuthTokenFromCurrentProfile,
5748
+ OPENAI_BRIDGE_SETTINGS_FILE,
5749
+ resolveOpenaiBridgeUpstream
5744
5750
  });
5745
5751
 
5746
5752
  function applyBuiltinProxyProvider(params = {}) {
@@ -8082,15 +8088,17 @@ function buildClaudeSharePayload(config = {}) {
8082
8088
  const apiKey = typeof config.apiKey === 'string' ? config.apiKey : '';
8083
8089
  const baseUrl = typeof config.baseUrl === 'string' ? config.baseUrl : '';
8084
8090
  const model = typeof config.model === 'string' ? config.model : '';
8091
+ const targetApi = normalizeClaudeTargetApi(config.targetApi);
8085
8092
 
8086
8093
  if (!baseUrl) return { error: 'Claude Base URL 未设置' };
8087
- if (!apiKey) return { error: 'Claude API 密钥未设置' };
8094
+ if (!apiKey && targetApi !== 'ollama') return { error: 'Claude API 密钥未设置' };
8088
8095
 
8089
8096
  return {
8090
8097
  payload: {
8091
8098
  baseUrl: baseUrl.trim(),
8092
8099
  apiKey: apiKey.trim(),
8093
- model: (model && model.trim()) || DEFAULT_CLAUDE_MODEL
8100
+ model: (model && model.trim()) || DEFAULT_CLAUDE_MODEL,
8101
+ targetApi
8094
8102
  }
8095
8103
  };
8096
8104
  }
@@ -9404,19 +9412,93 @@ function maskKey(key) {
9404
9412
  return key.substring(0, 4) + '...' + key.substring(key.length - 4);
9405
9413
  }
9406
9414
 
9415
+ function normalizeClaudeTargetApi(value) {
9416
+ const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
9417
+ if (raw === 'chat_completions' || raw === 'chat-completions' || raw === 'chat/completions') {
9418
+ return 'chat_completions';
9419
+ }
9420
+ if (raw === 'ollama') {
9421
+ return 'ollama';
9422
+ }
9423
+ return 'responses';
9424
+ }
9425
+
9426
+ function resetBuiltinClaudeProxySavedSettingsToResponses() {
9427
+ const proxySettingsResult = readJsonObjectFromFile(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS);
9428
+ const proxySettings = proxySettingsResult.ok && proxySettingsResult.data && typeof proxySettingsResult.data === 'object' && !Array.isArray(proxySettingsResult.data)
9429
+ ? proxySettingsResult.data
9430
+ : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS;
9431
+ writeJsonAtomic(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, {
9432
+ ...DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
9433
+ ...proxySettings,
9434
+ enabled: false,
9435
+ targetApi: 'responses'
9436
+ });
9437
+ }
9438
+
9407
9439
  // 应用到 Claude Code settings.json(跨平台)
9408
- function applyToClaudeSettings(config = {}) {
9409
- assertToolConfigWriteAllowed('claude');
9440
+ async function applyToClaudeSettings(config = {}) {
9441
+ let proxyStarted = false;
9410
9442
  try {
9443
+ assertToolConfigWriteAllowed('claude');
9411
9444
  const apiKey = (config.apiKey || '').trim();
9412
- if (!apiKey) {
9445
+ const targetApi = normalizeClaudeTargetApi(config.targetApi);
9446
+ if (!apiKey && targetApi !== 'ollama') {
9413
9447
  return { success: false, mode: 'settings-file', error: '请先输入 API Key' };
9414
9448
  }
9415
9449
 
9416
- const baseUrl = (config.baseUrl || 'https://open.bigmodel.cn/api/anthropic').trim();
9450
+ const configuredBaseUrl = typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
9451
+ const baseUrl = (configuredBaseUrl || (targetApi === 'ollama' ? 'http://127.0.0.1:11434' : 'https://open.bigmodel.cn/api/anthropic')).trim();
9417
9452
  const model = (config.model || DEFAULT_CLAUDE_MODEL).trim();
9453
+ let settingsBaseUrl = baseUrl;
9454
+ let settingsApiKey = apiKey;
9455
+ let proxyResult = null;
9456
+
9457
+ if (targetApi === 'chat_completions' || targetApi === 'ollama') {
9458
+ const upstreamProviderName = typeof config.name === 'string' ? config.name.trim() : '';
9459
+ if (targetApi === 'chat_completions' && !configuredBaseUrl && !upstreamProviderName) {
9460
+ return {
9461
+ success: false,
9462
+ mode: 'claude-proxy',
9463
+ error: 'chat_completions 模式需要显式的上游 Base URL 或可解析的 provider 名称'
9464
+ };
9465
+ }
9466
+ await stopBuiltinClaudeProxyRuntime();
9467
+ const proxyToken = crypto.randomBytes(24).toString('hex');
9468
+ proxyResult = await startBuiltinClaudeProxyRuntime({
9469
+ enabled: true,
9470
+ host: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host,
9471
+ provider: upstreamProviderName,
9472
+ authSource: 'provider',
9473
+ targetApi,
9474
+ timeoutMs: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs,
9475
+ upstreamProviderName,
9476
+ ...(configuredBaseUrl ? { upstreamBaseUrl: configuredBaseUrl } : {}),
9477
+ upstreamApiKey: apiKey
9478
+ });
9479
+ if (!proxyResult || proxyResult.error || proxyResult.success === false || !proxyResult.listenUrl) {
9480
+ await stopBuiltinClaudeProxyRuntime();
9481
+ resetBuiltinClaudeProxySavedSettingsToResponses();
9482
+ return {
9483
+ success: false,
9484
+ mode: 'claude-proxy',
9485
+ error: (proxyResult && proxyResult.error) || '启动 Claude 兼容代理失败'
9486
+ };
9487
+ }
9488
+ proxyStarted = true;
9489
+ settingsBaseUrl = proxyResult.listenUrl;
9490
+ settingsApiKey = proxyToken;
9491
+ } else {
9492
+ await stopBuiltinClaudeProxyRuntime();
9493
+ resetBuiltinClaudeProxySavedSettingsToResponses();
9494
+ }
9495
+
9418
9496
  const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
9419
9497
  if (!readResult.ok) {
9498
+ if (proxyStarted) {
9499
+ await stopBuiltinClaudeProxyRuntime();
9500
+ resetBuiltinClaudeProxySavedSettingsToResponses();
9501
+ }
9420
9502
  return { success: false, mode: 'settings-file', error: readResult.error };
9421
9503
  }
9422
9504
 
@@ -9427,8 +9509,8 @@ function applyToClaudeSettings(config = {}) {
9427
9509
 
9428
9510
  const nextEnv = {
9429
9511
  ...currentEnv,
9430
- ANTHROPIC_API_KEY: apiKey,
9431
- ANTHROPIC_BASE_URL: baseUrl,
9512
+ ANTHROPIC_API_KEY: settingsApiKey,
9513
+ ANTHROPIC_BASE_URL: settingsBaseUrl,
9432
9514
  ANTHROPIC_MODEL: model
9433
9515
  };
9434
9516
  delete nextEnv.ANTHROPIC_AUTH_TOKEN;
@@ -9445,7 +9527,8 @@ function applyToClaudeSettings(config = {}) {
9445
9527
 
9446
9528
  const result = {
9447
9529
  success: true,
9448
- mode: 'settings-file',
9530
+ mode: targetApi === 'responses' ? 'settings-file' : 'claude-proxy',
9531
+ targetApi,
9449
9532
  targetPath: CLAUDE_SETTINGS_FILE,
9450
9533
  updatedKeys: [
9451
9534
  'env.ANTHROPIC_API_KEY',
@@ -9453,11 +9536,23 @@ function applyToClaudeSettings(config = {}) {
9453
9536
  'env.ANTHROPIC_MODEL'
9454
9537
  ]
9455
9538
  };
9539
+ if (proxyResult) {
9540
+ result.proxy = {
9541
+ running: true,
9542
+ listenUrl: proxyResult.listenUrl,
9543
+ upstreamProvider: proxyResult.upstreamProvider || '',
9544
+ mode: proxyResult.mode || (targetApi === 'ollama' ? 'anthropic-to-ollama' : 'anthropic-to-chat-completions')
9545
+ };
9546
+ }
9456
9547
  if (backupPath) {
9457
9548
  result.backupPath = backupPath;
9458
9549
  }
9459
9550
  return result;
9460
9551
  } catch (e) {
9552
+ if (proxyStarted) {
9553
+ try { await stopBuiltinClaudeProxyRuntime(); } catch (_) {}
9554
+ try { resetBuiltinClaudeProxySavedSettingsToResponses(); } catch (_) {}
9555
+ }
9461
9556
  return {
9462
9557
  success: false,
9463
9558
  mode: 'settings-file',
@@ -9570,6 +9665,40 @@ async function restoreCodexDir(payload) {
9570
9665
  }
9571
9666
 
9572
9667
  // CLI: 一行写入 Claude Code 配置
9668
+ function parseClaudeCommandArgs(argv = []) {
9669
+ const positionals = [];
9670
+ let targetApi = 'responses';
9671
+ for (let i = 0; i < argv.length; i += 1) {
9672
+ const token = String(argv[i] ?? '');
9673
+ if (token === '--target-api' || token === '--targetApi') {
9674
+ const nextValue = String(argv[i + 1] ?? '');
9675
+ if (!nextValue || nextValue.startsWith('--')) {
9676
+ throw new Error('错误: --target-api 需要一个值(responses、chat_completions 或 ollama)');
9677
+ }
9678
+ targetApi = normalizeClaudeTargetApi(nextValue);
9679
+ i += 1;
9680
+ continue;
9681
+ }
9682
+ positionals.push(token);
9683
+ }
9684
+
9685
+ const baseUrl = positionals[0];
9686
+ if (targetApi === 'ollama' && positionals.length === 2) {
9687
+ return {
9688
+ baseUrl,
9689
+ apiKey: '',
9690
+ model: positionals[1],
9691
+ targetApi
9692
+ };
9693
+ }
9694
+ return {
9695
+ baseUrl,
9696
+ apiKey: positionals[1],
9697
+ model: positionals[2],
9698
+ targetApi
9699
+ };
9700
+ }
9701
+
9573
9702
  async function cmdClaude(args = []) {
9574
9703
  const argv = Array.isArray(args) ? args : [];
9575
9704
  // 无参数 → 代理启动
@@ -9577,7 +9706,7 @@ async function cmdClaude(args = []) {
9577
9706
  return runProxyCommand('Claude', 'claude', [], '', { autoFlag: '--dangerously-skip-permissions' });
9578
9707
  }
9579
9708
  // 有参数 → 配置写入
9580
- const [baseUrl, apiKey, model] = argv;
9709
+ const { baseUrl, apiKey, model, targetApi } = parseClaudeCommandArgs(argv);
9581
9710
  const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
9582
9711
  const normalizedKey = typeof apiKey === 'string' ? apiKey.trim() : '';
9583
9712
  const normalizedModel = typeof model === 'string' && model.trim()
@@ -9586,19 +9715,21 @@ async function cmdClaude(args = []) {
9586
9715
 
9587
9716
  const silent = false;
9588
9717
 
9589
- if (!normalizedBaseUrl || !normalizedKey) {
9718
+ if (!normalizedBaseUrl || (!normalizedKey && targetApi !== 'ollama')) {
9590
9719
  if (!silent) {
9591
- console.error('用法: codexmate claude <BaseURL> <API密钥> [模型]');
9720
+ console.error('用法: codexmate claude <BaseURL> <API密钥> [模型] [--target-api responses|chat_completions|ollama]');
9592
9721
  console.log('\n示例:');
9593
9722
  console.log(' codexmate claude https://open.bigmodel.cn/api/anthropic sk-ant-xxx glm-4.7');
9723
+ console.log(" codexmate claude http://127.0.0.1:11434 '' llama3.1:8b --target-api ollama");
9594
9724
  }
9595
- throw new Error('BaseURL 和 API 密钥必填');
9725
+ throw new Error(targetApi === 'ollama' ? 'BaseURL 必填' : 'BaseURL 和 API 密钥必填');
9596
9726
  }
9597
9727
 
9598
- const result = applyToClaudeSettings({
9728
+ const result = await applyToClaudeSettings({
9599
9729
  baseUrl: normalizedBaseUrl,
9600
9730
  apiKey: normalizedKey,
9601
- model: normalizedModel
9731
+ model: normalizedModel,
9732
+ targetApi
9602
9733
  });
9603
9734
 
9604
9735
  if (!result || result.success === false) {
@@ -11105,6 +11236,31 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11105
11236
  case 'install-status':
11106
11237
  result = buildInstallStatusReport();
11107
11238
  break;
11239
+ case 'version-status': {
11240
+ const currentVersion = (() => {
11241
+ try {
11242
+ const pkg = require('./package.json');
11243
+ return pkg && pkg.version ? pkg.version : '';
11244
+ } catch (_) {
11245
+ return '';
11246
+ }
11247
+ })();
11248
+ try {
11249
+ const force = !!(params && params.force);
11250
+ result = await fetchLatestVersionStatus({ currentVersion, timeoutMs: 2000, cacheTtlMs: force ? 0 : undefined });
11251
+ } catch (e) {
11252
+ result = {
11253
+ currentVersion,
11254
+ latestVersion: '',
11255
+ updateAvailable: false,
11256
+ source: 'npm',
11257
+ checkedAt: new Date().toISOString(),
11258
+ cached: false,
11259
+ error: e && e.message ? e.message : '获取最新版本失败'
11260
+ };
11261
+ }
11262
+ break;
11263
+ }
11108
11264
  case 'list':
11109
11265
  result = buildMcpProviderListPayload();
11110
11266
  break;
@@ -11297,7 +11453,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11297
11453
  result = applyClaudeSettingsRaw(params || {});
11298
11454
  break;
11299
11455
  case 'apply-claude-config':
11300
- result = applyToClaudeSettings(params.config);
11456
+ result = await applyToClaudeSettings(params.config);
11301
11457
  if (result && !result.error) {
11302
11458
  const cfgName = (params && params.config && typeof params.config.name === 'string') ? params.config.name : '';
11303
11459
  const cfgFrom = (params && typeof params.previousName === 'string') ? params.previousName : '';
@@ -15894,9 +16050,20 @@ function createMcpTools(options = {}) {
15894
16050
  properties: {
15895
16051
  apiKey: { type: 'string' },
15896
16052
  baseUrl: { type: 'string' },
15897
- model: { type: 'string' }
16053
+ model: { type: 'string' },
16054
+ name: { type: 'string' },
16055
+ targetApi: { type: 'string' }
15898
16056
  },
15899
- required: ['apiKey'],
16057
+ allOf: [{
16058
+ if: {
16059
+ not: {
16060
+ type: 'object',
16061
+ properties: { targetApi: { type: 'string', pattern: '^[\\s]*[oO][lL][lL][aA][mM][aA][\\s]*$' } },
16062
+ required: ['targetApi']
16063
+ }
16064
+ },
16065
+ then: { required: ['apiKey'] }
16066
+ }],
15900
16067
  additionalProperties: false
15901
16068
  },
15902
16069
  handler: async (args = {}) => applyToClaudeSettings(args || {})
@@ -16352,7 +16519,7 @@ function printMainHelp() {
16352
16519
  console.log(' codexmate add <名称> <URL> [密钥] [--bridge <openai>]');
16353
16520
  console.log(' codexmate delete <名称> 删除提供商');
16354
16521
  console.log(' codexmate claude 等同于 claude --dangerously-skip-permissions');
16355
- console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
16522
+ console.log(' codexmate claude <BaseURL> <API密钥> [模型] [--target-api responses|chat_completions|ollama] 写入 Claude Code 配置');
16356
16523
  console.log(' codexmate auth <list|import|switch|delete|status> 认证管理');
16357
16524
  console.log(' codexmate add-model <模型> 添加模型');
16358
16525
  console.log(' codexmate delete-model <模型> 删除模型');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -73,6 +73,7 @@ document.addEventListener('DOMContentLoaded', () => {
73
73
  showOpenclawConfigModal: false,
74
74
  showConfigTemplateModal: false,
75
75
  showAgentsModal: false,
76
+ promptsSubTab: 'codex',
76
77
  showSkillsModal: false,
77
78
  showHealthCheckModal: false,
78
79
  showCodexBridgePoolModal: false,
@@ -270,6 +271,12 @@ document.addEventListener('DOMContentLoaded', () => {
270
271
  installRegistryPreset: 'default',
271
272
  installRegistryCustom: '',
272
273
  installStatusTargets: null,
274
+ appLatestVersion: '',
275
+ appVersionStatusLoading: false,
276
+ appVersionStatusError: '',
277
+ appVersionStatusChecked: false,
278
+ appVersionStatusCheckedAt: '',
279
+ appVersionStatusSource: '',
273
280
  newProvider: { name: '', url: '', key: '', model: '', useTransform: false },
274
281
  resetConfigLoading: false,
275
282
  editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false },
@@ -277,12 +284,13 @@ document.addEventListener('DOMContentLoaded', () => {
277
284
  currentClaudeConfig: '',
278
285
  currentClaudeModel: '',
279
286
  claudeCustomModelDraft: '',
280
- editingConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
287
+ editingConfig: { name: '', apiKey: '', baseUrl: '', model: '', targetApi: 'responses' },
281
288
  claudeConfigs: {
282
289
  '智谱GLM': {
283
290
  apiKey: '',
284
291
  baseUrl: 'https://open.bigmodel.cn/api/anthropic',
285
292
  model: 'glm-4.7',
293
+ targetApi: 'responses',
286
294
  hasKey: false
287
295
  }
288
296
  },
@@ -290,7 +298,8 @@ document.addEventListener('DOMContentLoaded', () => {
290
298
  name: '',
291
299
  apiKey: '',
292
300
  baseUrl: '',
293
- model: ''
301
+ model: '',
302
+ targetApi: 'responses'
294
303
  },
295
304
  currentOpenclawConfig: '',
296
305
  openclawConfigs: {
@@ -478,7 +487,7 @@ document.addEventListener('DOMContentLoaded', () => {
478
487
  }
479
488
  {
480
489
  const NAV_STATE_STORAGE_KEY = 'codexmateNavState.v1';
481
- const mainTabSet = new Set(['dashboard', 'config', 'sessions', 'usage', 'orchestration', 'market', 'plugins', 'docs', 'settings', 'trash']);
490
+ const mainTabSet = new Set(['dashboard', 'config', 'sessions', 'usage', 'orchestration', 'market', 'plugins', 'docs', 'settings', 'trash', 'prompts']);
482
491
  let restored = null;
483
492
  try {
484
493
  const raw = localStorage.getItem(NAV_STATE_STORAGE_KEY) || '';
@@ -554,6 +563,14 @@ document.addEventListener('DOMContentLoaded', () => {
554
563
  config.apiKey = '';
555
564
  config.hasKey = false;
556
565
  }
566
+ const targetApiRaw = typeof config.targetApi === 'string' ? config.targetApi.trim().toLowerCase() : '';
567
+ if (targetApiRaw === 'chat_completions' || targetApiRaw === 'chat-completions' || targetApiRaw === 'chat/completions') {
568
+ config.targetApi = 'chat_completions';
569
+ } else if (targetApiRaw === 'ollama') {
570
+ config.targetApi = 'ollama';
571
+ } else {
572
+ config.targetApi = 'responses';
573
+ }
557
574
  }
558
575
  localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
559
576
  } catch (e) {
@@ -630,6 +647,9 @@ document.addEventListener('DOMContentLoaded', () => {
630
647
  }
631
648
  }
632
649
  }
650
+ if (typeof this.loadAppVersionStatus === 'function') {
651
+ void this.loadAppVersionStatus({ silent: true });
652
+ }
633
653
  void this.refreshClaudeSelectionFromSettings({ silent: true });
634
654
  void this.syncDefaultOpenclawConfigEntry({ silent: true });
635
655
  };
@@ -691,6 +711,19 @@ document.addEventListener('DOMContentLoaded', () => {
691
711
  this.clearSessionTimelineRefs();
692
712
  },
693
713
 
714
+ watch: {
715
+ mainTab(newTab) {
716
+ if (newTab === 'prompts' && typeof this.loadPromptsContent === 'function') {
717
+ this.loadPromptsContent();
718
+ }
719
+ },
720
+ promptsSubTab() {
721
+ if (this.mainTab === 'prompts' && typeof this.loadPromptsContent === 'function') {
722
+ this.loadPromptsContent();
723
+ }
724
+ }
725
+ },
726
+
694
727
  computed: createAppComputed(),
695
728
  methods: createAppMethods()
696
729
  };
package/web-ui/index.html CHANGED
@@ -24,6 +24,7 @@
24
24
  <!-- @include ./partials/index/panel-trash.html -->
25
25
  <!-- @include ./partials/index/panel-market.html -->
26
26
  <!-- @include ./partials/index/panel-plugins.html -->
27
+ <!-- @include ./partials/index/panel-prompts.html -->
27
28
  <!-- @include ./partials/index/layout-footer.html -->
28
29
  <!-- @include ./partials/index/modals-basic.html -->
29
30
  <!-- @include ./partials/index/modal-webhook.html -->
@@ -69,13 +69,21 @@ export function normalizeClaudeConfig(config) {
69
69
  const useKey = normalizeClaudeValue(safe.useKey);
70
70
  const externalCredentialType = normalizeClaudeValue(safe.externalCredentialType)
71
71
  || (apiKey ? '' : (authToken ? 'auth-token' : (useKey ? 'claude-code-use-key' : '')));
72
+ const targetApiRaw = normalizeClaudeValue(safe.targetApi).toLowerCase();
73
+ let targetApi = 'responses';
74
+ if (targetApiRaw === 'chat_completions' || targetApiRaw === 'chat-completions' || targetApiRaw === 'chat/completions') {
75
+ targetApi = 'chat_completions';
76
+ } else if (targetApiRaw === 'ollama') {
77
+ targetApi = 'ollama';
78
+ }
72
79
  return {
73
80
  apiKey,
74
81
  baseUrl: normalizeClaudeValue(safe.baseUrl),
75
82
  model: normalizeClaudeValue(safe.model),
76
83
  authToken,
77
84
  useKey,
78
- externalCredentialType
85
+ externalCredentialType,
86
+ targetApi
79
87
  };
80
88
  }
81
89
 
@@ -102,6 +110,60 @@ function normalizeClaudeComparableUrl(value) {
102
110
  return trimmed.replace(/\/+$/g, '');
103
111
  }
104
112
 
113
+ function isLoopbackClaudeProxyUrl(value) {
114
+ const raw = normalizeClaudeComparableUrl(value);
115
+ if (!raw) return false;
116
+ try {
117
+ const parsed = new URL(raw);
118
+ if (parsed.protocol !== 'http:') return false;
119
+ const host = normalizeClaudeValue(parsed.hostname).toLowerCase();
120
+ return host === '127.0.0.1' || host === 'localhost' || host === '[::1]' || host === '::1';
121
+ } catch (_) {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ export function isLikelyBuiltinClaudeProxySettingsEnv(env = {}) {
127
+ const normalized = normalizeClaudeSettingsEnv(env);
128
+ return !!(
129
+ normalized.baseUrl
130
+ && normalized.model
131
+ && /^[a-f0-9]{48}$/i.test(normalized.apiKey)
132
+ && isLoopbackClaudeProxyUrl(normalized.baseUrl)
133
+ );
134
+ }
135
+
136
+ function isClaudeTransformConfig(config = {}) {
137
+ const targetApi = normalizeClaudeConfig(config).targetApi;
138
+ return targetApi === 'chat_completions' || targetApi === 'ollama';
139
+ }
140
+
141
+ export function matchBuiltinClaudeProxyConfigFromSettings(claudeConfigs = {}, env = {}, preferredName = '') {
142
+ if (!isLikelyBuiltinClaudeProxySettingsEnv(env)) {
143
+ return '';
144
+ }
145
+ const normalizedSettings = normalizeClaudeSettingsEnv(env);
146
+ const preferred = normalizeClaudeValue(preferredName);
147
+ if (preferred && claudeConfigs && claudeConfigs[preferred]) {
148
+ const config = normalizeClaudeConfig(claudeConfigs[preferred]);
149
+ if (isClaudeTransformConfig(config) && config.model === normalizedSettings.model) {
150
+ return preferred;
151
+ }
152
+ }
153
+
154
+ const matches = [];
155
+ for (const [name, config] of Object.entries(claudeConfigs || {})) {
156
+ const normalizedConfig = normalizeClaudeConfig(config);
157
+ if (!isClaudeTransformConfig(normalizedConfig)) {
158
+ continue;
159
+ }
160
+ if (normalizedConfig.model === normalizedSettings.model) {
161
+ matches.push(name);
162
+ }
163
+ }
164
+ return matches.length === 1 ? matches[0] : '';
165
+ }
166
+
105
167
  function hasClaudeCredential(config = {}) {
106
168
  return !!(config.apiKey || config.authToken || config.useKey);
107
169
  }
@@ -156,7 +218,8 @@ export function findDuplicateClaudeConfigName(claudeConfigs = {}, config) {
156
218
  continue;
157
219
  }
158
220
  if (normalizeClaudeComparableUrl(normalizedExisting.baseUrl) !== comparableUrl
159
- || normalizedExisting.model !== normalized.model) {
221
+ || normalizedExisting.model !== normalized.model
222
+ || normalizedExisting.targetApi !== normalized.targetApi) {
160
223
  continue;
161
224
  }
162
225
  if (normalized.apiKey && normalizedExisting.apiKey === normalized.apiKey) {