aicodeswitch 5.1.3 → 5.2.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/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 +362 -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/config-managed-fields.js +2 -0
- package/dist/server/fs-database.js +14 -1
- package/dist/server/main.js +1017 -13
- package/dist/server/proxy-server.js +605 -134
- package/dist/server/rules-status-service.js +16 -3
- package/dist/server/transformers/model-rewrite-transform.js +128 -0
- package/dist/ui/assets/index-Cws89pD2.js +828 -0
- package/dist/ui/assets/index-CzfKxImD.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-CMoQtBmK.css +0 -1
- package/dist/ui/assets/index-CXdNTFiX.js +0 -532
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ AI Code Switch 是帮助你在本地管理 AI 编程工具接入大模型的工
|
|
|
10
10
|
|
|
11
11
|
- 视频演示:[https://www.bilibili.com/video/BV1uEznBuEJd/](https://www.bilibili.com/video/BV1uEznBuEJd/?from=github)
|
|
12
12
|
- 1分钟让Claude Code接入GLM国产模型:[https://www.bilibili.com/video/BV1a865B8ErA/](https://www.bilibili.com/video/BV1a865B8ErA/)
|
|
13
|
+
- 1分钟让Codex接入免费模型Agnes:[https://www.bilibili.com/video/BV1tQ7Q63Eva/](https://www.bilibili.com/video/BV1tQ7Q63Eva/) Codex接入Deepseek可参考该方法实现
|
|
13
14
|
|
|
14
15
|
## 功能要点
|
|
15
16
|
|
package/bin/restore.js
CHANGED
|
@@ -5,6 +5,7 @@ const chalk = require('chalk');
|
|
|
5
5
|
const boxen = require('boxen');
|
|
6
6
|
const ora = require('ora');
|
|
7
7
|
const { parseToml, stringifyToml, mergeJsonSettings, mergeTomlSettings, atomicWriteFile } = require('./utils/config-helpers');
|
|
8
|
+
const { CLAUDE_SETTINGS_MANAGED_FIELDS, CLAUDE_JSON_MANAGED_FIELDS, CODEX_CONFIG_MANAGED_FIELDS, CODEX_AUTH_MANAGED_FIELDS } = require('./utils/managed-fields');
|
|
8
9
|
const { isServerRunning, getServerInfo } = require('./utils/get-server');
|
|
9
10
|
const { findPidByPort } = require('./utils/port-utils');
|
|
10
11
|
|
|
@@ -85,12 +86,19 @@ const restoreClaudeConfig = () => {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
// 防御性清理:移除 currentSettings 中 ANTHROPIC_API_KEY 的空值,
|
|
90
|
+
// 防止旧版本代理写入的空值覆盖 backup 中的真实 Key
|
|
91
|
+
if (currentSettings?.env?.ANTHROPIC_API_KEY === '' && backupSettings?.env?.ANTHROPIC_API_KEY) {
|
|
92
|
+
delete currentSettings.env.ANTHROPIC_API_KEY;
|
|
93
|
+
if (Object.keys(currentSettings.env).length === 0) {
|
|
94
|
+
delete currentSettings.env;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
const mergedSettings = mergeJsonSettings(
|
|
89
99
|
backupSettings,
|
|
90
100
|
currentSettings,
|
|
91
|
-
|
|
92
|
-
'env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', 'env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
|
|
93
|
-
'permissions', 'skipDangerousModePermissionPrompt']
|
|
101
|
+
CLAUDE_SETTINGS_MANAGED_FIELDS
|
|
94
102
|
);
|
|
95
103
|
|
|
96
104
|
atomicWriteFile(claudeSettingsPath, JSON.stringify(mergedSettings, null, 2));
|
|
@@ -127,7 +135,7 @@ const restoreClaudeConfig = () => {
|
|
|
127
135
|
const mergedJson = mergeJsonSettings(
|
|
128
136
|
backupJson,
|
|
129
137
|
currentJson,
|
|
130
|
-
|
|
138
|
+
CLAUDE_JSON_MANAGED_FIELDS
|
|
131
139
|
);
|
|
132
140
|
|
|
133
141
|
atomicWriteFile(claudeJsonPath, JSON.stringify(mergedJson, null, 2));
|
|
@@ -176,8 +184,7 @@ const restoreCodexConfig = () => {
|
|
|
176
184
|
const mergedConfig = mergeTomlSettings(
|
|
177
185
|
backupConfig,
|
|
178
186
|
currentConfig,
|
|
179
|
-
|
|
180
|
-
'preferred_auth_method', 'requires_openai_auth', 'enableRouteSelection', 'model_providers.aicodeswitch']
|
|
187
|
+
CODEX_CONFIG_MANAGED_FIELDS
|
|
181
188
|
);
|
|
182
189
|
|
|
183
190
|
atomicWriteFile(codexConfigPath, stringifyToml(mergedConfig));
|
|
@@ -214,7 +221,7 @@ const restoreCodexConfig = () => {
|
|
|
214
221
|
const mergedAuth = mergeJsonSettings(
|
|
215
222
|
backupAuth,
|
|
216
223
|
currentAuth,
|
|
217
|
-
|
|
224
|
+
CODEX_AUTH_MANAGED_FIELDS
|
|
218
225
|
);
|
|
219
226
|
|
|
220
227
|
atomicWriteFile(codexAuthPath, JSON.stringify(mergedAuth, null, 2));
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 管理字段定义(CLI 侧)
|
|
3
|
+
* 必须与 src/server/config-managed-fields.ts 保持同步
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Claude Code settings.json 管理字段列表
|
|
8
|
+
*/
|
|
9
|
+
const CLAUDE_SETTINGS_MANAGED_FIELDS = [
|
|
10
|
+
'env.ANTHROPIC_AUTH_TOKEN',
|
|
11
|
+
'env.ANTHROPIC_API_KEY',
|
|
12
|
+
'env.ANTHROPIC_BASE_URL',
|
|
13
|
+
'env.API_TIMEOUT_MS',
|
|
14
|
+
'env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
|
15
|
+
'env.CLAUDE_CODE_MAX_RETRIES',
|
|
16
|
+
'env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
|
|
17
|
+
'env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE',
|
|
18
|
+
'env.ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
19
|
+
'env.ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
20
|
+
'env.ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
21
|
+
'permissions',
|
|
22
|
+
'skipDangerousModePermissionPrompt',
|
|
23
|
+
'effortLevel',
|
|
24
|
+
'model',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Claude Code .claude.json 管理字段列表
|
|
29
|
+
*/
|
|
30
|
+
const CLAUDE_JSON_MANAGED_FIELDS = [
|
|
31
|
+
'hasCompletedOnboarding',
|
|
32
|
+
'mcpServers',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Codex config.toml 管理字段列表
|
|
37
|
+
*/
|
|
38
|
+
const CODEX_CONFIG_MANAGED_FIELDS = [
|
|
39
|
+
'model_provider',
|
|
40
|
+
'model',
|
|
41
|
+
'model_reasoning_effort',
|
|
42
|
+
'disable_response_storage',
|
|
43
|
+
'preferred_auth_method',
|
|
44
|
+
'requires_openai_auth',
|
|
45
|
+
'enableRouteSelection',
|
|
46
|
+
'model_providers.aicodeswitch',
|
|
47
|
+
'mcp_servers',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Codex auth.json 管理字段列表
|
|
52
|
+
*/
|
|
53
|
+
const CODEX_AUTH_MANAGED_FIELDS = [
|
|
54
|
+
'OPENAI_API_KEY',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
CLAUDE_SETTINGS_MANAGED_FIELDS,
|
|
59
|
+
CLAUDE_JSON_MANAGED_FIELDS,
|
|
60
|
+
CODEX_CONFIG_MANAGED_FIELDS,
|
|
61
|
+
CODEX_AUTH_MANAGED_FIELDS,
|
|
62
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.AccessKeyModule = void 0;
|
|
16
|
+
/**
|
|
17
|
+
* AccessKey 模块入口
|
|
18
|
+
* 统一导出所有子模块,并提供模块级别的初始化和持久化方法
|
|
19
|
+
*/
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
22
|
+
const manager_1 = require("./manager");
|
|
23
|
+
const policy_manager_1 = require("./policy-manager");
|
|
24
|
+
const quota_checker_1 = require("./quota-checker");
|
|
25
|
+
const usage_tracker_1 = require("./usage-tracker");
|
|
26
|
+
const key_logger_1 = require("./key-logger");
|
|
27
|
+
const key_session_tracker_1 = require("./key-session-tracker");
|
|
28
|
+
const key_resolver_1 = require("./key-resolver");
|
|
29
|
+
class AccessKeyModule {
|
|
30
|
+
constructor(dataPath) {
|
|
31
|
+
Object.defineProperty(this, "keyManager", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: void 0
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(this, "policyManager", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
value: void 0
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(this, "quotaChecker", {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
writable: true,
|
|
47
|
+
value: void 0
|
|
48
|
+
});
|
|
49
|
+
Object.defineProperty(this, "usageTracker", {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
writable: true,
|
|
53
|
+
value: void 0
|
|
54
|
+
});
|
|
55
|
+
Object.defineProperty(this, "keyLogger", {
|
|
56
|
+
enumerable: true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
writable: true,
|
|
59
|
+
value: void 0
|
|
60
|
+
});
|
|
61
|
+
Object.defineProperty(this, "keySessionTracker", {
|
|
62
|
+
enumerable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true,
|
|
65
|
+
value: void 0
|
|
66
|
+
});
|
|
67
|
+
Object.defineProperty(this, "keyResolver", {
|
|
68
|
+
enumerable: true,
|
|
69
|
+
configurable: true,
|
|
70
|
+
writable: true,
|
|
71
|
+
value: void 0
|
|
72
|
+
});
|
|
73
|
+
Object.defineProperty(this, "accessKeysFile", {
|
|
74
|
+
enumerable: true,
|
|
75
|
+
configurable: true,
|
|
76
|
+
writable: true,
|
|
77
|
+
value: void 0
|
|
78
|
+
});
|
|
79
|
+
Object.defineProperty(this, "policiesFile", {
|
|
80
|
+
enumerable: true,
|
|
81
|
+
configurable: true,
|
|
82
|
+
writable: true,
|
|
83
|
+
value: void 0
|
|
84
|
+
});
|
|
85
|
+
Object.defineProperty(this, "flushTimer", {
|
|
86
|
+
enumerable: true,
|
|
87
|
+
configurable: true,
|
|
88
|
+
writable: true,
|
|
89
|
+
value: null
|
|
90
|
+
});
|
|
91
|
+
this.accessKeysFile = path_1.default.join(dataPath, 'access-keys.json');
|
|
92
|
+
this.policiesFile = path_1.default.join(dataPath, 'policies.json');
|
|
93
|
+
this.keyManager = new manager_1.AccessKeyManager();
|
|
94
|
+
this.policyManager = new policy_manager_1.PolicyManager();
|
|
95
|
+
this.quotaChecker = new quota_checker_1.QuotaChecker();
|
|
96
|
+
this.usageTracker = new usage_tracker_1.UsageTracker(dataPath);
|
|
97
|
+
this.keyLogger = new key_logger_1.KeyLogger(dataPath);
|
|
98
|
+
this.keySessionTracker = new key_session_tracker_1.KeySessionTracker(dataPath);
|
|
99
|
+
this.keyResolver = new key_resolver_1.KeyResolver(this.keyManager, this.policyManager);
|
|
100
|
+
}
|
|
101
|
+
/** 初始化模块 */
|
|
102
|
+
initialize() {
|
|
103
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
// 加载持久化数据
|
|
105
|
+
const [keys, policies] = yield Promise.all([
|
|
106
|
+
this.loadJsonFile(this.accessKeysFile, []),
|
|
107
|
+
this.loadJsonFile(this.policiesFile, []),
|
|
108
|
+
]);
|
|
109
|
+
this.keyManager.load(keys);
|
|
110
|
+
this.policyManager.load(policies);
|
|
111
|
+
// 初始化子模块
|
|
112
|
+
yield Promise.all([
|
|
113
|
+
this.usageTracker.initialize(),
|
|
114
|
+
this.keyLogger.initialize(),
|
|
115
|
+
this.keySessionTracker.initialize(),
|
|
116
|
+
]);
|
|
117
|
+
// 启动自动刷新
|
|
118
|
+
this.usageTracker.startAutoFlush();
|
|
119
|
+
this.startAutoSave();
|
|
120
|
+
console.log(`[AccessKey] Module initialized: ${keys.length} keys, ${policies.length} policies`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** 保存所有数据到磁盘 */
|
|
124
|
+
save() {
|
|
125
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
126
|
+
yield Promise.all([
|
|
127
|
+
this.saveJsonFile(this.accessKeysFile, this.keyManager.dump()),
|
|
128
|
+
this.saveJsonFile(this.policiesFile, this.policyManager.dump()),
|
|
129
|
+
this.usageTracker.flush(),
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** 关闭模块 */
|
|
134
|
+
shutdown() {
|
|
135
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
136
|
+
if (this.flushTimer) {
|
|
137
|
+
clearInterval(this.flushTimer);
|
|
138
|
+
this.flushTimer = null;
|
|
139
|
+
}
|
|
140
|
+
this.usageTracker.stopAutoFlush();
|
|
141
|
+
yield this.save();
|
|
142
|
+
console.log('[AccessKey] Module shutdown completed');
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** 标记数据已修改,需要保存 */
|
|
146
|
+
markDirty() {
|
|
147
|
+
// 下一个自动保存周期会处理
|
|
148
|
+
}
|
|
149
|
+
startAutoSave() {
|
|
150
|
+
this.flushTimer = setInterval(() => {
|
|
151
|
+
this.save().catch(err => console.error('[AccessKey] Auto save error:', err));
|
|
152
|
+
}, 5000);
|
|
153
|
+
}
|
|
154
|
+
loadJsonFile(filePath, defaultValue) {
|
|
155
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
156
|
+
try {
|
|
157
|
+
const data = yield promises_1.default.readFile(filePath, 'utf-8');
|
|
158
|
+
return JSON.parse(data);
|
|
159
|
+
}
|
|
160
|
+
catch (_a) {
|
|
161
|
+
return defaultValue;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
saveJsonFile(filePath, data) {
|
|
166
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
167
|
+
const tmpPath = filePath + '.tmp';
|
|
168
|
+
yield promises_1.default.writeFile(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
169
|
+
yield promises_1.default.rename(tmpPath, filePath);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exports.AccessKeyModule = AccessKeyModule;
|
|
@@ -0,0 +1,362 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
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"));
|
|
23
|
+
class KeyLogger {
|
|
24
|
+
constructor(dataPath) {
|
|
25
|
+
Object.defineProperty(this, "dataPath", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: void 0
|
|
30
|
+
});
|
|
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;
|
|
58
|
+
}
|
|
59
|
+
/** 初始化 */
|
|
60
|
+
initialize() {
|
|
61
|
+
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 });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** 写入一条日志 */
|
|
67
|
+
addLog(keyId, keyName, logData) {
|
|
68
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
69
|
+
const log = Object.assign(Object.assign({}, logData), { id: crypto_1.default.randomUUID(), keyId,
|
|
70
|
+
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);
|
|
105
|
+
}
|
|
106
|
+
// 写入日志(带锁)
|
|
107
|
+
yield this.writeToShard(keyId, targetShard, log);
|
|
108
|
+
// 更新索引
|
|
109
|
+
targetShard.count += 1;
|
|
110
|
+
targetShard.endTime = Date.now();
|
|
111
|
+
yield this.saveShardIndex(keyId, index);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** 获取 Key 的日志列表(分页 + 过滤) */
|
|
115
|
+
getLogs(keyId, options) {
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
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
|
+
const offset = (options.page - 1) * options.pageSize;
|
|
130
|
+
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);
|
|
212
|
+
}
|
|
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);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/** 按 sessionId 过滤日志(用于密钥会话的日志查询) */
|
|
224
|
+
getLogsBySessionId(keyId_1, sessionId_1) {
|
|
225
|
+
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);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
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) {
|
|
309
|
+
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);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
writeToShard(keyId, shard, log) {
|
|
318
|
+
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* () {
|
|
325
|
+
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);
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
this.shardWriteLocks.delete(lockKey);
|
|
343
|
+
}
|
|
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
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
exports.KeyLogger = KeyLogger;
|