@adversity/coding-tool-x 3.1.0 → 3.1.1
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 +15 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
- package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
- package/dist/web/assets/Home-Di2qsylF.css +1 -0
- package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
- package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
- package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-Ufv5rCa5.css +1 -0
- package/dist/web/assets/index-lAkrRC3h.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 +39 -2
- package/src/config/loader.js +74 -8
- 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 +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +198 -0
- package/src/server/api/opencode-sessions.js +403 -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/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +30 -18
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +15 -3
- package/src/server/index.js +165 -58
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +27 -18
- 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-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 +26 -12
- package/src/server/services/env-manager.js +126 -18
- 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 +206 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +663 -0
- package/src/server/services/opencode-settings-manager.js +342 -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/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 +132 -3
- 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,342 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
6
|
+
const CONFIG_PATHS = {
|
|
7
|
+
config: path.join(CONFIG_DIR, 'config.json'),
|
|
8
|
+
opencode: path.join(CONFIG_DIR, 'opencode.json'),
|
|
9
|
+
opencodec: path.join(CONFIG_DIR, 'opencode.jsonc')
|
|
10
|
+
};
|
|
11
|
+
const BACKUP_SUFFIX = '.cc-tool-backup';
|
|
12
|
+
const EMPTY_SENTINEL = '__CC_TOOL_NO_FILE__';
|
|
13
|
+
const PROXY_PROVIDER_ID = 'ctx-proxy';
|
|
14
|
+
const LEGACY_PROVIDER_ID = 'openai';
|
|
15
|
+
const PROXY_API_KEY = 'PROXY_KEY';
|
|
16
|
+
|
|
17
|
+
function ensureConfigDir() {
|
|
18
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
19
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getBackupPath(filePath) {
|
|
24
|
+
return `${filePath}${BACKUP_SUFFIX}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function selectConfigPath() {
|
|
28
|
+
if (fs.existsSync(CONFIG_PATHS.opencodec)) return CONFIG_PATHS.opencodec;
|
|
29
|
+
if (fs.existsSync(CONFIG_PATHS.opencode)) return CONFIG_PATHS.opencode;
|
|
30
|
+
if (fs.existsSync(CONFIG_PATHS.config)) return CONFIG_PATHS.config;
|
|
31
|
+
return CONFIG_PATHS.opencode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stripJsonComments(input) {
|
|
35
|
+
let result = '';
|
|
36
|
+
let inString = false;
|
|
37
|
+
let stringChar = '';
|
|
38
|
+
let i = 0;
|
|
39
|
+
|
|
40
|
+
while (i < input.length) {
|
|
41
|
+
const ch = input[i];
|
|
42
|
+
const next = input[i + 1];
|
|
43
|
+
|
|
44
|
+
if (inString) {
|
|
45
|
+
result += ch;
|
|
46
|
+
if (ch === '\\') {
|
|
47
|
+
if (next) {
|
|
48
|
+
result += next;
|
|
49
|
+
i += 2;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
} else if (ch === stringChar) {
|
|
53
|
+
inString = false;
|
|
54
|
+
}
|
|
55
|
+
i += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (ch === '"' || ch === '\'') {
|
|
60
|
+
inString = true;
|
|
61
|
+
stringChar = ch;
|
|
62
|
+
result += ch;
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (ch === '/' && next === '/') {
|
|
68
|
+
i += 2;
|
|
69
|
+
while (i < input.length && input[i] !== '\n') i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (ch === '/' && next === '*') {
|
|
74
|
+
i += 2;
|
|
75
|
+
while (i < input.length - 1 && !(input[i] === '*' && input[i + 1] === '/')) i += 1;
|
|
76
|
+
i += 2;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
result += ch;
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readConfig(filePath) {
|
|
88
|
+
if (!fs.existsSync(filePath)) return {};
|
|
89
|
+
|
|
90
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
91
|
+
if (!raw.trim()) return {};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
if (filePath.endsWith('.jsonc')) {
|
|
95
|
+
return JSON.parse(stripJsonComments(raw));
|
|
96
|
+
}
|
|
97
|
+
return JSON.parse(raw);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
throw new Error(`Failed to parse ${path.basename(filePath)}: ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeConfig(filePath, config) {
|
|
104
|
+
ensureConfigDir();
|
|
105
|
+
const content = JSON.stringify(config, null, 2);
|
|
106
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeOpenCodeModel(modelId) {
|
|
110
|
+
const normalized = String(modelId || '').trim();
|
|
111
|
+
if (!normalized) {
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// OpenCode 要求格式为 provider/model。这里统一绑定到 ctx-proxy provider,
|
|
116
|
+
// 避免落到内置 openai provider 的模型清单。
|
|
117
|
+
if (normalized.startsWith(`${PROXY_PROVIDER_ID}/`)) {
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
return `${PROXY_PROVIDER_ID}/${normalized}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isLocalProxyBaseUrl(url) {
|
|
124
|
+
const value = String(url || '').trim();
|
|
125
|
+
return value.includes('127.0.0.1') || value.includes('localhost');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isLegacyProxyProvider(provider) {
|
|
129
|
+
if (!provider || typeof provider !== 'object') return false;
|
|
130
|
+
const apiKey = provider?.options?.apiKey;
|
|
131
|
+
const baseUrl = provider?.options?.baseURL;
|
|
132
|
+
return apiKey === PROXY_API_KEY && isLocalProxyBaseUrl(baseUrl);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isManagedProxyProvider(provider) {
|
|
136
|
+
if (!provider || typeof provider !== 'object') return false;
|
|
137
|
+
const apiKey = provider?.options?.apiKey;
|
|
138
|
+
const baseUrl = provider?.options?.baseURL;
|
|
139
|
+
return apiKey === PROXY_API_KEY && isLocalProxyBaseUrl(baseUrl);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isManagedProxyConfig(config) {
|
|
143
|
+
if (!config || typeof config !== 'object') return false;
|
|
144
|
+
return isManagedProxyProvider(config?.provider?.[PROXY_PROVIDER_ID])
|
|
145
|
+
|| isLegacyProxyProvider(config?.provider?.[LEGACY_PROVIDER_ID]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildModelsMap(models = [], fallbackModel = '') {
|
|
149
|
+
const map = {};
|
|
150
|
+
const seen = new Set();
|
|
151
|
+
|
|
152
|
+
const add = (value) => {
|
|
153
|
+
if (typeof value !== 'string') return;
|
|
154
|
+
const trimmed = value.trim();
|
|
155
|
+
if (!trimmed) return;
|
|
156
|
+
const key = trimmed.toLowerCase();
|
|
157
|
+
if (seen.has(key)) return;
|
|
158
|
+
seen.add(key);
|
|
159
|
+
map[trimmed] = { name: trimmed };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(models)) {
|
|
163
|
+
models.forEach(add);
|
|
164
|
+
}
|
|
165
|
+
add(fallbackModel);
|
|
166
|
+
|
|
167
|
+
return map;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveProxyBaseUrl(config) {
|
|
171
|
+
return config?.provider?.[PROXY_PROVIDER_ID]?.options?.baseURL
|
|
172
|
+
|| config?.provider?.[LEGACY_PROVIDER_ID]?.options?.baseURL
|
|
173
|
+
|| '';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function backupConfig(filePath) {
|
|
177
|
+
ensureConfigDir();
|
|
178
|
+
const backupPath = getBackupPath(filePath);
|
|
179
|
+
|
|
180
|
+
if (fs.existsSync(backupPath)) {
|
|
181
|
+
// 防止历史残留备份误伤:若当前配置已回到“非代理托管态”,刷新备份为当前真实配置。
|
|
182
|
+
// 这样 stop/restore 不会把用户配置恢复成陈旧快照(或空文件哨兵)。
|
|
183
|
+
try {
|
|
184
|
+
const backupContent = fs.readFileSync(backupPath, 'utf8');
|
|
185
|
+
if (backupContent === EMPTY_SENTINEL && fs.existsSync(filePath)) {
|
|
186
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
187
|
+
fs.writeFileSync(backupPath, content, 'utf8');
|
|
188
|
+
return { success: true, alreadyExists: true };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const current = readConfig(filePath);
|
|
192
|
+
if (!isManagedProxyConfig(current)) {
|
|
193
|
+
if (fs.existsSync(filePath)) {
|
|
194
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
195
|
+
fs.writeFileSync(backupPath, content, 'utf8');
|
|
196
|
+
} else {
|
|
197
|
+
fs.writeFileSync(backupPath, EMPTY_SENTINEL, 'utf8');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
// ignore backup refresh errors, fallback to existing backup
|
|
202
|
+
}
|
|
203
|
+
return { success: true, alreadyExists: true };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (fs.existsSync(filePath)) {
|
|
207
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
208
|
+
fs.writeFileSync(backupPath, content, 'utf8');
|
|
209
|
+
} else {
|
|
210
|
+
fs.writeFileSync(backupPath, EMPTY_SENTINEL, 'utf8');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { success: true, alreadyExists: false };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function restoreConfig(filePath) {
|
|
217
|
+
const backupPath = getBackupPath(filePath);
|
|
218
|
+
if (!fs.existsSync(backupPath)) return false;
|
|
219
|
+
|
|
220
|
+
const content = fs.readFileSync(backupPath, 'utf8');
|
|
221
|
+
if (content === EMPTY_SENTINEL) {
|
|
222
|
+
if (fs.existsSync(filePath)) {
|
|
223
|
+
fs.unlinkSync(filePath);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
ensureConfigDir();
|
|
227
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fs.unlinkSync(backupPath);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function configExists() {
|
|
235
|
+
return fs.existsSync(CONFIG_PATHS.opencodec)
|
|
236
|
+
|| fs.existsSync(CONFIG_PATHS.opencode)
|
|
237
|
+
|| fs.existsSync(CONFIG_PATHS.config);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function hasBackup() {
|
|
241
|
+
return fs.existsSync(getBackupPath(CONFIG_PATHS.opencodec))
|
|
242
|
+
|| fs.existsSync(getBackupPath(CONFIG_PATHS.opencode))
|
|
243
|
+
|| fs.existsSync(getBackupPath(CONFIG_PATHS.config));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function setProxyConfig(proxyPort, options = {}) {
|
|
247
|
+
const filePath = selectConfigPath();
|
|
248
|
+
backupConfig(filePath);
|
|
249
|
+
|
|
250
|
+
const config = readConfig(filePath);
|
|
251
|
+
const next = (config && typeof config === 'object') ? config : {};
|
|
252
|
+
|
|
253
|
+
if (!next.provider || typeof next.provider !== 'object') {
|
|
254
|
+
next.provider = {};
|
|
255
|
+
}
|
|
256
|
+
// 清理历史 openai 代理注入,避免 /models 出现与代理无关的 openai 模型列表。
|
|
257
|
+
if (isLegacyProxyProvider(next.provider[LEGACY_PROVIDER_ID])) {
|
|
258
|
+
delete next.provider[LEGACY_PROVIDER_ID];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (Object.prototype.hasOwnProperty.call(next.provider[LEGACY_PROVIDER_ID] || {}, 'model')) {
|
|
262
|
+
delete next.provider[LEGACY_PROVIDER_ID].model;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const modelsMap = buildModelsMap(options.models, options.model);
|
|
266
|
+
const modelIds = Object.keys(modelsMap);
|
|
267
|
+
|
|
268
|
+
if (modelIds.length > 0) {
|
|
269
|
+
next.provider[PROXY_PROVIDER_ID] = {
|
|
270
|
+
npm: '@ai-sdk/openai-compatible',
|
|
271
|
+
name: 'CTX Proxy',
|
|
272
|
+
options: {
|
|
273
|
+
baseURL: `http://127.0.0.1:${proxyPort}/v1`,
|
|
274
|
+
apiKey: PROXY_API_KEY
|
|
275
|
+
},
|
|
276
|
+
models: modelsMap
|
|
277
|
+
};
|
|
278
|
+
} else {
|
|
279
|
+
// 无模型时不暴露 provider,避免出现误导性的 provider.openai/provider 列表。
|
|
280
|
+
delete next.provider[PROXY_PROVIDER_ID];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 写入顶层 model(OpenCode 要求 provider/model 格式),无显式模型时兜底第一个模型。
|
|
284
|
+
const fallbackModel = options.model || modelIds[0] || '';
|
|
285
|
+
if (fallbackModel) {
|
|
286
|
+
const resolvedModel = normalizeOpenCodeModel(fallbackModel);
|
|
287
|
+
if (resolvedModel) {
|
|
288
|
+
next.model = resolvedModel;
|
|
289
|
+
}
|
|
290
|
+
} else if (String(next.model || '').startsWith(`${PROXY_PROVIDER_ID}/`) || String(next.model || '').startsWith(`${LEGACY_PROVIDER_ID}/`)) {
|
|
291
|
+
delete next.model;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
writeConfig(filePath, next);
|
|
295
|
+
|
|
296
|
+
return { success: true, port: proxyPort, path: filePath };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function restoreSettings() {
|
|
300
|
+
const restored = [
|
|
301
|
+
restoreConfig(CONFIG_PATHS.opencodec),
|
|
302
|
+
restoreConfig(CONFIG_PATHS.opencode),
|
|
303
|
+
restoreConfig(CONFIG_PATHS.config)
|
|
304
|
+
].some(Boolean);
|
|
305
|
+
|
|
306
|
+
return { success: restored };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isProxyConfig() {
|
|
310
|
+
try {
|
|
311
|
+
const filePath = selectConfigPath();
|
|
312
|
+
if (!fs.existsSync(filePath)) return false;
|
|
313
|
+
const config = readConfig(filePath);
|
|
314
|
+
const baseUrl = resolveProxyBaseUrl(config);
|
|
315
|
+
return isLocalProxyBaseUrl(baseUrl);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getCurrentProxyPort() {
|
|
322
|
+
try {
|
|
323
|
+
if (!isProxyConfig()) return null;
|
|
324
|
+
const filePath = selectConfigPath();
|
|
325
|
+
const config = readConfig(filePath);
|
|
326
|
+
const baseUrl = resolveProxyBaseUrl(config);
|
|
327
|
+
const match = baseUrl.match(/:(\d+)/);
|
|
328
|
+
return match ? parseInt(match[1], 10) : null;
|
|
329
|
+
} catch (err) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
configExists,
|
|
336
|
+
hasBackup,
|
|
337
|
+
setProxyConfig,
|
|
338
|
+
restoreSettings,
|
|
339
|
+
isProxyConfig,
|
|
340
|
+
getCurrentProxyPort,
|
|
341
|
+
CONFIG_PATHS
|
|
342
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OpenCode 统计服务 - 数据采集和存储
|
|
7
|
+
*
|
|
8
|
+
* 文件结构:
|
|
9
|
+
* ~/.cc-tool/
|
|
10
|
+
* ├── opencode-statistics.json # OpenCode 总体统计
|
|
11
|
+
* └── opencode-daily-stats/
|
|
12
|
+
* ├── 2025-12-05.json # 每日汇总统计
|
|
13
|
+
* └── ...
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// 获取基础目录
|
|
17
|
+
function getBaseDir() {
|
|
18
|
+
const dir = path.join(os.homedir(), '.cc-tool');
|
|
19
|
+
if (!fs.existsSync(dir)) {
|
|
20
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 获取每日统计目录
|
|
26
|
+
function getDailyStatsDir() {
|
|
27
|
+
const dir = path.join(getBaseDir(), 'opencode-daily-stats');
|
|
28
|
+
if (!fs.existsSync(dir)) {
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 获取统计文件路径
|
|
35
|
+
function getStatisticsFilePath() {
|
|
36
|
+
return path.join(getBaseDir(), 'opencode-statistics.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 获取每日统计文件路径
|
|
40
|
+
function getDailyStatsFilePath(date) {
|
|
41
|
+
return path.join(getDailyStatsDir(), `${date}.json`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 初始化统计对象
|
|
45
|
+
function initStatsObject() {
|
|
46
|
+
return {
|
|
47
|
+
requests: 0,
|
|
48
|
+
tokens: {
|
|
49
|
+
input: 0,
|
|
50
|
+
output: 0,
|
|
51
|
+
reasoning: 0,
|
|
52
|
+
cached: 0,
|
|
53
|
+
total: 0
|
|
54
|
+
},
|
|
55
|
+
cost: 0
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 加载总体统计
|
|
60
|
+
function loadStatistics() {
|
|
61
|
+
const filePath = getStatisticsFilePath();
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(filePath)) {
|
|
64
|
+
const data = fs.readFileSync(filePath, 'utf8');
|
|
65
|
+
return JSON.parse(data);
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error('[OpenCode Statistics] Failed to load statistics:', err);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
version: '1.0',
|
|
73
|
+
lastUpdated: new Date().toISOString(),
|
|
74
|
+
global: {
|
|
75
|
+
totalRequests: 0,
|
|
76
|
+
totalTokens: 0,
|
|
77
|
+
totalCost: 0
|
|
78
|
+
},
|
|
79
|
+
byChannel: {},
|
|
80
|
+
byModel: {}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 保存总体统计
|
|
85
|
+
function saveStatistics(stats) {
|
|
86
|
+
const filePath = getStatisticsFilePath();
|
|
87
|
+
stats.lastUpdated = new Date().toISOString();
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
fs.writeFileSync(filePath, JSON.stringify(stats, null, 2), 'utf8');
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error('[OpenCode Statistics] Failed to save statistics:', err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 加载每日统计
|
|
97
|
+
function loadDailyStats(date) {
|
|
98
|
+
const filePath = getDailyStatsFilePath(date);
|
|
99
|
+
try {
|
|
100
|
+
if (fs.existsSync(filePath)) {
|
|
101
|
+
const data = fs.readFileSync(filePath, 'utf8');
|
|
102
|
+
return JSON.parse(data);
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error('[OpenCode Statistics] Failed to load daily stats:', err);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
date: date,
|
|
110
|
+
summary: {
|
|
111
|
+
requests: 0,
|
|
112
|
+
tokens: 0,
|
|
113
|
+
cost: 0
|
|
114
|
+
},
|
|
115
|
+
byChannel: {},
|
|
116
|
+
byModel: {}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 保存每日统计
|
|
121
|
+
function saveDailyStats(date, stats) {
|
|
122
|
+
const filePath = getDailyStatsFilePath(date);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
fs.writeFileSync(filePath, JSON.stringify(stats, null, 2), 'utf8');
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error('[OpenCode Statistics] Failed to save daily stats:', err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 更新统计数据
|
|
132
|
+
function updateStats(stats, tokens, cost) {
|
|
133
|
+
stats.requests += 1;
|
|
134
|
+
if (stats.tokens) {
|
|
135
|
+
stats.tokens.input += tokens.input || 0;
|
|
136
|
+
stats.tokens.output += tokens.output || 0;
|
|
137
|
+
stats.tokens.reasoning += tokens.reasoning || 0;
|
|
138
|
+
stats.tokens.cached += tokens.cached || 0;
|
|
139
|
+
stats.tokens.total += tokens.total || 0;
|
|
140
|
+
}
|
|
141
|
+
stats.cost += cost || 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 记录一次请求
|
|
146
|
+
* @param {Object} requestData - 请求数据
|
|
147
|
+
*/
|
|
148
|
+
function recordRequest(requestData) {
|
|
149
|
+
try {
|
|
150
|
+
const {
|
|
151
|
+
timestamp = new Date().toISOString(),
|
|
152
|
+
channel,
|
|
153
|
+
channelId,
|
|
154
|
+
model,
|
|
155
|
+
tokens = {},
|
|
156
|
+
cost = 0
|
|
157
|
+
} = requestData;
|
|
158
|
+
|
|
159
|
+
// 计算 total tokens
|
|
160
|
+
const totalTokens = (tokens.input || 0) + (tokens.output || 0) + (tokens.reasoning || 0);
|
|
161
|
+
tokens.total = totalTokens;
|
|
162
|
+
|
|
163
|
+
// 1. 更新总体统计
|
|
164
|
+
const globalStats = loadStatistics();
|
|
165
|
+
|
|
166
|
+
globalStats.global.totalRequests += 1;
|
|
167
|
+
globalStats.global.totalTokens += totalTokens;
|
|
168
|
+
globalStats.global.totalCost += cost || 0;
|
|
169
|
+
|
|
170
|
+
// 按渠道统计
|
|
171
|
+
if (channelId) {
|
|
172
|
+
if (!globalStats.byChannel[channelId]) {
|
|
173
|
+
globalStats.byChannel[channelId] = {
|
|
174
|
+
name: channel || channelId,
|
|
175
|
+
...initStatsObject(),
|
|
176
|
+
firstUsed: timestamp,
|
|
177
|
+
lastUsed: timestamp
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
updateStats(globalStats.byChannel[channelId], tokens, cost);
|
|
181
|
+
globalStats.byChannel[channelId].lastUsed = timestamp;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 按模型统计
|
|
185
|
+
if (model) {
|
|
186
|
+
if (!globalStats.byModel[model]) {
|
|
187
|
+
globalStats.byModel[model] = initStatsObject();
|
|
188
|
+
}
|
|
189
|
+
updateStats(globalStats.byModel[model], tokens, cost);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
saveStatistics(globalStats);
|
|
193
|
+
|
|
194
|
+
// 2. 更新每日统计
|
|
195
|
+
const date = new Date(timestamp).toISOString().split('T')[0];
|
|
196
|
+
const dailyStats = loadDailyStats(date);
|
|
197
|
+
|
|
198
|
+
dailyStats.summary.requests += 1;
|
|
199
|
+
dailyStats.summary.tokens += totalTokens;
|
|
200
|
+
dailyStats.summary.cost += cost || 0;
|
|
201
|
+
|
|
202
|
+
// 每日 - 按渠道统计
|
|
203
|
+
if (channelId) {
|
|
204
|
+
if (!dailyStats.byChannel[channelId]) {
|
|
205
|
+
dailyStats.byChannel[channelId] = {
|
|
206
|
+
name: channel || channelId,
|
|
207
|
+
...initStatsObject()
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
updateStats(dailyStats.byChannel[channelId], tokens, cost);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 每日 - 按模型统计
|
|
214
|
+
if (model) {
|
|
215
|
+
if (!dailyStats.byModel[model]) {
|
|
216
|
+
dailyStats.byModel[model] = initStatsObject();
|
|
217
|
+
}
|
|
218
|
+
updateStats(dailyStats.byModel[model], tokens, cost);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
saveDailyStats(date, dailyStats);
|
|
222
|
+
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error('[OpenCode Statistics] Failed to record request:', err);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 获取总体统计
|
|
230
|
+
*/
|
|
231
|
+
function getStatistics() {
|
|
232
|
+
return loadStatistics();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 获取每日统计
|
|
237
|
+
*/
|
|
238
|
+
function getDailyStatistics(date) {
|
|
239
|
+
return loadDailyStats(date);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 获取今日统计
|
|
244
|
+
*/
|
|
245
|
+
function getTodayStatistics() {
|
|
246
|
+
const today = new Date().toISOString().split('T')[0];
|
|
247
|
+
return loadDailyStats(today);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
recordRequest,
|
|
252
|
+
getStatistics,
|
|
253
|
+
getDailyStatistics,
|
|
254
|
+
getTodayStatistics
|
|
255
|
+
};
|