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,459 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Gemini 渠道管理服务(多渠道架构)
|
|
8
|
+
*
|
|
9
|
+
* Gemini 配置结构:
|
|
10
|
+
* - .env: 环境变量配置 (GOOGLE_GEMINI_BASE_URL, GEMINI_API_KEY, GEMINI_MODEL)
|
|
11
|
+
* - settings.json: 认证模式和 MCP 配置
|
|
12
|
+
* - 我们的 gemini-channels.json: 完整渠道信息(用于管理)
|
|
13
|
+
*
|
|
14
|
+
* 多渠道模式:
|
|
15
|
+
* - 使用 enabled 字段标记渠道是否启用
|
|
16
|
+
* - 使用 weight 和 maxConcurrency 控制负载均衡
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
function normalizeGatewaySourceType(value, fallback = 'gemini') {
|
|
20
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
21
|
+
if (normalized === 'claude') return 'claude';
|
|
22
|
+
if (normalized === 'codex') return 'codex';
|
|
23
|
+
if (normalized === 'gemini') return 'gemini';
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 获取 Gemini 配置目录
|
|
28
|
+
function getGeminiDir() {
|
|
29
|
+
return path.join(os.homedir(), '.gemini');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 获取渠道存储文件路径
|
|
33
|
+
function getChannelsFilePath() {
|
|
34
|
+
const ccToolDir = path.join(os.homedir(), '.cc-tool');
|
|
35
|
+
if (!fs.existsSync(ccToolDir)) {
|
|
36
|
+
fs.mkdirSync(ccToolDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
return path.join(ccToolDir, 'gemini-channels.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 检查是否在代理模式
|
|
42
|
+
function isProxyConfig() {
|
|
43
|
+
const envPath = path.join(getGeminiDir(), '.env');
|
|
44
|
+
if (!fs.existsSync(envPath)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
50
|
+
// 检查 GOOGLE_GEMINI_BASE_URL 是否指向本地代理
|
|
51
|
+
const match = content.match(/GOOGLE_GEMINI_BASE_URL\s*=\s*(.+)/);
|
|
52
|
+
if (match) {
|
|
53
|
+
const baseUrl = match[1].trim();
|
|
54
|
+
return baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost');
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error('[Gemini Channels] Error checking proxy config:', err);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 读取所有渠道(从我们的存储文件)
|
|
64
|
+
function loadChannels() {
|
|
65
|
+
const filePath = getChannelsFilePath();
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(filePath)) {
|
|
68
|
+
// 尝试从 .env 初始化
|
|
69
|
+
return initializeFromEnv();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
74
|
+
const data = JSON.parse(content);
|
|
75
|
+
// 确保渠道有 enabled 字段(兼容旧数据)
|
|
76
|
+
if (data.channels) {
|
|
77
|
+
data.channels = data.channels.map(ch => {
|
|
78
|
+
return {
|
|
79
|
+
...ch,
|
|
80
|
+
enabled: ch.enabled !== false, // 默认启用
|
|
81
|
+
weight: ch.weight || 1,
|
|
82
|
+
maxConcurrency: ch.maxConcurrency || null,
|
|
83
|
+
modelRedirects: ch.modelRedirects || [],
|
|
84
|
+
speedTestModel: ch.speedTestModel || null,
|
|
85
|
+
gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType, 'gemini')
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return data;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('[Gemini Channels] Failed to parse channels file:', err);
|
|
92
|
+
return { channels: [] };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 从现有 .env 初始化渠道
|
|
97
|
+
function initializeFromEnv() {
|
|
98
|
+
const envPath = path.join(getGeminiDir(), '.env');
|
|
99
|
+
|
|
100
|
+
const defaultData = { channels: [] };
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(envPath)) {
|
|
103
|
+
saveChannels(defaultData);
|
|
104
|
+
return defaultData;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
109
|
+
const env = {};
|
|
110
|
+
|
|
111
|
+
// 解析 .env 文件
|
|
112
|
+
envContent.split('\n').forEach(line => {
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
if (!trimmed || trimmed.startsWith('#')) return;
|
|
115
|
+
|
|
116
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
117
|
+
if (match) {
|
|
118
|
+
env[match[1].trim()] = match[2].trim();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (env.GOOGLE_GEMINI_BASE_URL && env.GEMINI_API_KEY) {
|
|
123
|
+
const channel = {
|
|
124
|
+
id: crypto.randomUUID(),
|
|
125
|
+
name: 'Default',
|
|
126
|
+
baseUrl: env.GOOGLE_GEMINI_BASE_URL,
|
|
127
|
+
apiKey: env.GEMINI_API_KEY,
|
|
128
|
+
model: env.GEMINI_MODEL || 'gemini-2.5-pro',
|
|
129
|
+
enabled: true,
|
|
130
|
+
weight: 1,
|
|
131
|
+
maxConcurrency: null,
|
|
132
|
+
gatewaySourceType: 'gemini',
|
|
133
|
+
createdAt: Date.now(),
|
|
134
|
+
updatedAt: Date.now()
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const data = {
|
|
138
|
+
channels: [channel]
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
saveChannels(data);
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
saveChannels(defaultData);
|
|
146
|
+
return defaultData;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error('[Gemini Channels] Failed to initialize from .env:', err);
|
|
149
|
+
saveChannels(defaultData);
|
|
150
|
+
return defaultData;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 保存渠道数据
|
|
155
|
+
function saveChannels(data) {
|
|
156
|
+
const filePath = getChannelsFilePath();
|
|
157
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 获取所有渠道
|
|
161
|
+
function getChannels() {
|
|
162
|
+
const data = loadChannels();
|
|
163
|
+
return {
|
|
164
|
+
channels: data.channels || []
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 添加渠道
|
|
169
|
+
function createChannel(name, baseUrl, apiKey, model = 'gemini-2.5-pro', extraConfig = {}) {
|
|
170
|
+
const data = loadChannels();
|
|
171
|
+
|
|
172
|
+
// 检查名称是否已存在
|
|
173
|
+
const existing = data.channels.find(c => c.name === name);
|
|
174
|
+
if (existing) {
|
|
175
|
+
throw new Error(`Channel name "${name}" already exists`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const newChannel = {
|
|
179
|
+
id: crypto.randomUUID(),
|
|
180
|
+
name,
|
|
181
|
+
baseUrl,
|
|
182
|
+
apiKey,
|
|
183
|
+
model,
|
|
184
|
+
websiteUrl: extraConfig.websiteUrl || '',
|
|
185
|
+
enabled: extraConfig.enabled !== false, // 默认启用
|
|
186
|
+
weight: extraConfig.weight || 1,
|
|
187
|
+
maxConcurrency: extraConfig.maxConcurrency || null,
|
|
188
|
+
modelRedirects: extraConfig.modelRedirects || [],
|
|
189
|
+
speedTestModel: extraConfig.speedTestModel || null,
|
|
190
|
+
gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'gemini'),
|
|
191
|
+
createdAt: Date.now(),
|
|
192
|
+
updatedAt: Date.now()
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
data.channels.push(newChannel);
|
|
196
|
+
saveChannels(data);
|
|
197
|
+
|
|
198
|
+
// 写入 Gemini 配置文件
|
|
199
|
+
writeGeminiConfigForMultiChannel(data.channels);
|
|
200
|
+
|
|
201
|
+
return newChannel;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 更新渠道
|
|
205
|
+
function updateChannel(channelId, updates) {
|
|
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
|
+
const oldChannel = data.channels[index];
|
|
214
|
+
|
|
215
|
+
// 检查名称冲突
|
|
216
|
+
if (updates.name && updates.name !== oldChannel.name) {
|
|
217
|
+
const existing = data.channels.find(c => c.name === updates.name && c.id !== channelId);
|
|
218
|
+
if (existing) {
|
|
219
|
+
throw new Error(`Channel name "${updates.name}" already exists`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const merged = { ...oldChannel, ...updates };
|
|
224
|
+
const nextChannel = {
|
|
225
|
+
...merged,
|
|
226
|
+
id: channelId, // 保持 ID 不变
|
|
227
|
+
createdAt: oldChannel.createdAt, // 保持创建时间
|
|
228
|
+
modelRedirects: updates.modelRedirects !== undefined ? updates.modelRedirects : (oldChannel.modelRedirects || []),
|
|
229
|
+
speedTestModel: updates.speedTestModel !== undefined ? updates.speedTestModel : (oldChannel.speedTestModel || null),
|
|
230
|
+
gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'gemini'),
|
|
231
|
+
updatedAt: Date.now()
|
|
232
|
+
};
|
|
233
|
+
data.channels[index] = nextChannel;
|
|
234
|
+
|
|
235
|
+
// Get proxy status
|
|
236
|
+
const { getGeminiProxyStatus } = require('../gemini-proxy-server');
|
|
237
|
+
const proxyStatus = getGeminiProxyStatus();
|
|
238
|
+
const isProxyRunning = proxyStatus.running;
|
|
239
|
+
|
|
240
|
+
// Single-channel enforcement when proxy is OFF: enabling a channel disables all others
|
|
241
|
+
if (!isProxyRunning && nextChannel.enabled && !oldChannel.enabled) {
|
|
242
|
+
data.channels.forEach((ch, i) => {
|
|
243
|
+
if (i !== index && ch.enabled) {
|
|
244
|
+
ch.enabled = false;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
console.log(`[Gemini Single-channel mode] Enabled "${nextChannel.name}", disabled all others`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Prevent disabling last enabled channel when proxy is OFF
|
|
251
|
+
if (!isProxyRunning && !nextChannel.enabled && oldChannel.enabled) {
|
|
252
|
+
const enabledCount = data.channels.filter(ch => ch.enabled).length;
|
|
253
|
+
if (enabledCount === 0) {
|
|
254
|
+
throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
saveChannels(data);
|
|
259
|
+
|
|
260
|
+
// Sync .env when proxy is OFF and the channel is (or just became) enabled
|
|
261
|
+
if (!isProxyRunning && nextChannel.enabled) {
|
|
262
|
+
console.log(`[Gemini Settings-sync] Proxy is OFF and channel "${nextChannel.name}" is enabled, syncing .env...`);
|
|
263
|
+
applyChannelToSettings(channelId, data.channels);
|
|
264
|
+
} else {
|
|
265
|
+
// 更新 Gemini 配置文件 (full rewrite for non-active-channel changes)
|
|
266
|
+
writeGeminiConfigForMultiChannel(data.channels);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return data.channels[index];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 将指定渠道应用到 Gemini 配置文件
|
|
274
|
+
*
|
|
275
|
+
* @param {string} channelId - 渠道 ID
|
|
276
|
+
* @param {Array} channels - 渠道列表(可选,避免重复读取)
|
|
277
|
+
* @returns {Object} 应用的渠道
|
|
278
|
+
*/
|
|
279
|
+
function applyChannelToSettings(channelId, channels = null) {
|
|
280
|
+
const data = channels ? { channels } : loadChannels();
|
|
281
|
+
const channel = data.channels.find(c => c.id === channelId);
|
|
282
|
+
|
|
283
|
+
if (!channel) {
|
|
284
|
+
throw new Error('Channel not found');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// In single-channel mode, only this channel should be enabled
|
|
288
|
+
data.channels.forEach(ch => {
|
|
289
|
+
ch.enabled = ch.id === channelId;
|
|
290
|
+
});
|
|
291
|
+
// Only persist when we loaded from disk (not when called with in-memory channels from updateChannel)
|
|
292
|
+
if (!channels) {
|
|
293
|
+
saveChannels(data);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const geminiDir = getGeminiDir();
|
|
297
|
+
|
|
298
|
+
if (!fs.existsSync(geminiDir)) {
|
|
299
|
+
fs.mkdirSync(geminiDir, { recursive: true });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const envPath = path.join(geminiDir, '.env');
|
|
303
|
+
|
|
304
|
+
// 构建 .env 内容
|
|
305
|
+
const effectiveApiKey = getEffectiveApiKey(channel) || '';
|
|
306
|
+
const envContent = `GOOGLE_GEMINI_BASE_URL=${channel.baseUrl}
|
|
307
|
+
GEMINI_API_KEY=${effectiveApiKey}
|
|
308
|
+
GEMINI_MODEL=${channel.model}
|
|
309
|
+
`;
|
|
310
|
+
|
|
311
|
+
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
312
|
+
|
|
313
|
+
// 设置 .env 文件权限为 600 (仅所有者可读写)
|
|
314
|
+
if (process.platform !== 'win32') {
|
|
315
|
+
fs.chmodSync(envPath, 0o600);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 确保 settings.json 存在并配置正确的认证模式
|
|
319
|
+
const settingsPath = path.join(geminiDir, 'settings.json');
|
|
320
|
+
let settings = {};
|
|
321
|
+
|
|
322
|
+
if (fs.existsSync(settingsPath)) {
|
|
323
|
+
try {
|
|
324
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
325
|
+
} catch (err) {
|
|
326
|
+
console.warn('[Gemini Channels] Failed to read settings.json, creating new');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 设置认证模式为 gemini-api-key(第三方 API)
|
|
331
|
+
settings.security = settings.security || {};
|
|
332
|
+
settings.security.auth = settings.security.auth || {};
|
|
333
|
+
settings.security.auth.selectedType = 'gemini-api-key';
|
|
334
|
+
|
|
335
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
336
|
+
|
|
337
|
+
console.log(`[Gemini Channels] Applied channel ${channel.name} to .env`);
|
|
338
|
+
return channel;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 删除渠道
|
|
342
|
+
async function deleteChannel(channelId) {
|
|
343
|
+
const data = loadChannels();
|
|
344
|
+
|
|
345
|
+
const index = data.channels.findIndex(c => c.id === channelId);
|
|
346
|
+
if (index === -1) {
|
|
347
|
+
throw new Error('Channel not found');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
data.channels.splice(index, 1);
|
|
351
|
+
saveChannels(data);
|
|
352
|
+
|
|
353
|
+
// 更新 Gemini 配置文件
|
|
354
|
+
writeGeminiConfigForMultiChannel(data.channels);
|
|
355
|
+
|
|
356
|
+
return { success: true };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 写入 Gemini 配置文件 (.env) - 多渠道模式
|
|
360
|
+
function writeGeminiConfigForMultiChannel(allChannels) {
|
|
361
|
+
const geminiDir = getGeminiDir();
|
|
362
|
+
|
|
363
|
+
if (!fs.existsSync(geminiDir)) {
|
|
364
|
+
fs.mkdirSync(geminiDir, { recursive: true });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const envPath = path.join(geminiDir, '.env');
|
|
368
|
+
|
|
369
|
+
// 获取第一个启用的渠道作为默认配置
|
|
370
|
+
const enabledChannels = allChannels.filter(c => c.enabled !== false);
|
|
371
|
+
const defaultChannel = enabledChannels[0] || allChannels[0];
|
|
372
|
+
|
|
373
|
+
if (!defaultChannel) {
|
|
374
|
+
// 没有渠道,写入空配置
|
|
375
|
+
const envContent = `# Gemini Configuration\n# No channels configured\n`;
|
|
376
|
+
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 构建 .env 内容
|
|
381
|
+
const effectiveApiKey = getEffectiveApiKey(defaultChannel) || '';
|
|
382
|
+
const envContent = `GOOGLE_GEMINI_BASE_URL=${defaultChannel.baseUrl}
|
|
383
|
+
GEMINI_API_KEY=${effectiveApiKey}
|
|
384
|
+
GEMINI_MODEL=${defaultChannel.model}
|
|
385
|
+
`;
|
|
386
|
+
|
|
387
|
+
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
388
|
+
|
|
389
|
+
// 设置 .env 文件权限为 600 (仅所有者可读写)
|
|
390
|
+
if (process.platform !== 'win32') {
|
|
391
|
+
fs.chmodSync(envPath, 0o600);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 确保 settings.json 存在并配置正确的认证模式
|
|
395
|
+
const settingsPath = path.join(geminiDir, 'settings.json');
|
|
396
|
+
let settings = {};
|
|
397
|
+
|
|
398
|
+
if (fs.existsSync(settingsPath)) {
|
|
399
|
+
try {
|
|
400
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.warn('[Gemini Channels] Failed to read settings.json, creating new');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 设置认证模式为 gemini-api-key(第三方 API)
|
|
407
|
+
settings.security = settings.security || {};
|
|
408
|
+
settings.security.auth = settings.security.auth || {};
|
|
409
|
+
settings.security.auth.selectedType = 'gemini-api-key';
|
|
410
|
+
|
|
411
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 获取所有启用的渠道(供调度器使用)
|
|
415
|
+
function getEnabledChannels() {
|
|
416
|
+
const data = loadChannels();
|
|
417
|
+
return data.channels.filter(c => c.enabled !== false);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function getEffectiveApiKey(channel) {
|
|
421
|
+
return channel.apiKey || null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 保存渠道顺序
|
|
425
|
+
function saveChannelOrder(order) {
|
|
426
|
+
const data = loadChannels();
|
|
427
|
+
|
|
428
|
+
// 按照给定的顺序重新排列
|
|
429
|
+
const orderedChannels = [];
|
|
430
|
+
for (const id of order) {
|
|
431
|
+
const channel = data.channels.find(c => c.id === id);
|
|
432
|
+
if (channel) {
|
|
433
|
+
orderedChannels.push(channel);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 添加不在顺序中的渠道(新添加的)
|
|
438
|
+
for (const channel of data.channels) {
|
|
439
|
+
if (!orderedChannels.find(c => c.id === channel.id)) {
|
|
440
|
+
orderedChannels.push(channel);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
data.channels = orderedChannels;
|
|
445
|
+
saveChannels(data);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports = {
|
|
449
|
+
getChannels,
|
|
450
|
+
createChannel,
|
|
451
|
+
updateChannel,
|
|
452
|
+
deleteChannel,
|
|
453
|
+
getEnabledChannels,
|
|
454
|
+
getEffectiveApiKey,
|
|
455
|
+
saveChannelOrder,
|
|
456
|
+
isProxyConfig,
|
|
457
|
+
getGeminiDir,
|
|
458
|
+
applyChannelToSettings
|
|
459
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 获取 Gemini 配置目录
|
|
7
|
+
*/
|
|
8
|
+
function getGeminiDir() {
|
|
9
|
+
return path.join(os.homedir(), '.gemini');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 读取 .env 文件
|
|
14
|
+
*/
|
|
15
|
+
function loadEnv() {
|
|
16
|
+
const envPath = path.join(getGeminiDir(), '.env');
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(envPath)) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
24
|
+
const env = {};
|
|
25
|
+
|
|
26
|
+
content.split('\n').forEach(line => {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed || trimmed.startsWith('#')) return;
|
|
29
|
+
|
|
30
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
31
|
+
if (match) {
|
|
32
|
+
env[match[1].trim()] = match[2].trim();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return env;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('[Gemini] Failed to parse .env:', err);
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 读取 settings.json
|
|
45
|
+
*/
|
|
46
|
+
function loadSettings() {
|
|
47
|
+
const settingsPath = path.join(getGeminiDir(), 'settings.json');
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(settingsPath)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('[Gemini] Failed to parse settings.json:', err);
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 检查 Gemini CLI 是否已安装
|
|
63
|
+
*/
|
|
64
|
+
function isGeminiInstalled() {
|
|
65
|
+
return fs.existsSync(getGeminiDir());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
getGeminiDir,
|
|
70
|
+
loadEnv,
|
|
71
|
+
loadSettings,
|
|
72
|
+
isGeminiInstalled
|
|
73
|
+
};
|