aicodeswitch 5.2.12 → 5.2.13

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 CHANGED
@@ -192,8 +192,6 @@ aicodeswitch内部,会根据“源类型”来转换数据。例如,你的
192
192
  目前仅支持以下配置:
193
193
 
194
194
  ```
195
- # aicodeswitch的服务IP
196
- HOST=127.0.0.1
197
195
  # aicodeswitch的服务端口
198
196
  PORT=4567
199
197
 
@@ -202,6 +200,12 @@ PORT=4567
202
200
  # AUTH=
203
201
  ```
204
202
 
203
+ > 服务监听地址由 AUTH 模式自动决定,无需也无法通过配置修改:
204
+ > - AUTH 关闭:仅监听 `127.0.0.1`(本机访问)
205
+ > - AUTH 开启:监听 `0.0.0.0`(允许远端 AccessKey 客户端连接)
206
+ >
207
+ > 写入本地编程工具(Claude Code / Codex)配置文件的地址、以及 UI / CLI 展示的访问地址,统一使用 `127.0.0.1`。
208
+
205
209
  ## 常见问题
206
210
 
207
211
  ### 1\. 如何切换供应商?
package/UPGRADE.md CHANGED
@@ -1,6 +1,7 @@
1
- 升级到 v5.0.0 版本将会有破坏性影响,你需要注意:
1
+ 升级到 v6.0.0 版本将会有破坏性影响,你需要注意:
2
2
 
3
- * 我们移除了 `Deepseek` 类型的服务,它原本的 `reasoning_content` 特殊字段已被合并进入 `chat completions` 类型,升级会自动完成数据库迁移,你不需要手动修改。
4
- * 我们启用了新的数据流格式转化器,请你实时关注新版本,如遇到问题,我们会实时修复发布新版本。
5
- * 我们对路由管理进行了升级,现在你可以将 aicodeswitch 使用于其他**任意的编程工具**,之需要填写对应的 API 即可。升级后,路由本身不再绑定claude-code/codex,你需要在Claude Code配置、Codex配置中,激活想要用来服务的路由,让这两个最重要的编程工具能够正常工作(我们会在升级过程中帮你自动迁移数据库,因此,你不需要手动处理)。
3
+ * 我们升级了日志系统,以解决老的日志系统所带来的存储问题。新日志系统会迁移你的日志数据,但是,为了确保日志数据不丢失,我们会在下一个版本中才删除老的日志文件,这也就意味着你的电脑上可能同时存在两份日志文件,占用空间。为了节约你的电脑磁盘空间,你可以在运行一段时间感觉正常之后,删除 `~/.aicodeswitch/fs-db/logs` 目录来移除老的日志文件。
6
4
 
5
+ 本次升级带来了新的大功能:
6
+
7
+ * 「任务雷达」功能,帮助你时刻掌握当前任务状态,历史任务分布,通过对任务雷达的分析,你可以掌握自己的编程习惯,从而作出适当的调整。
@@ -36,16 +36,16 @@ const getServerInfo = () => {
36
36
  path.join(os.homedir(), '.aicodeswitch', 'aicodeswitch.conf')
37
37
  ];
38
38
 
39
- let host = '127.0.0.1';
39
+ // 监听地址现由 AUTH 模式决定(AUTH 开→0.0.0.0 / AUTH 关→127.0.0.1),HOST 已忽略;
40
+ // 本机 dashboard 的访问地址恒为回环地址,CLI 展示与自动打开统一用 127.0.0.1。
41
+ const host = '127.0.0.1';
40
42
  let port = 4567;
41
43
 
42
44
  for (const dotenvPath of possiblePaths) {
43
45
  if (fs.existsSync(dotenvPath)) {
44
46
  const content = fs.readFileSync(dotenvPath, 'utf-8');
45
- const hostMatch = content.match(/HOST=(.+)/);
46
47
  const portMatch = content.match(/PORT=(.+)/);
47
48
 
48
- if (hostMatch) host = hostMatch[1].trim();
49
49
  if (portMatch) port = parseInt(portMatch[1].trim(), 10);
50
50
  break;
51
51
  }
@@ -27,7 +27,7 @@ const key_logger_1 = require("./key-logger");
27
27
  const key_session_tracker_1 = require("./key-session-tracker");
28
28
  const key_resolver_1 = require("./key-resolver");
29
29
  class AccessKeyModule {
30
- constructor(dataPath) {
30
+ constructor(dataPath, logStore) {
31
31
  Object.defineProperty(this, "keyManager", {
32
32
  enumerable: true,
33
33
  configurable: true,
@@ -94,7 +94,7 @@ class AccessKeyModule {
94
94
  this.policyManager = new policy_manager_1.PolicyManager();
95
95
  this.quotaChecker = new quota_checker_1.QuotaChecker();
96
96
  this.usageTracker = new usage_tracker_1.UsageTracker(dataPath);
97
- this.keyLogger = new key_logger_1.KeyLogger(dataPath);
97
+ this.keyLogger = new key_logger_1.KeyLogger(dataPath, logStore);
98
98
  this.keySessionTracker = new key_session_tracker_1.KeySessionTracker(dataPath);
99
99
  this.keyResolver = new key_resolver_1.KeyResolver(this.keyManager, this.policyManager);
100
100
  }
@@ -8,353 +8,107 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
11
  Object.defineProperty(exports, "__esModule", { value: true });
15
12
  exports.KeyLogger = void 0;
16
- /**
17
- * Key 级日志管理器
18
- * 每个 AccessKey 有独立的日志空间,完全与现有日志系统隔离
19
- */
20
- const path_1 = __importDefault(require("path"));
21
- const promises_1 = __importDefault(require("fs/promises"));
22
- const crypto_1 = __importDefault(require("crypto"));
13
+ const LOG_RETENTION_DAYS = 30;
23
14
  class KeyLogger {
24
- constructor(dataPath) {
25
- Object.defineProperty(this, "dataPath", {
15
+ constructor(_dataPath, logStore) {
16
+ Object.defineProperty(this, "logStore", {
26
17
  enumerable: true,
27
18
  configurable: true,
28
19
  writable: true,
29
20
  value: void 0
30
21
  });
31
- Object.defineProperty(this, "MAX_SHARD_SIZE", {
32
- enumerable: true,
33
- configurable: true,
34
- writable: true,
35
- value: 10 * 1024 * 1024
36
- }); // 10MB
37
- Object.defineProperty(this, "LOG_RETENTION_DAYS", {
38
- enumerable: true,
39
- configurable: true,
40
- writable: true,
41
- value: 30
42
- });
43
- /** 分片索引缓存 keyId → LogShardIndex[] */
44
- Object.defineProperty(this, "shardIndexCache", {
45
- enumerable: true,
46
- configurable: true,
47
- writable: true,
48
- value: new Map()
49
- });
50
- /** 分片写入锁 */
51
- Object.defineProperty(this, "shardWriteLocks", {
52
- enumerable: true,
53
- configurable: true,
54
- writable: true,
55
- value: new Map()
56
- });
57
- this.dataPath = dataPath;
22
+ this.logStore = logStore;
58
23
  }
59
- /** 初始化 */
24
+ /** 初始化(存储由 LogStore 统一管理,此处保留为兼容空方法) */
60
25
  initialize() {
61
26
  return __awaiter(this, void 0, void 0, function* () {
62
- const logsDir = path_1.default.join(this.dataPath, 'key-logs');
63
- yield promises_1.default.mkdir(logsDir, { recursive: true });
27
+ // no-op
64
28
  });
65
29
  }
66
- /** 写入一条日志 */
30
+ ns(keyId) {
31
+ return `key:${keyId}`;
32
+ }
33
+ /** 写入一条日志(追加写,O(单条)) */
67
34
  addLog(keyId, keyName, logData) {
68
35
  return __awaiter(this, void 0, void 0, function* () {
69
- const log = Object.assign(Object.assign({}, logData), { id: crypto_1.default.randomUUID(), keyId,
36
+ const log = Object.assign(Object.assign({}, logData), { keyId,
70
37
  keyName });
71
- const keyDir = this.getKeyLogDir(keyId);
72
- yield promises_1.default.mkdir(keyDir, { recursive: true });
73
- const index = yield this.getShardIndex(keyId);
74
- const now = new Date();
75
- const dateStr = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}`;
76
- // 找到或创建当前分片
77
- let targetShard = null;
78
- for (const shard of index) {
79
- if (shard.date === dateStr && shard.count < 1000) {
80
- // 检查文件大小
81
- try {
82
- const stat = yield promises_1.default.stat(path_1.default.join(keyDir, shard.filename));
83
- if (stat.size < this.MAX_SHARD_SIZE) {
84
- targetShard = shard;
85
- break;
86
- }
87
- }
88
- catch (_a) {
89
- targetShard = shard;
90
- break;
91
- }
92
- }
93
- }
94
- if (!targetShard) {
95
- // 创建新分片
96
- const seq = index.filter(s => s.date === dateStr).length + 1;
97
- targetShard = {
98
- filename: seq === 1 ? `logs-${dateStr}.json` : `logs-${dateStr}-${String(seq).padStart(3, '0')}.json`,
99
- date: dateStr,
100
- startTime: Date.now(),
101
- endTime: Date.now(),
102
- count: 0,
103
- };
104
- index.push(targetShard);
38
+ // 保留原 id(如有)或由 LogStore 生成
39
+ if (!log.id) {
40
+ log.id = undefined; // 让 LogStore.append 生成 UUID
105
41
  }
106
- // 写入日志(带锁)
107
- yield this.writeToShard(keyId, targetShard, log);
108
- // 更新索引
109
- targetShard.count += 1;
110
- targetShard.endTime = Date.now();
111
- yield this.saveShardIndex(keyId, index);
42
+ yield this.logStore.append(this.ns(keyId), log);
112
43
  });
113
44
  }
114
- /** 获取 Key 的日志列表(分页 + 过滤) */
45
+ /** 获取 Key 的日志列表(分页 + 日期/类型/搜索过滤) */
115
46
  getLogs(keyId, options) {
116
47
  return __awaiter(this, void 0, void 0, function* () {
117
48
  var _a;
118
- const index = yield this.getShardIndex(keyId);
119
- let filteredShards = index;
120
- if (options.startDate) {
121
- filteredShards = filteredShards.filter(s => s.date >= options.startDate);
122
- }
123
- if (options.endDate) {
124
- filteredShards = filteredShards.filter(s => s.date <= options.endDate);
125
- }
126
- const needsFilter = !!(options.contentType || options.search);
127
- const searchLower = (_a = options.search) === null || _a === void 0 ? void 0 : _a.toLowerCase();
128
- // 计算分页偏移
129
49
  const offset = (options.page - 1) * options.pageSize;
130
50
  const limit = options.pageSize;
131
- // 如果不需要细粒度过滤,使用快速分片级分页
132
- if (!needsFilter) {
133
- const total = filteredShards.reduce((sum, s) => sum + s.count, 0);
134
- const allLogs = [];
135
- let skipped = 0;
136
- let collected = 0;
137
- for (let i = filteredShards.length - 1; i >= 0 && collected < limit; i++) {
138
- const shard = filteredShards[i];
139
- const logs = yield this.readShardFile(keyId, shard.filename);
140
- const reversed = logs.reverse();
141
- for (const log of reversed) {
142
- if (skipped < offset) {
143
- skipped++;
144
- continue;
145
- }
146
- allLogs.push(log);
147
- collected++;
148
- if (collected >= limit)
149
- break;
150
- }
151
- }
152
- return { data: allLogs, total };
153
- }
154
- // 需要细粒度过滤:先收集所有匹配日志,再分页
155
- const matchedLogs = [];
156
- for (let i = filteredShards.length - 1; i >= 0; i--) {
157
- const shard = filteredShards[i];
158
- const logs = yield this.readShardFile(keyId, shard.filename);
159
- const reversed = logs.reverse();
160
- for (const log of reversed) {
161
- // 类型过滤
162
- if (options.contentType && log.contentType !== options.contentType)
163
- continue;
164
- // 搜索过滤(匹配路径、模型)
165
- if (searchLower) {
166
- const path = (log.path || '').toLowerCase();
167
- const model = (log.requestModel || log.targetModel || '').toLowerCase();
168
- const error = (log.error || '').toLowerCase();
169
- if (!path.includes(searchLower) && !model.includes(searchLower) && !error.includes(searchLower))
170
- continue;
171
- }
172
- matchedLogs.push(log);
173
- }
174
- }
175
- const total = matchedLogs.length;
176
- const data = matchedLogs.slice(offset, offset + limit);
177
- return { data, total };
178
- });
179
- }
180
- /** 清理过期日志 */
181
- cleanupOldLogs() {
182
- return __awaiter(this, void 0, void 0, function* () {
183
- const logsDir = path_1.default.join(this.dataPath, 'key-logs');
184
- let keyDirs;
185
- try {
186
- keyDirs = yield promises_1.default.readdir(logsDir);
187
- }
188
- catch (_a) {
189
- return;
190
- }
191
- const cutoffTime = Date.now() - this.LOG_RETENTION_DAYS * 24 * 3600 * 1000;
192
- for (const keyId of keyDirs) {
193
- const keyDir = path_1.default.join(logsDir, keyId);
194
- const stat = yield promises_1.default.stat(keyDir);
195
- if (!stat.isDirectory())
196
- continue;
197
- const index = yield this.getShardIndex(keyId);
198
- let changed = false;
199
- for (let i = index.length - 1; i >= 0; i--) {
200
- if (index[i].endTime < cutoffTime) {
201
- // 删除过期分片文件
202
- try {
203
- yield promises_1.default.unlink(path_1.default.join(keyDir, index[i].filename));
204
- }
205
- catch ( /* ignore */_b) { /* ignore */ }
206
- index.splice(i, 1);
207
- changed = true;
208
- }
209
- }
210
- if (changed) {
211
- yield this.saveShardIndex(keyId, index);
51
+ const since = options.startDate ? Date.parse(options.startDate) : undefined;
52
+ const until = options.endDate ? Date.parse(options.endDate) + 86400000 - 1 : undefined;
53
+ const contentType = options.contentType;
54
+ const searchLower = (_a = options.search) === null || _a === void 0 ? void 0 : _a.toLowerCase();
55
+ const match = (log) => {
56
+ if (contentType && log.contentType !== contentType)
57
+ return false;
58
+ if (searchLower) {
59
+ const p = (log.path || '').toLowerCase();
60
+ const m = (log.requestModel || log.targetModel || '').toLowerCase();
61
+ const e = (log.error || '').toLowerCase();
62
+ if (!p.includes(searchLower) && !m.includes(searchLower) && !e.includes(searchLower))
63
+ return false;
212
64
  }
213
- }
214
- });
215
- }
216
- /** 获取 Key 的日志总数 */
217
- getLogsCount(keyId) {
218
- return __awaiter(this, void 0, void 0, function* () {
219
- const index = yield this.getShardIndex(keyId);
220
- return index.reduce((sum, s) => sum + s.count, 0);
65
+ return true;
66
+ };
67
+ const needFilter = !!(contentType || searchLower);
68
+ if (!needFilter && since == null && until == null) {
69
+ // 纯分页:用 getRecent + count
70
+ const [data, total] = yield Promise.all([
71
+ this.logStore.getRecent(this.ns(keyId), { limit, offset }),
72
+ this.logStore.count(this.ns(keyId)),
73
+ ]);
74
+ return { data: data, total };
75
+ }
76
+ const { data, total } = yield this.logStore.getFiltered(this.ns(keyId), {
77
+ since: Number.isFinite(since) ? since : undefined,
78
+ until: Number.isFinite(until) ? until : undefined,
79
+ match: needFilter ? match : undefined,
80
+ limit,
81
+ offset,
82
+ });
83
+ return { data: data, total };
221
84
  });
222
85
  }
223
- /** 按 sessionId 过滤日志(用于密钥会话的日志查询) */
86
+ /** 按 sessionId 过滤日志(用于密钥会话的日志查询,字节偏移随机读) */
224
87
  getLogsBySessionId(keyId_1, sessionId_1) {
225
88
  return __awaiter(this, arguments, void 0, function* (keyId, sessionId, limit = 10000) {
226
- const index = yield this.getShardIndex(keyId);
227
- const allLogs = [];
228
- // 从最新的分片开始扫描
229
- for (let i = index.length - 1; i >= 0 && allLogs.length < limit; i--) {
230
- const shard = index[i];
231
- const logs = yield this.readShardFile(keyId, shard.filename);
232
- for (let j = logs.length - 1; j >= 0 && allLogs.length < limit; j--) {
233
- if (this.logBelongsToSession(logs[j], sessionId)) {
234
- allLogs.push(logs[j]);
235
- }
236
- }
237
- }
238
- // 按时间正序排列(用于对话视图)
239
- return allLogs.sort((a, b) => a.timestamp - b.timestamp);
89
+ const logs = yield this.logStore.getBySession(this.ns(keyId), sessionId, { limit });
90
+ // timestamp 正序(对话视图所需)
91
+ return logs.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
240
92
  });
241
93
  }
242
- /** 判断日志是否属于指定会话 */
243
- logBelongsToSession(log, sessionId) {
244
- var _a;
245
- // Codex: 检查 headers 中的 session-id
246
- const headers = log.headers;
247
- if (headers) {
248
- const sid = headers['session-id'] || headers['session_id'];
249
- if (typeof sid === 'string' && sid === sessionId)
250
- return true;
251
- if (Array.isArray(sid) && sid[0] === sessionId)
252
- return true;
253
- }
254
- // Claude Code: 检查 body.metadata.user_id
255
- if (log.body) {
256
- try {
257
- const body = typeof log.body === 'string' ? JSON.parse(log.body) : log.body;
258
- const rawUserId = (_a = body === null || body === void 0 ? void 0 : body.metadata) === null || _a === void 0 ? void 0 : _a.user_id;
259
- if (rawUserId) {
260
- // 复用 ProxyServer 的 session ID 提取逻辑
261
- let extractedId = null;
262
- try {
263
- const parsed = JSON.parse(rawUserId);
264
- if (parsed && typeof parsed === 'object' && parsed.session_id) {
265
- extractedId = parsed.session_id;
266
- }
267
- }
268
- catch (_b) {
269
- extractedId = rawUserId;
270
- }
271
- if (extractedId === sessionId)
272
- return true;
273
- }
274
- }
275
- catch ( /* ignore */_c) { /* ignore */ }
276
- }
277
- return false;
278
- }
279
- // ---- helpers ----
280
- getKeyLogDir(keyId) {
281
- return path_1.default.join(this.dataPath, 'key-logs', keyId);
282
- }
283
- getShardIndexPath(keyId) {
284
- return path_1.default.join(this.getKeyLogDir(keyId), 'logs-index.json');
285
- }
286
- getShardIndex(keyId) {
287
- return __awaiter(this, void 0, void 0, function* () {
288
- if (this.shardIndexCache.has(keyId)) {
289
- return this.shardIndexCache.get(keyId);
290
- }
291
- try {
292
- const data = yield promises_1.default.readFile(this.getShardIndexPath(keyId), 'utf-8');
293
- const parsed = JSON.parse(data);
294
- // 防御性过滤:确保返回有效数组,剔除 null/undefined 或缺少 filename 的条目
295
- const index = Array.isArray(parsed)
296
- ? parsed.filter((s) => s != null && typeof s === 'object' && 'filename' in s)
297
- : [];
298
- this.shardIndexCache.set(keyId, index);
299
- return index;
300
- }
301
- catch (_a) {
302
- const index = [];
303
- this.shardIndexCache.set(keyId, index);
304
- return index;
305
- }
306
- });
307
- }
308
- saveShardIndex(keyId, index) {
94
+ /** 获取 Key 的日志总数 */
95
+ getLogsCount(keyId) {
309
96
  return __awaiter(this, void 0, void 0, function* () {
310
- this.shardIndexCache.set(keyId, index);
311
- const filePath = this.getShardIndexPath(keyId);
312
- const tmpPath = filePath + '.tmp';
313
- yield promises_1.default.writeFile(tmpPath, JSON.stringify(index, null, 2), 'utf-8');
314
- yield promises_1.default.rename(tmpPath, filePath);
97
+ return this.logStore.count(this.ns(keyId));
315
98
  });
316
99
  }
317
- writeToShard(keyId, shard, log) {
100
+ /** 清理所有 AccessKey 的过期日志(30 天,整文件删除) */
101
+ cleanupOldLogs() {
318
102
  return __awaiter(this, void 0, void 0, function* () {
319
- const lockKey = `${keyId}:${shard.filename}`;
320
- // 等待现有写入完成
321
- while (this.shardWriteLocks.has(lockKey)) {
322
- yield this.shardWriteLocks.get(lockKey);
323
- }
324
- const writePromise = (() => __awaiter(this, void 0, void 0, function* () {
103
+ for (const nsName of this.logStore.listNamespaces()) {
104
+ if (!nsName.startsWith('key:'))
105
+ continue;
325
106
  try {
326
- const keyDir = this.getKeyLogDir(keyId);
327
- const shardPath = path_1.default.join(keyDir, shard.filename);
328
- let logs = [];
329
- try {
330
- const data = yield promises_1.default.readFile(shardPath, 'utf-8');
331
- logs = JSON.parse(data);
332
- }
333
- catch (_a) {
334
- // 新文件
335
- }
336
- logs.push(log);
337
- const tmpPath = shardPath + '.tmp';
338
- yield promises_1.default.writeFile(tmpPath, JSON.stringify(logs), 'utf-8');
339
- yield promises_1.default.rename(tmpPath, shardPath);
107
+ yield this.logStore.retain(nsName, LOG_RETENTION_DAYS);
340
108
  }
341
- finally {
342
- this.shardWriteLocks.delete(lockKey);
109
+ catch (_a) {
110
+ // 忽略单个 key 失败
343
111
  }
344
- }))();
345
- this.shardWriteLocks.set(lockKey, writePromise);
346
- yield writePromise;
347
- });
348
- }
349
- readShardFile(keyId, filename) {
350
- return __awaiter(this, void 0, void 0, function* () {
351
- const shardPath = path_1.default.join(this.getKeyLogDir(keyId), filename);
352
- try {
353
- const data = yield promises_1.default.readFile(shardPath, 'utf-8');
354
- return JSON.parse(data);
355
- }
356
- catch (_a) {
357
- return [];
358
112
  }
359
113
  });
360
114
  }