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.
Files changed (32) hide show
  1. package/README.md +1 -0
  2. package/bin/restore.js +14 -7
  3. package/bin/utils/managed-fields.js +62 -0
  4. package/dist/server/access-keys/index.js +173 -0
  5. package/dist/server/access-keys/key-logger.js +358 -0
  6. package/dist/server/access-keys/key-resolver.js +51 -0
  7. package/dist/server/access-keys/key-session-tracker.js +217 -0
  8. package/dist/server/access-keys/manager.js +206 -0
  9. package/dist/server/access-keys/policy-manager.js +144 -0
  10. package/dist/server/access-keys/quota-checker.js +197 -0
  11. package/dist/server/access-keys/usage-tracker.js +279 -0
  12. package/dist/server/auth.js +16 -4
  13. package/dist/server/coding-plan-headers.js +121 -0
  14. package/dist/server/config-managed-fields.js +2 -0
  15. package/dist/server/conversions/index.js +8 -0
  16. package/dist/server/conversions/utils/tool-result.js +35 -0
  17. package/dist/server/fs-database.js +72 -1
  18. package/dist/server/main.js +1162 -13
  19. package/dist/server/proxy-server.js +662 -128
  20. package/dist/server/rules-status-service.js +32 -3
  21. package/dist/server/session-launcher.js +282 -0
  22. package/dist/server/session-migration.js +419 -0
  23. package/dist/server/transformers/chunk-collector.js +28 -1
  24. package/dist/server/transformers/model-rewrite-transform.js +128 -0
  25. package/dist/ui/assets/claude-XtpLmGtF.webp +0 -0
  26. package/dist/ui/assets/index-Cws89pD2.js +828 -0
  27. package/dist/ui/assets/index-CzfKxImD.css +1 -0
  28. package/dist/ui/assets/openai-CPEiZpaN.webp +0 -0
  29. package/dist/ui/index.html +2 -2
  30. package/package.json +1 -1
  31. package/dist/ui/assets/index-BHR12ImE.css +0 -1
  32. 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.ruleStates.set(ruleId, {
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.ruleStates.set(ruleId, {
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
+ }