codexmate 0.0.57 → 0.1.2

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.2",
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,
@@ -286,7 +292,7 @@ document.addEventListener('DOMContentLoaded', () => {
286
292
  appVersionStatusSource: '',
287
293
  newProvider: { name: '', url: '', key: '', model: '', useTransform: false },
288
294
  resetConfigLoading: false,
289
- editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false },
295
+ editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false, useTransform: false },
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
  }
@@ -206,13 +206,13 @@ export function createAgentsMethods(options = {}) {
206
206
  this.agentsContext = 'openclaw-workspace';
207
207
  this.agentsWorkspaceFileName = fileName;
208
208
  this.agentsModalTitle = tr('modal.agents.title.openclawWorkspaceFile', `OpenClaw 工作区文件: ${fileName}`, { fileName });
209
- this.agentsModalHint = tr('modal.agents.hint.openclawWorkspaceFile', `保存后会写入 OpenClaw Workspace 下的 ${fileName}。`, { fileName });
209
+ this.agentsModalHint = tr('modal.agents.hint.openclawWorkspaceFile', `Workspace / ${fileName}`, { fileName });
210
210
  return;
211
211
  }
212
212
  this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
213
213
  if (this.agentsContext === 'openclaw') {
214
214
  this.agentsModalTitle = tr('modal.agents.title.openclaw', 'OpenClaw AGENTS.md 编辑器');
215
- this.agentsModalHint = tr('modal.agents.hint.openclaw', '保存后会写入 OpenClaw Workspace 下的 AGENTS.md');
215
+ this.agentsModalHint = tr('modal.agents.hint.openclaw', 'Workspace / AGENTS.md');
216
216
  } else {
217
217
  this.agentsModalTitle = tr('modal.agents.title.default', 'AGENTS.md 编辑器');
218
218
  this.agentsModalHint = tr('modal.agents.hint.default', '保存后会写入目标 AGENTS.md(与 config.toml 同级)。');
@@ -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() {
@@ -224,6 +224,31 @@ function readPreferredProviderModels(records) {
224
224
  return [];
225
225
  }
226
226
 
227
+ function readNestedValue(record, path) {
228
+ if (!isPlainRecord(record)) return undefined;
229
+ let cursor = record;
230
+ for (const key of path) {
231
+ if (!isPlainRecord(cursor) || !Object.prototype.hasOwnProperty.call(cursor, key)) {
232
+ return undefined;
233
+ }
234
+ cursor = cursor[key];
235
+ }
236
+ return cursor;
237
+ }
238
+
239
+ function formatOpenclawSummaryValue(value, fallback = '未配置') {
240
+ if (Array.isArray(value)) {
241
+ const list = value
242
+ .map(item => (typeof item === 'string' ? item.trim() : String(item || '').trim()))
243
+ .filter(Boolean);
244
+ return list.length ? list.join(' / ') : fallback;
245
+ }
246
+ if (value === true) return '启用';
247
+ if (value === false) return '关闭';
248
+ const text = typeof value === 'string' ? value.trim() : String(value ?? '').trim();
249
+ return text || fallback;
250
+ }
251
+
227
252
  export function createOpenclawCoreMethods() {
228
253
  return {
229
254
  getOpenclawParser() {
@@ -472,6 +497,68 @@ export function createOpenclawCoreMethods() {
472
497
  };
473
498
  },
474
499
 
500
+ getOpenclawConfigSummary(config) {
501
+ const content = config && typeof config.content === 'string' ? config.content : '';
502
+ if (!content.trim()) {
503
+ return [];
504
+ }
505
+ const parsed = this.parseOpenclawContent(content, { allowEmpty: true });
506
+ if (!parsed.ok || !isPlainRecord(parsed.data)) {
507
+ return [
508
+ { key: 'parse-error', label: '配置状态', value: '解析失败', tone: 'warning' }
509
+ ];
510
+ }
511
+ const data = parsed.data;
512
+ const agentDefaults = readNestedValue(data, ['agents', 'defaults']);
513
+ const modelConfig = isPlainRecord(agentDefaults) ? agentDefaults.model : undefined;
514
+ const primaryModel = isPlainRecord(modelConfig)
515
+ ? modelConfig.primary
516
+ : (typeof modelConfig === 'string' ? modelConfig : readNestedValue(data, ['agent', 'model']));
517
+ const fallbacks = isPlainRecord(modelConfig) && Array.isArray(modelConfig.fallbacks)
518
+ ? modelConfig.fallbacks
519
+ : [];
520
+ const workspace = isPlainRecord(agentDefaults) ? agentDefaults.workspace : '';
521
+ const browser = isPlainRecord(data.browser) ? data.browser : {};
522
+ const browserMode = browser.enabled === false
523
+ ? '关闭'
524
+ : (browser.headless === true ? 'Headless' : (browser.headless === false ? '可见窗口' : '未声明'));
525
+ const executablePath = typeof browser.executablePath === 'string' ? browser.executablePath.trim() : '';
526
+ const providerNames = collectDistinctProviderKeys(
527
+ readNestedValue(data, ['models', 'providers']),
528
+ data.providers
529
+ );
530
+ return [
531
+ { key: 'primary', label: '默认模型', value: formatOpenclawSummaryValue(primaryModel) },
532
+ { key: 'fallbacks', label: 'Fallback', value: formatOpenclawSummaryValue(fallbacks) },
533
+ { key: 'workspace', label: 'Workspace', value: formatOpenclawSummaryValue(workspace) },
534
+ { key: 'browser', label: 'Browser', value: browserMode },
535
+ { key: 'browser-path', label: 'Chrome', value: formatOpenclawSummaryValue(executablePath) },
536
+ { key: 'providers', label: 'Providers', value: providerNames.length ? providerNames.join(' / ') : '未配置' }
537
+ ];
538
+ },
539
+
540
+ getOpenclawStatusSummaryItems() {
541
+ const current = this.openclawConfigs && this.currentOpenclawConfig
542
+ ? this.openclawConfigs[this.currentOpenclawConfig]
543
+ : null;
544
+ const configPath = this.openclawConfigPath || '~/.openclaw/openclaw.json';
545
+ return [
546
+ { key: 'config-path', label: 'Config', value: configPath, tone: this.openclawConfigExists ? 'ok' : 'warning' },
547
+ ...this.getOpenclawConfigSummary(current || {}).slice(0, 4)
548
+ ];
549
+ },
550
+
551
+ getOpenclawQuickWorkspaceFiles() {
552
+ return ['SOUL.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md'];
553
+ },
554
+
555
+ openOpenclawQuickWorkspaceFile(fileName) {
556
+ const normalized = typeof fileName === 'string' ? fileName.trim() : '';
557
+ if (!normalized) return;
558
+ this.openclawWorkspaceFileName = normalized;
559
+ this.openOpenclawWorkspaceEditor();
560
+ },
561
+
475
562
  syncOpenclawQuickFromText(options = {}) {
476
563
  const silent = !!options.silent;
477
564
  const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
@@ -76,11 +76,12 @@ function getProviderValidationForContext(vm, mode = 'add') {
76
76
  const url = normalizeProviderUrl(draft && draft.url);
77
77
  const model = normalizeText(draft && draft.model);
78
78
  const key = normalizeText(draft && draft.key);
79
+ const useTransform = !!(draft && draft.useTransform);
79
80
  const errors = {
80
81
  name: '',
81
82
  url: '',
82
83
  key: '',
83
- model: ''
84
+ model: '',
84
85
  };
85
86
 
86
87
  if (mode === 'add') {
@@ -117,6 +118,7 @@ function getProviderValidationForContext(vm, mode = 'add') {
117
118
  url,
118
119
  key,
119
120
  model,
121
+ useTransform,
120
122
  errors,
121
123
  ok: !errors.name && !errors.url && !errors.key && !errors.model
122
124
  };
@@ -323,7 +325,7 @@ export function createProvidersMethods(options = {}) {
323
325
  url: cloneUrl,
324
326
  key: '',
325
327
  model: '',
326
- useTransform: isTransform
328
+ useTransform: isTransform,
327
329
  };
328
330
  this.showAddProviderKey = false;
329
331
  this.showAddModal = true;
@@ -335,7 +337,7 @@ export function createProvidersMethods(options = {}) {
335
337
  url: '',
336
338
  key: '',
337
339
  model: '',
338
- useTransform: false
340
+ useTransform: false,
339
341
  };
340
342
  this.showAddProviderKey = false;
341
343
  this.showAddModal = true;
@@ -363,7 +365,7 @@ export function createProvidersMethods(options = {}) {
363
365
  nonEditable: typeof provider.nonEditable === 'boolean'
364
366
  ? provider.nonEditable
365
367
  : this.isNonDeletableProvider(provider),
366
- useTransform: isTransformProvider
368
+ useTransform: isTransformProvider,
367
369
  };
368
370
  this._editProviderOriginalKey = '';
369
371
  this._editProviderRealKeyLoaded = false;
@@ -449,7 +451,8 @@ export function createProvidersMethods(options = {}) {
449
451
  ...p,
450
452
  url: validation.url,
451
453
  key: keyUpdated ? maskKeyLocal(params.key) : p.key,
452
- hasKey: keyUpdated ? !!params.key : p.hasKey
454
+ hasKey: keyUpdated ? !!params.key : p.hasKey,
455
+ codexmate_bridge: params.useTransform ? 'openai' : p.codexmate_bridge,
453
456
  };
454
457
  }
455
458
  return p;
@@ -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
  }