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/README.md +1 -0
- package/README.vi.md +170 -0
- package/cli.js +553 -5
- package/package.json +1 -1
- package/web-ui/app.js +38 -0
- package/web-ui/logic.claude.mjs +8 -0
- package/web-ui/modules/app.methods.claude-config.mjs +72 -5
- package/web-ui/modules/app.methods.index.mjs +4 -0
- package/web-ui/modules/app.methods.navigation.mjs +3 -0
- package/web-ui/modules/app.methods.provider-cache.mjs +223 -0
- package/web-ui/modules/app.methods.session-actions.mjs +12 -0
- package/web-ui/modules/app.methods.session-browser.mjs +3 -0
- package/web-ui/modules/app.methods.session-trash.mjs +3 -0
- package/web-ui/modules/app.methods.startup-claude.mjs +13 -3
- package/web-ui/modules/app.methods.web-ui-preferences.mjs +161 -0
- package/web-ui/modules/i18n/locales/en.mjs +65 -1
- package/web-ui/modules/i18n/locales/ja.mjs +62 -1
- package/web-ui/modules/i18n/locales/vi.mjs +917 -0
- package/web-ui/modules/i18n/locales/zh-tw.mjs +62 -1
- package/web-ui/modules/i18n/locales/zh.mjs +62 -1
- package/web-ui/partials/index/layout-header.html +18 -5
- package/web-ui/partials/index/modals-basic.html +182 -0
- package/web-ui/partials/index/panel-sessions.html +2 -2
- package/web-ui/partials/index/panel-settings.html +20 -0
- package/web-ui/res/web-ui-render.precompiled.js +397 -20
- package/web-ui/styles/layout-shell.css +51 -1
- package/web-ui/styles/settings-panel.css +398 -0
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
|
|
9496
|
-
const
|
|
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
|
|
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 = (
|
|
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
|
|
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;
|