codexmate 0.0.54 → 0.0.56

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 +3 -0
  2. package/README.vi.md +172 -0
  3. package/README.zh.md +2 -0
  4. package/cli/local-bridge.js +221 -22
  5. package/cli.js +666 -6
  6. package/package.json +1 -1
  7. package/web-ui/app.js +40 -0
  8. package/web-ui/logic.claude.mjs +8 -0
  9. package/web-ui/modules/app.computed.session.mjs +210 -0
  10. package/web-ui/modules/app.methods.claude-config.mjs +196 -13
  11. package/web-ui/modules/app.methods.codex-config.mjs +294 -65
  12. package/web-ui/modules/app.methods.index.mjs +4 -0
  13. package/web-ui/modules/app.methods.navigation.mjs +7 -1
  14. package/web-ui/modules/app.methods.provider-cache.mjs +223 -0
  15. package/web-ui/modules/app.methods.providers.mjs +15 -1
  16. package/web-ui/modules/app.methods.runtime.mjs +7 -2
  17. package/web-ui/modules/app.methods.session-actions.mjs +36 -0
  18. package/web-ui/modules/app.methods.session-browser.mjs +3 -0
  19. package/web-ui/modules/app.methods.session-trash.mjs +3 -0
  20. package/web-ui/modules/app.methods.startup-claude.mjs +47 -4
  21. package/web-ui/modules/app.methods.web-ui-preferences.mjs +161 -0
  22. package/web-ui/modules/i18n/locales/en.mjs +106 -3
  23. package/web-ui/modules/i18n/locales/ja.mjs +103 -3
  24. package/web-ui/modules/i18n/locales/vi.mjs +950 -0
  25. package/web-ui/modules/i18n/locales/zh-tw.mjs +103 -3
  26. package/web-ui/modules/i18n/locales/zh.mjs +103 -3
  27. package/web-ui/modules/provider-default-names.mjs +25 -0
  28. package/web-ui/partials/index/layout-header.html +18 -5
  29. package/web-ui/partials/index/modal-health-check.html +69 -5
  30. package/web-ui/partials/index/modals-basic.html +182 -0
  31. package/web-ui/partials/index/panel-config-codex.html +2 -2
  32. package/web-ui/partials/index/panel-sessions.html +99 -19
  33. package/web-ui/partials/index/panel-settings.html +20 -0
  34. package/web-ui/res/web-ui-render.precompiled.js +742 -113
  35. package/web-ui/session-helpers.mjs +4 -1
  36. package/web-ui/styles/layout-shell.css +51 -1
  37. package/web-ui/styles/responsive.css +98 -0
  38. package/web-ui/styles/sessions-preview.css +212 -2
  39. package/web-ui/styles/sessions-toolbar-trash.css +61 -18
  40. package/web-ui/styles/settings-panel.css +398 -0
  41. package/web-ui/styles/skills-list.css +122 -0
  42. package/web-ui/styles/titles-cards.css +52 -0
package/cli.js CHANGED
@@ -218,6 +218,29 @@ const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
218
218
  const CODEBUDDY_DIR = path.join(os.homedir(), '.codebuddy');
219
219
  const CODEBUDDY_PROJECTS_DIR = path.join(CODEBUDDY_DIR, 'projects');
220
220
  const CODEXMATE_DIR = path.join(os.homedir(), '.codexmate');
221
+ const PROVIDER_CACHE_FILE_GROUPS = Object.freeze({
222
+ claude: [
223
+ 'claude-providers.json'
224
+ ],
225
+ codex: [
226
+ 'codex-providers.json',
227
+ 'codex-provider-current-models.json'
228
+ ],
229
+ opencode: [
230
+ 'opencode-providers.json',
231
+ 'opencode-provider-current-models.json'
232
+ ]
233
+ });
234
+ const PROVIDER_CACHE_PROVIDER_FILES = Object.freeze([
235
+ 'codex-providers.json',
236
+ 'claude-providers.json',
237
+ 'opencode-providers.json'
238
+ ]);
239
+ const PROVIDER_CACHE_CURRENT_MODEL_FILES = Object.freeze([
240
+ 'codex-provider-current-models.json',
241
+ 'opencode-provider-current-models.json'
242
+ ]);
243
+ const PROVIDER_CACHE_MAX_FILE_BYTES = 256 * 1024;
221
244
  const CODEXMATE_PREFERENCES_FILE = path.join(CODEXMATE_DIR, 'preferences.json');
222
245
  const CODEXMATE_OPENCODE_DIR = path.join(CODEXMATE_DIR, 'opencode');
223
246
  const CODEXMATE_OPENCODE_PROVIDER_STORE_FILE = path.join(CODEXMATE_OPENCODE_DIR, 'providers.json');
@@ -915,6 +938,102 @@ function writeCodexmatePreferences(preferences) {
915
938
  writeJsonAtomic(CODEXMATE_PREFERENCES_FILE, isPlainObject(preferences) ? preferences : {});
916
939
  }
917
940
 
941
+ function normalizeShareCommandPrefixPreference(value) {
942
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
943
+ return normalized === 'codexmate' ? 'codexmate' : 'npm start';
944
+ }
945
+
946
+ function normalizeBooleanPreference(value, defaultValue = true) {
947
+ if (value === true) return true;
948
+ if (value === false) return false;
949
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
950
+ if (normalized === '1' || normalized === 'true' || normalized === 'on' || normalized === 'yes') return true;
951
+ if (normalized === '0' || normalized === 'false' || normalized === 'off' || normalized === 'no') return false;
952
+ return defaultValue !== false;
953
+ }
954
+
955
+ function normalizeSessionTrashRetentionPreference(value) {
956
+ const numeric = Number(value);
957
+ if (!Number.isFinite(numeric) || numeric < 1) return 30;
958
+ return Math.min(365, Math.max(1, Math.floor(numeric)));
959
+ }
960
+
961
+ function normalizeSessionTimelineStylePreference(value) {
962
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
963
+ return normalized === 'bar' ? 'bar' : 'dots';
964
+ }
965
+
966
+ function normalizeSettingsTabPreference(value) {
967
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
968
+ return normalized === 'data' ? 'data' : 'general';
969
+ }
970
+
971
+ function normalizeMainTabPreference(value) {
972
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
973
+ const allowed = new Set(['dashboard', 'config', 'sessions', 'usage', 'orchestration', 'market', 'plugins', 'docs', 'settings', 'trash', 'prompts']);
974
+ return allowed.has(normalized) ? normalized : 'dashboard';
975
+ }
976
+
977
+ function normalizeConfigModePreference(value) {
978
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
979
+ return ['codex', 'claude', 'openclaw', 'opencode'].includes(normalized) ? normalized : 'codex';
980
+ }
981
+
982
+ function normalizeUsageTimeRangePreference(value) {
983
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
984
+ if (normalized === 'all' || normalized === '30d') return normalized;
985
+ return '7d';
986
+ }
987
+
988
+ function normalizePromptsSubTabPreference(value) {
989
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
990
+ return normalized === 'claude-project' ? 'claude-project' : 'codex';
991
+ }
992
+
993
+ function normalizeWebUiPreferences(value = {}) {
994
+ const source = isPlainObject(value) ? value : {};
995
+ const navigation = isPlainObject(source.navigation) ? source.navigation : {};
996
+ return {
997
+ shareCommandPrefix: normalizeShareCommandPrefixPreference(source.shareCommandPrefix),
998
+ sessionTrashEnabled: normalizeBooleanPreference(source.sessionTrashEnabled, true),
999
+ sessionTrashRetentionDays: normalizeSessionTrashRetentionPreference(source.sessionTrashRetentionDays),
1000
+ sessionTimelineStyle: normalizeSessionTimelineStylePreference(source.sessionTimelineStyle),
1001
+ configTemplateDiffConfirmEnabled: normalizeBooleanPreference(source.configTemplateDiffConfirmEnabled, true),
1002
+ sessionsUsageTimeRange: normalizeUsageTimeRangePreference(source.sessionsUsageTimeRange),
1003
+ promptsSubTab: normalizePromptsSubTabPreference(source.promptsSubTab),
1004
+ projectClaudeMdPath: typeof source.projectClaudeMdPath === 'string' ? source.projectClaudeMdPath : '',
1005
+ navigation: {
1006
+ mainTab: normalizeMainTabPreference(navigation.mainTab),
1007
+ configMode: normalizeConfigModePreference(navigation.configMode),
1008
+ settingsTab: normalizeSettingsTabPreference(navigation.settingsTab),
1009
+ skillsTargetApp: navigation.skillsTargetApp === 'claude' ? 'claude' : 'codex',
1010
+ promptTemplatesMode: navigation.promptTemplatesMode === 'manage' ? 'manage' : 'compose'
1011
+ }
1012
+ };
1013
+ }
1014
+
1015
+ function readWebUiPreferences() {
1016
+ const preferences = readCodexmatePreferences();
1017
+ return normalizeWebUiPreferences(preferences.webUi || {});
1018
+ }
1019
+
1020
+ function setWebUiPreferences(params = {}) {
1021
+ const preferences = readCodexmatePreferences();
1022
+ const current = isPlainObject(preferences.webUi) ? preferences.webUi : {};
1023
+ const incoming = isPlainObject(params && params.preferences) ? params.preferences : {};
1024
+ const next = normalizeWebUiPreferences({
1025
+ ...current,
1026
+ ...incoming,
1027
+ navigation: {
1028
+ ...(isPlainObject(current.navigation) ? current.navigation : {}),
1029
+ ...(isPlainObject(incoming.navigation) ? incoming.navigation : {})
1030
+ }
1031
+ });
1032
+ preferences.webUi = next;
1033
+ writeCodexmatePreferences(preferences);
1034
+ return { success: true, preferences: next };
1035
+ }
1036
+
918
1037
  function readToolConfigPermissions() {
919
1038
  const preferences = readCodexmatePreferences();
920
1039
  return normalizeToolConfigPermissions(preferences.toolConfigPermissions || TOOL_CONFIG_PERMISSION_DEFAULTS);
@@ -973,7 +1092,8 @@ function getApiToolConfigWriteTarget(action) {
973
1092
  'restore-claude-dir',
974
1093
  'claude-local-bridge-toggle',
975
1094
  'claude-local-bridge-set-excluded',
976
- 'claude-local-bridge-sync-providers'
1095
+ 'claude-local-bridge-sync-providers',
1096
+ 'delete-provider-cache-record'
977
1097
  ]);
978
1098
  const opencodeWriteActions = new Set([
979
1099
  'apply-opencode-config',
@@ -2506,6 +2626,513 @@ function updateProviderInConfig(params = {}) {
2506
2626
  }
2507
2627
  }
2508
2628
 
2629
+
2630
+ function redactProviderCacheValue(value) {
2631
+ const secretKeyPattern = /(?:^key$|api[_-]?key|auth[_-]?token|access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|passwd|secret|credential|authorization|bearer|cookie|session|private[_-]?key|client[_-]?secret|x-api-key)/i;
2632
+ const secretQueryPattern = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|passwd|secret|credential|authorization|client[_-]?secret|key)/i;
2633
+ const secretValuePattern = /^(?:bearer\s+|sk-[A-Za-z0-9]|sk_[A-Za-z0-9]|gsk_|AIza|xox[baprs]-|gh[pousr]_|or-[A-Za-z0-9]|ds-[A-Za-z0-9])/i;
2634
+ const redactSecretString = (text) => String(text || '') ? '***' : '';
2635
+ const redactUrlString = (text) => {
2636
+ if (typeof text !== 'string' || !/^https?:\/\//i.test(text)) return text;
2637
+ try {
2638
+ const parsed = new URL(text);
2639
+ if (parsed.username) parsed.username = '***';
2640
+ if (parsed.password) parsed.password = '***';
2641
+ for (const key of Array.from(parsed.searchParams.keys())) {
2642
+ if (secretQueryPattern.test(key)) parsed.searchParams.set(key, '***');
2643
+ }
2644
+ return parsed.toString();
2645
+ } catch (_) {
2646
+ return text.replace(/([?&](?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|passwd|secret|credential|authorization|client[_-]?secret|key)=)[^&#]*/gi, '$1***');
2647
+ }
2648
+ };
2649
+ const visit = (input, key = '') => {
2650
+ if (secretKeyPattern.test(key)) {
2651
+ if (typeof input === 'boolean' || typeof input === 'number') return input;
2652
+ if (input === null || input === undefined || input === '') return input === undefined ? null : input;
2653
+ return redactSecretString(input);
2654
+ }
2655
+ if (Array.isArray(input)) {
2656
+ return input.map((item) => visit(item, key));
2657
+ }
2658
+ if (isPlainObject(input)) {
2659
+ const output = {};
2660
+ for (const [childKey, childValue] of Object.entries(input)) {
2661
+ output[childKey] = visit(childValue, childKey);
2662
+ }
2663
+ return output;
2664
+ }
2665
+ if (typeof input === 'string') {
2666
+ const urlRedacted = redactUrlString(input);
2667
+ if (urlRedacted !== input) return urlRedacted;
2668
+ if (secretValuePattern.test(input.trim())) return redactSecretString(input.trim());
2669
+ }
2670
+ return input;
2671
+ };
2672
+ return visit(value);
2673
+ }
2674
+
2675
+ function getProviderCacheDisplayPath(fileName) {
2676
+ return `~/.codexmate/${fileName}`;
2677
+ }
2678
+
2679
+ function sanitizeProviderCacheErrorMessage(message, fileName, fallback = '读取缓存文件失败') {
2680
+ const raw = typeof message === 'string' && message.trim() ? message : fallback;
2681
+ const displayPath = getProviderCacheDisplayPath(fileName);
2682
+ const absolutePath = path.join(CODEXMATE_DIR, fileName);
2683
+ return raw
2684
+ .split(absolutePath).join(displayPath)
2685
+ .split(CODEXMATE_DIR).join('~/.codexmate');
2686
+ }
2687
+
2688
+ function pickProviderCacheString(source, keys) {
2689
+ if (!isPlainObject(source)) return '';
2690
+ for (const key of keys) {
2691
+ const value = source[key];
2692
+ if (typeof value === 'string' && value.trim()) return value.trim();
2693
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
2694
+ }
2695
+ return '';
2696
+ }
2697
+
2698
+ function summarizeProviderCacheEntry(name, entry = {}) {
2699
+ const provider = isPlainObject(entry) ? entry : {};
2700
+ const providerName = pickProviderCacheString(provider, ['name', 'id', 'provider', 'title']) || String(name || '').trim() || 'provider';
2701
+ const baseUrl = pickProviderCacheString(provider, ['base_url', 'baseUrl', 'url', 'endpoint']);
2702
+ const wireApi = pickProviderCacheString(provider, ['wire_api', 'wireApi', 'api', 'type']);
2703
+ const authMethod = pickProviderCacheString(provider, ['preferred_auth_method', 'authMethod', 'auth_method']);
2704
+ const model = pickProviderCacheString(provider, ['model', 'default_model', 'defaultModel']);
2705
+ return {
2706
+ name: providerName,
2707
+ baseUrl: baseUrl ? redactProviderCacheValue(baseUrl) : '',
2708
+ wireApi,
2709
+ authMethod: authMethod ? redactProviderCacheValue(authMethod) : '',
2710
+ model,
2711
+ data: redactProviderCacheValue(provider)
2712
+ };
2713
+ }
2714
+
2715
+ function extractProviderCacheSummaries(data) {
2716
+ const providers = [];
2717
+ const seen = new Set();
2718
+ const addProvider = (name, entry) => {
2719
+ const summary = summarizeProviderCacheEntry(name, entry);
2720
+ const key = `${summary.name}\u0000${summary.baseUrl}\u0000${summary.wireApi}`;
2721
+ if (seen.has(key)) return;
2722
+ seen.add(key);
2723
+ providers.push(summary);
2724
+ };
2725
+ const visitContainer = (container) => {
2726
+ if (!container) return;
2727
+ if (Array.isArray(container)) {
2728
+ for (const item of container) {
2729
+ if (!isPlainObject(item)) continue;
2730
+ addProvider(pickProviderCacheString(item, ['name', 'id', 'provider']) || `provider-${providers.length + 1}`, item);
2731
+ }
2732
+ return;
2733
+ }
2734
+ if (!isPlainObject(container)) return;
2735
+ for (const [name, entry] of Object.entries(container)) {
2736
+ if (isPlainObject(entry)) addProvider(name, entry);
2737
+ }
2738
+ };
2739
+
2740
+ if (isPlainObject(data)) {
2741
+ visitContainer(data.providers);
2742
+ visitContainer(data.configs);
2743
+ visitContainer(data.providerConfigs);
2744
+ visitContainer(data.items);
2745
+ if (providers.length === 0 && (data.base_url || data.baseUrl || data.url || data.endpoint)) {
2746
+ addProvider(pickProviderCacheString(data, ['name', 'id', 'provider']) || 'default', data);
2747
+ }
2748
+ } else if (Array.isArray(data)) {
2749
+ visitContainer(data);
2750
+ }
2751
+ return providers.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')));
2752
+ }
2753
+
2754
+ function buildProviderCacheFileRecord(fileName) {
2755
+ const filePath = path.join(CODEXMATE_DIR, fileName);
2756
+ const displayPath = getProviderCacheDisplayPath(fileName);
2757
+ const record = {
2758
+ name: fileName,
2759
+ path: displayPath,
2760
+ displayPath,
2761
+ exists: false,
2762
+ ok: true,
2763
+ tooLarge: false,
2764
+ size: 0,
2765
+ mtime: '',
2766
+ data: null,
2767
+ providers: [],
2768
+ providerCount: 0,
2769
+ error: ''
2770
+ };
2771
+ if (!fs.existsSync(filePath)) {
2772
+ return record;
2773
+ }
2774
+ record.exists = true;
2775
+ try {
2776
+ const stat = fs.statSync(filePath);
2777
+ if (!stat.isFile()) {
2778
+ record.ok = false;
2779
+ record.error = '缓存路径不是普通文件,已跳过读取';
2780
+ return record;
2781
+ }
2782
+ record.size = Number(stat.size) || 0;
2783
+ record.mtime = stat.mtime instanceof Date && !Number.isNaN(stat.mtime.getTime())
2784
+ ? stat.mtime.toISOString()
2785
+ : '';
2786
+ } catch (e) {
2787
+ record.ok = false;
2788
+ record.error = sanitizeProviderCacheErrorMessage(e && e.message ? e.message : String(e || '读取缓存文件状态失败'), fileName, '读取缓存文件状态失败');
2789
+ return record;
2790
+ }
2791
+ if (record.size > PROVIDER_CACHE_MAX_FILE_BYTES) {
2792
+ record.ok = false;
2793
+ record.tooLarge = true;
2794
+ record.error = `缓存文件过大,已跳过 JSON 读取(${record.size} bytes > ${PROVIDER_CACHE_MAX_FILE_BYTES} bytes)`;
2795
+ return record;
2796
+ }
2797
+ try {
2798
+ const content = stripUtf8Bom(fs.readFileSync(filePath, 'utf-8'));
2799
+ const parsed = content.trim() ? JSON.parse(content) : null;
2800
+ record.data = redactProviderCacheValue(parsed);
2801
+ record.providers = extractProviderCacheSummaries(parsed);
2802
+ record.providerCount = record.providers.length;
2803
+ } catch (e) {
2804
+ record.ok = false;
2805
+ record.error = sanitizeProviderCacheErrorMessage(e && e.message ? e.message : String(e || '读取缓存文件失败'), fileName);
2806
+ }
2807
+ return record;
2808
+ }
2809
+
2810
+ function listProviderCacheFileNamesForGroup(groupKey) {
2811
+ const defaults = PROVIDER_CACHE_FILE_GROUPS[groupKey] || [];
2812
+ const names = new Set(defaults);
2813
+ try {
2814
+ if (fs.existsSync(CODEXMATE_DIR)) {
2815
+ for (const fileName of fs.readdirSync(CODEXMATE_DIR)) {
2816
+ if (typeof fileName !== 'string' || !fileName.endsWith('.json')) continue;
2817
+ if (groupKey === 'opencode' && /^opencode[-_]/i.test(fileName)) {
2818
+ names.add(fileName);
2819
+ }
2820
+ }
2821
+ }
2822
+ } catch (_) {}
2823
+ return Array.from(names).sort((a, b) => a.localeCompare(b));
2824
+ }
2825
+
2826
+ function readProviderCacheRecords() {
2827
+ const groupLabels = {
2828
+ claude: 'Claude',
2829
+ codex: 'Codex',
2830
+ opencode: 'OpenCode'
2831
+ };
2832
+ const groups = Object.keys(PROVIDER_CACHE_FILE_GROUPS).map((key) => {
2833
+ const files = listProviderCacheFileNamesForGroup(key).map((fileName) => buildProviderCacheFileRecord(fileName));
2834
+ return {
2835
+ key,
2836
+ label: groupLabels[key] || key,
2837
+ files,
2838
+ existingCount: files.filter((file) => file && file.exists).length
2839
+ };
2840
+ });
2841
+ return {
2842
+ root: '~/.codexmate',
2843
+ maxFileBytes: PROVIDER_CACHE_MAX_FILE_BYTES,
2844
+ generatedAt: new Date().toISOString(),
2845
+ groups
2846
+ };
2847
+ }
2848
+
2849
+ function readProviderCacheJsonObject(fileName) {
2850
+ const filePath = path.join(CODEXMATE_DIR, fileName);
2851
+ try {
2852
+ if (!fs.existsSync(filePath)) return {};
2853
+ const stat = fs.statSync(filePath);
2854
+ if (!stat.isFile()) return {};
2855
+ const parsed = JSON.parse(stripUtf8Bom(fs.readFileSync(filePath, 'utf-8')) || '{}');
2856
+ return isPlainObject(parsed) ? parsed : {};
2857
+ } catch (_) {
2858
+ return {};
2859
+ }
2860
+ }
2861
+
2862
+ function writeProviderCacheJsonObject(fileName, data) {
2863
+ ensureDir(CODEXMATE_DIR);
2864
+ const filePath = path.join(CODEXMATE_DIR, fileName);
2865
+ writeJsonAtomic(filePath, isPlainObject(data) ? data : {});
2866
+ try {
2867
+ fs.chmodSync(filePath, 0o600);
2868
+ } catch (_) {}
2869
+ return getProviderCacheDisplayPath(fileName);
2870
+ }
2871
+
2872
+ function normalizeProviderCacheProviderMap(rawProviders) {
2873
+ const providers = {};
2874
+ if (Array.isArray(rawProviders)) {
2875
+ for (const item of rawProviders) {
2876
+ if (!isPlainObject(item)) continue;
2877
+ const name = pickProviderCacheString(item, ['name', 'id', 'provider']);
2878
+ if (name) providers[name] = item;
2879
+ }
2880
+ return providers;
2881
+ }
2882
+ if (isPlainObject(rawProviders)) {
2883
+ for (const [name, entry] of Object.entries(rawProviders)) {
2884
+ if (isPlainObject(entry)) providers[name] = entry;
2885
+ }
2886
+ }
2887
+ return providers;
2888
+ }
2889
+
2890
+ function removeProviderFromProviderCacheContainer(rawProviders, providerName) {
2891
+ const targetName = typeof providerName === 'string' ? providerName.trim() : '';
2892
+ if (!targetName) return { value: rawProviders, changed: false };
2893
+
2894
+ const matchesProviderName = (name) => String(name || '').trim() === targetName;
2895
+ if (Array.isArray(rawProviders)) {
2896
+ const filtered = rawProviders.filter((item) => {
2897
+ if (!isPlainObject(item)) return true;
2898
+ const itemName = pickProviderCacheString(item, ['name', 'id', 'provider']);
2899
+ return !matchesProviderName(itemName);
2900
+ });
2901
+ return { value: filtered, changed: filtered.length !== rawProviders.length };
2902
+ }
2903
+ if (!isPlainObject(rawProviders)) return { value: rawProviders, changed: false };
2904
+
2905
+ const next = { ...rawProviders };
2906
+ let changed = false;
2907
+ for (const [name, entry] of Object.entries(rawProviders)) {
2908
+ const entryName = isPlainObject(entry)
2909
+ ? pickProviderCacheString(entry, ['name', 'id', 'provider'])
2910
+ : '';
2911
+ if (matchesProviderName(name) || matchesProviderName(entryName)) {
2912
+ delete next[name];
2913
+ changed = true;
2914
+ }
2915
+ }
2916
+ return { value: next, changed };
2917
+ }
2918
+
2919
+ function resolveProviderCacheDeleteGroups(groups) {
2920
+ const requested = Array.isArray(groups) ? groups : (groups ? [groups] : []);
2921
+ const normalized = requested
2922
+ .map((item) => String(item || '').trim().toLowerCase())
2923
+ .filter((item) => Object.prototype.hasOwnProperty.call(PROVIDER_CACHE_FILE_GROUPS, item));
2924
+ return normalized.length ? Array.from(new Set(normalized)) : Object.keys(PROVIDER_CACHE_FILE_GROUPS);
2925
+ }
2926
+
2927
+ function removeProviderFromProviderCacheRecords(providerName, options = {}) {
2928
+ const targetName = typeof providerName === 'string' ? providerName.trim() : '';
2929
+ const summary = { removed: false, providerFiles: [], currentModelFiles: [] };
2930
+ if (!targetName) return summary;
2931
+
2932
+ const groups = resolveProviderCacheDeleteGroups(options.groups || options.group);
2933
+ const providerFiles = groups
2934
+ .flatMap((group) => PROVIDER_CACHE_FILE_GROUPS[group] || [])
2935
+ .filter((fileName) => PROVIDER_CACHE_PROVIDER_FILES.includes(fileName));
2936
+ const currentModelFiles = groups
2937
+ .flatMap((group) => PROVIDER_CACHE_FILE_GROUPS[group] || [])
2938
+ .filter((fileName) => PROVIDER_CACHE_CURRENT_MODEL_FILES.includes(fileName));
2939
+
2940
+ for (const fileName of Array.from(new Set(providerFiles))) {
2941
+ const existing = readProviderCacheJsonObject(fileName);
2942
+ if (!isPlainObject(existing) || !Object.prototype.hasOwnProperty.call(existing, 'providers')) continue;
2943
+ const removed = removeProviderFromProviderCacheContainer(existing.providers, targetName);
2944
+ if (!removed.changed) continue;
2945
+ writeProviderCacheJsonObject(fileName, {
2946
+ ...existing,
2947
+ generatedAt: new Date().toISOString(),
2948
+ providers: removed.value
2949
+ });
2950
+ summary.removed = true;
2951
+ summary.providerFiles.push(fileName);
2952
+ }
2953
+
2954
+ for (const fileName of Array.from(new Set(currentModelFiles))) {
2955
+ const existing = readProviderCacheJsonObject(fileName);
2956
+ if (!isPlainObject(existing) || !Object.prototype.hasOwnProperty.call(existing, targetName)) continue;
2957
+ const next = { ...existing };
2958
+ delete next[targetName];
2959
+ writeProviderCacheJsonObject(fileName, next);
2960
+ summary.removed = true;
2961
+ summary.currentModelFiles.push(fileName);
2962
+ }
2963
+ return summary;
2964
+ }
2965
+
2966
+ function deleteProviderCacheRecord(params = {}) {
2967
+ const name = typeof params.name === 'string' ? params.name.trim() : '';
2968
+ if (!name) return { error: '名称不能为空' };
2969
+ const group = typeof params.group === 'string' ? params.group.trim().toLowerCase() : '';
2970
+ const groups = resolveProviderCacheDeleteGroups(group || params.groups);
2971
+ const summary = removeProviderFromProviderCacheRecords(name, { groups });
2972
+ return {
2973
+ success: true,
2974
+ name,
2975
+ groups,
2976
+ removed: summary.removed,
2977
+ providerFiles: summary.providerFiles,
2978
+ currentModelFiles: summary.currentModelFiles,
2979
+ records: readProviderCacheRecords()
2980
+ };
2981
+ }
2982
+
2983
+ function readClaudeProviderCacheProvider(name) {
2984
+ const targetName = typeof name === 'string' ? name.trim() : '';
2985
+ if (!targetName) return null;
2986
+ const cached = normalizeProviderCacheProviderMap(readProviderCacheJsonObject('claude-providers.json').providers);
2987
+ const entry = cached[targetName];
2988
+ return isPlainObject(entry) ? { name: targetName, ...entry } : null;
2989
+ }
2990
+
2991
+ function readClaudeProviderCacheConfigs() {
2992
+ const cached = normalizeProviderCacheProviderMap(readProviderCacheJsonObject('claude-providers.json').providers);
2993
+ const providers = [];
2994
+ for (const [name, entry] of Object.entries(cached)) {
2995
+ if (!name || !isPlainObject(entry)) continue;
2996
+ const baseUrl = typeof entry.baseUrl === 'string' ? entry.baseUrl.trim() : '';
2997
+ const model = typeof entry.model === 'string' ? entry.model.trim() : '';
2998
+ if (!baseUrl || !model) continue;
2999
+ providers.push({
3000
+ name,
3001
+ baseUrl,
3002
+ model,
3003
+ targetApi: normalizeClaudeTargetApi(entry.targetApi),
3004
+ hasKey: typeof entry.apiKey === 'string' && entry.apiKey.trim().length > 0,
3005
+ providerCacheRef: name,
3006
+ source: 'provider-cache'
3007
+ });
3008
+ }
3009
+ return { providers: providers.sort((a, b) => a.name.localeCompare(b.name)) };
3010
+ }
3011
+
3012
+ function buildProviderCacheSyncProviders() {
3013
+ const configResult = readConfigOrVirtualDefault();
3014
+ if (hasConfigLoadError(configResult)) {
3015
+ return { error: (configResult.error && configResult.error.configPublicReason) || '读取 config.toml 失败' };
3016
+ }
3017
+ const config = configResult.config || {};
3018
+ const providers = isPlainObject(config.model_providers) ? config.model_providers : {};
3019
+ const currentModels = readCurrentModels();
3020
+ const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
3021
+ const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
3022
+ const syncProviders = [];
3023
+
3024
+ for (const [name, provider] of Object.entries(providers)) {
3025
+ if (!name || !isPlainObject(provider) || isBuiltinManagedProvider(name)) continue;
3026
+ const bridgeType = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
3027
+ const isOpenaiBridgeProvider = bridgeType === 'openai'
3028
+ || (typeof provider.base_url === 'string' && provider.base_url.includes('/bridge/openai/'));
3029
+ let baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
3030
+ let apiKey = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method : '';
3031
+ if (isOpenaiBridgeProvider) {
3032
+ const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, name);
3033
+ if (upstream && !upstream.error) {
3034
+ baseUrl = upstream.baseUrl || baseUrl;
3035
+ apiKey = upstream.apiKey || apiKey;
3036
+ }
3037
+ }
3038
+ const wireApi = typeof provider.wire_api === 'string' && provider.wire_api.trim()
3039
+ ? provider.wire_api.trim()
3040
+ : 'responses';
3041
+ const model = typeof currentModels[name] === 'string' && currentModels[name].trim()
3042
+ ? currentModels[name].trim()
3043
+ : (activeProvider === name ? activeModel : '');
3044
+ syncProviders.push({
3045
+ name,
3046
+ baseUrl,
3047
+ apiKey,
3048
+ wireApi,
3049
+ model,
3050
+ bridge: bridgeType || (isOpenaiBridgeProvider ? 'openai' : '')
3051
+ });
3052
+ }
3053
+ return { providers: syncProviders.sort((a, b) => a.name.localeCompare(b.name)) };
3054
+ }
3055
+
3056
+ function mergeProviderCacheFile(fileName, nextProviders, buildEntry) {
3057
+ const existing = readProviderCacheJsonObject(fileName);
3058
+ const existingProviders = normalizeProviderCacheProviderMap(existing.providers);
3059
+ const providers = {};
3060
+ for (const provider of nextProviders) {
3061
+ const previous = isPlainObject(providers[provider.name]) ? providers[provider.name] : {};
3062
+ const cachedPrevious = isPlainObject(existingProviders[provider.name]) ? existingProviders[provider.name] : previous;
3063
+ providers[provider.name] = { ...cachedPrevious, ...buildEntry(provider) };
3064
+ }
3065
+ const next = {
3066
+ ...existing,
3067
+ version: Number(existing.version) > 0 ? Number(existing.version) : 1,
3068
+ generatedAt: new Date().toISOString(),
3069
+ providers
3070
+ };
3071
+ const displayPath = writeProviderCacheJsonObject(fileName, next);
3072
+ return { path: displayPath, providerCount: Object.keys(providers).length };
3073
+ }
3074
+
3075
+ function mergeProviderCacheCurrentModelsFile(fileName, nextProviders) {
3076
+ const existing = readProviderCacheJsonObject(fileName);
3077
+ const next = {};
3078
+ for (const provider of nextProviders) {
3079
+ if (provider.model) {
3080
+ next[provider.name] = provider.model;
3081
+ } else if (typeof existing[provider.name] === 'string' && existing[provider.name].trim()) {
3082
+ next[provider.name] = existing[provider.name];
3083
+ }
3084
+ }
3085
+ const displayPath = writeProviderCacheJsonObject(fileName, next);
3086
+ return { path: displayPath, modelCount: Object.keys(next).length };
3087
+ }
3088
+
3089
+ function syncProviderCacheRecords() {
3090
+ const built = buildProviderCacheSyncProviders();
3091
+ if (built.error) return { error: built.error };
3092
+ const providers = built.providers || [];
3093
+ if (providers.length === 0) {
3094
+ return { errorKey: 'modal.providerCache.noSyncableProviders', error: 'No syncable providers' };
3095
+ }
3096
+
3097
+ const writtenFiles = [];
3098
+ writtenFiles.push(mergeProviderCacheFile('codex-providers.json', providers, (provider) => ({
3099
+ name: provider.name,
3100
+ base_url: provider.baseUrl,
3101
+ wire_api: provider.wireApi,
3102
+ preferred_auth_method: provider.apiKey,
3103
+ model: provider.model,
3104
+ ...(provider.bridge ? { codexmate_bridge: provider.bridge } : {})
3105
+ })));
3106
+ writtenFiles.push(mergeProviderCacheCurrentModelsFile('codex-provider-current-models.json', providers));
3107
+ writtenFiles.push(mergeProviderCacheFile('claude-providers.json', providers, (provider) => ({
3108
+ name: provider.name,
3109
+ baseUrl: provider.baseUrl,
3110
+ apiKey: provider.apiKey,
3111
+ model: provider.model,
3112
+ targetApi: normalizeClaudeTargetApi(provider.wireApi),
3113
+ ...(provider.bridge ? { bridge: provider.bridge } : {})
3114
+ })));
3115
+ writtenFiles.push(mergeProviderCacheFile('opencode-providers.json', providers, (provider) => ({
3116
+ name: provider.name,
3117
+ baseUrl: provider.baseUrl,
3118
+ apiKey: provider.apiKey,
3119
+ model: provider.model,
3120
+ disabled: false,
3121
+ ...(provider.bridge ? { bridge: provider.bridge } : {})
3122
+ })));
3123
+ writtenFiles.push(mergeProviderCacheCurrentModelsFile('opencode-provider-current-models.json', providers));
3124
+
3125
+ return {
3126
+ success: true,
3127
+ summary: {
3128
+ providerCount: providers.length,
3129
+ fileCount: writtenFiles.length,
3130
+ writtenFiles
3131
+ },
3132
+ records: readProviderCacheRecords()
3133
+ };
3134
+ }
3135
+
2509
3136
  function getProviderKey(params = {}) {
2510
3137
  const name = typeof params.name === 'string' ? params.name.trim() : '';
2511
3138
  if (!name) return { error: '名称不能为空' };
@@ -2694,6 +3321,7 @@ function performProviderDeletion(name, options = {}) {
2694
3321
 
2695
3322
  writeCurrentModels(currentModels);
2696
3323
  writeConfig(updatedContent.trimEnd() + lineEnding);
3324
+ removeProviderFromProviderCacheRecords(name);
2697
3325
 
2698
3326
  return result;
2699
3327
  }
@@ -9492,21 +10120,35 @@ async function applyToClaudeSettings(config = {}) {
9492
10120
  let proxyStarted = false;
9493
10121
  try {
9494
10122
  assertToolConfigWriteAllowed('claude');
9495
- const apiKey = (config.apiKey || '').trim();
9496
- const targetApi = normalizeClaudeTargetApi(config.targetApi);
10123
+ const providerCacheRef = typeof config.providerCacheRef === 'string' ? config.providerCacheRef.trim() : '';
10124
+ const cachedProvider = providerCacheRef ? readClaudeProviderCacheProvider(providerCacheRef) : null;
10125
+ if (providerCacheRef && !cachedProvider) {
10126
+ return { success: false, mode: 'provider-cache', error: '缓存中的 Claude provider 不存在,请重新同步' };
10127
+ }
10128
+ const effectiveConfig = cachedProvider
10129
+ ? {
10130
+ ...config,
10131
+ apiKey: cachedProvider.apiKey || config.apiKey || '',
10132
+ baseUrl: cachedProvider.baseUrl || config.baseUrl || '',
10133
+ model: cachedProvider.model || config.model || '',
10134
+ targetApi: cachedProvider.targetApi || config.targetApi || 'responses'
10135
+ }
10136
+ : config;
10137
+ const apiKey = (effectiveConfig.apiKey || '').trim();
10138
+ const targetApi = normalizeClaudeTargetApi(effectiveConfig.targetApi);
9497
10139
  if (!apiKey && targetApi !== 'ollama') {
9498
10140
  return { success: false, mode: 'settings-file', error: '请先输入 API Key' };
9499
10141
  }
9500
10142
 
9501
- const configuredBaseUrl = typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
10143
+ const configuredBaseUrl = typeof effectiveConfig.baseUrl === 'string' ? effectiveConfig.baseUrl.trim() : '';
9502
10144
  const baseUrl = (configuredBaseUrl || (targetApi === 'ollama' ? 'http://127.0.0.1:11434' : 'https://open.bigmodel.cn/api/anthropic')).trim();
9503
- const model = (config.model || DEFAULT_CLAUDE_MODEL).trim();
10145
+ const model = (effectiveConfig.model || DEFAULT_CLAUDE_MODEL).trim();
9504
10146
  let settingsBaseUrl = baseUrl;
9505
10147
  let settingsApiKey = apiKey;
9506
10148
  let proxyResult = null;
9507
10149
 
9508
10150
  if (targetApi === 'chat_completions' || targetApi === 'ollama') {
9509
- const upstreamProviderName = typeof config.name === 'string' ? config.name.trim() : '';
10151
+ const upstreamProviderName = typeof effectiveConfig.name === 'string' ? effectiveConfig.name.trim() : '';
9510
10152
  if (targetApi === 'chat_completions' && !configuredBaseUrl && !upstreamProviderName) {
9511
10153
  return {
9512
10154
  success: false,
@@ -11695,6 +12337,12 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11695
12337
  case 'get-tool-config-permissions':
11696
12338
  result = { permissions: readToolConfigPermissions() };
11697
12339
  break;
12340
+ case 'get-web-ui-preferences':
12341
+ result = { preferences: readWebUiPreferences() };
12342
+ break;
12343
+ case 'set-web-ui-preferences':
12344
+ result = setWebUiPreferences(params || {});
12345
+ break;
11698
12346
  case 'set-tool-config-permission':
11699
12347
  result = setToolConfigPermission(params || {});
11700
12348
  break;
@@ -11839,6 +12487,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11839
12487
  case 'get-provider-key':
11840
12488
  result = getProviderKey(params || {});
11841
12489
  break;
12490
+ case 'get-provider-cache-records':
12491
+ result = readProviderCacheRecords();
12492
+ break;
12493
+ case 'get-claude-provider-cache-configs':
12494
+ result = readClaudeProviderCacheConfigs();
12495
+ break;
12496
+ case 'sync-provider-cache-records':
12497
+ result = syncProviderCacheRecords();
12498
+ break;
12499
+ case 'delete-provider-cache-record':
12500
+ result = deleteProviderCacheRecord(params || {});
12501
+ break;
11842
12502
  case 'delete-provider':
11843
12503
  result = deleteProviderFromConfig(params || {});
11844
12504
  break;