codexmate 0.0.57 → 0.1.1

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.js CHANGED
@@ -2561,6 +2561,28 @@ function buildClaudeSettingsDiff(params = {}) {
2561
2561
  };
2562
2562
  }
2563
2563
 
2564
+
2565
+ function normalizeOpenaiBridgeMaxRetries(value, fallback = 2) {
2566
+ const raw = Number(value);
2567
+ const fallbackRaw = Number(fallback);
2568
+ const base = Number.isFinite(raw) ? raw : (Number.isFinite(fallbackRaw) ? fallbackRaw : 2);
2569
+ return Math.min(10, Math.max(2, Math.floor(base)));
2570
+ }
2571
+
2572
+ function resolveProviderOpenaiBridgeMaxRetries(provider) {
2573
+ if (!provider || typeof provider !== 'object') return 2;
2574
+ if (provider.codexmate_bridge_max_retries !== undefined) {
2575
+ return normalizeOpenaiBridgeMaxRetries(provider.codexmate_bridge_max_retries);
2576
+ }
2577
+ if (provider.openai_bridge_max_retries !== undefined) {
2578
+ return normalizeOpenaiBridgeMaxRetries(provider.openai_bridge_max_retries);
2579
+ }
2580
+ if (provider.max_retries !== undefined) {
2581
+ return normalizeOpenaiBridgeMaxRetries(provider.max_retries);
2582
+ }
2583
+ return 2;
2584
+ }
2585
+
2564
2586
  function addProviderToConfig(params = {}) {
2565
2587
  const name = typeof params.name === 'string' ? params.name.trim() : '';
2566
2588
  const url = typeof params.url === 'string' ? params.url.trim() : '';
@@ -2575,6 +2597,8 @@ function addProviderToConfig(params = {}) {
2575
2597
  ? params.model.trim()
2576
2598
  : fallbackModel;
2577
2599
  const useTransform = !!params.useTransform;
2600
+ const hasOpenaiBridgeMaxRetries = params.openaiBridgeMaxRetries !== undefined || params.maxRetries !== undefined;
2601
+ const openaiBridgeMaxRetries = normalizeOpenaiBridgeMaxRetries(params.openaiBridgeMaxRetries ?? params.maxRetries);
2578
2602
  const allowManaged = !!params.allowManaged;
2579
2603
  const normalizedUrl = normalizeBaseUrl(url);
2580
2604
 
@@ -2634,7 +2658,7 @@ function addProviderToConfig(params = {}) {
2634
2658
  const requiresOpenaiAuth = useTransform;
2635
2659
 
2636
2660
  if (useTransform) {
2637
- const saveRes = upsertOpenaiBridgeProvider(OPENAI_BRIDGE_SETTINGS_FILE, name, normalizedUrl, key);
2661
+ const saveRes = upsertOpenaiBridgeProvider(OPENAI_BRIDGE_SETTINGS_FILE, name, normalizedUrl, key, undefined, { maxRetries: openaiBridgeMaxRetries });
2638
2662
  if (saveRes && saveRes.error) {
2639
2663
  return { error: String(saveRes.error) };
2640
2664
  }
@@ -2646,6 +2670,7 @@ function addProviderToConfig(params = {}) {
2646
2670
  ).toString().replace(/\/+$/g, '');
2647
2671
  authKeyForConfig = 'codexmate';
2648
2672
  extraLines.push(`codexmate_bridge = "openai"`);
2673
+ extraLines.push(`codexmate_bridge_max_retries = ${openaiBridgeMaxRetries}`);
2649
2674
  }
2650
2675
 
2651
2676
  const safeUrl = escapeTomlBasicString(baseUrlForConfig);
@@ -2689,6 +2714,8 @@ function updateProviderInConfig(params = {}) {
2689
2714
  ? String(params.key).trim()
2690
2715
  : undefined;
2691
2716
  const useTransform = !!params.useTransform;
2717
+ const hasOpenaiBridgeMaxRetries = params.openaiBridgeMaxRetries !== undefined || params.maxRetries !== undefined;
2718
+ const openaiBridgeMaxRetries = normalizeOpenaiBridgeMaxRetries(params.openaiBridgeMaxRetries ?? params.maxRetries);
2692
2719
  const allowManaged = !!params.allowManaged;
2693
2720
 
2694
2721
  if (!name) return { error: '名称不能为空' };
@@ -2703,7 +2730,7 @@ function updateProviderInConfig(params = {}) {
2703
2730
  }
2704
2731
 
2705
2732
  try {
2706
- cmdUpdate(name, url || undefined, key, true, { allowManaged, useTransform });
2733
+ cmdUpdate(name, url || undefined, key, true, { allowManaged, useTransform, ...(hasOpenaiBridgeMaxRetries ? { openaiBridgeMaxRetries } : {}) });
2707
2734
  return { success: true };
2708
2735
  } catch (e) {
2709
2736
  return { error: e.message || '更新失败' };
@@ -9846,6 +9873,8 @@ function cmdDelete(name, silent = false) {
9846
9873
  function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
9847
9874
  const allowManaged = !!(options && options.allowManaged);
9848
9875
  const forceUseTransform = !!(options && options.useTransform);
9876
+ const hasOpenaiBridgeMaxRetries = options && Object.prototype.hasOwnProperty.call(options, 'openaiBridgeMaxRetries');
9877
+ const openaiBridgeMaxRetries = normalizeOpenaiBridgeMaxRetries(options && options.openaiBridgeMaxRetries);
9849
9878
  const normalizedBaseUrl = baseUrl === undefined ? undefined : normalizeBaseUrl(baseUrl);
9850
9879
  if (!name) {
9851
9880
  if (!silent) console.error('错误: 提供商名称必填');
@@ -10005,6 +10034,32 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
10005
10034
  return next;
10006
10035
  };
10007
10036
 
10037
+ const replaceTomlNumberField = (block, fieldName, rawValue) => {
10038
+ const numberValue = String(Math.floor(Number(rawValue)));
10039
+ const escapedFieldName = escapeRegex(fieldName);
10040
+ const multilineRanges = collectTomlMultilineStringRanges(block);
10041
+ const withCommentRegex = new RegExp(`^(\\s*${escapedFieldName}\\s*=\\s*)([-+]?\\d+(?:\\.\\d+)?)(\\s+#.*)?$`, 'mg');
10042
+ let replaced = false;
10043
+ let next = block.replace(withCommentRegex, (full, prefix, _value, suffix = '', offset) => {
10044
+ if (replaced || isIndexInRanges(offset, multilineRanges)) {
10045
+ return full;
10046
+ }
10047
+ replaced = true;
10048
+ return `${prefix}${numberValue}${suffix}`;
10049
+ });
10050
+ if (!replaced) {
10051
+ const keyIndentMatch = block.match(/^(\s*)[A-Za-z0-9_.-]+\s*=/m);
10052
+ const indent = keyIndentMatch ? keyIndentMatch[1] : '';
10053
+ const lineEnding = block.includes('\r\n') ? '\r\n' : '\n';
10054
+ const tailMatch = block.match(/(\s*)$/);
10055
+ const tail = tailMatch ? tailMatch[1] : '';
10056
+ const body = block.slice(0, block.length - tail.length);
10057
+ const separator = body.endsWith('\n') || body.endsWith('\r') ? '' : lineEnding;
10058
+ next = `${body}${separator}${indent}${fieldName} = ${numberValue}${tail}`;
10059
+ }
10060
+ return next;
10061
+ };
10062
+
10008
10063
  const replaceTomlBooleanField = (block, fieldName, rawValue) => {
10009
10064
  const boolValue = rawValue ? 'true' : 'false';
10010
10065
  const escapedFieldName = escapeRegex(fieldName);
@@ -10061,7 +10116,9 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
10061
10116
  : existingApiKey;
10062
10117
 
10063
10118
  if (upstreamBaseUrl) {
10064
- const saveRes = upsertOpenaiBridgeProvider(OPENAI_BRIDGE_SETTINGS_FILE, name, upstreamBaseUrl, upstreamApiKey);
10119
+ const saveRes = upsertOpenaiBridgeProvider(OPENAI_BRIDGE_SETTINGS_FILE, name, upstreamBaseUrl, upstreamApiKey, undefined, {
10120
+ maxRetries: hasOpenaiBridgeMaxRetries ? openaiBridgeMaxRetries : resolveProviderOpenaiBridgeMaxRetries(providerConfig)
10121
+ });
10065
10122
  if (saveRes && saveRes.error) {
10066
10123
  throw new Error(String(saveRes.error));
10067
10124
  }
@@ -10077,6 +10134,9 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
10077
10134
  updatedBlock = replaceTomlBooleanField(updatedBlock, 'requires_openai_auth', true);
10078
10135
  updatedBlock = replaceTomlStringField(updatedBlock, 'preferred_auth_method', 'codexmate');
10079
10136
  updatedBlock = replaceTomlStringField(updatedBlock, 'codexmate_bridge', 'openai');
10137
+ if (hasOpenaiBridgeMaxRetries) {
10138
+ updatedBlock = replaceTomlNumberField(updatedBlock, 'codexmate_bridge_max_retries', openaiBridgeMaxRetries);
10139
+ }
10080
10140
  } else {
10081
10141
  if (normalizedBaseUrl) {
10082
10142
  updatedBlock = replaceTomlStringField(updatedBlock, 'base_url', normalizedBaseUrl);
@@ -12819,7 +12879,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
12819
12879
  break;
12820
12880
  }
12821
12881
  // 不返回 apiKey(敏感信息),仅返回用户填过的上游 URL
12822
- result = { baseUrl: upstream.baseUrl, hasApiKey: !!(upstream.apiKey) };
12882
+ const config = readConfig();
12883
+ const provider = config.model_providers && config.model_providers[name];
12884
+ const providerMaxRetries = resolveProviderOpenaiBridgeMaxRetries(provider);
12885
+ result = { baseUrl: upstream.baseUrl, hasApiKey: !!(upstream.apiKey), maxRetries: providerMaxRetries };
12823
12886
  break;
12824
12887
  }
12825
12888
  case 'list-sessions':
@@ -15059,11 +15122,13 @@ function buildMcpProviderListPayload() {
15059
15122
  upstreamUrl = upstream.baseUrl.trim();
15060
15123
  }
15061
15124
  }
15125
+ const openaiBridgeMaxRetries = resolveProviderOpenaiBridgeMaxRetries(p);
15062
15126
  return {
15063
15127
  name,
15064
15128
  url: p.base_url || '',
15065
15129
  upstreamUrl,
15066
15130
  codexmate_bridge: bridge,
15131
+ openaiBridgeMaxRetries,
15067
15132
  key: maskKey(p.preferred_auth_method || ''),
15068
15133
  hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
15069
15134
  models: Array.isArray(p.models)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.57",
3
+ "version": "0.1.1",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -74,6 +74,12 @@ document.addEventListener('DOMContentLoaded', () => {
74
74
  showAgentsModal: false,
75
75
  promptsSubTab: 'codex',
76
76
  projectClaudeMdPath: '',
77
+ promptPresets: [],
78
+ selectedPromptPresetId: '',
79
+ promptPresetNameDraft: '',
80
+ promptPresetRenameDraft: {},
81
+ promptPresetSaving: false,
82
+ __skipNextPromptsSubTabLoad: false,
77
83
  projectPathOptions: [],
78
84
  projectPathOptionsLoading: false,
79
85
  showSkillsModal: false,
@@ -284,9 +290,9 @@ document.addEventListener('DOMContentLoaded', () => {
284
290
  appVersionStatusChecked: false,
285
291
  appVersionStatusCheckedAt: '',
286
292
  appVersionStatusSource: '',
287
- newProvider: { name: '', url: '', key: '', model: '', useTransform: false },
293
+ newProvider: { name: '', url: '', key: '', model: '', useTransform: false, openaiBridgeMaxRetries: 2 },
288
294
  resetConfigLoading: false,
289
- editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false },
295
+ editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false, useTransform: false, openaiBridgeMaxRetries: 2 },
290
296
  newModelName: '',
291
297
  currentClaudeConfig: '',
292
298
  currentClaudeModel: '',
@@ -737,6 +743,10 @@ document.addEventListener('DOMContentLoaded', () => {
737
743
  if (typeof this.persistWebUiPreferences === 'function') {
738
744
  this.persistWebUiPreferences({ promptsSubTab: newVal });
739
745
  }
746
+ if (this.__skipNextPromptsSubTabLoad) {
747
+ this.__skipNextPromptsSubTabLoad = false;
748
+ return;
749
+ }
740
750
  if (this.mainTab === 'prompts' && typeof this.loadPromptsContent === 'function') {
741
751
  this.loadPromptsContent();
742
752
  }
@@ -708,8 +708,193 @@ export function createAgentsMethods(options = {}) {
708
708
  }
709
709
  },
710
710
 
711
+
712
+ normalizePromptPresetName(name) {
713
+ return typeof name === 'string' ? name.trim() : '';
714
+ },
715
+ buildPromptPresetId() {
716
+ const randomPart = Math.random().toString(36).slice(2, 8);
717
+ return `prompt-preset-${Date.now()}-${randomPart}`;
718
+ },
719
+ getPromptPresetRenameDraft(preset) {
720
+ if (!preset || !preset.id) return '';
721
+ if (Object.prototype.hasOwnProperty.call(this.promptPresetRenameDraft || {}, preset.id)) {
722
+ return this.promptPresetRenameDraft[preset.id];
723
+ }
724
+ return preset.name || '';
725
+ },
726
+ setPromptPresetRenameDraft(id, value) {
727
+ if (!id) return;
728
+ this.promptPresetRenameDraft = {
729
+ ...(this.promptPresetRenameDraft || {}),
730
+ [id]: value
731
+ };
732
+ },
733
+ formatPromptPresetTime(value) {
734
+ if (typeof value !== 'string' || !value) {
735
+ return this.t('common.none');
736
+ }
737
+ const date = new Date(value);
738
+ if (Number.isNaN(date.getTime())) {
739
+ return value;
740
+ }
741
+ return date.toLocaleString();
742
+ },
743
+ findPromptPresetByName(name, excludeId = '') {
744
+ const normalizedName = this.normalizePromptPresetName(name).toLowerCase();
745
+ return (Array.isArray(this.promptPresets) ? this.promptPresets : []).find((preset) => {
746
+ if (!preset || preset.id === excludeId) return false;
747
+ return this.normalizePromptPresetName(preset.name).toLowerCase() === normalizedName;
748
+ }) || null;
749
+ },
750
+ async persistPromptPresets() {
751
+ if (typeof this.persistWebUiPreferences === 'function') {
752
+ await this.persistWebUiPreferences({ promptPresets: this.promptPresets });
753
+ }
754
+ },
755
+ getCurrentPromptPresetDefaultName() {
756
+ if (this.promptsSubTab === 'claude-project') {
757
+ const projectPath = typeof this.projectClaudeMdPath === 'string' ? this.projectClaudeMdPath.trim() : '';
758
+ return projectPath
759
+ ? this.t('prompts.presets.defaultName.project', { path: projectPath })
760
+ : this.t('prompts.subTab.project');
761
+ }
762
+ const fileName = typeof this.agentsPath === 'string' && this.agentsPath.trim()
763
+ ? (this.agentsPath.trim().split(/[\\/]/).filter(Boolean).pop() || '')
764
+ : '';
765
+ return fileName || this.t('prompts.subTab.codex');
766
+ },
767
+ async saveEditorPromptAsPreset() {
768
+ if (this.promptPresetSaving || this.agentsLoading || this.agentsDiffVisible) return;
769
+ const name = this.getCurrentPromptPresetDefaultName();
770
+ const content = typeof this.agentsContent === 'string' ? this.agentsContent : '';
771
+ if (!content.trim()) {
772
+ this.showMessage(this.t('prompts.presets.error.emptyContent'), 'error');
773
+ return;
774
+ }
775
+ const confirmed = await this.requestConfirmDialog({
776
+ title: this.t('prompts.presets.confirm.addCurrentTitle'),
777
+ message: this.t('prompts.presets.confirm.addCurrentMessage', { name }),
778
+ confirmText: this.t('prompts.presets.addCurrent'),
779
+ cancelText: this.t('common.cancel'),
780
+ danger: false
781
+ });
782
+ if (!confirmed) return;
783
+ this.promptPresetNameDraft = name;
784
+ await this.saveCurrentPromptAsPreset();
785
+ },
786
+ async saveCurrentPromptAsPreset() {
787
+ if (this.promptPresetSaving || this.agentsLoading) return;
788
+ const draftName = this.normalizePromptPresetName(this.promptPresetNameDraft);
789
+ const name = draftName || this.normalizePromptPresetName(this.getCurrentPromptPresetDefaultName());
790
+ const content = typeof this.agentsContent === 'string' ? this.agentsContent : '';
791
+ if (!name) {
792
+ this.showMessage(this.t('prompts.presets.error.emptyName'), 'error');
793
+ return;
794
+ }
795
+ if (!content.trim()) {
796
+ this.showMessage(this.t('prompts.presets.error.emptyContent'), 'error');
797
+ return;
798
+ }
799
+ const existing = this.findPromptPresetByName(name);
800
+ if (existing) {
801
+ const confirmed = await this.requestConfirmDialog({
802
+ title: this.t('prompts.presets.confirm.overwriteTitle'),
803
+ message: this.t('prompts.presets.confirm.overwriteMessage', { name }),
804
+ confirmText: this.t('prompts.presets.confirm.overwriteConfirm'),
805
+ cancelText: this.t('common.cancel'),
806
+ danger: false
807
+ });
808
+ if (!confirmed) return;
809
+ }
810
+ this.promptPresetSaving = true;
811
+ try {
812
+ const now = new Date().toISOString();
813
+ if (existing) {
814
+ this.promptPresets = this.promptPresets.map((preset) => preset.id === existing.id
815
+ ? { ...preset, name, content, updatedAt: now }
816
+ : preset);
817
+ this.selectedPromptPresetId = existing.id;
818
+ } else {
819
+ const preset = {
820
+ id: this.buildPromptPresetId(),
821
+ name,
822
+ content,
823
+ updatedAt: now
824
+ };
825
+ this.promptPresets = [preset, ...this.promptPresets];
826
+ this.selectedPromptPresetId = preset.id;
827
+ }
828
+ this.promptPresetNameDraft = '';
829
+ await this.persistPromptPresets();
830
+ this.showMessage(this.t('prompts.presets.toast.saved'), 'success');
831
+ } finally {
832
+ this.promptPresetSaving = false;
833
+ }
834
+ },
835
+ async applyPromptPresetToEditor(preset) {
836
+ if (!preset || typeof preset.content !== 'string' || !preset.content) return;
837
+ this.agentsContent = preset.content;
838
+ this.onAgentsContentInput();
839
+ this.selectedPromptPresetId = preset.id;
840
+ this.showMessage(this.t('prompts.presets.toast.pasted'), 'success');
841
+ },
842
+ async applyPromptPresetSelection(event) {
843
+ const select = event && event.target;
844
+ const presetId = select && typeof select.value === 'string' ? select.value : '';
845
+ if (!presetId) return;
846
+ const preset = (Array.isArray(this.promptPresets) ? this.promptPresets : []).find((item) => item && item.id === presetId);
847
+ await this.applyPromptPresetToEditor(preset);
848
+ if (select) select.value = '';
849
+ },
850
+ async renamePromptPreset(preset) {
851
+ if (!preset || !preset.id) return;
852
+ const name = this.normalizePromptPresetName(this.getPromptPresetRenameDraft(preset));
853
+ if (!name) {
854
+ this.showMessage(this.t('prompts.presets.error.emptyName'), 'error');
855
+ return;
856
+ }
857
+ const existing = this.findPromptPresetByName(name, preset.id);
858
+ if (existing) {
859
+ this.showMessage(this.t('prompts.presets.error.duplicateName'), 'error');
860
+ return;
861
+ }
862
+ if (name === preset.name) {
863
+ this.showMessage(this.t('toast.noChanges'), 'info');
864
+ return;
865
+ }
866
+ const now = new Date().toISOString();
867
+ this.promptPresets = this.promptPresets.map((item) => item.id === preset.id
868
+ ? { ...item, name, updatedAt: now }
869
+ : item);
870
+ const drafts = { ...(this.promptPresetRenameDraft || {}) };
871
+ delete drafts[preset.id];
872
+ this.promptPresetRenameDraft = drafts;
873
+ await this.persistPromptPresets();
874
+ this.showMessage(this.t('prompts.presets.toast.renamed'), 'success');
875
+ },
876
+ async deletePromptPreset(preset) {
877
+ if (!preset || !preset.id) return;
878
+ const confirmed = await this.requestConfirmDialog({
879
+ title: this.t('prompts.presets.confirm.deleteTitle'),
880
+ message: this.t('prompts.presets.confirm.deleteMessage', { name: preset.name || '' }),
881
+ confirmText: this.t('common.delete'),
882
+ cancelText: this.t('common.cancel'),
883
+ danger: true
884
+ });
885
+ if (!confirmed) return;
886
+ this.promptPresets = this.promptPresets.filter((item) => item.id !== preset.id);
887
+ if (this.selectedPromptPresetId === preset.id) {
888
+ this.selectedPromptPresetId = '';
889
+ }
890
+ const drafts = { ...(this.promptPresetRenameDraft || {}) };
891
+ delete drafts[preset.id];
892
+ this.promptPresetRenameDraft = drafts;
893
+ await this.persistPromptPresets();
894
+ this.showMessage(this.t('prompts.presets.toast.deleted'), 'success');
895
+ },
711
896
  switchPromptsSubTab(subTab) {
712
- const normalized = subTab === 'claude-project' ? 'claude-project' : 'codex';
897
+ const normalized = subTab === 'claude-project' ? subTab : 'codex';
713
898
  if (normalized === 'claude-project' && !this.projectPathOptions.length && !this.projectPathOptionsLoading) {
714
899
  this.loadProjectPathOptions();
715
900
  }
@@ -718,6 +903,9 @@ export function createAgentsMethods(options = {}) {
718
903
  return;
719
904
  }
720
905
  this.promptsSubTab = normalized;
906
+ this.$nextTick(() => {
907
+ document.querySelector('.main-panel')?.scrollTo({ top: 0, left: 0, behavior: 'auto' });
908
+ });
721
909
  },
722
910
 
723
911
  async loadPromptsContent() {
@@ -41,6 +41,13 @@ function findProviderByName(list, name) {
41
41
  return (Array.isArray(list) ? list : []).find((item) => item && normalizeText(item.name) === target) || null;
42
42
  }
43
43
 
44
+ function normalizeBridgeMaxRetries(value, fallback = 2) {
45
+ const raw = Number(value);
46
+ const fallbackRaw = Number(fallback);
47
+ const base = Number.isFinite(raw) ? raw : (Number.isFinite(fallbackRaw) ? fallbackRaw : 2);
48
+ return Math.min(10, Math.max(2, Math.floor(base)));
49
+ }
50
+
44
51
  function normalizeProviderDraftState(target) {
45
52
  if (!target || typeof target !== 'object') return;
46
53
  if (typeof target.name === 'string') {
@@ -55,6 +62,9 @@ function normalizeProviderDraftState(target) {
55
62
  if (typeof target.key === 'string') {
56
63
  target.key = target.key.trim();
57
64
  }
65
+ if (target.useTransform || target.openaiBridgeMaxRetries !== undefined) {
66
+ target.openaiBridgeMaxRetries = normalizeBridgeMaxRetries(target.openaiBridgeMaxRetries);
67
+ }
58
68
  }
59
69
 
60
70
  function maskKeyLocal(key) {
@@ -76,11 +86,14 @@ function getProviderValidationForContext(vm, mode = 'add') {
76
86
  const url = normalizeProviderUrl(draft && draft.url);
77
87
  const model = normalizeText(draft && draft.model);
78
88
  const key = normalizeText(draft && draft.key);
89
+ const useTransform = !!(draft && draft.useTransform);
90
+ const openaiBridgeMaxRetries = normalizeBridgeMaxRetries(draft && draft.openaiBridgeMaxRetries);
79
91
  const errors = {
80
92
  name: '',
81
93
  url: '',
82
94
  key: '',
83
- model: ''
95
+ model: '',
96
+ openaiBridgeMaxRetries: ''
84
97
  };
85
98
 
86
99
  if (mode === 'add') {
@@ -111,14 +124,20 @@ function getProviderValidationForContext(vm, mode = 'add') {
111
124
  errors.model = '模型名称必填';
112
125
  }
113
126
 
127
+ if (useTransform && openaiBridgeMaxRetries < 2) {
128
+ errors.openaiBridgeMaxRetries = '重试次数最小为 2';
129
+ }
130
+
114
131
  return {
115
132
  mode,
116
133
  name,
117
134
  url,
118
135
  key,
119
136
  model,
137
+ useTransform,
138
+ openaiBridgeMaxRetries,
120
139
  errors,
121
- ok: !errors.name && !errors.url && !errors.key && !errors.model
140
+ ok: !errors.name && !errors.url && !errors.key && !errors.model && !errors.openaiBridgeMaxRetries
122
141
  };
123
142
  }
124
143
 
@@ -172,7 +191,7 @@ export function createProvidersMethods(options = {}) {
172
191
  normalizeProviderDraftState(this.newProvider);
173
192
  const validation = getProviderValidationForContext(this, 'add');
174
193
  if (!validation.ok) {
175
- return this.showMessage(validation.errors.name || validation.errors.url || validation.errors.key || validation.errors.model || this.t('toast.provider.fieldsRequired'), 'error');
194
+ return this.showMessage(validation.errors.name || validation.errors.url || validation.errors.key || validation.errors.model || validation.errors.openaiBridgeMaxRetries || this.t('toast.provider.fieldsRequired'), 'error');
176
195
  }
177
196
 
178
197
  try {
@@ -184,6 +203,7 @@ export function createProvidersMethods(options = {}) {
184
203
  };
185
204
  if (this.newProvider && this.newProvider.useTransform) {
186
205
  payload.useTransform = true;
206
+ payload.openaiBridgeMaxRetries = validation.openaiBridgeMaxRetries;
187
207
  }
188
208
  const suggestedModel = validation.model;
189
209
  const res = await api('add-provider', payload);
@@ -198,6 +218,7 @@ export function createProvidersMethods(options = {}) {
198
218
  url: validation.url,
199
219
  upstreamUrl: '',
200
220
  codexmate_bridge: payload.useTransform ? 'openai' : '',
221
+ openaiBridgeMaxRetries: payload.useTransform ? validation.openaiBridgeMaxRetries : undefined,
201
222
  key: maskKeyLocal(payload.key),
202
223
  hasKey: !!payload.key,
203
224
  models: suggestedModel ? [{ id: suggestedModel, name: suggestedModel, cost: null, contextWindow: undefined, maxTokens: undefined }] : [],
@@ -323,7 +344,8 @@ export function createProvidersMethods(options = {}) {
323
344
  url: cloneUrl,
324
345
  key: '',
325
346
  model: '',
326
- useTransform: isTransform
347
+ useTransform: isTransform,
348
+ openaiBridgeMaxRetries: normalizeBridgeMaxRetries(provider.openaiBridgeMaxRetries)
327
349
  };
328
350
  this.showAddProviderKey = false;
329
351
  this.showAddModal = true;
@@ -335,7 +357,8 @@ export function createProvidersMethods(options = {}) {
335
357
  url: '',
336
358
  key: '',
337
359
  model: '',
338
- useTransform: false
360
+ useTransform: false,
361
+ openaiBridgeMaxRetries: 2
339
362
  };
340
363
  this.showAddProviderKey = false;
341
364
  this.showAddModal = true;
@@ -363,7 +386,8 @@ export function createProvidersMethods(options = {}) {
363
386
  nonEditable: typeof provider.nonEditable === 'boolean'
364
387
  ? provider.nonEditable
365
388
  : this.isNonDeletableProvider(provider),
366
- useTransform: isTransformProvider
389
+ useTransform: isTransformProvider,
390
+ openaiBridgeMaxRetries: normalizeBridgeMaxRetries(provider.openaiBridgeMaxRetries)
367
391
  };
368
392
  this._editProviderOriginalKey = '';
369
393
  this._editProviderRealKeyLoaded = false;
@@ -404,6 +428,9 @@ export function createProvidersMethods(options = {}) {
404
428
  && res.baseUrl.trim()
405
429
  ) {
406
430
  this.editingProvider.url = normalizeProviderUrl(res.baseUrl);
431
+ if (res.maxRetries !== undefined) {
432
+ this.editingProvider.openaiBridgeMaxRetries = normalizeBridgeMaxRetries(res.maxRetries);
433
+ }
407
434
  }
408
435
  } catch (_) {
409
436
  // ignore
@@ -420,12 +447,13 @@ export function createProvidersMethods(options = {}) {
420
447
  normalizeProviderDraftState(this.editingProvider);
421
448
  const validation = getProviderValidationForContext(this, 'edit');
422
449
  if (!validation.ok) {
423
- return this.showMessage(validation.errors.name || validation.errors.url || this.t('toast.provider.urlRequired'), 'error');
450
+ return this.showMessage(validation.errors.name || validation.errors.url || validation.errors.openaiBridgeMaxRetries || this.t('toast.provider.urlRequired'), 'error');
424
451
  }
425
452
 
426
453
  const params = { name: validation.name, url: validation.url };
427
454
  if (this.editingProvider && this.editingProvider.useTransform) {
428
455
  params.useTransform = true;
456
+ params.openaiBridgeMaxRetries = validation.openaiBridgeMaxRetries;
429
457
  }
430
458
  if (this._editProviderRealKeyLoaded) {
431
459
  const currentKey = typeof this.editingProvider.key === 'string' ? this.editingProvider.key : '';
@@ -449,7 +477,9 @@ export function createProvidersMethods(options = {}) {
449
477
  ...p,
450
478
  url: validation.url,
451
479
  key: keyUpdated ? maskKeyLocal(params.key) : p.key,
452
- hasKey: keyUpdated ? !!params.key : p.hasKey
480
+ hasKey: keyUpdated ? !!params.key : p.hasKey,
481
+ codexmate_bridge: params.useTransform ? 'openai' : p.codexmate_bridge,
482
+ openaiBridgeMaxRetries: params.useTransform ? validation.openaiBridgeMaxRetries : p.openaiBridgeMaxRetries
453
483
  };
454
484
  }
455
485
  return p;
@@ -467,7 +497,7 @@ export function createProvidersMethods(options = {}) {
467
497
  this.showEditProviderKey = false;
468
498
  this._editProviderOriginalKey = '';
469
499
  this._editProviderRealKeyLoaded = false;
470
- this.editingProvider = { name: '', url: '', key: '', readOnly: false, nonEditable: false, useTransform: false };
500
+ this.editingProvider = { name: '', url: '', key: '', readOnly: false, nonEditable: false, useTransform: false, openaiBridgeMaxRetries: 2 };
471
501
  },
472
502
 
473
503
  toggleEditProviderKey() {
@@ -550,7 +580,7 @@ export function createProvidersMethods(options = {}) {
550
580
  closeAddModal() {
551
581
  this.showAddModal = false;
552
582
  this.showAddProviderKey = false;
553
- this.newProvider = { name: nextCodexProviderName(this.providersList), url: '', key: '', model: '', useTransform: false };
583
+ this.newProvider = { name: nextCodexProviderName(this.providersList), url: '', key: '', model: '', useTransform: false, openaiBridgeMaxRetries: 2 };
554
584
  },
555
585
 
556
586
  toggleAddProviderKey() {
@@ -62,7 +62,27 @@ function normalizeUsageTimeRange(value) {
62
62
 
63
63
  function normalizePromptsSubTab(value) {
64
64
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
65
- return normalized === 'claude-project' ? 'claude-project' : 'codex';
65
+ if (normalized === 'claude-project') return normalized;
66
+ return 'codex';
67
+ }
68
+
69
+ function normalizePromptPresets(value) {
70
+ if (!Array.isArray(value)) return [];
71
+ const seen = new Set();
72
+ return value
73
+ .filter(item => item && typeof item === 'object')
74
+ .map((item, index) => {
75
+ const id = typeof item.id === 'string' && item.id.trim()
76
+ ? item.id.trim()
77
+ : `preset-${Date.now()}-${index}`;
78
+ const name = typeof item.name === 'string' ? item.name.trim() : '';
79
+ const content = typeof item.content === 'string' ? item.content : '';
80
+ const updatedAt = typeof item.updatedAt === 'string' && item.updatedAt.trim()
81
+ ? item.updatedAt.trim()
82
+ : new Date(0).toISOString();
83
+ return { id, name, content, updatedAt };
84
+ })
85
+ .filter(item => item.id && item.name && !seen.has(item.id) && (seen.add(item.id), true));
66
86
  }
67
87
 
68
88
  function normalizeSessionSortMode(value) {
@@ -313,6 +333,7 @@ export function createWebUiPreferencesMethods(options = {}) {
313
333
  : this.configTemplateDiffConfirmEnabled !== false,
314
334
  sessionsUsageTimeRange: normalizeUsageTimeRange(hasOwn(source, 'sessionsUsageTimeRange') ? source.sessionsUsageTimeRange : this.sessionsUsageTimeRange),
315
335
  promptsSubTab: normalizePromptsSubTab(hasOwn(source, 'promptsSubTab') ? source.promptsSubTab : this.promptsSubTab),
336
+ promptPresets: normalizePromptPresets(hasOwn(source, 'promptPresets') ? source.promptPresets : this.promptPresets),
316
337
  projectClaudeMdPath: typeof source.projectClaudeMdPath === 'string'
317
338
  ? source.projectClaudeMdPath
318
339
  : (typeof this.projectClaudeMdPath === 'string' ? this.projectClaudeMdPath : ''),
@@ -365,6 +386,12 @@ export function createWebUiPreferencesMethods(options = {}) {
365
386
  if (typeof source.promptsSubTab === 'string') {
366
387
  this.promptsSubTab = normalizePromptsSubTab(source.promptsSubTab);
367
388
  }
389
+ if (Array.isArray(source.promptPresets)) {
390
+ this.promptPresets = normalizePromptPresets(source.promptPresets);
391
+ if (this.selectedPromptPresetId && !this.promptPresets.some(preset => preset.id === this.selectedPromptPresetId)) {
392
+ this.selectedPromptPresetId = '';
393
+ }
394
+ }
368
395
  if (typeof source.projectClaudeMdPath === 'string') {
369
396
  this.projectClaudeMdPath = source.projectClaudeMdPath;
370
397
  }