@adversity/coding-tool-x 2.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 +333 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
- package/dist/web/assets/index-aL3cKxSK.css +41 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.png +0 -0
- package/docs/CHANGELOG.md +582 -0
- package/docs/DIRECTORY_MIGRATION.md +112 -0
- package/docs/PROJECT_STRUCTURE.md +396 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +73 -0
- package/src/commands/channels.js +504 -0
- package/src/commands/cli-type.js +99 -0
- package/src/commands/daemon.js +286 -0
- package/src/commands/doctor.js +332 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +259 -0
- package/src/commands/port-config.js +115 -0
- package/src/commands/proxy-control.js +258 -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/stats.js +224 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +222 -0
- package/src/commands/ui.js +92 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +40 -0
- package/src/config/loader.js +75 -0
- package/src/config/paths.js +121 -0
- package/src/index.js +373 -0
- package/src/reset-config.js +92 -0
- package/src/server/api/agents.js +248 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +258 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +312 -0
- package/src/server/api/codex-projects.js +91 -0
- package/src/server/api/codex-proxy.js +182 -0
- package/src/server/api/codex-sessions.js +491 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +245 -0
- package/src/server/api/config-templates.js +182 -0
- package/src/server/api/config.js +147 -0
- package/src/server/api/convert.js +127 -0
- package/src/server/api/dashboard.js +125 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +261 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +160 -0
- package/src/server/api/gemini-sessions.js +397 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +118 -0
- package/src/server/api/mcp.js +336 -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 +235 -0
- package/src/server/api/rules.js +271 -0
- package/src/server/api/sessions.js +595 -0
- package/src/server/api/settings.js +61 -0
- package/src/server/api/skills.js +305 -0
- package/src/server/api/statistics.js +91 -0
- package/src/server/api/terminal.js +202 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +407 -0
- package/src/server/codex-proxy-server.js +538 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +518 -0
- package/src/server/index.js +305 -0
- package/src/server/proxy-server.js +469 -0
- package/src/server/services/agents-service.js +354 -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 +234 -0
- package/src/server/services/channels.js +347 -0
- package/src/server/services/codex-channels.js +625 -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 +665 -0
- package/src/server/services/codex-settings-manager.js +397 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +255 -0
- package/src/server/services/commands-service.js +360 -0
- package/src/server/services/config-templates-service.js +732 -0
- package/src/server/services/env-checker.js +307 -0
- package/src/server/services/env-manager.js +300 -0
- package/src/server/services/favorites.js +163 -0
- package/src/server/services/gemini-channels.js +333 -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 +253 -0
- package/src/server/services/health-check.js +399 -0
- package/src/server/services/mcp-service.js +1188 -0
- package/src/server/services/prompts-service.js +492 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/pty-manager.js +435 -0
- package/src/server/services/rules-service.js +401 -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 +757 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +965 -0
- package/src/server/services/speed-test.js +545 -0
- package/src/server/services/statistics-service.js +386 -0
- package/src/server/services/terminal-commands.js +155 -0
- package/src/server/services/terminal-config.js +140 -0
- package/src/server/services/terminal-detector.js +306 -0
- package/src/server/services/ui-config.js +130 -0
- package/src/server/services/workspace-service.js +662 -0
- package/src/server/utils/pricing.js +41 -0
- package/src/server/websocket-server.js +557 -0
- package/src/ui/menu.js +129 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +94 -0
- package/src/utils/session.js +239 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const FAVORITES_DIR = path.join(os.homedir(), '.claude', 'cc-tool');
|
|
6
|
+
const FAVORITES_FILE = path.join(FAVORITES_DIR, 'favorites.json');
|
|
7
|
+
|
|
8
|
+
// 内存缓存
|
|
9
|
+
let favoritesCache = null;
|
|
10
|
+
let cacheInitialized = false;
|
|
11
|
+
|
|
12
|
+
const DEFAULT_FAVORITES = {
|
|
13
|
+
claude: [],
|
|
14
|
+
codex: [],
|
|
15
|
+
gemini: []
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Ensure favorites directory exists
|
|
19
|
+
function ensureFavoritesDir() {
|
|
20
|
+
if (!fs.existsSync(FAVORITES_DIR)) {
|
|
21
|
+
fs.mkdirSync(FAVORITES_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 从文件读取并缓存
|
|
26
|
+
function readFavoritesFromFile() {
|
|
27
|
+
ensureFavoritesDir();
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(FAVORITES_FILE)) {
|
|
30
|
+
return { ...DEFAULT_FAVORITES };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(FAVORITES_FILE, 'utf8');
|
|
35
|
+
const data = JSON.parse(content);
|
|
36
|
+
return {
|
|
37
|
+
claude: data.claude || [],
|
|
38
|
+
codex: data.codex || [],
|
|
39
|
+
gemini: data.gemini || []
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Error reading favorites file:', error);
|
|
43
|
+
return { ...DEFAULT_FAVORITES };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 初始化缓存(延迟初始化)
|
|
48
|
+
function initializeCache() {
|
|
49
|
+
if (cacheInitialized) return;
|
|
50
|
+
favoritesCache = readFavoritesFromFile();
|
|
51
|
+
cacheInitialized = true;
|
|
52
|
+
|
|
53
|
+
// 监听文件变化,更新缓存
|
|
54
|
+
try {
|
|
55
|
+
fs.watchFile(FAVORITES_FILE, { persistent: false }, () => {
|
|
56
|
+
favoritesCache = readFavoritesFromFile();
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error('Failed to watch favorites file:', err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Load all favorites(使用缓存)
|
|
64
|
+
function loadFavorites() {
|
|
65
|
+
if (!cacheInitialized) {
|
|
66
|
+
initializeCache();
|
|
67
|
+
}
|
|
68
|
+
return JSON.parse(JSON.stringify(favoritesCache)); // 深拷贝返回
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Save favorites(同时更新缓存)
|
|
72
|
+
function saveFavorites(favorites) {
|
|
73
|
+
ensureFavoritesDir();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
fs.writeFileSync(FAVORITES_FILE, JSON.stringify(favorites, null, 2), 'utf8');
|
|
77
|
+
// 同时更新缓存
|
|
78
|
+
favoritesCache = JSON.parse(JSON.stringify(favorites));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Error saving favorites:', error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Add a favorite
|
|
86
|
+
function addFavorite(channel, sessionData) {
|
|
87
|
+
const favorites = loadFavorites();
|
|
88
|
+
|
|
89
|
+
if (!favorites[channel]) {
|
|
90
|
+
favorites[channel] = [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if already exists
|
|
94
|
+
const exists = favorites[channel].some(
|
|
95
|
+
fav => fav.sessionId === sessionData.sessionId && fav.projectName === sessionData.projectName
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!exists) {
|
|
99
|
+
favorites[channel].push({
|
|
100
|
+
...sessionData,
|
|
101
|
+
addedAt: Date.now()
|
|
102
|
+
});
|
|
103
|
+
saveFavorites(favorites);
|
|
104
|
+
return { success: true, favorites };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { success: false, message: 'Already exists', favorites };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Remove a favorite
|
|
111
|
+
function removeFavorite(channel, projectName, sessionId) {
|
|
112
|
+
const favorites = loadFavorites();
|
|
113
|
+
|
|
114
|
+
if (!favorites[channel]) {
|
|
115
|
+
return { success: false, message: 'Channel not found', favorites };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const index = favorites[channel].findIndex(
|
|
119
|
+
fav => fav.sessionId === sessionId && fav.projectName === projectName
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (index > -1) {
|
|
123
|
+
favorites[channel].splice(index, 1);
|
|
124
|
+
saveFavorites(favorites);
|
|
125
|
+
return { success: true, favorites };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { success: false, message: 'Not found', favorites };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check if a session is favorited
|
|
132
|
+
function isFavorite(channel, projectName, sessionId) {
|
|
133
|
+
const favorites = loadFavorites();
|
|
134
|
+
|
|
135
|
+
if (!favorites[channel]) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return favorites[channel].some(
|
|
140
|
+
fav => fav.sessionId === sessionId && fav.projectName === projectName
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get favorites for a specific channel
|
|
145
|
+
function getFavorites(channel) {
|
|
146
|
+
const favorites = loadFavorites();
|
|
147
|
+
return favorites[channel] || [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Get all favorites
|
|
151
|
+
function getAllFavorites() {
|
|
152
|
+
return loadFavorites();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
loadFavorites,
|
|
157
|
+
saveFavorites,
|
|
158
|
+
addFavorite,
|
|
159
|
+
removeFavorite,
|
|
160
|
+
isFavorite,
|
|
161
|
+
getFavorites,
|
|
162
|
+
getAllFavorites
|
|
163
|
+
};
|
|
@@ -0,0 +1,333 @@
|
|
|
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
|
+
// 获取 Gemini 配置目录
|
|
20
|
+
function getGeminiDir() {
|
|
21
|
+
return path.join(os.homedir(), '.gemini');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 获取渠道存储文件路径
|
|
25
|
+
function getChannelsFilePath() {
|
|
26
|
+
const ccToolDir = path.join(os.homedir(), '.claude', 'cc-tool');
|
|
27
|
+
if (!fs.existsSync(ccToolDir)) {
|
|
28
|
+
fs.mkdirSync(ccToolDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
return path.join(ccToolDir, 'gemini-channels.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 检查是否在代理模式
|
|
34
|
+
function isProxyConfig() {
|
|
35
|
+
const envPath = path.join(getGeminiDir(), '.env');
|
|
36
|
+
if (!fs.existsSync(envPath)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
42
|
+
// 检查 GOOGLE_GEMINI_BASE_URL 是否指向本地代理
|
|
43
|
+
const match = content.match(/GOOGLE_GEMINI_BASE_URL\s*=\s*(.+)/);
|
|
44
|
+
if (match) {
|
|
45
|
+
const baseUrl = match[1].trim();
|
|
46
|
+
return baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost');
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('[Gemini Channels] Error checking proxy config:', err);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 读取所有渠道(从我们的存储文件)
|
|
56
|
+
function loadChannels() {
|
|
57
|
+
const filePath = getChannelsFilePath();
|
|
58
|
+
|
|
59
|
+
if (!fs.existsSync(filePath)) {
|
|
60
|
+
// 尝试从 .env 初始化
|
|
61
|
+
return initializeFromEnv();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
66
|
+
const data = JSON.parse(content);
|
|
67
|
+
// 确保渠道有 enabled 字段(兼容旧数据)
|
|
68
|
+
if (data.channels) {
|
|
69
|
+
data.channels = data.channels.map(ch => ({
|
|
70
|
+
...ch,
|
|
71
|
+
enabled: ch.enabled !== false, // 默认启用
|
|
72
|
+
weight: ch.weight || 1,
|
|
73
|
+
maxConcurrency: ch.maxConcurrency || null
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
return data;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('[Gemini Channels] Failed to parse channels file:', err);
|
|
79
|
+
return { channels: [] };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 从现有 .env 初始化渠道
|
|
84
|
+
function initializeFromEnv() {
|
|
85
|
+
const envPath = path.join(getGeminiDir(), '.env');
|
|
86
|
+
|
|
87
|
+
const defaultData = { channels: [] };
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(envPath)) {
|
|
90
|
+
saveChannels(defaultData);
|
|
91
|
+
return defaultData;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
96
|
+
const env = {};
|
|
97
|
+
|
|
98
|
+
// 解析 .env 文件
|
|
99
|
+
envContent.split('\n').forEach(line => {
|
|
100
|
+
const trimmed = line.trim();
|
|
101
|
+
if (!trimmed || trimmed.startsWith('#')) return;
|
|
102
|
+
|
|
103
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
104
|
+
if (match) {
|
|
105
|
+
env[match[1].trim()] = match[2].trim();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (env.GOOGLE_GEMINI_BASE_URL && env.GEMINI_API_KEY) {
|
|
110
|
+
const channel = {
|
|
111
|
+
id: crypto.randomUUID(),
|
|
112
|
+
name: 'Default',
|
|
113
|
+
baseUrl: env.GOOGLE_GEMINI_BASE_URL,
|
|
114
|
+
apiKey: env.GEMINI_API_KEY,
|
|
115
|
+
model: env.GEMINI_MODEL || 'gemini-2.5-pro',
|
|
116
|
+
enabled: true,
|
|
117
|
+
weight: 1,
|
|
118
|
+
maxConcurrency: null,
|
|
119
|
+
createdAt: Date.now(),
|
|
120
|
+
updatedAt: Date.now()
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const data = {
|
|
124
|
+
channels: [channel]
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
saveChannels(data);
|
|
128
|
+
return data;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
saveChannels(defaultData);
|
|
132
|
+
return defaultData;
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error('[Gemini Channels] Failed to initialize from .env:', err);
|
|
135
|
+
saveChannels(defaultData);
|
|
136
|
+
return defaultData;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 保存渠道数据
|
|
141
|
+
function saveChannels(data) {
|
|
142
|
+
const filePath = getChannelsFilePath();
|
|
143
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 获取所有渠道
|
|
147
|
+
function getChannels() {
|
|
148
|
+
const data = loadChannels();
|
|
149
|
+
return {
|
|
150
|
+
channels: data.channels || []
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 添加渠道
|
|
155
|
+
function createChannel(name, baseUrl, apiKey, model = 'gemini-2.5-pro', extraConfig = {}) {
|
|
156
|
+
const data = loadChannels();
|
|
157
|
+
|
|
158
|
+
// 检查名称是否已存在
|
|
159
|
+
const existing = data.channels.find(c => c.name === name);
|
|
160
|
+
if (existing) {
|
|
161
|
+
throw new Error(`Channel name "${name}" already exists`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const newChannel = {
|
|
165
|
+
id: crypto.randomUUID(),
|
|
166
|
+
name,
|
|
167
|
+
baseUrl,
|
|
168
|
+
apiKey,
|
|
169
|
+
model,
|
|
170
|
+
websiteUrl: extraConfig.websiteUrl || '',
|
|
171
|
+
enabled: extraConfig.enabled !== false, // 默认启用
|
|
172
|
+
weight: extraConfig.weight || 1,
|
|
173
|
+
maxConcurrency: extraConfig.maxConcurrency || null,
|
|
174
|
+
createdAt: Date.now(),
|
|
175
|
+
updatedAt: Date.now()
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
data.channels.push(newChannel);
|
|
179
|
+
saveChannels(data);
|
|
180
|
+
|
|
181
|
+
// 写入 Gemini 配置文件
|
|
182
|
+
writeGeminiConfigForMultiChannel(data.channels);
|
|
183
|
+
|
|
184
|
+
return newChannel;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 更新渠道
|
|
188
|
+
function updateChannel(channelId, updates) {
|
|
189
|
+
const data = loadChannels();
|
|
190
|
+
const index = data.channels.findIndex(c => c.id === channelId);
|
|
191
|
+
|
|
192
|
+
if (index === -1) {
|
|
193
|
+
throw new Error('Channel not found');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const channel = data.channels[index];
|
|
197
|
+
|
|
198
|
+
// 检查名称冲突
|
|
199
|
+
if (updates.name && updates.name !== channel.name) {
|
|
200
|
+
const existing = data.channels.find(c => c.name === updates.name && c.id !== channelId);
|
|
201
|
+
if (existing) {
|
|
202
|
+
throw new Error(`Channel name "${updates.name}" already exists`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
data.channels[index] = {
|
|
207
|
+
...channel,
|
|
208
|
+
...updates,
|
|
209
|
+
id: channelId, // 保持 ID 不变
|
|
210
|
+
createdAt: channel.createdAt, // 保持创建时间
|
|
211
|
+
updatedAt: Date.now()
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
saveChannels(data);
|
|
215
|
+
|
|
216
|
+
// 更新 Gemini 配置文件
|
|
217
|
+
writeGeminiConfigForMultiChannel(data.channels);
|
|
218
|
+
|
|
219
|
+
return data.channels[index];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 删除渠道
|
|
223
|
+
function deleteChannel(channelId) {
|
|
224
|
+
const data = loadChannels();
|
|
225
|
+
|
|
226
|
+
const index = data.channels.findIndex(c => c.id === channelId);
|
|
227
|
+
if (index === -1) {
|
|
228
|
+
throw new Error('Channel not found');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
data.channels.splice(index, 1);
|
|
232
|
+
saveChannels(data);
|
|
233
|
+
|
|
234
|
+
// 更新 Gemini 配置文件
|
|
235
|
+
writeGeminiConfigForMultiChannel(data.channels);
|
|
236
|
+
|
|
237
|
+
return { success: true };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 写入 Gemini 配置文件 (.env) - 多渠道模式
|
|
241
|
+
function writeGeminiConfigForMultiChannel(allChannels) {
|
|
242
|
+
const geminiDir = getGeminiDir();
|
|
243
|
+
|
|
244
|
+
if (!fs.existsSync(geminiDir)) {
|
|
245
|
+
fs.mkdirSync(geminiDir, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const envPath = path.join(geminiDir, '.env');
|
|
249
|
+
|
|
250
|
+
// 获取第一个启用的渠道作为默认配置
|
|
251
|
+
const enabledChannels = allChannels.filter(c => c.enabled !== false);
|
|
252
|
+
const defaultChannel = enabledChannels[0] || allChannels[0];
|
|
253
|
+
|
|
254
|
+
if (!defaultChannel) {
|
|
255
|
+
// 没有渠道,写入空配置
|
|
256
|
+
const envContent = `# Gemini Configuration\n# No channels configured\n`;
|
|
257
|
+
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 构建 .env 内容
|
|
262
|
+
const envContent = `GOOGLE_GEMINI_BASE_URL=${defaultChannel.baseUrl}
|
|
263
|
+
GEMINI_API_KEY=${defaultChannel.apiKey}
|
|
264
|
+
GEMINI_MODEL=${defaultChannel.model}
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
268
|
+
|
|
269
|
+
// 设置 .env 文件权限为 600 (仅所有者可读写)
|
|
270
|
+
if (process.platform !== 'win32') {
|
|
271
|
+
fs.chmodSync(envPath, 0o600);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 确保 settings.json 存在并配置正确的认证模式
|
|
275
|
+
const settingsPath = path.join(geminiDir, 'settings.json');
|
|
276
|
+
let settings = {};
|
|
277
|
+
|
|
278
|
+
if (fs.existsSync(settingsPath)) {
|
|
279
|
+
try {
|
|
280
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.warn('[Gemini Channels] Failed to read settings.json, creating new');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 设置认证模式为 gemini-api-key(第三方 API)
|
|
287
|
+
settings.security = settings.security || {};
|
|
288
|
+
settings.security.auth = settings.security.auth || {};
|
|
289
|
+
settings.security.auth.selectedType = 'gemini-api-key';
|
|
290
|
+
|
|
291
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 获取所有启用的渠道(供调度器使用)
|
|
295
|
+
function getEnabledChannels() {
|
|
296
|
+
const data = loadChannels();
|
|
297
|
+
return data.channels.filter(c => c.enabled !== false);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 保存渠道顺序
|
|
301
|
+
function saveChannelOrder(order) {
|
|
302
|
+
const data = loadChannels();
|
|
303
|
+
|
|
304
|
+
// 按照给定的顺序重新排列
|
|
305
|
+
const orderedChannels = [];
|
|
306
|
+
for (const id of order) {
|
|
307
|
+
const channel = data.channels.find(c => c.id === id);
|
|
308
|
+
if (channel) {
|
|
309
|
+
orderedChannels.push(channel);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 添加不在顺序中的渠道(新添加的)
|
|
314
|
+
for (const channel of data.channels) {
|
|
315
|
+
if (!orderedChannels.find(c => c.id === channel.id)) {
|
|
316
|
+
orderedChannels.push(channel);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
data.channels = orderedChannels;
|
|
321
|
+
saveChannels(data);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = {
|
|
325
|
+
getChannels,
|
|
326
|
+
createChannel,
|
|
327
|
+
updateChannel,
|
|
328
|
+
deleteChannel,
|
|
329
|
+
getEnabledChannels,
|
|
330
|
+
saveChannelOrder,
|
|
331
|
+
isProxyConfig,
|
|
332
|
+
getGeminiDir
|
|
333
|
+
};
|
|
@@ -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
|
+
};
|