codexmate 0.0.54 → 0.0.55

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
@@ -218,6 +218,20 @@ 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_MAX_FILE_BYTES = 256 * 1024;
221
235
  const CODEXMATE_PREFERENCES_FILE = path.join(CODEXMATE_DIR, 'preferences.json');
222
236
  const CODEXMATE_OPENCODE_DIR = path.join(CODEXMATE_DIR, 'opencode');
223
237
  const CODEXMATE_OPENCODE_PROVIDER_STORE_FILE = path.join(CODEXMATE_OPENCODE_DIR, 'providers.json');
@@ -915,6 +929,102 @@ function writeCodexmatePreferences(preferences) {
915
929
  writeJsonAtomic(CODEXMATE_PREFERENCES_FILE, isPlainObject(preferences) ? preferences : {});
916
930
  }
917
931
 
932
+ function normalizeShareCommandPrefixPreference(value) {
933
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
934
+ return normalized === 'codexmate' ? 'codexmate' : 'npm start';
935
+ }
936
+
937
+ function normalizeBooleanPreference(value, defaultValue = true) {
938
+ if (value === true) return true;
939
+ if (value === false) return false;
940
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
941
+ if (normalized === '1' || normalized === 'true' || normalized === 'on' || normalized === 'yes') return true;
942
+ if (normalized === '0' || normalized === 'false' || normalized === 'off' || normalized === 'no') return false;
943
+ return defaultValue !== false;
944
+ }
945
+
946
+ function normalizeSessionTrashRetentionPreference(value) {
947
+ const numeric = Number(value);
948
+ if (!Number.isFinite(numeric) || numeric < 1) return 30;
949
+ return Math.min(365, Math.max(1, Math.floor(numeric)));
950
+ }
951
+
952
+ function normalizeSessionTimelineStylePreference(value) {
953
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
954
+ return normalized === 'bar' ? 'bar' : 'dots';
955
+ }
956
+
957
+ function normalizeSettingsTabPreference(value) {
958
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
959
+ return normalized === 'data' ? 'data' : 'general';
960
+ }
961
+
962
+ function normalizeMainTabPreference(value) {
963
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
964
+ const allowed = new Set(['dashboard', 'config', 'sessions', 'usage', 'orchestration', 'market', 'plugins', 'docs', 'settings', 'trash', 'prompts']);
965
+ return allowed.has(normalized) ? normalized : 'dashboard';
966
+ }
967
+
968
+ function normalizeConfigModePreference(value) {
969
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
970
+ return ['codex', 'claude', 'openclaw', 'opencode'].includes(normalized) ? normalized : 'codex';
971
+ }
972
+
973
+ function normalizeUsageTimeRangePreference(value) {
974
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
975
+ if (normalized === 'all' || normalized === '30d') return normalized;
976
+ return '7d';
977
+ }
978
+
979
+ function normalizePromptsSubTabPreference(value) {
980
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
981
+ return normalized === 'claude-project' ? 'claude-project' : 'codex';
982
+ }
983
+
984
+ function normalizeWebUiPreferences(value = {}) {
985
+ const source = isPlainObject(value) ? value : {};
986
+ const navigation = isPlainObject(source.navigation) ? source.navigation : {};
987
+ return {
988
+ shareCommandPrefix: normalizeShareCommandPrefixPreference(source.shareCommandPrefix),
989
+ sessionTrashEnabled: normalizeBooleanPreference(source.sessionTrashEnabled, true),
990
+ sessionTrashRetentionDays: normalizeSessionTrashRetentionPreference(source.sessionTrashRetentionDays),
991
+ sessionTimelineStyle: normalizeSessionTimelineStylePreference(source.sessionTimelineStyle),
992
+ configTemplateDiffConfirmEnabled: normalizeBooleanPreference(source.configTemplateDiffConfirmEnabled, true),
993
+ sessionsUsageTimeRange: normalizeUsageTimeRangePreference(source.sessionsUsageTimeRange),
994
+ promptsSubTab: normalizePromptsSubTabPreference(source.promptsSubTab),
995
+ projectClaudeMdPath: typeof source.projectClaudeMdPath === 'string' ? source.projectClaudeMdPath : '',
996
+ navigation: {
997
+ mainTab: normalizeMainTabPreference(navigation.mainTab),
998
+ configMode: normalizeConfigModePreference(navigation.configMode),
999
+ settingsTab: normalizeSettingsTabPreference(navigation.settingsTab),
1000
+ skillsTargetApp: navigation.skillsTargetApp === 'claude' ? 'claude' : 'codex',
1001
+ promptTemplatesMode: navigation.promptTemplatesMode === 'manage' ? 'manage' : 'compose'
1002
+ }
1003
+ };
1004
+ }
1005
+
1006
+ function readWebUiPreferences() {
1007
+ const preferences = readCodexmatePreferences();
1008
+ return normalizeWebUiPreferences(preferences.webUi || {});
1009
+ }
1010
+
1011
+ function setWebUiPreferences(params = {}) {
1012
+ const preferences = readCodexmatePreferences();
1013
+ const current = isPlainObject(preferences.webUi) ? preferences.webUi : {};
1014
+ const incoming = isPlainObject(params && params.preferences) ? params.preferences : {};
1015
+ const next = normalizeWebUiPreferences({
1016
+ ...current,
1017
+ ...incoming,
1018
+ navigation: {
1019
+ ...(isPlainObject(current.navigation) ? current.navigation : {}),
1020
+ ...(isPlainObject(incoming.navigation) ? incoming.navigation : {})
1021
+ }
1022
+ });
1023
+ preferences.webUi = next;
1024
+ writeCodexmatePreferences(preferences);
1025
+ return { success: true, preferences: next };
1026
+ }
1027
+
918
1028
  function readToolConfigPermissions() {
919
1029
  const preferences = readCodexmatePreferences();
920
1030
  return normalizeToolConfigPermissions(preferences.toolConfigPermissions || TOOL_CONFIG_PERMISSION_DEFAULTS);
@@ -2506,6 +2616,415 @@ function updateProviderInConfig(params = {}) {
2506
2616
  }
2507
2617
  }
2508
2618
 
2619
+
2620
+ function redactProviderCacheValue(value) {
2621
+ 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;
2622
+ const secretQueryPattern = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|passwd|secret|credential|authorization|client[_-]?secret|key)/i;
2623
+ 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;
2624
+ const redactSecretString = (text) => String(text || '') ? '***' : '';
2625
+ const redactUrlString = (text) => {
2626
+ if (typeof text !== 'string' || !/^https?:\/\//i.test(text)) return text;
2627
+ try {
2628
+ const parsed = new URL(text);
2629
+ if (parsed.username) parsed.username = '***';
2630
+ if (parsed.password) parsed.password = '***';
2631
+ for (const key of Array.from(parsed.searchParams.keys())) {
2632
+ if (secretQueryPattern.test(key)) parsed.searchParams.set(key, '***');
2633
+ }
2634
+ return parsed.toString();
2635
+ } catch (_) {
2636
+ return text.replace(/([?&](?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|passwd|secret|credential|authorization|client[_-]?secret|key)=)[^&#]*/gi, '$1***');
2637
+ }
2638
+ };
2639
+ const visit = (input, key = '') => {
2640
+ if (secretKeyPattern.test(key)) {
2641
+ if (typeof input === 'boolean' || typeof input === 'number') return input;
2642
+ if (input === null || input === undefined || input === '') return input === undefined ? null : input;
2643
+ return redactSecretString(input);
2644
+ }
2645
+ if (Array.isArray(input)) {
2646
+ return input.map((item) => visit(item, key));
2647
+ }
2648
+ if (isPlainObject(input)) {
2649
+ const output = {};
2650
+ for (const [childKey, childValue] of Object.entries(input)) {
2651
+ output[childKey] = visit(childValue, childKey);
2652
+ }
2653
+ return output;
2654
+ }
2655
+ if (typeof input === 'string') {
2656
+ const urlRedacted = redactUrlString(input);
2657
+ if (urlRedacted !== input) return urlRedacted;
2658
+ if (secretValuePattern.test(input.trim())) return redactSecretString(input.trim());
2659
+ }
2660
+ return input;
2661
+ };
2662
+ return visit(value);
2663
+ }
2664
+
2665
+ function getProviderCacheDisplayPath(fileName) {
2666
+ return `~/.codexmate/${fileName}`;
2667
+ }
2668
+
2669
+ function sanitizeProviderCacheErrorMessage(message, fileName, fallback = '读取缓存文件失败') {
2670
+ const raw = typeof message === 'string' && message.trim() ? message : fallback;
2671
+ const displayPath = getProviderCacheDisplayPath(fileName);
2672
+ const absolutePath = path.join(CODEXMATE_DIR, fileName);
2673
+ return raw
2674
+ .split(absolutePath).join(displayPath)
2675
+ .split(CODEXMATE_DIR).join('~/.codexmate');
2676
+ }
2677
+
2678
+ function pickProviderCacheString(source, keys) {
2679
+ if (!isPlainObject(source)) return '';
2680
+ for (const key of keys) {
2681
+ const value = source[key];
2682
+ if (typeof value === 'string' && value.trim()) return value.trim();
2683
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
2684
+ }
2685
+ return '';
2686
+ }
2687
+
2688
+ function summarizeProviderCacheEntry(name, entry = {}) {
2689
+ const provider = isPlainObject(entry) ? entry : {};
2690
+ const providerName = pickProviderCacheString(provider, ['name', 'id', 'provider', 'title']) || String(name || '').trim() || 'provider';
2691
+ const baseUrl = pickProviderCacheString(provider, ['base_url', 'baseUrl', 'url', 'endpoint']);
2692
+ const wireApi = pickProviderCacheString(provider, ['wire_api', 'wireApi', 'api', 'type']);
2693
+ const authMethod = pickProviderCacheString(provider, ['preferred_auth_method', 'authMethod', 'auth_method']);
2694
+ const model = pickProviderCacheString(provider, ['model', 'default_model', 'defaultModel']);
2695
+ return {
2696
+ name: providerName,
2697
+ baseUrl: baseUrl ? redactProviderCacheValue(baseUrl) : '',
2698
+ wireApi,
2699
+ authMethod: authMethod ? redactProviderCacheValue(authMethod) : '',
2700
+ model,
2701
+ data: redactProviderCacheValue(provider)
2702
+ };
2703
+ }
2704
+
2705
+ function extractProviderCacheSummaries(data) {
2706
+ const providers = [];
2707
+ const seen = new Set();
2708
+ const addProvider = (name, entry) => {
2709
+ const summary = summarizeProviderCacheEntry(name, entry);
2710
+ const key = `${summary.name}\u0000${summary.baseUrl}\u0000${summary.wireApi}`;
2711
+ if (seen.has(key)) return;
2712
+ seen.add(key);
2713
+ providers.push(summary);
2714
+ };
2715
+ const visitContainer = (container) => {
2716
+ if (!container) return;
2717
+ if (Array.isArray(container)) {
2718
+ for (const item of container) {
2719
+ if (!isPlainObject(item)) continue;
2720
+ addProvider(pickProviderCacheString(item, ['name', 'id', 'provider']) || `provider-${providers.length + 1}`, item);
2721
+ }
2722
+ return;
2723
+ }
2724
+ if (!isPlainObject(container)) return;
2725
+ for (const [name, entry] of Object.entries(container)) {
2726
+ if (isPlainObject(entry)) addProvider(name, entry);
2727
+ }
2728
+ };
2729
+
2730
+ if (isPlainObject(data)) {
2731
+ visitContainer(data.providers);
2732
+ visitContainer(data.configs);
2733
+ visitContainer(data.providerConfigs);
2734
+ visitContainer(data.items);
2735
+ if (providers.length === 0 && (data.base_url || data.baseUrl || data.url || data.endpoint)) {
2736
+ addProvider(pickProviderCacheString(data, ['name', 'id', 'provider']) || 'default', data);
2737
+ }
2738
+ } else if (Array.isArray(data)) {
2739
+ visitContainer(data);
2740
+ }
2741
+ return providers.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')));
2742
+ }
2743
+
2744
+ function buildProviderCacheFileRecord(fileName) {
2745
+ const filePath = path.join(CODEXMATE_DIR, fileName);
2746
+ const displayPath = getProviderCacheDisplayPath(fileName);
2747
+ const record = {
2748
+ name: fileName,
2749
+ path: displayPath,
2750
+ displayPath,
2751
+ exists: false,
2752
+ ok: true,
2753
+ tooLarge: false,
2754
+ size: 0,
2755
+ mtime: '',
2756
+ data: null,
2757
+ providers: [],
2758
+ providerCount: 0,
2759
+ error: ''
2760
+ };
2761
+ if (!fs.existsSync(filePath)) {
2762
+ return record;
2763
+ }
2764
+ record.exists = true;
2765
+ try {
2766
+ const stat = fs.statSync(filePath);
2767
+ if (!stat.isFile()) {
2768
+ record.ok = false;
2769
+ record.error = '缓存路径不是普通文件,已跳过读取';
2770
+ return record;
2771
+ }
2772
+ record.size = Number(stat.size) || 0;
2773
+ record.mtime = stat.mtime instanceof Date && !Number.isNaN(stat.mtime.getTime())
2774
+ ? stat.mtime.toISOString()
2775
+ : '';
2776
+ } catch (e) {
2777
+ record.ok = false;
2778
+ record.error = sanitizeProviderCacheErrorMessage(e && e.message ? e.message : String(e || '读取缓存文件状态失败'), fileName, '读取缓存文件状态失败');
2779
+ return record;
2780
+ }
2781
+ if (record.size > PROVIDER_CACHE_MAX_FILE_BYTES) {
2782
+ record.ok = false;
2783
+ record.tooLarge = true;
2784
+ record.error = `缓存文件过大,已跳过 JSON 读取(${record.size} bytes > ${PROVIDER_CACHE_MAX_FILE_BYTES} bytes)`;
2785
+ return record;
2786
+ }
2787
+ try {
2788
+ const content = stripUtf8Bom(fs.readFileSync(filePath, 'utf-8'));
2789
+ const parsed = content.trim() ? JSON.parse(content) : null;
2790
+ record.data = redactProviderCacheValue(parsed);
2791
+ record.providers = extractProviderCacheSummaries(parsed);
2792
+ record.providerCount = record.providers.length;
2793
+ } catch (e) {
2794
+ record.ok = false;
2795
+ record.error = sanitizeProviderCacheErrorMessage(e && e.message ? e.message : String(e || '读取缓存文件失败'), fileName);
2796
+ }
2797
+ return record;
2798
+ }
2799
+
2800
+ function listProviderCacheFileNamesForGroup(groupKey) {
2801
+ const defaults = PROVIDER_CACHE_FILE_GROUPS[groupKey] || [];
2802
+ const names = new Set(defaults);
2803
+ try {
2804
+ if (fs.existsSync(CODEXMATE_DIR)) {
2805
+ for (const fileName of fs.readdirSync(CODEXMATE_DIR)) {
2806
+ if (typeof fileName !== 'string' || !fileName.endsWith('.json')) continue;
2807
+ if (groupKey === 'opencode' && /^opencode[-_]/i.test(fileName)) {
2808
+ names.add(fileName);
2809
+ }
2810
+ }
2811
+ }
2812
+ } catch (_) {}
2813
+ return Array.from(names).sort((a, b) => a.localeCompare(b));
2814
+ }
2815
+
2816
+ function readProviderCacheRecords() {
2817
+ const groupLabels = {
2818
+ claude: 'Claude',
2819
+ codex: 'Codex',
2820
+ opencode: 'OpenCode'
2821
+ };
2822
+ const groups = Object.keys(PROVIDER_CACHE_FILE_GROUPS).map((key) => {
2823
+ const files = listProviderCacheFileNamesForGroup(key).map((fileName) => buildProviderCacheFileRecord(fileName));
2824
+ return {
2825
+ key,
2826
+ label: groupLabels[key] || key,
2827
+ files,
2828
+ existingCount: files.filter((file) => file && file.exists).length
2829
+ };
2830
+ });
2831
+ return {
2832
+ root: '~/.codexmate',
2833
+ maxFileBytes: PROVIDER_CACHE_MAX_FILE_BYTES,
2834
+ generatedAt: new Date().toISOString(),
2835
+ groups
2836
+ };
2837
+ }
2838
+
2839
+ function readProviderCacheJsonObject(fileName) {
2840
+ const filePath = path.join(CODEXMATE_DIR, fileName);
2841
+ try {
2842
+ if (!fs.existsSync(filePath)) return {};
2843
+ const stat = fs.statSync(filePath);
2844
+ if (!stat.isFile()) return {};
2845
+ const parsed = JSON.parse(stripUtf8Bom(fs.readFileSync(filePath, 'utf-8')) || '{}');
2846
+ return isPlainObject(parsed) ? parsed : {};
2847
+ } catch (_) {
2848
+ return {};
2849
+ }
2850
+ }
2851
+
2852
+ function writeProviderCacheJsonObject(fileName, data) {
2853
+ ensureDir(CODEXMATE_DIR);
2854
+ const filePath = path.join(CODEXMATE_DIR, fileName);
2855
+ writeJsonAtomic(filePath, isPlainObject(data) ? data : {});
2856
+ try {
2857
+ fs.chmodSync(filePath, 0o600);
2858
+ } catch (_) {}
2859
+ return getProviderCacheDisplayPath(fileName);
2860
+ }
2861
+
2862
+ function normalizeProviderCacheProviderMap(rawProviders) {
2863
+ const providers = {};
2864
+ if (Array.isArray(rawProviders)) {
2865
+ for (const item of rawProviders) {
2866
+ if (!isPlainObject(item)) continue;
2867
+ const name = pickProviderCacheString(item, ['name', 'id', 'provider']);
2868
+ if (name) providers[name] = item;
2869
+ }
2870
+ return providers;
2871
+ }
2872
+ if (isPlainObject(rawProviders)) {
2873
+ for (const [name, entry] of Object.entries(rawProviders)) {
2874
+ if (isPlainObject(entry)) providers[name] = entry;
2875
+ }
2876
+ }
2877
+ return providers;
2878
+ }
2879
+
2880
+ function readClaudeProviderCacheProvider(name) {
2881
+ const targetName = typeof name === 'string' ? name.trim() : '';
2882
+ if (!targetName) return null;
2883
+ const cached = normalizeProviderCacheProviderMap(readProviderCacheJsonObject('claude-providers.json').providers);
2884
+ const entry = cached[targetName];
2885
+ return isPlainObject(entry) ? { name: targetName, ...entry } : null;
2886
+ }
2887
+
2888
+ function readClaudeProviderCacheConfigs() {
2889
+ const cached = normalizeProviderCacheProviderMap(readProviderCacheJsonObject('claude-providers.json').providers);
2890
+ const providers = [];
2891
+ for (const [name, entry] of Object.entries(cached)) {
2892
+ if (!name || !isPlainObject(entry)) continue;
2893
+ const baseUrl = typeof entry.baseUrl === 'string' ? entry.baseUrl.trim() : '';
2894
+ const model = typeof entry.model === 'string' ? entry.model.trim() : '';
2895
+ if (!baseUrl || !model) continue;
2896
+ providers.push({
2897
+ name,
2898
+ baseUrl,
2899
+ model,
2900
+ targetApi: normalizeClaudeTargetApi(entry.targetApi),
2901
+ hasKey: typeof entry.apiKey === 'string' && entry.apiKey.trim().length > 0,
2902
+ providerCacheRef: name,
2903
+ source: 'provider-cache'
2904
+ });
2905
+ }
2906
+ return { providers: providers.sort((a, b) => a.name.localeCompare(b.name)) };
2907
+ }
2908
+
2909
+ function buildProviderCacheSyncProviders() {
2910
+ const configResult = readConfigOrVirtualDefault();
2911
+ if (hasConfigLoadError(configResult)) {
2912
+ return { error: (configResult.error && configResult.error.configPublicReason) || '读取 config.toml 失败' };
2913
+ }
2914
+ const config = configResult.config || {};
2915
+ const providers = isPlainObject(config.model_providers) ? config.model_providers : {};
2916
+ const currentModels = readCurrentModels();
2917
+ const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
2918
+ const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
2919
+ const syncProviders = [];
2920
+
2921
+ for (const [name, provider] of Object.entries(providers)) {
2922
+ if (!name || !isPlainObject(provider) || isBuiltinManagedProvider(name)) continue;
2923
+ const bridgeType = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
2924
+ const isOpenaiBridgeProvider = bridgeType === 'openai'
2925
+ || (typeof provider.base_url === 'string' && provider.base_url.includes('/bridge/openai/'));
2926
+ let baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
2927
+ let apiKey = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method : '';
2928
+ if (isOpenaiBridgeProvider) {
2929
+ const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, name);
2930
+ if (upstream && !upstream.error) {
2931
+ baseUrl = upstream.baseUrl || baseUrl;
2932
+ apiKey = upstream.apiKey || apiKey;
2933
+ }
2934
+ }
2935
+ const wireApi = typeof provider.wire_api === 'string' && provider.wire_api.trim()
2936
+ ? provider.wire_api.trim()
2937
+ : 'responses';
2938
+ const model = typeof currentModels[name] === 'string' && currentModels[name].trim()
2939
+ ? currentModels[name].trim()
2940
+ : (activeProvider === name ? activeModel : '');
2941
+ syncProviders.push({
2942
+ name,
2943
+ baseUrl,
2944
+ apiKey,
2945
+ wireApi,
2946
+ model,
2947
+ bridge: bridgeType || (isOpenaiBridgeProvider ? 'openai' : '')
2948
+ });
2949
+ }
2950
+ return { providers: syncProviders.sort((a, b) => a.name.localeCompare(b.name)) };
2951
+ }
2952
+
2953
+ function mergeProviderCacheFile(fileName, nextProviders, buildEntry) {
2954
+ const existing = readProviderCacheJsonObject(fileName);
2955
+ const existingProviders = normalizeProviderCacheProviderMap(existing.providers);
2956
+ const providers = { ...existingProviders };
2957
+ for (const provider of nextProviders) {
2958
+ const previous = isPlainObject(providers[provider.name]) ? providers[provider.name] : {};
2959
+ providers[provider.name] = { ...previous, ...buildEntry(provider) };
2960
+ }
2961
+ const next = {
2962
+ ...existing,
2963
+ version: Number(existing.version) > 0 ? Number(existing.version) : 1,
2964
+ generatedAt: new Date().toISOString(),
2965
+ providers
2966
+ };
2967
+ const displayPath = writeProviderCacheJsonObject(fileName, next);
2968
+ return { path: displayPath, providerCount: Object.keys(providers).length };
2969
+ }
2970
+
2971
+ function mergeProviderCacheCurrentModelsFile(fileName, nextProviders) {
2972
+ const existing = readProviderCacheJsonObject(fileName);
2973
+ const next = { ...existing };
2974
+ for (const provider of nextProviders) {
2975
+ if (provider.model) next[provider.name] = provider.model;
2976
+ }
2977
+ const displayPath = writeProviderCacheJsonObject(fileName, next);
2978
+ return { path: displayPath, modelCount: Object.keys(next).length };
2979
+ }
2980
+
2981
+ function syncProviderCacheRecords() {
2982
+ const built = buildProviderCacheSyncProviders();
2983
+ if (built.error) return { error: built.error };
2984
+ const providers = built.providers || [];
2985
+ if (providers.length === 0) {
2986
+ return { errorKey: 'modal.providerCache.noSyncableProviders', error: 'No syncable providers' };
2987
+ }
2988
+
2989
+ const writtenFiles = [];
2990
+ writtenFiles.push(mergeProviderCacheFile('codex-providers.json', providers, (provider) => ({
2991
+ name: provider.name,
2992
+ base_url: provider.baseUrl,
2993
+ wire_api: provider.wireApi,
2994
+ preferred_auth_method: provider.apiKey,
2995
+ model: provider.model,
2996
+ ...(provider.bridge ? { codexmate_bridge: provider.bridge } : {})
2997
+ })));
2998
+ writtenFiles.push(mergeProviderCacheCurrentModelsFile('codex-provider-current-models.json', providers));
2999
+ writtenFiles.push(mergeProviderCacheFile('claude-providers.json', providers, (provider) => ({
3000
+ name: provider.name,
3001
+ baseUrl: provider.baseUrl,
3002
+ apiKey: provider.apiKey,
3003
+ model: provider.model,
3004
+ targetApi: normalizeClaudeTargetApi(provider.wireApi),
3005
+ ...(provider.bridge ? { bridge: provider.bridge } : {})
3006
+ })));
3007
+ writtenFiles.push(mergeProviderCacheFile('opencode-providers.json', providers, (provider) => ({
3008
+ name: provider.name,
3009
+ baseUrl: provider.baseUrl,
3010
+ apiKey: provider.apiKey,
3011
+ model: provider.model,
3012
+ disabled: false,
3013
+ ...(provider.bridge ? { bridge: provider.bridge } : {})
3014
+ })));
3015
+ writtenFiles.push(mergeProviderCacheCurrentModelsFile('opencode-provider-current-models.json', providers));
3016
+
3017
+ return {
3018
+ success: true,
3019
+ summary: {
3020
+ providerCount: providers.length,
3021
+ fileCount: writtenFiles.length,
3022
+ writtenFiles
3023
+ },
3024
+ records: readProviderCacheRecords()
3025
+ };
3026
+ }
3027
+
2509
3028
  function getProviderKey(params = {}) {
2510
3029
  const name = typeof params.name === 'string' ? params.name.trim() : '';
2511
3030
  if (!name) return { error: '名称不能为空' };
@@ -9492,21 +10011,35 @@ async function applyToClaudeSettings(config = {}) {
9492
10011
  let proxyStarted = false;
9493
10012
  try {
9494
10013
  assertToolConfigWriteAllowed('claude');
9495
- const apiKey = (config.apiKey || '').trim();
9496
- const targetApi = normalizeClaudeTargetApi(config.targetApi);
10014
+ const providerCacheRef = typeof config.providerCacheRef === 'string' ? config.providerCacheRef.trim() : '';
10015
+ const cachedProvider = providerCacheRef ? readClaudeProviderCacheProvider(providerCacheRef) : null;
10016
+ if (providerCacheRef && !cachedProvider) {
10017
+ return { success: false, mode: 'provider-cache', error: '缓存中的 Claude provider 不存在,请重新同步' };
10018
+ }
10019
+ const effectiveConfig = cachedProvider
10020
+ ? {
10021
+ ...config,
10022
+ apiKey: cachedProvider.apiKey || config.apiKey || '',
10023
+ baseUrl: cachedProvider.baseUrl || config.baseUrl || '',
10024
+ model: cachedProvider.model || config.model || '',
10025
+ targetApi: cachedProvider.targetApi || config.targetApi || 'responses'
10026
+ }
10027
+ : config;
10028
+ const apiKey = (effectiveConfig.apiKey || '').trim();
10029
+ const targetApi = normalizeClaudeTargetApi(effectiveConfig.targetApi);
9497
10030
  if (!apiKey && targetApi !== 'ollama') {
9498
10031
  return { success: false, mode: 'settings-file', error: '请先输入 API Key' };
9499
10032
  }
9500
10033
 
9501
- const configuredBaseUrl = typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
10034
+ const configuredBaseUrl = typeof effectiveConfig.baseUrl === 'string' ? effectiveConfig.baseUrl.trim() : '';
9502
10035
  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();
10036
+ const model = (effectiveConfig.model || DEFAULT_CLAUDE_MODEL).trim();
9504
10037
  let settingsBaseUrl = baseUrl;
9505
10038
  let settingsApiKey = apiKey;
9506
10039
  let proxyResult = null;
9507
10040
 
9508
10041
  if (targetApi === 'chat_completions' || targetApi === 'ollama') {
9509
- const upstreamProviderName = typeof config.name === 'string' ? config.name.trim() : '';
10042
+ const upstreamProviderName = typeof effectiveConfig.name === 'string' ? effectiveConfig.name.trim() : '';
9510
10043
  if (targetApi === 'chat_completions' && !configuredBaseUrl && !upstreamProviderName) {
9511
10044
  return {
9512
10045
  success: false,
@@ -11695,6 +12228,12 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11695
12228
  case 'get-tool-config-permissions':
11696
12229
  result = { permissions: readToolConfigPermissions() };
11697
12230
  break;
12231
+ case 'get-web-ui-preferences':
12232
+ result = { preferences: readWebUiPreferences() };
12233
+ break;
12234
+ case 'set-web-ui-preferences':
12235
+ result = setWebUiPreferences(params || {});
12236
+ break;
11698
12237
  case 'set-tool-config-permission':
11699
12238
  result = setToolConfigPermission(params || {});
11700
12239
  break;
@@ -11839,6 +12378,15 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11839
12378
  case 'get-provider-key':
11840
12379
  result = getProviderKey(params || {});
11841
12380
  break;
12381
+ case 'get-provider-cache-records':
12382
+ result = readProviderCacheRecords();
12383
+ break;
12384
+ case 'get-claude-provider-cache-configs':
12385
+ result = readClaudeProviderCacheConfigs();
12386
+ break;
12387
+ case 'sync-provider-cache-records':
12388
+ result = syncProviderCacheRecords();
12389
+ break;
11842
12390
  case 'delete-provider':
11843
12391
  result = deleteProviderFromConfig(params || {});
11844
12392
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.54",
3
+ "version": "0.0.55",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {