coding-tool-x 3.3.8 → 3.4.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 +17 -2
- package/README.md +253 -326
- package/dist/web/assets/{Analytics-DLpoDZ2M.js → Analytics-DEjfL5Jx.js} +4 -4
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-D_hRb55W.js → ConfigTemplates-DkRL_-tf.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/Home-CF-L640I.js +1 -0
- package/dist/web/assets/{PluginManager-JXsyym1s.js → PluginManager-BzNYTdNB.js} +1 -1
- package/dist/web/assets/{ProjectList-DZWSeb-q.js → ProjectList-C0-JgHMM.js} +1 -1
- package/dist/web/assets/{SessionList-Cs624DR3.js → SessionList-CkZUdX5N.js} +1 -1
- package/dist/web/assets/{SkillManager-bEliz7qz.js → SkillManager-Cak0-4d4.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-J3RecFGn.js → WorkspaceManager-CGDJzwEr.js} +1 -1
- package/dist/web/assets/{icons-Cuc23WS7.js → icons-B5Pl4lrD.js} +1 -1
- package/dist/web/assets/index-D_WItvHE.js +2 -0
- package/dist/web/assets/index-Dz7v9OM0.css +1 -0
- package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
- package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
- package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
- package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
- package/dist/web/index.html +7 -7
- package/docs/home.png +0 -0
- package/package.json +13 -5
- package/src/commands/daemon.js +3 -2
- package/src/commands/security.js +1 -2
- package/src/config/paths.js +638 -93
- package/src/server/api/agents.js +1 -1
- package/src/server/api/claude-hooks.js +13 -8
- package/src/server/api/codex-proxy.js +5 -4
- package/src/server/api/hooks.js +45 -0
- package/src/server/api/plugins.js +0 -1
- package/src/server/api/statistics.js +4 -4
- package/src/server/api/ui-config.js +5 -0
- package/src/server/api/workspaces.js +1 -3
- package/src/server/codex-proxy-server.js +89 -59
- package/src/server/gemini-proxy-server.js +107 -88
- package/src/server/index.js +1 -0
- package/src/server/opencode-proxy-server.js +381 -225
- package/src/server/proxy-server.js +86 -60
- package/src/server/services/alias.js +3 -3
- package/src/server/services/channels.js +3 -2
- package/src/server/services/codex-channels.js +38 -87
- package/src/server/services/codex-env-manager.js +426 -0
- package/src/server/services/codex-settings-manager.js +15 -15
- package/src/server/services/codex-statistics-service.js +3 -27
- package/src/server/services/config-export-service.js +20 -7
- package/src/server/services/config-registry-service.js +3 -2
- package/src/server/services/config-sync-manager.js +1 -1
- package/src/server/services/favorites.js +4 -3
- package/src/server/services/gemini-channels.js +3 -3
- package/src/server/services/gemini-statistics-service.js +3 -25
- package/src/server/services/mcp-service.js +2 -3
- package/src/server/services/model-detector.js +4 -3
- package/src/server/services/native-oauth-adapters.js +2 -1
- package/src/server/services/network-access.js +39 -1
- package/src/server/services/notification-hooks.js +951 -0
- package/src/server/services/opencode-channels.js +6 -6
- package/src/server/services/opencode-sessions.js +2 -2
- package/src/server/services/opencode-statistics-service.js +3 -27
- package/src/server/services/plugins-service.js +110 -31
- package/src/server/services/prompts-service.js +2 -3
- package/src/server/services/proxy-log-helper.js +242 -0
- package/src/server/services/proxy-runtime.js +6 -4
- package/src/server/services/repo-scanner-base.js +12 -4
- package/src/server/services/request-logger.js +7 -7
- package/src/server/services/security-config.js +4 -4
- package/src/server/services/session-cache.js +2 -2
- package/src/server/services/sessions.js +2 -2
- package/src/server/services/skill-service.js +174 -55
- package/src/server/services/statistics-service.js +10 -6
- package/src/server/services/ui-config.js +4 -3
- package/src/server/services/workspace-service.js +101 -156
- package/src/server/websocket-server.js +5 -4
- package/dist/web/assets/Analytics-DuYvId7u.css +0 -1
- package/dist/web/assets/Home-BMoFdAwy.css +0 -1
- package/dist/web/assets/Home-DNwp-0J-.js +0 -1
- package/dist/web/assets/index-BXeSvAwU.js +0 -2
- package/dist/web/assets/index-DWAC3Tdv.css +0 -1
- package/docs/bannel.png +0 -0
- package/docs/model-redirection.md +0 -251
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const { PATHS, HOME_DIR } = require('../../config/paths');
|
|
5
|
+
|
|
6
|
+
const PROFILE_MARKER_START = '# >>> coding-tool codex env >>>';
|
|
7
|
+
const PROFILE_MARKER_END = '# <<< coding-tool codex env <<<';
|
|
8
|
+
|
|
9
|
+
function defaultEnvFilePath(configDir) {
|
|
10
|
+
return path.join(configDir, 'codex-env.sh');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function defaultStateFilePath(configDir) {
|
|
14
|
+
return path.join(configDir, 'codex-env-state.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureDir(dirPath) {
|
|
18
|
+
if (!fs.existsSync(dirPath)) {
|
|
19
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasValue(value) {
|
|
24
|
+
return String(value || '').trim() !== '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shellQuote(value) {
|
|
28
|
+
return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function powershellQuote(value) {
|
|
32
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildHomeRelativeShellPath(filePath, homeDir) {
|
|
36
|
+
const normalizedHome = path.resolve(homeDir);
|
|
37
|
+
const normalizedFilePath = path.resolve(filePath);
|
|
38
|
+
if (normalizedFilePath === normalizedHome) {
|
|
39
|
+
return '$HOME';
|
|
40
|
+
}
|
|
41
|
+
if (normalizedFilePath.startsWith(`${normalizedHome}${path.sep}`)) {
|
|
42
|
+
const relativePath = normalizedFilePath.slice(normalizedHome.length).replace(/\\/g, '/');
|
|
43
|
+
return `$HOME${relativePath}`;
|
|
44
|
+
}
|
|
45
|
+
return normalizedFilePath.replace(/\\/g, '/');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildSourceSnippet(envFilePath, homeDir) {
|
|
49
|
+
const shellPath = buildHomeRelativeShellPath(envFilePath, homeDir);
|
|
50
|
+
return [
|
|
51
|
+
PROFILE_MARKER_START,
|
|
52
|
+
`[ -f "${shellPath}" ] && . "${shellPath}"`,
|
|
53
|
+
PROFILE_MARKER_END
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stripManagedBlock(content = '') {
|
|
58
|
+
if (!content.includes(PROFILE_MARKER_START)) {
|
|
59
|
+
return content;
|
|
60
|
+
}
|
|
61
|
+
const pattern = new RegExp(
|
|
62
|
+
`\\n?${escapeRegex(PROFILE_MARKER_START)}[\\s\\S]*?${escapeRegex(PROFILE_MARKER_END)}\\n?`,
|
|
63
|
+
'g'
|
|
64
|
+
);
|
|
65
|
+
return content.replace(pattern, '\n').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function escapeRegex(value) {
|
|
69
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function upsertManagedBlock(content, snippet) {
|
|
73
|
+
const stripped = stripManagedBlock(content);
|
|
74
|
+
if (!stripped.trim()) {
|
|
75
|
+
return `${snippet}\n`;
|
|
76
|
+
}
|
|
77
|
+
return `${stripped.trimEnd()}\n\n${snippet}\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readJsonFile(filePath, fallbackValue) {
|
|
81
|
+
try {
|
|
82
|
+
if (!fs.existsSync(filePath)) {
|
|
83
|
+
return fallbackValue;
|
|
84
|
+
}
|
|
85
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
86
|
+
} catch {
|
|
87
|
+
return fallbackValue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeJsonFile(filePath, payload) {
|
|
92
|
+
ensureDir(path.dirname(filePath));
|
|
93
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readState(stateFilePath) {
|
|
97
|
+
const state = readJsonFile(stateFilePath, {
|
|
98
|
+
version: 1,
|
|
99
|
+
values: {},
|
|
100
|
+
profiles: []
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
version: 1,
|
|
104
|
+
values: state && typeof state.values === 'object' ? state.values : {},
|
|
105
|
+
profiles: Array.isArray(state?.profiles) ? state.profiles : []
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeEnvMap(envMap = {}) {
|
|
110
|
+
const normalized = {};
|
|
111
|
+
for (const [key, value] of Object.entries(envMap || {})) {
|
|
112
|
+
if (!key || !hasValue(value)) continue;
|
|
113
|
+
normalized[String(key).trim()] = String(value);
|
|
114
|
+
}
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildNextEnvValues(previousValues, envMap, { replace = true, removeKeys = [] } = {}) {
|
|
119
|
+
const nextValues = replace
|
|
120
|
+
? {}
|
|
121
|
+
: { ...previousValues };
|
|
122
|
+
|
|
123
|
+
if (replace) {
|
|
124
|
+
Object.assign(nextValues, normalizeEnvMap(envMap));
|
|
125
|
+
} else {
|
|
126
|
+
const normalizedIncoming = normalizeEnvMap(envMap);
|
|
127
|
+
for (const [key, value] of Object.entries(normalizedIncoming)) {
|
|
128
|
+
nextValues[key] = value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const key of removeKeys || []) {
|
|
133
|
+
delete nextValues[key];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const [key, value] of Object.entries(nextValues)) {
|
|
137
|
+
if (!hasValue(value)) {
|
|
138
|
+
delete nextValues[key];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return nextValues;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getPosixProfileCandidates(homeDir, shellEnv = process.env) {
|
|
146
|
+
const candidates = [
|
|
147
|
+
'.bashrc',
|
|
148
|
+
'.bash_profile',
|
|
149
|
+
'.bash_login',
|
|
150
|
+
'.zshrc',
|
|
151
|
+
'.zshenv',
|
|
152
|
+
'.zprofile',
|
|
153
|
+
'.zlogin',
|
|
154
|
+
'.profile'
|
|
155
|
+
].map(fileName => path.join(homeDir, fileName));
|
|
156
|
+
|
|
157
|
+
const shell = String(shellEnv.SHELL || '').toLowerCase();
|
|
158
|
+
const preferred = shell.includes('zsh')
|
|
159
|
+
? path.join(homeDir, '.zshrc')
|
|
160
|
+
: shell.includes('bash')
|
|
161
|
+
? path.join(homeDir, '.bashrc')
|
|
162
|
+
: path.join(homeDir, '.profile');
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
preferred,
|
|
166
|
+
candidates
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function writeTextFileIfChanged(filePath, content) {
|
|
171
|
+
const previous = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
|
|
172
|
+
if (previous === content) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
ensureDir(path.dirname(filePath));
|
|
176
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function syncPosixEnvironment(nextValues, previousState, options) {
|
|
181
|
+
const {
|
|
182
|
+
runtime,
|
|
183
|
+
homeDir,
|
|
184
|
+
stateFilePath,
|
|
185
|
+
shellEnv,
|
|
186
|
+
execSync
|
|
187
|
+
} = options;
|
|
188
|
+
const nextKeys = Object.keys(nextValues).sort();
|
|
189
|
+
let changed = false;
|
|
190
|
+
|
|
191
|
+
// 清理旧版本遗留的 shell profile 注入(迁移兼容)
|
|
192
|
+
const previousProfiles = Array.isArray(previousState.profiles) ? previousState.profiles : [];
|
|
193
|
+
if (previousProfiles.length > 0) {
|
|
194
|
+
const { candidates } = getPosixProfileCandidates(homeDir, shellEnv);
|
|
195
|
+
const cleanupTargets = new Set([
|
|
196
|
+
...previousProfiles,
|
|
197
|
+
...candidates.filter(filePath => fs.existsSync(filePath))
|
|
198
|
+
]);
|
|
199
|
+
for (const profilePath of cleanupTargets) {
|
|
200
|
+
if (!fs.existsSync(profilePath)) continue;
|
|
201
|
+
const currentContent = fs.readFileSync(profilePath, 'utf8');
|
|
202
|
+
if (!currentContent.includes(PROFILE_MARKER_START)) continue;
|
|
203
|
+
const nextContent = stripManagedBlock(currentContent);
|
|
204
|
+
const finalContent = nextContent ? `${nextContent}\n` : '';
|
|
205
|
+
changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// macOS:用 launchctl 写入全局环境变量,新开终端/进程即生效
|
|
210
|
+
if (runtime === 'darwin') {
|
|
211
|
+
applyLaunchctlEnvironment(previousState.values || {}, nextValues, execSync);
|
|
212
|
+
const prevValues = previousState.values || {};
|
|
213
|
+
const keysChanged =
|
|
214
|
+
Object.keys(nextValues).length !== Object.keys(prevValues).length ||
|
|
215
|
+
Object.entries(nextValues).some(([k, v]) => prevValues[k] !== v);
|
|
216
|
+
changed = changed || keysChanged;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Linux:写 ~/.config/environment.d/(桌面/systemd)+ ~/.profile(SSH/登录shell)
|
|
220
|
+
if (runtime === 'linux') {
|
|
221
|
+
changed = applyLinuxEnvironment(nextValues, homeDir) || changed;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (nextKeys.length > 0) {
|
|
225
|
+
writeJsonFile(stateFilePath, {
|
|
226
|
+
version: 1,
|
|
227
|
+
values: nextValues,
|
|
228
|
+
profiles: []
|
|
229
|
+
});
|
|
230
|
+
} else if (fs.existsSync(stateFilePath)) {
|
|
231
|
+
fs.unlinkSync(stateFilePath);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
changed,
|
|
236
|
+
reloadRequired: changed,
|
|
237
|
+
isFirstTime: Object.keys(previousState.values || {}).length === 0 && nextKeys.length > 0,
|
|
238
|
+
sourceCommand: null,
|
|
239
|
+
shellConfigPath: null,
|
|
240
|
+
shellConfigPaths: [],
|
|
241
|
+
envFilePath: null,
|
|
242
|
+
managedKeys: nextKeys
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function applyLinuxEnvironment(nextValues, homeDir) {
|
|
247
|
+
let changed = false;
|
|
248
|
+
|
|
249
|
+
// 1. ~/.config/environment.d/codex-env.conf(systemd 用户环境,桌面终端生效)
|
|
250
|
+
const envdDir = path.join(homeDir, '.config', 'environment.d');
|
|
251
|
+
const envdFile = path.join(envdDir, 'codex-env.conf');
|
|
252
|
+
const nextKeys = Object.keys(nextValues).sort();
|
|
253
|
+
|
|
254
|
+
if (nextKeys.length > 0) {
|
|
255
|
+
const envdContent = [
|
|
256
|
+
'# Managed by Coding-Tool',
|
|
257
|
+
...nextKeys.map(key => `${key}=${nextValues[key]}`),
|
|
258
|
+
''
|
|
259
|
+
].join('\n');
|
|
260
|
+
changed = writeTextFileIfChanged(envdFile, envdContent) || changed;
|
|
261
|
+
} else if (fs.existsSync(envdFile)) {
|
|
262
|
+
fs.unlinkSync(envdFile);
|
|
263
|
+
changed = true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 2. ~/.profile(登录 shell,SSH 和新终端均生效)
|
|
267
|
+
const profilePath = path.join(homeDir, '.profile');
|
|
268
|
+
if (nextKeys.length > 0) {
|
|
269
|
+
const exportLines = nextKeys.map(key => `export ${key}=${shellQuote(nextValues[key])}`).join('\n');
|
|
270
|
+
const snippet = [PROFILE_MARKER_START, exportLines, PROFILE_MARKER_END].join('\n');
|
|
271
|
+
const currentContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
|
|
272
|
+
const nextContent = upsertManagedBlock(currentContent, snippet);
|
|
273
|
+
changed = writeTextFileIfChanged(profilePath, nextContent) || changed;
|
|
274
|
+
} else if (fs.existsSync(profilePath)) {
|
|
275
|
+
const currentContent = fs.readFileSync(profilePath, 'utf8');
|
|
276
|
+
if (currentContent.includes(PROFILE_MARKER_START)) {
|
|
277
|
+
const nextContent = stripManagedBlock(currentContent);
|
|
278
|
+
const finalContent = nextContent ? `${nextContent}\n` : '';
|
|
279
|
+
changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return changed;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function applyLaunchctlEnvironment(previousValues, nextValues, execSync) {
|
|
287
|
+
const previousKeys = new Set(Object.keys(previousValues || {}));
|
|
288
|
+
for (const [key, value] of Object.entries(nextValues || {})) {
|
|
289
|
+
if (previousValues[key] === value) {
|
|
290
|
+
previousKeys.delete(key);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
runLaunchctlCommand(['setenv', key, value], execSync);
|
|
294
|
+
previousKeys.delete(key);
|
|
295
|
+
}
|
|
296
|
+
for (const key of previousKeys) {
|
|
297
|
+
runLaunchctlCommand(['unsetenv', key], execSync);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function runLaunchctlCommand(args, execSync) {
|
|
302
|
+
try {
|
|
303
|
+
execSync('launchctl', args, {
|
|
304
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
305
|
+
timeout: 3000
|
|
306
|
+
});
|
|
307
|
+
} catch {
|
|
308
|
+
// ignore launchctl failures; shell profile remains the durable source
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function syncWindowsEnvironment(nextValues, previousState, options) {
|
|
313
|
+
const { stateFilePath, execSync } = options;
|
|
314
|
+
const nextKeys = Object.keys(nextValues).sort();
|
|
315
|
+
const previousValues = previousState.values || {};
|
|
316
|
+
let changed = false;
|
|
317
|
+
|
|
318
|
+
for (const [key, value] of Object.entries(nextValues)) {
|
|
319
|
+
if (previousValues[key] === value) continue;
|
|
320
|
+
setWindowsUserEnv(key, value, execSync);
|
|
321
|
+
changed = true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const key of Object.keys(previousValues)) {
|
|
325
|
+
if (Object.prototype.hasOwnProperty.call(nextValues, key)) continue;
|
|
326
|
+
removeWindowsUserEnv(key, execSync);
|
|
327
|
+
changed = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (nextKeys.length > 0) {
|
|
331
|
+
writeJsonFile(stateFilePath, {
|
|
332
|
+
version: 1,
|
|
333
|
+
values: nextValues,
|
|
334
|
+
profiles: []
|
|
335
|
+
});
|
|
336
|
+
} else if (fs.existsSync(stateFilePath)) {
|
|
337
|
+
fs.unlinkSync(stateFilePath);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
changed,
|
|
342
|
+
reloadRequired: changed,
|
|
343
|
+
isFirstTime: Object.keys(previousValues).length === 0 && nextKeys.length > 0,
|
|
344
|
+
sourceCommand: null,
|
|
345
|
+
shellConfigPath: null,
|
|
346
|
+
shellConfigPaths: [],
|
|
347
|
+
envFilePath: null,
|
|
348
|
+
managedKeys: nextKeys
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function runWindowsEnvCommand(script, execSync) {
|
|
353
|
+
const candidates = ['powershell', 'pwsh'];
|
|
354
|
+
let lastError = null;
|
|
355
|
+
for (const command of candidates) {
|
|
356
|
+
try {
|
|
357
|
+
execSync(command, ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
358
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
359
|
+
timeout: 5000
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
lastError = error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
throw lastError || new Error('No PowerShell executable available');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function setWindowsUserEnv(key, value, execSync) {
|
|
370
|
+
runWindowsEnvCommand(
|
|
371
|
+
`[Environment]::SetEnvironmentVariable(${powershellQuote(key)}, ${powershellQuote(value)}, 'User')`,
|
|
372
|
+
execSync
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function removeWindowsUserEnv(key, execSync) {
|
|
377
|
+
runWindowsEnvCommand(
|
|
378
|
+
`[Environment]::SetEnvironmentVariable(${powershellQuote(key)}, $null, 'User')`,
|
|
379
|
+
execSync
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function syncCodexUserEnvironment(envMap = {}, options = {}) {
|
|
384
|
+
const configDir = options.configDir || PATHS.config;
|
|
385
|
+
const homeDir = options.homeDir || HOME_DIR;
|
|
386
|
+
const envFilePath = options.envFilePath || defaultEnvFilePath(configDir);
|
|
387
|
+
const stateFilePath = options.stateFilePath || defaultStateFilePath(configDir);
|
|
388
|
+
const runtime = options.runtime || process.platform;
|
|
389
|
+
const shellEnv = options.shellEnv || process.env;
|
|
390
|
+
const execSync = options.execFileSync || execFileSync;
|
|
391
|
+
const previousState = readState(stateFilePath);
|
|
392
|
+
const nextValues = buildNextEnvValues(previousState.values, envMap, {
|
|
393
|
+
replace: options.replace !== false,
|
|
394
|
+
removeKeys: options.removeKeys || []
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const syncOptions = {
|
|
398
|
+
runtime,
|
|
399
|
+
homeDir,
|
|
400
|
+
envFilePath,
|
|
401
|
+
stateFilePath,
|
|
402
|
+
shellEnv,
|
|
403
|
+
execSync
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
if (runtime === 'win32') {
|
|
407
|
+
return syncWindowsEnvironment(nextValues, previousState, syncOptions);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return syncPosixEnvironment(nextValues, previousState, syncOptions);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
module.exports = {
|
|
414
|
+
syncCodexUserEnvironment,
|
|
415
|
+
_test: {
|
|
416
|
+
buildHomeRelativeShellPath,
|
|
417
|
+
buildNextEnvValues,
|
|
418
|
+
buildSourceSnippet,
|
|
419
|
+
getPosixProfileCandidates,
|
|
420
|
+
readState,
|
|
421
|
+
shellQuote,
|
|
422
|
+
stripManagedBlock,
|
|
423
|
+
syncCodexUserEnvironment,
|
|
424
|
+
upsertManagedBlock
|
|
425
|
+
}
|
|
426
|
+
};
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
3
|
const toml = require('toml');
|
|
5
4
|
const tomlStringify = require('@iarna/toml').stringify;
|
|
6
|
-
const { resolvePreferredHomeDir } = require('../../utils/home-dir');
|
|
7
5
|
const { NATIVE_PATHS } = require('../../config/paths');
|
|
8
|
-
|
|
9
|
-
const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
|
|
6
|
+
const { syncCodexUserEnvironment } = require('./codex-env-manager');
|
|
10
7
|
|
|
11
8
|
// Codex 配置文件路径
|
|
12
9
|
function getConfigPath() {
|
|
@@ -143,7 +140,7 @@ function backupSettings() {
|
|
|
143
140
|
const configContent = fs.readFileSync(getConfigPath(), 'utf8');
|
|
144
141
|
fs.writeFileSync(getConfigBackupPath(), configContent, 'utf8');
|
|
145
142
|
|
|
146
|
-
// 备份 auth.json (
|
|
143
|
+
// 备份 auth.json (如果存在,主要用于 OAuth 状态回滚)
|
|
147
144
|
if (authExists()) {
|
|
148
145
|
const authContent = fs.readFileSync(getAuthPath(), 'utf8');
|
|
149
146
|
fs.writeFileSync(getAuthBackupPath(), authContent, 'utf8');
|
|
@@ -194,8 +191,10 @@ function restoreSettings() {
|
|
|
194
191
|
fs.unlinkSync(getAuthBackupPath());
|
|
195
192
|
}
|
|
196
193
|
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
syncCodexUserEnvironment({}, {
|
|
195
|
+
replace: false,
|
|
196
|
+
removeKeys: ['CC_PROXY_KEY']
|
|
197
|
+
});
|
|
199
198
|
|
|
200
199
|
console.log('Codex settings restored from backup');
|
|
201
200
|
return { success: true };
|
|
@@ -232,20 +231,21 @@ function setProxyConfig(proxyPort) {
|
|
|
232
231
|
// 写入配置
|
|
233
232
|
writeConfig(config);
|
|
234
233
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
234
|
+
const envResult = syncCodexUserEnvironment({
|
|
235
|
+
CC_PROXY_KEY: 'PROXY_KEY'
|
|
236
|
+
}, {
|
|
237
|
+
replace: false
|
|
238
|
+
});
|
|
239
239
|
|
|
240
|
-
// auth.json 已写入 CC_PROXY_KEY,Codex 优先读取 auth.json,无需注入 shell 配置文件
|
|
241
240
|
console.log(`Codex settings updated to use proxy on port ${proxyPort}`);
|
|
242
241
|
return {
|
|
243
242
|
success: true,
|
|
244
243
|
port: proxyPort,
|
|
245
244
|
envInjected: true,
|
|
246
|
-
isFirstTime:
|
|
247
|
-
shellConfigPath:
|
|
248
|
-
sourceCommand:
|
|
245
|
+
isFirstTime: envResult.isFirstTime,
|
|
246
|
+
shellConfigPath: envResult.shellConfigPath,
|
|
247
|
+
sourceCommand: envResult.sourceCommand,
|
|
248
|
+
reloadRequired: envResult.reloadRequired
|
|
249
249
|
};
|
|
250
250
|
} catch (err) {
|
|
251
251
|
throw new Error('Failed to set proxy config: ' + err.message);
|
|
@@ -4,36 +4,12 @@ const {
|
|
|
4
4
|
getDailyStatistics: getSharedDailyStatistics,
|
|
5
5
|
getTodayStatistics: getSharedTodayStatistics
|
|
6
6
|
} = require('./statistics-service');
|
|
7
|
+
const { normalizeUsageTokens, toNumber } = require('./proxy-log-helper');
|
|
7
8
|
|
|
8
9
|
const TOOL_TYPE = 'codex';
|
|
9
10
|
|
|
10
|
-
function toNumber(value) {
|
|
11
|
-
const num = Number(value);
|
|
12
|
-
return Number.isFinite(num) ? num : 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function normalizeToolTokens(tokens = {}) {
|
|
16
|
-
const input = toNumber(tokens.input);
|
|
17
|
-
const output = toNumber(tokens.output);
|
|
18
|
-
const reasoning = toNumber(tokens.reasoning);
|
|
19
|
-
const cached = toNumber(tokens.cached);
|
|
20
|
-
const cacheCreation = toNumber(tokens.cacheCreation);
|
|
21
|
-
const cacheRead = toNumber(tokens.cacheRead || cached);
|
|
22
|
-
const total = toNumber(tokens.total) || (input + output + reasoning);
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
input,
|
|
26
|
-
output,
|
|
27
|
-
reasoning,
|
|
28
|
-
cached,
|
|
29
|
-
cacheCreation,
|
|
30
|
-
cacheRead,
|
|
31
|
-
total
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
11
|
function toLegacyEntryShape(entry = {}, includeName = false) {
|
|
36
|
-
const normalized =
|
|
12
|
+
const normalized = normalizeUsageTokens(TOOL_TYPE, entry.tokens || {});
|
|
37
13
|
const result = {
|
|
38
14
|
requests: toNumber(entry.requests),
|
|
39
15
|
tokens: {
|
|
@@ -126,7 +102,7 @@ function buildDailyStatistics(sharedDaily = {}, fallbackDate) {
|
|
|
126
102
|
}
|
|
127
103
|
|
|
128
104
|
function recordRequest(requestData = {}) {
|
|
129
|
-
const normalizedTokens =
|
|
105
|
+
const normalizedTokens = normalizeUsageTokens(TOOL_TYPE, requestData.tokens || {});
|
|
130
106
|
return recordSharedRequest({
|
|
131
107
|
...requestData,
|
|
132
108
|
toolType: TOOL_TYPE,
|
|
@@ -39,11 +39,11 @@ const PLUGIN_SENSITIVE_PATTERNS = [
|
|
|
39
39
|
/\.p12$/i,
|
|
40
40
|
/\.pfx$/i
|
|
41
41
|
];
|
|
42
|
-
const CC_UI_CONFIG_PATH =
|
|
43
|
-
const CC_PROMPTS_PATH =
|
|
44
|
-
const CC_SECURITY_PATH =
|
|
45
|
-
const LEGACY_UI_CONFIG_PATH =
|
|
46
|
-
const LEGACY_NOTIFY_HOOK_PATH =
|
|
42
|
+
const CC_UI_CONFIG_PATH = PATHS.uiConfig;
|
|
43
|
+
const CC_PROMPTS_PATH = PATHS.prompts;
|
|
44
|
+
const CC_SECURITY_PATH = PATHS.security;
|
|
45
|
+
const LEGACY_UI_CONFIG_PATH = PATHS.uiConfig;
|
|
46
|
+
const LEGACY_NOTIFY_HOOK_PATH = PATHS.notifyHook;
|
|
47
47
|
const GEMINI_SETTINGS_PATH = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'settings.json');
|
|
48
48
|
const AGENT_PLATFORMS = ['claude', 'codex', 'opencode'];
|
|
49
49
|
const COMMAND_PLATFORMS = ['claude', 'opencode'];
|
|
@@ -58,8 +58,18 @@ function getOpenCodeConfigPaths() {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function getOpenCodeNotificationPluginPath() {
|
|
62
|
+
try {
|
|
63
|
+
const { getOpenCodeManagedPluginPath } = require('./notification-hooks');
|
|
64
|
+
return typeof getOpenCodeManagedPluginPath === 'function' ? getOpenCodeManagedPluginPath() : '';
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
function getNativeConfigSpecs() {
|
|
62
71
|
const openCodeConfigPaths = getOpenCodeConfigPaths();
|
|
72
|
+
const openCodeNotificationPluginPath = getOpenCodeNotificationPluginPath();
|
|
63
73
|
return {
|
|
64
74
|
claude: {
|
|
65
75
|
settings: { path: NATIVE_PATHS.claude.settings, format: 'json' }
|
|
@@ -75,7 +85,10 @@ function getNativeConfigSpecs() {
|
|
|
75
85
|
opencode: {
|
|
76
86
|
opencodeJsonc: { path: openCodeConfigPaths.opencodec, format: 'text' },
|
|
77
87
|
opencodeJson: { path: openCodeConfigPaths.opencode, format: 'text' },
|
|
78
|
-
configJson: { path: openCodeConfigPaths.config, format: 'text' }
|
|
88
|
+
configJson: { path: openCodeConfigPaths.config, format: 'text' },
|
|
89
|
+
...(openCodeNotificationPluginPath
|
|
90
|
+
? { codingToolNotifyPlugin: { path: openCodeNotificationPluginPath, format: 'text' } }
|
|
91
|
+
: {})
|
|
79
92
|
}
|
|
80
93
|
};
|
|
81
94
|
}
|
|
@@ -212,7 +225,7 @@ function buildExportReadme(exportData) {
|
|
|
212
225
|
- Prompts 预设
|
|
213
226
|
- 安全配置
|
|
214
227
|
- 高级配置(端口、日志、性能等)
|
|
215
|
-
-
|
|
228
|
+
- 通知 Hook / 插件脚本(如 notify-hook.js)
|
|
216
229
|
|
|
217
230
|
> 注意:配置包可能包含 API Key、Webhook 等敏感信息,请妥善保管。
|
|
218
231
|
`;
|
|
@@ -13,8 +13,8 @@ const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
|
13
13
|
|
|
14
14
|
// Configuration paths
|
|
15
15
|
const CC_TOOL_DIR = PATHS.base;
|
|
16
|
-
const REGISTRY_FILE =
|
|
17
|
-
const CONFIGS_DIR =
|
|
16
|
+
const REGISTRY_FILE = PATHS.configRegistry;
|
|
17
|
+
const CONFIGS_DIR = PATHS.configs;
|
|
18
18
|
|
|
19
19
|
// Claude Code native directories
|
|
20
20
|
const CLAUDE_HOME_DIR = path.dirname(NATIVE_PATHS.claude.settings);
|
|
@@ -113,6 +113,7 @@ class ConfigRegistryService {
|
|
|
113
113
|
*/
|
|
114
114
|
_ensureDirs() {
|
|
115
115
|
ensureDir(CC_TOOL_DIR);
|
|
116
|
+
ensureDir(path.dirname(this.registryPath));
|
|
116
117
|
ensureDir(this.configsDir);
|
|
117
118
|
|
|
118
119
|
for (const type of CONFIG_TYPES) {
|
|
@@ -24,7 +24,7 @@ const { PATHS, NATIVE_PATHS, HOME_DIR, ensureStorageDirMigrated } = require('../
|
|
|
24
24
|
|
|
25
25
|
// Paths
|
|
26
26
|
const HOME = HOME_DIR || os.homedir();
|
|
27
|
-
const CC_TOOL_CONFIGS =
|
|
27
|
+
const CC_TOOL_CONFIGS = PATHS.configs;
|
|
28
28
|
const CLAUDE_CODE_DIR = path.join(HOME, '.claude');
|
|
29
29
|
const CODEX_DIR = path.join(HOME, '.codex');
|
|
30
30
|
const GEMINI_DIR = path.join(HOME, '.gemini');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
2
3
|
const { PATHS } = require('../../config/paths');
|
|
3
4
|
|
|
4
|
-
const FAVORITES_DIR = PATHS.base;
|
|
5
5
|
const FAVORITES_FILE = PATHS.favorites;
|
|
6
6
|
|
|
7
7
|
// 内存缓存
|
|
@@ -17,8 +17,9 @@ const DEFAULT_FAVORITES = {
|
|
|
17
17
|
|
|
18
18
|
// Ensure favorites directory exists
|
|
19
19
|
function ensureFavoritesDir() {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const dir = path.dirname(FAVORITES_FILE);
|
|
21
|
+
if (!fs.existsSync(dir)) {
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -32,9 +32,9 @@ function getGeminiDir() {
|
|
|
32
32
|
|
|
33
33
|
// 获取渠道存储文件路径
|
|
34
34
|
function getChannelsFilePath() {
|
|
35
|
-
const
|
|
36
|
-
if (!fs.existsSync(
|
|
37
|
-
fs.mkdirSync(
|
|
35
|
+
const channelsDir = path.dirname(PATHS.channels.gemini);
|
|
36
|
+
if (!fs.existsSync(channelsDir)) {
|
|
37
|
+
fs.mkdirSync(channelsDir, { recursive: true });
|
|
38
38
|
}
|
|
39
39
|
return PATHS.channels.gemini;
|
|
40
40
|
}
|