@adversity/coding-tool-x 3.1.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
- package/dist/web/assets/Home-BJKPCBuk.css +1 -0
- package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
- package/dist/web/assets/Terminal-BasTyDut.js +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-CoB3zF0K.css +1 -0
- package/dist/web/assets/index-CryrSLv8.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +81 -12
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +41 -2
- package/src/config/loader.js +74 -8
- package/src/config/model-metadata.js +415 -0
- package/src/config/model-pricing.js +23 -93
- package/src/config/paths.js +105 -33
- package/src/index.js +64 -3
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +497 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +345 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/settings.js +111 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +36 -22
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +21 -7
- package/src/server/index.js +174 -58
- package/src/server/opencode-proxy-server.js +5486 -0
- package/src/server/proxy-server.js +33 -22
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- package/src/server/services/codex-sessions.js +105 -6
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +50 -13
- package/src/server/services/env-manager.js +155 -19
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +33 -44
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +208 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +156 -8
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
4
|
+
const { resolveModelMetadata } = require('../../config/model-metadata');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 根据模型 ID 查找 limit(context + output)
|
|
8
|
+
* 委托给集中式 model-metadata.js
|
|
9
|
+
*/
|
|
10
|
+
function resolveModelLimit(modelId) {
|
|
11
|
+
const meta = resolveModelMetadata(modelId);
|
|
12
|
+
return meta ? meta.limit : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 根据模型 ID 查找定价信息
|
|
17
|
+
* 委托给集中式 model-metadata.js
|
|
18
|
+
*/
|
|
19
|
+
function resolveModelCost(modelId) {
|
|
20
|
+
const meta = resolveModelMetadata(modelId);
|
|
21
|
+
return meta ? meta.pricing : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
25
|
+
const CONFIG_PATHS = {
|
|
26
|
+
config: path.join(CONFIG_DIR, 'config.json'),
|
|
27
|
+
opencode: path.join(CONFIG_DIR, 'opencode.json'),
|
|
28
|
+
opencodec: path.join(CONFIG_DIR, 'opencode.jsonc')
|
|
29
|
+
};
|
|
30
|
+
const BACKUP_SUFFIX = '.cc-tool-backup';
|
|
31
|
+
const EMPTY_SENTINEL = '__CC_TOOL_NO_FILE__';
|
|
32
|
+
const PROXY_PROVIDER_ID = 'ctx-proxy';
|
|
33
|
+
const LEGACY_PROVIDER_ID = 'openai';
|
|
34
|
+
const PROXY_API_KEY = 'PROXY_KEY';
|
|
35
|
+
const MANAGED_PROVIDER_MARKER = '__ctx_managed__';
|
|
36
|
+
|
|
37
|
+
function ensureConfigDir() {
|
|
38
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
39
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getBackupPath(filePath) {
|
|
44
|
+
return `${filePath}${BACKUP_SUFFIX}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function selectConfigPath() {
|
|
48
|
+
if (fs.existsSync(CONFIG_PATHS.opencodec)) return CONFIG_PATHS.opencodec;
|
|
49
|
+
if (fs.existsSync(CONFIG_PATHS.opencode)) return CONFIG_PATHS.opencode;
|
|
50
|
+
if (fs.existsSync(CONFIG_PATHS.config)) return CONFIG_PATHS.config;
|
|
51
|
+
return CONFIG_PATHS.opencode;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stripJsonComments(input) {
|
|
55
|
+
let result = '';
|
|
56
|
+
let inString = false;
|
|
57
|
+
let stringChar = '';
|
|
58
|
+
let i = 0;
|
|
59
|
+
|
|
60
|
+
while (i < input.length) {
|
|
61
|
+
const ch = input[i];
|
|
62
|
+
const next = input[i + 1];
|
|
63
|
+
|
|
64
|
+
if (inString) {
|
|
65
|
+
result += ch;
|
|
66
|
+
if (ch === '\\') {
|
|
67
|
+
if (next) {
|
|
68
|
+
result += next;
|
|
69
|
+
i += 2;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
} else if (ch === stringChar) {
|
|
73
|
+
inString = false;
|
|
74
|
+
}
|
|
75
|
+
i += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (ch === '"' || ch === '\'') {
|
|
80
|
+
inString = true;
|
|
81
|
+
stringChar = ch;
|
|
82
|
+
result += ch;
|
|
83
|
+
i += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (ch === '/' && next === '/') {
|
|
88
|
+
i += 2;
|
|
89
|
+
while (i < input.length && input[i] !== '\n') i += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (ch === '/' && next === '*') {
|
|
94
|
+
i += 2;
|
|
95
|
+
while (i < input.length - 1 && !(input[i] === '*' && input[i + 1] === '/')) i += 1;
|
|
96
|
+
i += 2;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
result += ch;
|
|
101
|
+
i += 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readConfig(filePath) {
|
|
108
|
+
if (!fs.existsSync(filePath)) return {};
|
|
109
|
+
|
|
110
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
111
|
+
if (!raw.trim()) return {};
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
if (filePath.endsWith('.jsonc')) {
|
|
115
|
+
return JSON.parse(stripJsonComments(raw));
|
|
116
|
+
}
|
|
117
|
+
return JSON.parse(raw);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
throw new Error(`Failed to parse ${path.basename(filePath)}: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function writeConfig(filePath, config) {
|
|
124
|
+
ensureConfigDir();
|
|
125
|
+
const content = JSON.stringify(config, null, 2);
|
|
126
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeOpenCodeModel(modelId, providerId) {
|
|
130
|
+
const normalized = String(modelId || '').trim();
|
|
131
|
+
if (!normalized) {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pid = String(providerId || PROXY_PROVIDER_ID).trim() || PROXY_PROVIDER_ID;
|
|
136
|
+
|
|
137
|
+
// Already has a provider/ prefix - keep as-is only if it matches the expected provider
|
|
138
|
+
if (normalized.includes('/')) {
|
|
139
|
+
return normalized;
|
|
140
|
+
}
|
|
141
|
+
return `${pid}/${normalized}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function sanitizeProviderKey(name) {
|
|
145
|
+
return String(name || '')
|
|
146
|
+
.toLowerCase()
|
|
147
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
148
|
+
.replace(/-+/g, '-')
|
|
149
|
+
.replace(/^-|-$/g, '')
|
|
150
|
+
|| 'channel';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isLocalProxyBaseUrl(url) {
|
|
154
|
+
const value = String(url || '').trim();
|
|
155
|
+
return value.includes('127.0.0.1') || value.includes('localhost');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isLegacyProxyProvider(provider) {
|
|
159
|
+
if (!provider || typeof provider !== 'object') return false;
|
|
160
|
+
const apiKey = provider?.options?.apiKey;
|
|
161
|
+
const baseUrl = provider?.options?.baseURL;
|
|
162
|
+
return apiKey === PROXY_API_KEY && isLocalProxyBaseUrl(baseUrl);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isManagedProxyProvider(provider) {
|
|
166
|
+
if (!provider || typeof provider !== 'object') return false;
|
|
167
|
+
const apiKey = provider?.options?.apiKey;
|
|
168
|
+
const baseUrl = provider?.options?.baseURL;
|
|
169
|
+
return apiKey === PROXY_API_KEY && isLocalProxyBaseUrl(baseUrl);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isManagedProxyConfig(config) {
|
|
173
|
+
if (!config || typeof config !== 'object') return false;
|
|
174
|
+
// Check legacy single-provider format
|
|
175
|
+
if (isManagedProxyProvider(config?.provider?.[PROXY_PROVIDER_ID])
|
|
176
|
+
|| isLegacyProxyProvider(config?.provider?.[LEGACY_PROVIDER_ID])) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
// Check per-channel provider format (any provider with PROXY_API_KEY + local baseURL)
|
|
180
|
+
if (config?.provider && typeof config.provider === 'object') {
|
|
181
|
+
return Object.values(config.provider).some(p => isManagedProxyProvider(p));
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildModelsMap(models = [], fallbackModel = '') {
|
|
187
|
+
const map = {};
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
|
|
190
|
+
const add = (value) => {
|
|
191
|
+
if (typeof value !== 'string') return;
|
|
192
|
+
const trimmed = value.trim();
|
|
193
|
+
if (!trimmed) return;
|
|
194
|
+
const key = trimmed.toLowerCase();
|
|
195
|
+
if (seen.has(key)) return;
|
|
196
|
+
seen.add(key);
|
|
197
|
+
|
|
198
|
+
const entry = { name: trimmed };
|
|
199
|
+
|
|
200
|
+
// 注入 limit(context + output),供 OpenCode 显示 "X% used"
|
|
201
|
+
// OpenCode schema 要求 limit 必须同时包含 context 和 output
|
|
202
|
+
const limit = resolveModelLimit(trimmed);
|
|
203
|
+
if (limit) {
|
|
204
|
+
entry.limit = { context: limit.context, output: limit.output };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 注入定价信息,供 OpenCode 计算 cost
|
|
208
|
+
const pricing = resolveModelCost(trimmed);
|
|
209
|
+
if (pricing) {
|
|
210
|
+
entry.cost = {
|
|
211
|
+
input: pricing.input,
|
|
212
|
+
output: pricing.output,
|
|
213
|
+
cache_read: pricing.cacheRead,
|
|
214
|
+
cache_write: pricing.cacheCreation
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
map[trimmed] = entry;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (Array.isArray(models)) {
|
|
222
|
+
models.forEach(add);
|
|
223
|
+
}
|
|
224
|
+
add(fallbackModel);
|
|
225
|
+
|
|
226
|
+
return map;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function resolveProxyBaseUrl(config) {
|
|
230
|
+
if (config?.provider?.[PROXY_PROVIDER_ID]?.options?.baseURL) {
|
|
231
|
+
return config.provider[PROXY_PROVIDER_ID].options.baseURL;
|
|
232
|
+
}
|
|
233
|
+
if (config?.provider?.[LEGACY_PROVIDER_ID]?.options?.baseURL) {
|
|
234
|
+
return config.provider[LEGACY_PROVIDER_ID].options.baseURL;
|
|
235
|
+
}
|
|
236
|
+
// Check per-channel managed providers
|
|
237
|
+
if (config?.provider && typeof config.provider === 'object') {
|
|
238
|
+
for (const p of Object.values(config.provider)) {
|
|
239
|
+
if (isManagedProxyProvider(p) && p?.options?.baseURL) {
|
|
240
|
+
return p.options.baseURL;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return '';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function backupConfig(filePath) {
|
|
248
|
+
ensureConfigDir();
|
|
249
|
+
const backupPath = getBackupPath(filePath);
|
|
250
|
+
|
|
251
|
+
if (fs.existsSync(backupPath)) {
|
|
252
|
+
// 防止历史残留备份误伤:若当前配置已回到“非代理托管态”,刷新备份为当前真实配置。
|
|
253
|
+
// 这样 stop/restore 不会把用户配置恢复成陈旧快照(或空文件哨兵)。
|
|
254
|
+
try {
|
|
255
|
+
const backupContent = fs.readFileSync(backupPath, 'utf8');
|
|
256
|
+
if (backupContent === EMPTY_SENTINEL && fs.existsSync(filePath)) {
|
|
257
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
258
|
+
fs.writeFileSync(backupPath, content, 'utf8');
|
|
259
|
+
return { success: true, alreadyExists: true };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const current = readConfig(filePath);
|
|
263
|
+
if (!isManagedProxyConfig(current)) {
|
|
264
|
+
if (fs.existsSync(filePath)) {
|
|
265
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
266
|
+
fs.writeFileSync(backupPath, content, 'utf8');
|
|
267
|
+
} else {
|
|
268
|
+
fs.writeFileSync(backupPath, EMPTY_SENTINEL, 'utf8');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
// ignore backup refresh errors, fallback to existing backup
|
|
273
|
+
}
|
|
274
|
+
return { success: true, alreadyExists: true };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (fs.existsSync(filePath)) {
|
|
278
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
279
|
+
fs.writeFileSync(backupPath, content, 'utf8');
|
|
280
|
+
} else {
|
|
281
|
+
fs.writeFileSync(backupPath, EMPTY_SENTINEL, 'utf8');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { success: true, alreadyExists: false };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function restoreConfig(filePath) {
|
|
288
|
+
const backupPath = getBackupPath(filePath);
|
|
289
|
+
if (!fs.existsSync(backupPath)) return false;
|
|
290
|
+
|
|
291
|
+
const content = fs.readFileSync(backupPath, 'utf8');
|
|
292
|
+
if (content === EMPTY_SENTINEL) {
|
|
293
|
+
if (fs.existsSync(filePath)) {
|
|
294
|
+
fs.unlinkSync(filePath);
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
ensureConfigDir();
|
|
298
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fs.unlinkSync(backupPath);
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function configExists() {
|
|
306
|
+
return fs.existsSync(CONFIG_PATHS.opencodec)
|
|
307
|
+
|| fs.existsSync(CONFIG_PATHS.opencode)
|
|
308
|
+
|| fs.existsSync(CONFIG_PATHS.config);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function hasBackup() {
|
|
312
|
+
return fs.existsSync(getBackupPath(CONFIG_PATHS.opencodec))
|
|
313
|
+
|| fs.existsSync(getBackupPath(CONFIG_PATHS.opencode))
|
|
314
|
+
|| fs.existsSync(getBackupPath(CONFIG_PATHS.config));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function setProxyConfig(proxyPort, options = {}) {
|
|
318
|
+
const filePath = selectConfigPath();
|
|
319
|
+
backupConfig(filePath);
|
|
320
|
+
|
|
321
|
+
const config = readConfig(filePath);
|
|
322
|
+
const next = (config && typeof config === 'object') ? config : {};
|
|
323
|
+
|
|
324
|
+
if (!next.provider || typeof next.provider !== 'object') {
|
|
325
|
+
next.provider = {};
|
|
326
|
+
}
|
|
327
|
+
// 清理历史 openai 代理注入,避免 /models 出现与代理无关的 openai 模型列表。
|
|
328
|
+
if (isLegacyProxyProvider(next.provider[LEGACY_PROVIDER_ID])) {
|
|
329
|
+
delete next.provider[LEGACY_PROVIDER_ID];
|
|
330
|
+
}
|
|
331
|
+
if (Object.prototype.hasOwnProperty.call(next.provider[LEGACY_PROVIDER_ID] || {}, 'model')) {
|
|
332
|
+
delete next.provider[LEGACY_PROVIDER_ID].model;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Remove old single ctx-proxy provider (superseded by per-channel providers)
|
|
336
|
+
delete next.provider[PROXY_PROVIDER_ID];
|
|
337
|
+
|
|
338
|
+
// Remove any previously managed per-channel providers that are no longer in the current list
|
|
339
|
+
Object.keys(next.provider).forEach((key) => {
|
|
340
|
+
if (isManagedProxyProvider(next.provider[key])) {
|
|
341
|
+
delete next.provider[key];
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const channels = Array.isArray(options.channels) ? options.channels : null;
|
|
346
|
+
|
|
347
|
+
if (channels && channels.length > 0) {
|
|
348
|
+
// Per-channel mode: write one provider entry per channel
|
|
349
|
+
const usedKeys = new Set();
|
|
350
|
+
let firstProviderId = null;
|
|
351
|
+
let firstModelId = null;
|
|
352
|
+
|
|
353
|
+
channels.forEach((ch) => {
|
|
354
|
+
const rawKey = sanitizeProviderKey(ch.providerKey || ch.name || '');
|
|
355
|
+
// Ensure uniqueness
|
|
356
|
+
let key = rawKey;
|
|
357
|
+
let suffix = 2;
|
|
358
|
+
while (usedKeys.has(key)) {
|
|
359
|
+
key = `${rawKey}-${suffix}`;
|
|
360
|
+
suffix += 1;
|
|
361
|
+
}
|
|
362
|
+
usedKeys.add(key);
|
|
363
|
+
|
|
364
|
+
const modelsMap = buildModelsMap(ch.models, ch.model);
|
|
365
|
+
const modelIds = Object.keys(modelsMap);
|
|
366
|
+
|
|
367
|
+
if (modelIds.length > 0) {
|
|
368
|
+
next.provider[key] = {
|
|
369
|
+
npm: '@ai-sdk/openai-compatible',
|
|
370
|
+
name: ch.name || key,
|
|
371
|
+
options: {
|
|
372
|
+
baseURL: `http://127.0.0.1:${proxyPort}/v1`,
|
|
373
|
+
apiKey: PROXY_API_KEY
|
|
374
|
+
},
|
|
375
|
+
models: modelsMap
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
if (firstProviderId === null) {
|
|
379
|
+
firstProviderId = key;
|
|
380
|
+
firstModelId = ch.model || modelIds[0] || null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Write top-level model pointing to first channel's first model
|
|
386
|
+
const topModel = options.model || (firstProviderId && firstModelId
|
|
387
|
+
? `${firstProviderId}/${firstModelId}`
|
|
388
|
+
: null);
|
|
389
|
+
if (topModel) {
|
|
390
|
+
const resolved = normalizeOpenCodeModel(topModel, firstProviderId || PROXY_PROVIDER_ID);
|
|
391
|
+
if (resolved) {
|
|
392
|
+
next.model = resolved;
|
|
393
|
+
}
|
|
394
|
+
} else if (isOldManagedModelRef(next.model)) {
|
|
395
|
+
delete next.model;
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
// Fallback: legacy flat-model mode (single ctx-proxy provider)
|
|
399
|
+
const modelsMap = buildModelsMap(options.models, options.model);
|
|
400
|
+
const modelIds = Object.keys(modelsMap);
|
|
401
|
+
|
|
402
|
+
if (modelIds.length > 0) {
|
|
403
|
+
next.provider[PROXY_PROVIDER_ID] = {
|
|
404
|
+
npm: '@ai-sdk/openai-compatible',
|
|
405
|
+
name: 'CTX Proxy',
|
|
406
|
+
options: {
|
|
407
|
+
baseURL: `http://127.0.0.1:${proxyPort}/v1`,
|
|
408
|
+
apiKey: PROXY_API_KEY
|
|
409
|
+
},
|
|
410
|
+
models: modelsMap
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const fallbackModel = options.model || modelIds[0] || '';
|
|
415
|
+
if (fallbackModel) {
|
|
416
|
+
const resolvedModel = normalizeOpenCodeModel(fallbackModel, PROXY_PROVIDER_ID);
|
|
417
|
+
if (resolvedModel) {
|
|
418
|
+
next.model = resolvedModel;
|
|
419
|
+
}
|
|
420
|
+
} else if (isOldManagedModelRef(next.model)) {
|
|
421
|
+
delete next.model;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
writeConfig(filePath, next);
|
|
426
|
+
|
|
427
|
+
return { success: true, port: proxyPort, path: filePath };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function isOldManagedModelRef(modelRef) {
|
|
431
|
+
const s = String(modelRef || '');
|
|
432
|
+
return s.startsWith(`${PROXY_PROVIDER_ID}/`) || s.startsWith(`${LEGACY_PROVIDER_ID}/`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function restoreSettings() {
|
|
436
|
+
const restored = [
|
|
437
|
+
restoreConfig(CONFIG_PATHS.opencodec),
|
|
438
|
+
restoreConfig(CONFIG_PATHS.opencode),
|
|
439
|
+
restoreConfig(CONFIG_PATHS.config)
|
|
440
|
+
].some(Boolean);
|
|
441
|
+
|
|
442
|
+
return { success: restored };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function isProxyConfig() {
|
|
446
|
+
try {
|
|
447
|
+
const filePath = selectConfigPath();
|
|
448
|
+
if (!fs.existsSync(filePath)) return false;
|
|
449
|
+
const config = readConfig(filePath);
|
|
450
|
+
const baseUrl = resolveProxyBaseUrl(config);
|
|
451
|
+
return isLocalProxyBaseUrl(baseUrl);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function getCurrentProxyPort() {
|
|
458
|
+
try {
|
|
459
|
+
if (!isProxyConfig()) return null;
|
|
460
|
+
const filePath = selectConfigPath();
|
|
461
|
+
const config = readConfig(filePath);
|
|
462
|
+
const baseUrl = resolveProxyBaseUrl(config);
|
|
463
|
+
const match = baseUrl.match(/:(\d+)/);
|
|
464
|
+
return match ? parseInt(match[1], 10) : null;
|
|
465
|
+
} catch (err) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
module.exports = {
|
|
471
|
+
configExists,
|
|
472
|
+
hasBackup,
|
|
473
|
+
setProxyConfig,
|
|
474
|
+
restoreSettings,
|
|
475
|
+
isProxyConfig,
|
|
476
|
+
getCurrentProxyPort,
|
|
477
|
+
CONFIG_PATHS
|
|
478
|
+
};
|