aicodeswitch 5.1.2 → 5.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/README.md +1 -0
- package/bin/restore.js +14 -7
- package/bin/utils/managed-fields.js +62 -0
- package/dist/server/access-keys/index.js +173 -0
- package/dist/server/access-keys/key-logger.js +358 -0
- package/dist/server/access-keys/key-resolver.js +51 -0
- package/dist/server/access-keys/key-session-tracker.js +217 -0
- package/dist/server/access-keys/manager.js +206 -0
- package/dist/server/access-keys/policy-manager.js +144 -0
- package/dist/server/access-keys/quota-checker.js +197 -0
- package/dist/server/access-keys/usage-tracker.js +279 -0
- package/dist/server/auth.js +16 -4
- package/dist/server/coding-plan-headers.js +121 -0
- package/dist/server/config-managed-fields.js +2 -0
- package/dist/server/conversions/index.js +8 -0
- package/dist/server/conversions/utils/tool-result.js +35 -0
- package/dist/server/fs-database.js +72 -1
- package/dist/server/main.js +1162 -13
- package/dist/server/proxy-server.js +662 -128
- package/dist/server/rules-status-service.js +32 -3
- package/dist/server/session-launcher.js +282 -0
- package/dist/server/session-migration.js +419 -0
- package/dist/server/transformers/chunk-collector.js +28 -1
- package/dist/server/transformers/model-rewrite-transform.js +128 -0
- package/dist/ui/assets/claude-XtpLmGtF.webp +0 -0
- package/dist/ui/assets/index-Cws89pD2.js +828 -0
- package/dist/ui/assets/index-CzfKxImD.css +1 -0
- package/dist/ui/assets/openai-CPEiZpaN.webp +0 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BHR12ImE.css +0 -1
- package/dist/ui/assets/index-CumAhpXg.js +0 -517
|
@@ -10,12 +10,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.rulesStatusBroadcaster = exports.RulesStatusBroadcaster = void 0;
|
|
13
|
+
const events_1 = require("events");
|
|
13
14
|
/**
|
|
14
15
|
* 规则状态管理服务
|
|
15
16
|
* 负责管理所有规则的状态(使用中、空闲、错误、挂起)
|
|
16
17
|
*/
|
|
17
|
-
class RulesStatusBroadcaster {
|
|
18
|
+
class RulesStatusBroadcaster extends events_1.EventEmitter {
|
|
18
19
|
constructor() {
|
|
20
|
+
super();
|
|
19
21
|
Object.defineProperty(this, "ruleStates", {
|
|
20
22
|
enumerable: true,
|
|
21
23
|
configurable: true,
|
|
@@ -70,6 +72,8 @@ class RulesStatusBroadcaster {
|
|
|
70
72
|
writable: true,
|
|
71
73
|
value: 30000
|
|
72
74
|
}); // error 状态30秒后自动恢复
|
|
75
|
+
// 允许多标签页场景下的多连接监听
|
|
76
|
+
this.setMaxListeners(50);
|
|
73
77
|
// 启动定期状态检查定时器
|
|
74
78
|
this.startSyncInterval();
|
|
75
79
|
}
|
|
@@ -99,7 +103,7 @@ class RulesStatusBroadcaster {
|
|
|
99
103
|
data.timestamp &&
|
|
100
104
|
now - data.timestamp > this.ERROR_RECOVERY_TIMEOUT) {
|
|
101
105
|
console.log(`[RulesStatusBroadcaster] 规则 ${ruleId} 错误状态已超时,自动恢复为 idle 状态`);
|
|
102
|
-
this.
|
|
106
|
+
this.updateRuleStatus({
|
|
103
107
|
ruleId,
|
|
104
108
|
status: 'idle',
|
|
105
109
|
routeId: data.routeId,
|
|
@@ -130,7 +134,7 @@ class RulesStatusBroadcaster {
|
|
|
130
134
|
if (!isBlacklisted) {
|
|
131
135
|
// 黑名单已过期,恢复为 idle 状态
|
|
132
136
|
console.log(`[RulesStatusBroadcaster] 规则 ${ruleId} 黑名单已过期,恢复为 idle 状态`);
|
|
133
|
-
this.
|
|
137
|
+
this.updateRuleStatus({
|
|
134
138
|
ruleId,
|
|
135
139
|
status: 'idle',
|
|
136
140
|
routeId,
|
|
@@ -172,6 +176,8 @@ class RulesStatusBroadcaster {
|
|
|
172
176
|
updateRuleStatus(data) {
|
|
173
177
|
// 更新本地状态
|
|
174
178
|
this.ruleStates.set(data.ruleId, data);
|
|
179
|
+
// 通知 SSE 客户端状态已变更
|
|
180
|
+
this.emit('statusChanged', data);
|
|
175
181
|
}
|
|
176
182
|
/**
|
|
177
183
|
* 标记规则正在使用
|
|
@@ -251,6 +257,22 @@ class RulesStatusBroadcaster {
|
|
|
251
257
|
timestamp: Date.now(),
|
|
252
258
|
});
|
|
253
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* 刷新规则使用中的不活动定时器(轻量级,仅重置定时器,不修改状态)
|
|
262
|
+
* 用于 streaming 过程中持续保持 in_use 状态
|
|
263
|
+
*/
|
|
264
|
+
refreshRuleInUse(routeId, ruleId) {
|
|
265
|
+
const currentStatus = this.ruleStates.get(ruleId);
|
|
266
|
+
// 仅当状态已经是 in_use 时才刷新定时器
|
|
267
|
+
if ((currentStatus === null || currentStatus === void 0 ? void 0 : currentStatus.status) !== 'in_use')
|
|
268
|
+
return;
|
|
269
|
+
const timeoutKey = `${routeId}:${ruleId}`;
|
|
270
|
+
this.clearRuleTimeout(timeoutKey);
|
|
271
|
+
const timeout = setTimeout(() => {
|
|
272
|
+
this.markRuleIdle(routeId, ruleId);
|
|
273
|
+
}, this.INACTIVITY_TIMEOUT);
|
|
274
|
+
this.ruleTimeouts.set(timeoutKey, timeout);
|
|
275
|
+
}
|
|
254
276
|
/**
|
|
255
277
|
* 更新规则使用量
|
|
256
278
|
*/
|
|
@@ -272,6 +294,12 @@ class RulesStatusBroadcaster {
|
|
|
272
294
|
*/
|
|
273
295
|
clearRuleStatus(ruleId) {
|
|
274
296
|
this.ruleStates.delete(ruleId);
|
|
297
|
+
// 通知 SSE 客户端状态已清除(恢复为 idle)
|
|
298
|
+
this.emit('statusChanged', {
|
|
299
|
+
ruleId,
|
|
300
|
+
status: 'idle',
|
|
301
|
+
timestamp: Date.now(),
|
|
302
|
+
});
|
|
275
303
|
}
|
|
276
304
|
/**
|
|
277
305
|
* 获取当前活动的规则列表
|
|
@@ -304,6 +332,7 @@ class RulesStatusBroadcaster {
|
|
|
304
332
|
this.idleDebounceTimeouts.forEach((timeout) => clearTimeout(timeout));
|
|
305
333
|
this.idleDebounceTimeouts.clear();
|
|
306
334
|
this.ruleStates.clear();
|
|
335
|
+
this.removeAllListeners();
|
|
307
336
|
}
|
|
308
337
|
}
|
|
309
338
|
exports.RulesStatusBroadcaster = RulesStatusBroadcaster;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.resolveProjectDir = resolveProjectDir;
|
|
13
|
+
exports.writePromptToTempFile = writePromptToTempFile;
|
|
14
|
+
exports.cleanupTempFile = cleanupTempFile;
|
|
15
|
+
exports.cleanupOldTempFiles = cleanupOldTempFiles;
|
|
16
|
+
exports.launchTargetTool = launchTargetTool;
|
|
17
|
+
exports.launchTargetWithFallback = launchTargetWithFallback;
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const fs_1 = require("fs");
|
|
20
|
+
const path_1 = require("path");
|
|
21
|
+
const os_1 = require("os");
|
|
22
|
+
function which(cmd) {
|
|
23
|
+
try {
|
|
24
|
+
const command = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`;
|
|
25
|
+
(0, child_process_1.execSync)(command, { stdio: 'ignore' });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch (_a) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isToolInstalled(tool) {
|
|
33
|
+
if (tool === 'claude-code')
|
|
34
|
+
return which('claude');
|
|
35
|
+
if (tool === 'codex')
|
|
36
|
+
return which('codex');
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
function getToolCli(tool) {
|
|
40
|
+
return tool === 'claude-code' ? 'claude' : 'codex';
|
|
41
|
+
}
|
|
42
|
+
// ─── 项目目录自动推断 ───
|
|
43
|
+
/**
|
|
44
|
+
* 从 Claude Code session 文件中查找 cwd
|
|
45
|
+
* Claude Code 在 ~/.claude/sessions/<PID>.json 中记录 sessionId 和 cwd
|
|
46
|
+
*/
|
|
47
|
+
function resolveProjectDirFromClaudeSessions(sessionId) {
|
|
48
|
+
try {
|
|
49
|
+
const sessionsDir = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'sessions');
|
|
50
|
+
if (!(0, fs_1.existsSync)(sessionsDir))
|
|
51
|
+
return null;
|
|
52
|
+
const files = (0, fs_1.readdirSync)(sessionsDir).filter(f => f.endsWith('.json'));
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
try {
|
|
55
|
+
const content = (0, fs_1.readFileSync)((0, path_1.join)(sessionsDir, file), 'utf-8');
|
|
56
|
+
const meta = JSON.parse(content);
|
|
57
|
+
if (meta.sessionId === sessionId && meta.cwd) {
|
|
58
|
+
return meta.cwd;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch ( /* skip unreadable files */_a) { /* skip unreadable files */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch ( /* ignore */_b) { /* ignore */ }
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 从 Codex session 文件中查找 cwd
|
|
69
|
+
* Codex 在 ~/.codex/sessions/ 目录下存储 JSONL 格式的会话文件
|
|
70
|
+
*/
|
|
71
|
+
function resolveProjectDirFromCodexSessions(sessionId) {
|
|
72
|
+
try {
|
|
73
|
+
const sessionsDir = (0, path_1.join)((0, os_1.homedir)(), '.codex', 'sessions');
|
|
74
|
+
if (!(0, fs_1.existsSync)(sessionsDir))
|
|
75
|
+
return null;
|
|
76
|
+
const files = (0, fs_1.readdirSync)(sessionsDir);
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
// Codex session 文件名格式: <uuid>.jsonl 或在子目录中
|
|
79
|
+
const filePath = (0, path_1.join)(sessionsDir, file);
|
|
80
|
+
try {
|
|
81
|
+
if ((0, fs_1.existsSync)(filePath) && !filePath.endsWith('.jsonl'))
|
|
82
|
+
continue;
|
|
83
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
84
|
+
const lines = content.trim().split('\n');
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
try {
|
|
87
|
+
const entry = JSON.parse(line);
|
|
88
|
+
// Codex session 文件中可能包含 cwd 字段
|
|
89
|
+
if (entry.sessionId === sessionId && entry.cwd)
|
|
90
|
+
return entry.cwd;
|
|
91
|
+
if (entry.cwd && line.includes(sessionId))
|
|
92
|
+
return entry.cwd;
|
|
93
|
+
}
|
|
94
|
+
catch ( /* skip unparseable lines */_a) { /* skip unparseable lines */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch ( /* skip unreadable files */_b) { /* skip unreadable files */ }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch ( /* ignore */_c) { /* ignore */ }
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 自动推断会话对应的项目目录
|
|
105
|
+
*/
|
|
106
|
+
function resolveProjectDir(sessionId, _sourceTool) {
|
|
107
|
+
// 优先从 Claude Code sessions 查找
|
|
108
|
+
let cwd = resolveProjectDirFromClaudeSessions(sessionId);
|
|
109
|
+
if (cwd)
|
|
110
|
+
return cwd;
|
|
111
|
+
// 然后从 Codex sessions 查找
|
|
112
|
+
cwd = resolveProjectDirFromCodexSessions(sessionId);
|
|
113
|
+
if (cwd)
|
|
114
|
+
return cwd;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
// ─── 临时文件管理 ───
|
|
118
|
+
function writePromptToTempFile(prompt, sessionId) {
|
|
119
|
+
const fileName = `aicodeswitch-migration-${sessionId}.txt`;
|
|
120
|
+
const filePath = (0, path_1.join)((0, os_1.tmpdir)(), fileName);
|
|
121
|
+
(0, fs_1.writeFileSync)(filePath, prompt, 'utf-8');
|
|
122
|
+
return filePath;
|
|
123
|
+
}
|
|
124
|
+
function cleanupTempFile(filePath) {
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
try {
|
|
127
|
+
(0, fs_1.unlinkSync)(filePath);
|
|
128
|
+
}
|
|
129
|
+
catch ( /* already cleaned up */_a) { /* already cleaned up */ }
|
|
130
|
+
}, 30000);
|
|
131
|
+
}
|
|
132
|
+
function cleanupOldTempFiles() {
|
|
133
|
+
try {
|
|
134
|
+
const dir = (0, os_1.tmpdir)();
|
|
135
|
+
const files = (0, fs_1.readdirSync)(dir);
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
if (file.startsWith('aicodeswitch-migration-') && file.endsWith('.txt')) {
|
|
139
|
+
const fullPath = (0, path_1.join)(dir, file);
|
|
140
|
+
try {
|
|
141
|
+
const { statSync } = require('fs');
|
|
142
|
+
const stat = statSync(fullPath);
|
|
143
|
+
if (now - stat.mtimeMs > 3600000) {
|
|
144
|
+
(0, fs_1.unlinkSync)(fullPath);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch ( /* ignore per-file errors */_a) { /* ignore per-file errors */ }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch ( /* ignore */_b) { /* ignore */ }
|
|
152
|
+
}
|
|
153
|
+
// ─── 命令构建 ───
|
|
154
|
+
function buildCommand(tool, promptFilePath, projectDir) {
|
|
155
|
+
const cli = getToolCli(tool);
|
|
156
|
+
const cdPrefix = projectDir ? `cd "${projectDir}" && ` : '';
|
|
157
|
+
const readCmd = process.platform === 'win32' ? `type "${promptFilePath}"` : `cat "${promptFilePath}"`;
|
|
158
|
+
return `${cdPrefix}${readCmd} | ${cli}`;
|
|
159
|
+
}
|
|
160
|
+
// ─── 终端启动 ───
|
|
161
|
+
function launchViaOSAScript(command) {
|
|
162
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
const escaped = command.replace(/"/g, '\\"');
|
|
165
|
+
const script = `tell app "Terminal" to do script "${escaped}"`;
|
|
166
|
+
const child = (0, child_process_1.spawn)('osascript', ['-e', script], {
|
|
167
|
+
stdio: 'ignore',
|
|
168
|
+
detached: true,
|
|
169
|
+
});
|
|
170
|
+
child.on('error', () => {
|
|
171
|
+
// Try iTerm2 as fallback
|
|
172
|
+
const itermScript = `tell app "iTerm" to tell current window to set newTab to (create tab with default profile) then write session 1 of newTab text "${escaped}"`;
|
|
173
|
+
const child2 = (0, child_process_1.spawn)('osascript', ['-e', itermScript], {
|
|
174
|
+
stdio: 'ignore',
|
|
175
|
+
detached: true,
|
|
176
|
+
});
|
|
177
|
+
child2.on('error', () => resolve(null));
|
|
178
|
+
child2.on('spawn', () => resolve(child2.pid || 0));
|
|
179
|
+
});
|
|
180
|
+
child.on('spawn', () => resolve(child.pid || 0));
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function launchViaTerminal(command) {
|
|
185
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
186
|
+
return new Promise((resolve) => {
|
|
187
|
+
const child = (0, child_process_1.spawn)('gnome-terminal', ['--', 'bash', '-c', `${command}; exec bash`], {
|
|
188
|
+
stdio: 'ignore',
|
|
189
|
+
detached: true,
|
|
190
|
+
});
|
|
191
|
+
child.on('error', () => {
|
|
192
|
+
const child2 = (0, child_process_1.spawn)('xterm', ['-e', `${command}; exec bash`], {
|
|
193
|
+
stdio: 'ignore',
|
|
194
|
+
detached: true,
|
|
195
|
+
});
|
|
196
|
+
child2.on('error', () => resolve(null));
|
|
197
|
+
child2.on('spawn', () => resolve(child2.pid || 0));
|
|
198
|
+
});
|
|
199
|
+
child.on('spawn', () => resolve(child.pid || 0));
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// ─── 回退结果 ───
|
|
204
|
+
function createFallbackResult(tool, command, promptFilePath) {
|
|
205
|
+
const toolName = tool === 'claude-code' ? 'Claude Code' : 'Codex';
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
method: 'fallback',
|
|
209
|
+
reason: `无法自动启动 ${toolName}`,
|
|
210
|
+
command,
|
|
211
|
+
promptFilePath,
|
|
212
|
+
fallbackSuggestions: [
|
|
213
|
+
`请在终端中执行: ${command}`,
|
|
214
|
+
'或复制下方 Prompt 内容,在新会话中粘贴',
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// ─── 主启动逻辑 ───
|
|
219
|
+
function launchTargetTool(tool, promptFilePath, projectDir) {
|
|
220
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
221
|
+
const command = buildCommand(tool, promptFilePath, projectDir);
|
|
222
|
+
try {
|
|
223
|
+
let pid = null;
|
|
224
|
+
if (process.platform === 'darwin') {
|
|
225
|
+
pid = yield launchViaOSAScript(command);
|
|
226
|
+
}
|
|
227
|
+
else if (process.platform === 'linux') {
|
|
228
|
+
pid = yield launchViaTerminal(command);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// Windows: open new cmd window
|
|
232
|
+
const cdPrefix = projectDir ? `cd /d "${projectDir}" && ` : '';
|
|
233
|
+
const cli = getToolCli(tool);
|
|
234
|
+
const winCommand = `start cmd /k "${cdPrefix}type "${promptFilePath}" | ${cli}"`;
|
|
235
|
+
yield new Promise((resolve, reject) => {
|
|
236
|
+
const child = (0, child_process_1.spawn)('cmd', ['/c', winCommand], {
|
|
237
|
+
stdio: 'ignore',
|
|
238
|
+
detached: true,
|
|
239
|
+
});
|
|
240
|
+
child.on('error', reject);
|
|
241
|
+
child.on('spawn', () => resolve());
|
|
242
|
+
});
|
|
243
|
+
pid = 0;
|
|
244
|
+
}
|
|
245
|
+
if (pid !== null) {
|
|
246
|
+
return {
|
|
247
|
+
success: true,
|
|
248
|
+
method: 'cli-launch',
|
|
249
|
+
pid: pid || undefined,
|
|
250
|
+
command,
|
|
251
|
+
promptFilePath,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (_a) {
|
|
256
|
+
// Fall through to fallback
|
|
257
|
+
}
|
|
258
|
+
return createFallbackResult(tool, command, promptFilePath);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function launchTargetWithFallback(tool, promptFilePath, prompt, projectDir) {
|
|
262
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
263
|
+
const installed = isToolInstalled(tool);
|
|
264
|
+
if (!installed) {
|
|
265
|
+
const toolName = tool === 'claude-code' ? 'Claude Code' : 'Codex';
|
|
266
|
+
const command = buildCommand(tool, promptFilePath, projectDir);
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
method: 'fallback',
|
|
270
|
+
reason: `${toolName} CLI (${getToolCli(tool)}) not found in PATH`,
|
|
271
|
+
command,
|
|
272
|
+
prompt,
|
|
273
|
+
promptFilePath,
|
|
274
|
+
fallbackSuggestions: [
|
|
275
|
+
`请在终端中执行: ${command}`,
|
|
276
|
+
'或复制下方 Prompt 内容,在新会话中粘贴',
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return launchTargetTool(tool, promptFilePath, projectDir);
|
|
281
|
+
});
|
|
282
|
+
}
|