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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QuotaChecker = void 0;
|
|
4
|
+
class QuotaChecker {
|
|
5
|
+
constructor() {
|
|
6
|
+
/** per keyId: 滑动窗口(60秒) */
|
|
7
|
+
Object.defineProperty(this, "rpmTracker", {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true,
|
|
11
|
+
value: new Map()
|
|
12
|
+
});
|
|
13
|
+
/** per keyId: 当前并发数 */
|
|
14
|
+
Object.defineProperty(this, "concurrentTracker", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: new Map()
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 检查所有配额限制,返回 null 表示通过,否则返回错误信息
|
|
23
|
+
*/
|
|
24
|
+
checkQuota(policy, usage, keyId, requestModel) {
|
|
25
|
+
// 1. 模型过滤
|
|
26
|
+
if (requestModel) {
|
|
27
|
+
if (policy.allowedModels && policy.allowedModels.length > 0) {
|
|
28
|
+
if (!policy.allowedModels.some(m => this.matchModel(m, requestModel))) {
|
|
29
|
+
return { error: 'MODEL_NOT_ALLOWED', message: `模型 ${requestModel} 不在允许列表中`, httpStatus: 403 };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (policy.blockedModels && policy.blockedModels.length > 0) {
|
|
33
|
+
if (policy.blockedModels.some(m => this.matchModel(m, requestModel))) {
|
|
34
|
+
return { error: 'MODEL_NOT_ALLOWED', message: `模型 ${requestModel} 已被禁止使用`, httpStatus: 403 };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!usage)
|
|
39
|
+
return null; // 无用量数据,跳过其余检查
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
// 2. Token 日限额
|
|
42
|
+
if (policy.dailyTokenLimit) {
|
|
43
|
+
const limit = policy.dailyTokenLimit * 1000; // k → 实际 tokens
|
|
44
|
+
if (this.isPeriodExpired(usage.periods.daily.periodStart, 'daily')) {
|
|
45
|
+
// 周期已重置,通过
|
|
46
|
+
}
|
|
47
|
+
else if (usage.periods.daily.tokens >= limit) {
|
|
48
|
+
return { error: 'TOKEN_QUOTA_EXCEEDED', message: '今日 Token 配额已用尽', httpStatus: 429, dimension: 'dailyTokenLimit', usage: usage.periods.daily.tokens, limit };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 3. Token 周限额
|
|
52
|
+
if (policy.weeklyTokenLimit) {
|
|
53
|
+
const limit = policy.weeklyTokenLimit * 1000;
|
|
54
|
+
if (this.isPeriodExpired(usage.periods.weekly.periodStart, 'weekly')) {
|
|
55
|
+
// 周期已重置,通过
|
|
56
|
+
}
|
|
57
|
+
else if (usage.periods.weekly.tokens >= limit) {
|
|
58
|
+
return { error: 'TOKEN_QUOTA_EXCEEDED', message: '本周 Token 配额已用尽', httpStatus: 429, dimension: 'weeklyTokenLimit', usage: usage.periods.weekly.tokens, limit };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 4. Token 月限额
|
|
62
|
+
if (policy.monthlyTokenLimit) {
|
|
63
|
+
const limit = policy.monthlyTokenLimit * 1000;
|
|
64
|
+
if (this.isPeriodExpired(usage.periods.monthly.periodStart, 'monthly')) {
|
|
65
|
+
// 周期已重置,通过
|
|
66
|
+
}
|
|
67
|
+
else if (usage.periods.monthly.tokens >= limit) {
|
|
68
|
+
return { error: 'TOKEN_QUOTA_EXCEEDED', message: '本月 Token 配额已用尽', httpStatus: 429, dimension: 'monthlyTokenLimit', usage: usage.periods.monthly.tokens, limit };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 5. Token 自定义周期
|
|
72
|
+
if (policy.customTokenLimit && policy.customTokenResetHours && usage.periods.custom) {
|
|
73
|
+
const limit = policy.customTokenLimit * 1000;
|
|
74
|
+
const resetMs = policy.customTokenResetHours * 3600 * 1000;
|
|
75
|
+
if (now - usage.periods.custom.periodStart >= resetMs) {
|
|
76
|
+
// 周期已重置,通过
|
|
77
|
+
}
|
|
78
|
+
else if (usage.periods.custom.tokens >= limit) {
|
|
79
|
+
return { error: 'TOKEN_QUOTA_EXCEEDED', message: 'Token 配额已用尽', httpStatus: 429, dimension: 'customTokenLimit', usage: usage.periods.custom.tokens, limit };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// 6. 请求日限额
|
|
83
|
+
if (policy.dailyRequestLimit) {
|
|
84
|
+
if (!this.isPeriodExpired(usage.periods.daily.periodStart, 'daily') && usage.periods.daily.requests >= policy.dailyRequestLimit) {
|
|
85
|
+
return { error: 'REQUEST_QUOTA_EXCEEDED', message: '今日请求次数已用尽', httpStatus: 429, dimension: 'dailyRequestLimit', usage: usage.periods.daily.requests, limit: policy.dailyRequestLimit };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 7. 请求周限额
|
|
89
|
+
if (policy.weeklyRequestLimit) {
|
|
90
|
+
if (!this.isPeriodExpired(usage.periods.weekly.periodStart, 'weekly') && usage.periods.weekly.requests >= policy.weeklyRequestLimit) {
|
|
91
|
+
return { error: 'REQUEST_QUOTA_EXCEEDED', message: '本周请求次数已用尽', httpStatus: 429, dimension: 'weeklyRequestLimit', usage: usage.periods.weekly.requests, limit: policy.weeklyRequestLimit };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// 8. 请求月限额
|
|
95
|
+
if (policy.monthlyRequestLimit) {
|
|
96
|
+
if (!this.isPeriodExpired(usage.periods.monthly.periodStart, 'monthly') && usage.periods.monthly.requests >= policy.monthlyRequestLimit) {
|
|
97
|
+
return { error: 'REQUEST_QUOTA_EXCEEDED', message: '本月请求次数已用尽', httpStatus: 429, dimension: 'monthlyRequestLimit', usage: usage.periods.monthly.requests, limit: policy.monthlyRequestLimit };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 9. 请求自定义周期
|
|
101
|
+
if (policy.customRequestLimit && policy.customRequestResetHours && usage.periods.custom) {
|
|
102
|
+
const resetMs = policy.customRequestResetHours * 3600 * 1000;
|
|
103
|
+
if (now - usage.periods.custom.periodStart < resetMs && usage.periods.custom.requests >= policy.customRequestLimit) {
|
|
104
|
+
return { error: 'REQUEST_QUOTA_EXCEEDED', message: '请求次数配额已用尽', httpStatus: 429, dimension: 'customRequestLimit', usage: usage.periods.custom.requests, limit: policy.customRequestLimit };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 10. RPM 检查
|
|
108
|
+
if (policy.rpmLimit) {
|
|
109
|
+
const counter = this.getRpmCounter(keyId);
|
|
110
|
+
this.cleanExpiredTimestamps(counter, now);
|
|
111
|
+
if (counter.timestamps.length >= policy.rpmLimit) {
|
|
112
|
+
return { error: 'RPM_LIMIT_EXCEEDED', message: `每分钟请求数超过限制 (${policy.rpmLimit})`, httpStatus: 429, dimension: 'rpmLimit', usage: counter.timestamps.length, limit: policy.rpmLimit };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// 11. 并发检查
|
|
116
|
+
if (policy.concurrentLimit) {
|
|
117
|
+
const current = this.concurrentTracker.get(keyId) || 0;
|
|
118
|
+
if (current >= policy.concurrentLimit) {
|
|
119
|
+
return { error: 'CONCURRENT_LIMIT_EXCEEDED', message: `并发请求数超过限制 (${policy.concurrentLimit})`, httpStatus: 429, dimension: 'concurrentLimit', usage: current, limit: policy.concurrentLimit };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null; // 所有检查通过
|
|
123
|
+
}
|
|
124
|
+
/** 请求开始:增加并发计数 + RPM 计数 */
|
|
125
|
+
onRequestStart(keyId, policy) {
|
|
126
|
+
// 并发 +1
|
|
127
|
+
const current = this.concurrentTracker.get(keyId) || 0;
|
|
128
|
+
this.concurrentTracker.set(keyId, current + 1);
|
|
129
|
+
// RPM +1
|
|
130
|
+
if (policy.rpmLimit) {
|
|
131
|
+
const counter = this.getRpmCounter(keyId);
|
|
132
|
+
counter.timestamps.push(Date.now());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** 请求结束:减少并发计数 */
|
|
136
|
+
onRequestEnd(keyId) {
|
|
137
|
+
const current = this.concurrentTracker.get(keyId) || 0;
|
|
138
|
+
this.concurrentTracker.set(keyId, Math.max(0, current - 1));
|
|
139
|
+
}
|
|
140
|
+
/** 获取当前并发数 */
|
|
141
|
+
getConcurrentCount(keyId) {
|
|
142
|
+
return this.concurrentTracker.get(keyId) || 0;
|
|
143
|
+
}
|
|
144
|
+
/** 获取当前 RPM */
|
|
145
|
+
getCurrentRpm(keyId) {
|
|
146
|
+
const counter = this.rpmTracker.get(keyId);
|
|
147
|
+
if (!counter)
|
|
148
|
+
return 0;
|
|
149
|
+
this.cleanExpiredTimestamps(counter, Date.now());
|
|
150
|
+
return counter.timestamps.length;
|
|
151
|
+
}
|
|
152
|
+
// ---- helpers ----
|
|
153
|
+
getRpmCounter(keyId) {
|
|
154
|
+
let counter = this.rpmTracker.get(keyId);
|
|
155
|
+
if (!counter) {
|
|
156
|
+
counter = { timestamps: [] };
|
|
157
|
+
this.rpmTracker.set(keyId, counter);
|
|
158
|
+
}
|
|
159
|
+
return counter;
|
|
160
|
+
}
|
|
161
|
+
cleanExpiredTimestamps(counter, now) {
|
|
162
|
+
const windowMs = 60000; // 60 seconds
|
|
163
|
+
counter.timestamps = counter.timestamps.filter(t => now - t < windowMs);
|
|
164
|
+
}
|
|
165
|
+
isPeriodExpired(periodStart, type) {
|
|
166
|
+
const now = new Date();
|
|
167
|
+
switch (type) {
|
|
168
|
+
case 'daily': {
|
|
169
|
+
// UTC 00:00 重置
|
|
170
|
+
const todayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
171
|
+
return periodStart < todayStart;
|
|
172
|
+
}
|
|
173
|
+
case 'weekly': {
|
|
174
|
+
// 周一 UTC 00:00 重置
|
|
175
|
+
const day = now.getUTCDay();
|
|
176
|
+
const mondayOffset = day === 0 ? 6 : day - 1;
|
|
177
|
+
const mondayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - mondayOffset);
|
|
178
|
+
return periodStart < mondayStart;
|
|
179
|
+
}
|
|
180
|
+
case 'monthly': {
|
|
181
|
+
// 每月 1 日 UTC 00:00 重置
|
|
182
|
+
const monthStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1);
|
|
183
|
+
return periodStart < monthStart;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** 模型名匹配(支持前缀匹配) */
|
|
188
|
+
matchModel(pattern, model) {
|
|
189
|
+
if (pattern === model)
|
|
190
|
+
return true;
|
|
191
|
+
// 支持前缀匹配,如 "claude-sonnet" 匹配 "claude-sonnet-4-20250514"
|
|
192
|
+
if (model.startsWith(pattern))
|
|
193
|
+
return true;
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
exports.QuotaChecker = QuotaChecker;
|
|
@@ -0,0 +1,279 @@
|
|
|
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.UsageTracker = void 0;
|
|
16
|
+
/**
|
|
17
|
+
* Key 级用量追踪器
|
|
18
|
+
* 负责用量的持久化、周期重置、历史记录
|
|
19
|
+
*/
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
22
|
+
class UsageTracker {
|
|
23
|
+
constructor(dataPath) {
|
|
24
|
+
Object.defineProperty(this, "dataPath", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: void 0
|
|
29
|
+
});
|
|
30
|
+
/** 内存缓存 keyId → KeyUsage */
|
|
31
|
+
Object.defineProperty(this, "cache", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: new Map()
|
|
36
|
+
});
|
|
37
|
+
/** 脏数据标记 */
|
|
38
|
+
Object.defineProperty(this, "dirty", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: new Set()
|
|
43
|
+
});
|
|
44
|
+
/** debounce 写入定时器 */
|
|
45
|
+
Object.defineProperty(this, "flushTimer", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
writable: true,
|
|
49
|
+
value: null
|
|
50
|
+
});
|
|
51
|
+
Object.defineProperty(this, "FLUSH_INTERVAL", {
|
|
52
|
+
enumerable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
writable: true,
|
|
55
|
+
value: 5000
|
|
56
|
+
}); // 5 秒
|
|
57
|
+
this.dataPath = dataPath;
|
|
58
|
+
}
|
|
59
|
+
/** 初始化,确保目录存在 */
|
|
60
|
+
initialize() {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
const usageDir = path_1.default.join(this.dataPath, 'key-usage');
|
|
63
|
+
yield promises_1.default.mkdir(usageDir, { recursive: true });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** 获取 KeyUsage(优先从缓存读取) */
|
|
67
|
+
getUsage(keyId) {
|
|
68
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
69
|
+
if (this.cache.has(keyId)) {
|
|
70
|
+
return this.cache.get(keyId);
|
|
71
|
+
}
|
|
72
|
+
const usage = yield this.loadUsageFile(keyId);
|
|
73
|
+
this.cache.set(keyId, usage);
|
|
74
|
+
return usage;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** 记录一次请求的 Token 消耗 */
|
|
78
|
+
recordTokenUsage(keyId, tokenUsage) {
|
|
79
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
const usage = yield this.getUsage(keyId);
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const totalTokens = tokenUsage.totalTokens || (tokenUsage.inputTokens + tokenUsage.outputTokens);
|
|
83
|
+
// 更新 lifetime
|
|
84
|
+
usage.lifetime.totalTokens += totalTokens;
|
|
85
|
+
usage.lifetime.inputTokens += tokenUsage.inputTokens;
|
|
86
|
+
usage.lifetime.outputTokens += tokenUsage.outputTokens;
|
|
87
|
+
usage.lifetime.totalRequests += 1;
|
|
88
|
+
// 更新周期用量(自动检测重置)
|
|
89
|
+
this.updatePeriodTokens(usage.periods.daily, now, 'daily', totalTokens);
|
|
90
|
+
this.updatePeriodTokens(usage.periods.weekly, now, 'weekly', totalTokens);
|
|
91
|
+
this.updatePeriodTokens(usage.periods.monthly, now, 'monthly', totalTokens);
|
|
92
|
+
if (usage.periods.custom) {
|
|
93
|
+
this.updatePeriodTokens(usage.periods.custom, now, 'custom', totalTokens);
|
|
94
|
+
}
|
|
95
|
+
// 更新日历史
|
|
96
|
+
this.updateDailyHistory(usage, now, totalTokens, 0);
|
|
97
|
+
// 标记脏数据
|
|
98
|
+
this.markDirty(keyId);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** 记录一次请求(仅计数,无 Token) */
|
|
102
|
+
recordRequest(keyId) {
|
|
103
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
const usage = yield this.getUsage(keyId);
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
// 更新周期请求计数
|
|
107
|
+
this.updatePeriodRequests(usage.periods.daily, now, 'daily');
|
|
108
|
+
this.updatePeriodRequests(usage.periods.weekly, now, 'weekly');
|
|
109
|
+
this.updatePeriodRequests(usage.periods.monthly, now, 'monthly');
|
|
110
|
+
if (usage.periods.custom) {
|
|
111
|
+
this.updatePeriodRequests(usage.periods.custom, now, 'custom');
|
|
112
|
+
}
|
|
113
|
+
this.markDirty(keyId);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** 记录错误 */
|
|
117
|
+
recordError(keyId) {
|
|
118
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
119
|
+
const usage = yield this.getUsage(keyId);
|
|
120
|
+
usage.lifetime.errorCount += 1;
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const today = this.formatDate(now);
|
|
123
|
+
const record = usage.dailyHistory.find(h => h.date === today);
|
|
124
|
+
if (record) {
|
|
125
|
+
record.errors += 1;
|
|
126
|
+
}
|
|
127
|
+
this.markDirty(keyId);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** 获取用量趋势 */
|
|
131
|
+
getTrend(keyId_1) {
|
|
132
|
+
return __awaiter(this, arguments, void 0, function* (keyId, days = 30) {
|
|
133
|
+
const usage = yield this.getUsage(keyId);
|
|
134
|
+
return usage.dailyHistory.slice(-days);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** 刷新脏数据到磁盘 */
|
|
138
|
+
flush() {
|
|
139
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
if (this.dirty.size === 0)
|
|
141
|
+
return;
|
|
142
|
+
const keys = Array.from(this.dirty);
|
|
143
|
+
this.dirty.clear();
|
|
144
|
+
yield Promise.all(keys.map(keyId => this.saveUsageFile(keyId)));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** 定时刷新 */
|
|
148
|
+
startAutoFlush() {
|
|
149
|
+
this.flushTimer = setInterval(() => {
|
|
150
|
+
this.flush().catch(err => console.error('[UsageTracker] Auto flush error:', err));
|
|
151
|
+
}, this.FLUSH_INTERVAL);
|
|
152
|
+
}
|
|
153
|
+
/** 停止自动刷新 */
|
|
154
|
+
stopAutoFlush() {
|
|
155
|
+
if (this.flushTimer) {
|
|
156
|
+
clearInterval(this.flushTimer);
|
|
157
|
+
this.flushTimer = null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ---- helpers ----
|
|
161
|
+
loadUsageFile(keyId) {
|
|
162
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
163
|
+
const filePath = this.getUsageFilePath(keyId);
|
|
164
|
+
try {
|
|
165
|
+
const data = yield promises_1.default.readFile(filePath, 'utf-8');
|
|
166
|
+
return JSON.parse(data);
|
|
167
|
+
}
|
|
168
|
+
catch (_a) {
|
|
169
|
+
return this.createEmptyUsage(keyId);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
saveUsageFile(keyId) {
|
|
174
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
175
|
+
const usage = this.cache.get(keyId);
|
|
176
|
+
if (!usage)
|
|
177
|
+
return;
|
|
178
|
+
const filePath = this.getUsageFilePath(keyId);
|
|
179
|
+
const dir = path_1.default.dirname(filePath);
|
|
180
|
+
yield promises_1.default.mkdir(dir, { recursive: true });
|
|
181
|
+
// 原子写入
|
|
182
|
+
const tmpPath = filePath + '.tmp';
|
|
183
|
+
yield promises_1.default.writeFile(tmpPath, JSON.stringify(usage, null, 2), 'utf-8');
|
|
184
|
+
yield promises_1.default.rename(tmpPath, filePath);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
getUsageFilePath(keyId) {
|
|
188
|
+
return path_1.default.join(this.dataPath, 'key-usage', `${keyId}.json`);
|
|
189
|
+
}
|
|
190
|
+
createEmptyUsage(keyId) {
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
return {
|
|
193
|
+
keyId,
|
|
194
|
+
lifetime: { totalTokens: 0, inputTokens: 0, outputTokens: 0, totalRequests: 0, errorCount: 0 },
|
|
195
|
+
periods: {
|
|
196
|
+
daily: { tokens: 0, requests: 0, periodStart: now },
|
|
197
|
+
weekly: { tokens: 0, requests: 0, periodStart: now },
|
|
198
|
+
monthly: { tokens: 0, requests: 0, periodStart: now },
|
|
199
|
+
},
|
|
200
|
+
dailyHistory: [],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** 更新周期 Token(自动检测重置) */
|
|
204
|
+
updatePeriodTokens(period, now, type, tokens) {
|
|
205
|
+
if (this.isPeriodExpired(period.periodStart, now, type)) {
|
|
206
|
+
period.tokens = tokens;
|
|
207
|
+
period.requests = 0;
|
|
208
|
+
period.periodStart = now;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
period.tokens += tokens;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** 更新周期请求计数(自动检测重置) */
|
|
215
|
+
updatePeriodRequests(period, now, type) {
|
|
216
|
+
if (this.isPeriodExpired(period.periodStart, now, type)) {
|
|
217
|
+
period.requests = 1;
|
|
218
|
+
period.tokens = 0;
|
|
219
|
+
period.periodStart = now;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
period.requests += 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** 检查周期是否已过期(需要重置) */
|
|
226
|
+
isPeriodExpired(periodStart, now, type) {
|
|
227
|
+
switch (type) {
|
|
228
|
+
case 'daily': {
|
|
229
|
+
const d1 = new Date(periodStart);
|
|
230
|
+
const d2 = new Date(now);
|
|
231
|
+
return d1.getUTCFullYear() !== d2.getUTCFullYear() ||
|
|
232
|
+
d1.getUTCMonth() !== d2.getUTCMonth() ||
|
|
233
|
+
d1.getUTCDate() !== d2.getUTCDate();
|
|
234
|
+
}
|
|
235
|
+
case 'weekly': {
|
|
236
|
+
// 简单判断:如果超过 7 天
|
|
237
|
+
return (now - periodStart) > 7 * 24 * 3600 * 1000;
|
|
238
|
+
}
|
|
239
|
+
case 'monthly': {
|
|
240
|
+
const d1 = new Date(periodStart);
|
|
241
|
+
const d2 = new Date(now);
|
|
242
|
+
return d1.getUTCFullYear() !== d2.getUTCFullYear() || d1.getUTCMonth() !== d2.getUTCMonth();
|
|
243
|
+
}
|
|
244
|
+
case 'custom':
|
|
245
|
+
// 自定义周期由外部处理
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** 更新日历史记录 */
|
|
250
|
+
updateDailyHistory(usage, now, tokens, errors) {
|
|
251
|
+
const today = this.formatDate(now);
|
|
252
|
+
const existing = usage.dailyHistory.find(h => h.date === today);
|
|
253
|
+
if (existing) {
|
|
254
|
+
existing.tokens += tokens;
|
|
255
|
+
existing.requests += 1;
|
|
256
|
+
existing.errors += errors;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
usage.dailyHistory.push({
|
|
260
|
+
date: today,
|
|
261
|
+
tokens,
|
|
262
|
+
requests: 1,
|
|
263
|
+
errors,
|
|
264
|
+
});
|
|
265
|
+
// 保留最近 90 天
|
|
266
|
+
if (usage.dailyHistory.length > 90) {
|
|
267
|
+
usage.dailyHistory = usage.dailyHistory.slice(-90);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
formatDate(ts) {
|
|
272
|
+
const d = new Date(ts);
|
|
273
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
|
|
274
|
+
}
|
|
275
|
+
markDirty(keyId) {
|
|
276
|
+
this.dirty.add(keyId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
exports.UsageTracker = UsageTracker;
|
package/dist/server/auth.js
CHANGED
|
@@ -60,6 +60,10 @@ function verifyToken(token) {
|
|
|
60
60
|
*
|
|
61
61
|
* 如果未启用鉴权,直接放行
|
|
62
62
|
* 如果启用鉴权但 token 无效,返回 401
|
|
63
|
+
*
|
|
64
|
+
* 认证 Header 策略:
|
|
65
|
+
* - `Access-Token: <jwt_token>` — 管理面板 JWT 认证
|
|
66
|
+
* - `Authorization: Bearer ...` — 可能是 AccessKey(sk_ 前缀),跳过此中间件,由代理引擎处理
|
|
63
67
|
*/
|
|
64
68
|
function authMiddleware(req, res, next) {
|
|
65
69
|
// 如果未启用鉴权,直接放行
|
|
@@ -67,14 +71,22 @@ function authMiddleware(req, res, next) {
|
|
|
67
71
|
next();
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
70
|
-
//
|
|
74
|
+
// 如果请求带有 Authorization header,视为 AccessKey 请求,跳过管理面板认证
|
|
75
|
+
// 由代理引擎在业务前置对 AccessKey 进行鉴权
|
|
71
76
|
const authHeader = req.headers.authorization;
|
|
72
|
-
if (
|
|
77
|
+
if (authHeader) {
|
|
78
|
+
next();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// 从 Access-Token header 或查询参数中提取 JWT token
|
|
82
|
+
// 查询参数支持用于 EventSource 等 API(无法设置自定义 Header)
|
|
83
|
+
const accessToken = req.headers['access-token']
|
|
84
|
+
|| req.query.token;
|
|
85
|
+
if (!accessToken) {
|
|
73
86
|
res.status(401).json({ error: 'Unauthorized: Missing or invalid token' });
|
|
74
87
|
return;
|
|
75
88
|
}
|
|
76
|
-
|
|
77
|
-
if (verifyToken(token)) {
|
|
89
|
+
if (verifyToken(accessToken)) {
|
|
78
90
|
next();
|
|
79
91
|
}
|
|
80
92
|
else {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 编程套餐 Headers 覆盖模块
|
|
4
|
+
*
|
|
5
|
+
* 当 APIService 启用 enableCodingPlan 时,将发送到上游的请求 Headers
|
|
6
|
+
* 覆盖为对应编程工具(Claude Code / Codex)的标准 Headers,
|
|
7
|
+
* 使供应商验证通过。
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.applyCodingPlanHeaders = applyCodingPlanHeaders;
|
|
14
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
15
|
+
/**
|
|
16
|
+
* 代理已设置的需要保留的 Headers
|
|
17
|
+
* 这些 Headers 由 buildUpstreamHeaders 设置,不能被覆盖删除
|
|
18
|
+
*/
|
|
19
|
+
const KEEP_HEADERS = new Set([
|
|
20
|
+
'authorization', // 认证头
|
|
21
|
+
'x-api-key', // Claude 认证头
|
|
22
|
+
'x-goog-api-key', // Gemini 认证头
|
|
23
|
+
'content-type', // 内容类型
|
|
24
|
+
'accept', // 接受类型
|
|
25
|
+
'accept-encoding', // 编码
|
|
26
|
+
'connection', // 连接
|
|
27
|
+
'content-length', // 内容长度
|
|
28
|
+
'anthropic-version', // Claude API 版本
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* 构建 Claude Code 标准请求 Headers
|
|
32
|
+
*/
|
|
33
|
+
function buildClaudeCodeHeaders(sessionId) {
|
|
34
|
+
return {
|
|
35
|
+
'user-agent': 'claude-cli/2.1.168 (external, claude-vscode, agent-sdk/0.3.168)',
|
|
36
|
+
'x-claude-code-session-id': sessionId,
|
|
37
|
+
'x-stainless-arch': 'arm64',
|
|
38
|
+
'x-stainless-lang': 'js',
|
|
39
|
+
'x-stainless-os': 'MacOS',
|
|
40
|
+
'x-stainless-package-version': '0.94.0',
|
|
41
|
+
'x-stainless-retry-count': '0',
|
|
42
|
+
'x-stainless-runtime': 'node',
|
|
43
|
+
'x-stainless-runtime-version': 'v24.3.0',
|
|
44
|
+
'x-stainless-timeout': '3000',
|
|
45
|
+
'anthropic-beta': 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,effort-2025-11-24',
|
|
46
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
47
|
+
'x-app': 'cli',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 构建 Codex 标准请求 Headers
|
|
52
|
+
*/
|
|
53
|
+
function buildCodexHeaders(sessionId) {
|
|
54
|
+
return {
|
|
55
|
+
'x-codex-beta-features': 'terminal_resize_reflow,remote_compaction_v2',
|
|
56
|
+
'x-codex-turn-metadata': JSON.stringify({
|
|
57
|
+
session_id: sessionId,
|
|
58
|
+
thread_id: sessionId,
|
|
59
|
+
thread_source: 'user',
|
|
60
|
+
turn_id: crypto_1.default.randomUUID(),
|
|
61
|
+
sandbox: 'none',
|
|
62
|
+
workspace_kind: 'project',
|
|
63
|
+
request_kind: 'turn',
|
|
64
|
+
}),
|
|
65
|
+
'x-codex-window-id': `${sessionId}:0`,
|
|
66
|
+
'x-client-request-id': sessionId,
|
|
67
|
+
'session-id': sessionId,
|
|
68
|
+
'thread-id': sessionId,
|
|
69
|
+
'originator': 'codex_vscode',
|
|
70
|
+
'user-agent': 'codex_vscode/0.137.0-alpha.4 (Mac OS 26.5.0; arm64) unknown (VS Code; 26.602.40724)',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 判断 sourceType 是否为 Claude 源
|
|
75
|
+
*/
|
|
76
|
+
function isClaudeSourceType(sourceType) {
|
|
77
|
+
return sourceType === 'claude' || sourceType === 'claude-chat';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 判断 sourceType 是否为 OpenAI 源
|
|
81
|
+
*/
|
|
82
|
+
function isOpenAISourceType(sourceType) {
|
|
83
|
+
return sourceType === 'openai' || sourceType === 'openai-chat';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 应用编程工具 Headers 覆盖
|
|
87
|
+
*
|
|
88
|
+
* 当 service.enableCodingPlan 为 true 时调用。
|
|
89
|
+
* 清除原始请求中无关的 Headers,注入对应编程工具的标准 Headers。
|
|
90
|
+
*
|
|
91
|
+
* - Claude 源(claude/claude-chat)→ 注入 Claude Code Headers
|
|
92
|
+
* - OpenAI 源(openai/openai-chat)→ 注入 Codex Headers
|
|
93
|
+
* - Gemini 源不处理,保持原样
|
|
94
|
+
*
|
|
95
|
+
* @param headers 当前已构建的上游 Headers(会被原地修改)
|
|
96
|
+
* @param sourceType 上游服务的源类型
|
|
97
|
+
*/
|
|
98
|
+
function applyCodingPlanHeaders(headers, sourceType) {
|
|
99
|
+
const isClaude = isClaudeSourceType(sourceType);
|
|
100
|
+
const isOpenAI = isOpenAISourceType(sourceType);
|
|
101
|
+
// Gemini 源不需要 Headers 覆盖
|
|
102
|
+
if (!isClaude && !isOpenAI) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const sessionId = crypto_1.default.randomUUID();
|
|
106
|
+
// 1. 删除不在保留列表中的 Headers
|
|
107
|
+
for (const key of Object.keys(headers)) {
|
|
108
|
+
if (!KEEP_HEADERS.has(key.toLowerCase())) {
|
|
109
|
+
delete headers[key];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// 2. 注入编程工具标准 Headers
|
|
113
|
+
// 2. 注入编程工具标准 Headers
|
|
114
|
+
const toolHeaders = isClaude
|
|
115
|
+
? buildClaudeCodeHeaders(sessionId)
|
|
116
|
+
: buildCodexHeaders(sessionId);
|
|
117
|
+
for (const [key, value] of Object.entries(toolHeaders)) {
|
|
118
|
+
headers[key] = value;
|
|
119
|
+
}
|
|
120
|
+
console.log(`\x1b[36m[CodingPlan-Headers]\x1b[0m Applied ${isClaude ? 'Claude Code' : 'Codex'} header override for upstream sourceType=${sourceType}`);
|
|
121
|
+
}
|
|
@@ -6,9 +6,11 @@ exports.getManagedFields = exports.CODEX_AUTH_MANAGED_FIELDS = exports.CODEX_CON
|
|
|
6
6
|
*/
|
|
7
7
|
exports.CLAUDE_SETTINGS_MANAGED_FIELDS = [
|
|
8
8
|
{ path: ['env', 'ANTHROPIC_AUTH_TOKEN'] },
|
|
9
|
+
{ path: ['env', 'ANTHROPIC_API_KEY'], optional: true },
|
|
9
10
|
{ path: ['env', 'ANTHROPIC_BASE_URL'] },
|
|
10
11
|
{ path: ['env', 'API_TIMEOUT_MS'] },
|
|
11
12
|
{ path: ['env', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'] },
|
|
13
|
+
{ path: ['env', 'CLAUDE_CODE_MAX_RETRIES'] },
|
|
12
14
|
{ path: ['env', 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'], optional: true },
|
|
13
15
|
{ path: ['env', 'CLAUDE_AUTOCOMPACT_PCT_OVERRIDE'], optional: true },
|
|
14
16
|
{ path: ['env', 'ANTHROPIC_DEFAULT_HAIKU_MODEL'], optional: true },
|
|
@@ -91,6 +91,7 @@ const response_js_12 = require("./pairs/gemini-responses/response.js");
|
|
|
91
91
|
const streaming_js_12 = require("./pairs/gemini-responses/streaming.js");
|
|
92
92
|
// --- Provider-driven post-processing ---
|
|
93
93
|
const mapper_js_2 = require("./thinking/mapper.js");
|
|
94
|
+
const tool_result_js_1 = require("./utils/tool-result.js");
|
|
94
95
|
const effort_js_1 = require("./thinking/effort.js");
|
|
95
96
|
// ============================================================
|
|
96
97
|
// Public API: Request Transformation
|
|
@@ -294,6 +295,13 @@ function buildTargetBody(options) {
|
|
|
294
295
|
if (toFormat === 'claude' && result.thinking && result.messages) {
|
|
295
296
|
result.messages = (0, mapper_js_2.fixThinkingHistory)(result.messages, 'claude');
|
|
296
297
|
}
|
|
298
|
+
// --- Ensure tool_result blocks have id for Claude-compatible providers ---
|
|
299
|
+
// Some providers (e.g. GLM) require an id field on tool_result content blocks,
|
|
300
|
+
// but standard Claude API tool_result blocks only have tool_use_id without id.
|
|
301
|
+
if (toFormat === 'claude' && result.messages) {
|
|
302
|
+
const { messages: patchedMessages } = (0, tool_result_js_1.ensureToolResultIds)(result.messages);
|
|
303
|
+
result.messages = patchedMessages;
|
|
304
|
+
}
|
|
297
305
|
return result;
|
|
298
306
|
}
|
|
299
307
|
/** Identity converter that passes events through unchanged */
|