coding-tool-x 3.2.0
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 +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.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-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -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 +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -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 +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- package/src/utils/session.js +240 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
function normalizeAddress(address) {
|
|
2
|
+
if (!address) return '';
|
|
3
|
+
const trimmed = String(address).trim();
|
|
4
|
+
if (trimmed.startsWith('::ffff:')) {
|
|
5
|
+
return trimmed.slice(7);
|
|
6
|
+
}
|
|
7
|
+
return trimmed;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isLoopbackAddress(address) {
|
|
11
|
+
const normalized = normalizeAddress(address).toLowerCase();
|
|
12
|
+
if (!normalized) return false;
|
|
13
|
+
if (normalized === '::1' || normalized === 'localhost') return true;
|
|
14
|
+
if (normalized === '127.0.0.1') return true;
|
|
15
|
+
if (normalized.startsWith('127.')) return true;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isLoopbackRequest(req) {
|
|
20
|
+
if (!req) return false;
|
|
21
|
+
const socketAddress = req.socket && req.socket.remoteAddress;
|
|
22
|
+
if (!isLoopbackAddress(socketAddress)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const forwarded = req.headers && req.headers['x-forwarded-for'];
|
|
26
|
+
if (typeof forwarded === 'string' && forwarded.trim()) {
|
|
27
|
+
const first = forwarded.split(',')[0].trim();
|
|
28
|
+
return isLoopbackAddress(first);
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createRemoteMutationGuard(options = {}) {
|
|
34
|
+
const enabled = options.enabled === true;
|
|
35
|
+
const allowRemoteMutation = options.allowRemoteMutation === true;
|
|
36
|
+
const message = options.message || 'LAN 模式下禁止远程写操作';
|
|
37
|
+
|
|
38
|
+
return (req, res, next) => {
|
|
39
|
+
if (!enabled || allowRemoteMutation) {
|
|
40
|
+
return next();
|
|
41
|
+
}
|
|
42
|
+
if (isLoopbackRequest(req)) {
|
|
43
|
+
return next();
|
|
44
|
+
}
|
|
45
|
+
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
|
|
46
|
+
return next();
|
|
47
|
+
}
|
|
48
|
+
return res.status(403).json({
|
|
49
|
+
error: message,
|
|
50
|
+
code: 'LAN_REMOTE_WRITE_BLOCKED'
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createRemoteRouteGuard(options = {}) {
|
|
56
|
+
const enabled = options.enabled === true;
|
|
57
|
+
const allowRemoteAccess = options.allowRemoteAccess === true;
|
|
58
|
+
const message = options.message || 'LAN 模式下禁止远程访问该接口';
|
|
59
|
+
|
|
60
|
+
return (req, res, next) => {
|
|
61
|
+
if (!enabled || allowRemoteAccess) {
|
|
62
|
+
return next();
|
|
63
|
+
}
|
|
64
|
+
if (isLoopbackRequest(req)) {
|
|
65
|
+
return next();
|
|
66
|
+
}
|
|
67
|
+
return res.status(403).json({
|
|
68
|
+
error: message,
|
|
69
|
+
code: 'LAN_REMOTE_ROUTE_BLOCKED'
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
normalizeAddress,
|
|
76
|
+
isLoopbackAddress,
|
|
77
|
+
isLoopbackRequest,
|
|
78
|
+
createRemoteMutationGuard,
|
|
79
|
+
createRemoteRouteGuard
|
|
80
|
+
};
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OpenCode 渠道管理服务
|
|
8
|
+
* 存储位置: ~/.cc-tool/opencode-channels.json
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function normalizeGatewaySourceType(value) {
|
|
12
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
13
|
+
if (normalized === 'claude') return 'claude';
|
|
14
|
+
if (normalized === 'gemini') return 'gemini';
|
|
15
|
+
return 'codex';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeApiKey(value) {
|
|
19
|
+
if (typeof value !== 'string') return '';
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
return trimmed || '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeHostFromBaseUrl(baseUrl) {
|
|
25
|
+
const value = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
|
26
|
+
if (!value) return '';
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(value);
|
|
29
|
+
return String(parsed.hostname || '').trim().toLowerCase().replace(/^www\./, '');
|
|
30
|
+
} catch {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeChannelName(value) {
|
|
36
|
+
return String(value || '')
|
|
37
|
+
.trim()
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/\s+/g, ' ');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 获取渠道存储文件路径
|
|
43
|
+
function getChannelsFilePath() {
|
|
44
|
+
const ccToolDir = path.join(os.homedir(), '.cc-tool');
|
|
45
|
+
if (!fs.existsSync(ccToolDir)) {
|
|
46
|
+
fs.mkdirSync(ccToolDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
return path.join(ccToolDir, 'opencode-channels.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getCodexChannelsFilePath() {
|
|
52
|
+
const ccToolDir = path.join(os.homedir(), '.cc-tool');
|
|
53
|
+
if (!fs.existsSync(ccToolDir)) {
|
|
54
|
+
fs.mkdirSync(ccToolDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
return path.join(ccToolDir, 'codex-channels.json');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 读取所有渠道
|
|
60
|
+
function loadChannels() {
|
|
61
|
+
const filePath = getChannelsFilePath();
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(filePath)) {
|
|
64
|
+
return { channels: [] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
69
|
+
const data = JSON.parse(content);
|
|
70
|
+
// 确保渠道有必要字段(兼容旧数据)
|
|
71
|
+
if (data.channels) {
|
|
72
|
+
data.channels = data.channels.map(ch => {
|
|
73
|
+
const normalized = {
|
|
74
|
+
...ch,
|
|
75
|
+
enabled: ch.enabled !== false,
|
|
76
|
+
weight: ch.weight || 1,
|
|
77
|
+
maxConcurrency: ch.maxConcurrency || null,
|
|
78
|
+
modelRedirects: ch.modelRedirects || [],
|
|
79
|
+
speedTestModel: ch.speedTestModel || null,
|
|
80
|
+
wireApi: ch.wireApi || 'openai', // OpenCode 默认使用 OpenAI 兼容格式
|
|
81
|
+
gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType),
|
|
82
|
+
allowedModels: ch.allowedModels || []
|
|
83
|
+
};
|
|
84
|
+
normalized.providerKey = deriveProviderKey(normalized);
|
|
85
|
+
return normalized;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return data;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error('[OpenCode Channels] Failed to parse channels file:', err);
|
|
91
|
+
return { channels: [] };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function deriveProviderKey(channel) {
|
|
96
|
+
const base = channel.wireApi || channel.providerKey || 'opencode';
|
|
97
|
+
if (typeof base === 'string' && base.startsWith('opencode_')) {
|
|
98
|
+
return base;
|
|
99
|
+
}
|
|
100
|
+
return `opencode_${base}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 保存渠道数据
|
|
104
|
+
function saveChannels(data) {
|
|
105
|
+
const filePath = getChannelsFilePath();
|
|
106
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 获取所有渠道
|
|
110
|
+
function getChannels() {
|
|
111
|
+
const data = loadChannels();
|
|
112
|
+
return {
|
|
113
|
+
channels: data.channels || []
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 添加渠道
|
|
118
|
+
function createChannel(name, baseUrl, apiKey, extraConfig = {}) {
|
|
119
|
+
const data = loadChannels();
|
|
120
|
+
|
|
121
|
+
const newChannel = {
|
|
122
|
+
id: crypto.randomUUID(),
|
|
123
|
+
name,
|
|
124
|
+
baseUrl,
|
|
125
|
+
apiKey,
|
|
126
|
+
wireApi: extraConfig.wireApi || 'openai',
|
|
127
|
+
enabled: extraConfig.enabled !== false,
|
|
128
|
+
weight: extraConfig.weight || 1,
|
|
129
|
+
maxConcurrency: extraConfig.maxConcurrency || null,
|
|
130
|
+
modelRedirects: extraConfig.modelRedirects || [],
|
|
131
|
+
speedTestModel: extraConfig.speedTestModel || null,
|
|
132
|
+
model: extraConfig.model || null,
|
|
133
|
+
gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType),
|
|
134
|
+
providerKey: extraConfig.providerKey || null,
|
|
135
|
+
presetId: extraConfig.presetId || null,
|
|
136
|
+
websiteUrl: extraConfig.websiteUrl || '',
|
|
137
|
+
allowedModels: extraConfig.allowedModels || [],
|
|
138
|
+
createdAt: Date.now(),
|
|
139
|
+
updatedAt: Date.now()
|
|
140
|
+
};
|
|
141
|
+
newChannel.providerKey = extraConfig.providerKey || deriveProviderKey(newChannel);
|
|
142
|
+
|
|
143
|
+
data.channels.push(newChannel);
|
|
144
|
+
saveChannels(data);
|
|
145
|
+
|
|
146
|
+
return newChannel;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 更新渠道
|
|
150
|
+
function updateChannel(channelId, updates) {
|
|
151
|
+
const data = loadChannels();
|
|
152
|
+
const index = data.channels.findIndex(c => c.id === channelId);
|
|
153
|
+
|
|
154
|
+
if (index === -1) {
|
|
155
|
+
throw new Error('Channel not found');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const oldChannel = data.channels[index];
|
|
159
|
+
|
|
160
|
+
const merged = {
|
|
161
|
+
...oldChannel,
|
|
162
|
+
...updates,
|
|
163
|
+
id: channelId,
|
|
164
|
+
createdAt: oldChannel.createdAt,
|
|
165
|
+
modelRedirects: updates.modelRedirects !== undefined ? updates.modelRedirects : (oldChannel.modelRedirects || []),
|
|
166
|
+
speedTestModel: updates.speedTestModel !== undefined ? updates.speedTestModel : (oldChannel.speedTestModel || null),
|
|
167
|
+
gatewaySourceType: normalizeGatewaySourceType(
|
|
168
|
+
updates.gatewaySourceType !== undefined
|
|
169
|
+
? updates.gatewaySourceType
|
|
170
|
+
: oldChannel.gatewaySourceType
|
|
171
|
+
),
|
|
172
|
+
updatedAt: Date.now()
|
|
173
|
+
};
|
|
174
|
+
merged.providerKey = updates.providerKey || deriveProviderKey(merged);
|
|
175
|
+
data.channels[index] = merged;
|
|
176
|
+
|
|
177
|
+
// Get proxy status
|
|
178
|
+
const { getOpenCodeProxyStatus } = require('../opencode-proxy-server');
|
|
179
|
+
const proxyStatus = getOpenCodeProxyStatus();
|
|
180
|
+
const isProxyRunning = proxyStatus.running;
|
|
181
|
+
|
|
182
|
+
// Single-channel enforcement when proxy is OFF: enabling a channel disables all others
|
|
183
|
+
if (!isProxyRunning && merged.enabled && !oldChannel.enabled) {
|
|
184
|
+
data.channels.forEach((ch, i) => {
|
|
185
|
+
if (i !== index && ch.enabled) {
|
|
186
|
+
ch.enabled = false;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
console.log(`[OpenCode Single-channel mode] Enabled "${merged.name}", disabled all others`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Prevent disabling last enabled channel when proxy is OFF
|
|
193
|
+
if (!isProxyRunning && !merged.enabled && oldChannel.enabled) {
|
|
194
|
+
const enabledCount = data.channels.filter(ch => ch.enabled).length;
|
|
195
|
+
if (enabledCount === 0) {
|
|
196
|
+
throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
saveChannels(data);
|
|
201
|
+
return data.channels[index];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 删除渠道
|
|
205
|
+
async function deleteChannel(channelId) {
|
|
206
|
+
const data = loadChannels();
|
|
207
|
+
const index = data.channels.findIndex(c => c.id === channelId);
|
|
208
|
+
|
|
209
|
+
if (index === -1) {
|
|
210
|
+
throw new Error('Channel not found');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
data.channels.splice(index, 1);
|
|
214
|
+
saveChannels(data);
|
|
215
|
+
|
|
216
|
+
return { success: true };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 获取所有启用的渠道
|
|
220
|
+
function getEnabledChannels() {
|
|
221
|
+
const data = loadChannels();
|
|
222
|
+
return data.channels.filter(c => c.enabled !== false);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 保存渠道顺序
|
|
226
|
+
function saveChannelOrder(order) {
|
|
227
|
+
const data = loadChannels();
|
|
228
|
+
|
|
229
|
+
const orderedChannels = [];
|
|
230
|
+
for (const id of order) {
|
|
231
|
+
const channel = data.channels.find(c => c.id === id);
|
|
232
|
+
if (channel) {
|
|
233
|
+
orderedChannels.push(channel);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 添加不在顺序中的渠道
|
|
238
|
+
for (const channel of data.channels) {
|
|
239
|
+
if (!orderedChannels.find(c => c.id === channel.id)) {
|
|
240
|
+
orderedChannels.push(channel);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
data.channels = orderedChannels;
|
|
245
|
+
saveChannels(data);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function loadCodexChannels() {
|
|
249
|
+
const filePath = getCodexChannelsFilePath();
|
|
250
|
+
if (!fs.existsSync(filePath)) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
255
|
+
const data = JSON.parse(content);
|
|
256
|
+
return Array.isArray(data?.channels) ? data.channels : [];
|
|
257
|
+
} catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function collectCodexFallbackApiKeys(channel) {
|
|
263
|
+
if (String(process.env.OPENCODE_DISABLE_CODEX_KEY_FALLBACK || '').trim() === '1') {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const targetHost = normalizeHostFromBaseUrl(channel?.baseUrl);
|
|
268
|
+
const targetName = normalizeChannelName(channel?.name);
|
|
269
|
+
const targetId = String(channel?.id || '').trim();
|
|
270
|
+
const codexChannels = loadCodexChannels();
|
|
271
|
+
if (codexChannels.length === 0) {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const matches = [];
|
|
276
|
+
for (const codexChannel of codexChannels) {
|
|
277
|
+
const apiKey = normalizeApiKey(codexChannel?.apiKey || codexChannel?.key || '');
|
|
278
|
+
if (!apiKey) continue;
|
|
279
|
+
|
|
280
|
+
const codexName = normalizeChannelName(codexChannel?.name);
|
|
281
|
+
const codexHost = normalizeHostFromBaseUrl(codexChannel?.baseUrl);
|
|
282
|
+
const codexId = String(codexChannel?.id || '').trim();
|
|
283
|
+
|
|
284
|
+
let score = 0;
|
|
285
|
+
if (targetHost && codexHost && targetHost === codexHost) {
|
|
286
|
+
score += 100;
|
|
287
|
+
}
|
|
288
|
+
if (targetName && codexName && targetName === codexName) {
|
|
289
|
+
score += 90;
|
|
290
|
+
} else if (targetName && codexName && (targetName.includes(codexName) || codexName.includes(targetName))) {
|
|
291
|
+
score += 60;
|
|
292
|
+
}
|
|
293
|
+
if (targetId && codexId && targetId === codexId) {
|
|
294
|
+
score += 40;
|
|
295
|
+
}
|
|
296
|
+
if (codexChannel?.enabled !== false) {
|
|
297
|
+
score += 10;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (score > 0) {
|
|
301
|
+
matches.push({ score, apiKey });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
matches.sort((a, b) => b.score - a.score);
|
|
306
|
+
|
|
307
|
+
const unique = [];
|
|
308
|
+
const seen = new Set();
|
|
309
|
+
for (const item of matches) {
|
|
310
|
+
if (!item?.apiKey || seen.has(item.apiKey)) continue;
|
|
311
|
+
seen.add(item.apiKey);
|
|
312
|
+
unique.push(item.apiKey);
|
|
313
|
+
}
|
|
314
|
+
return unique;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeBooleanLike(value) {
|
|
318
|
+
if (typeof value === 'boolean') return value;
|
|
319
|
+
if (typeof value !== 'string') return null;
|
|
320
|
+
const lowered = value.trim().toLowerCase();
|
|
321
|
+
if (lowered === '1' || lowered === 'true' || lowered === 'yes' || lowered === 'on') return true;
|
|
322
|
+
if (lowered === '0' || lowered === 'false' || lowered === 'no' || lowered === 'off') return false;
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function getEffectiveApiKeyCandidates(channel) {
|
|
327
|
+
const ownApiKey = normalizeApiKey(channel?.apiKey || channel?.key || '');
|
|
328
|
+
const codexFallbackKeys = collectCodexFallbackApiKeys(channel);
|
|
329
|
+
const explicitPreferCodex = normalizeBooleanLike(channel?.preferCodexApiKey);
|
|
330
|
+
const envPreferCodex = normalizeBooleanLike(process.env.OPENCODE_PREFER_CODEX_API_KEY);
|
|
331
|
+
const defaultPreferCodex = false;
|
|
332
|
+
const preferCodex = explicitPreferCodex ?? envPreferCodex ?? defaultPreferCodex;
|
|
333
|
+
|
|
334
|
+
const ordered = preferCodex
|
|
335
|
+
? [...codexFallbackKeys, ownApiKey]
|
|
336
|
+
: [ownApiKey, ...codexFallbackKeys];
|
|
337
|
+
|
|
338
|
+
const seen = new Set();
|
|
339
|
+
const candidates = [];
|
|
340
|
+
for (const key of ordered) {
|
|
341
|
+
const normalized = normalizeApiKey(key);
|
|
342
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
343
|
+
seen.add(normalized);
|
|
344
|
+
candidates.push(normalized);
|
|
345
|
+
}
|
|
346
|
+
return candidates;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 获取渠道的有效 API Key
|
|
351
|
+
*/
|
|
352
|
+
async function getEffectiveApiKey(channel) {
|
|
353
|
+
const candidates = getEffectiveApiKeyCandidates(channel);
|
|
354
|
+
return candidates[0] || null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = {
|
|
358
|
+
getChannels,
|
|
359
|
+
createChannel,
|
|
360
|
+
updateChannel,
|
|
361
|
+
deleteChannel,
|
|
362
|
+
getEnabledChannels,
|
|
363
|
+
saveChannelOrder,
|
|
364
|
+
getEffectiveApiKey,
|
|
365
|
+
getEffectiveApiKeyCandidates
|
|
366
|
+
};
|