@f2a/network 0.1.2 → 0.1.3
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/package.json +8 -1
- package/.github/workflows/ci.yml +0 -113
- package/.github/workflows/publish.yml +0 -60
- package/MONOREPO.md +0 -58
- package/SKILL.md +0 -137
- package/dist/adapters/openclaw.d.ts +0 -103
- package/dist/adapters/openclaw.d.ts.map +0 -1
- package/dist/adapters/openclaw.js +0 -297
- package/dist/adapters/openclaw.js.map +0 -1
- package/dist/core/connection-manager.d.ts +0 -80
- package/dist/core/connection-manager.d.ts.map +0 -1
- package/dist/core/connection-manager.js +0 -235
- package/dist/core/connection-manager.js.map +0 -1
- package/dist/core/connection-manager.test.d.ts +0 -2
- package/dist/core/connection-manager.test.d.ts.map +0 -1
- package/dist/core/connection-manager.test.js +0 -52
- package/dist/core/connection-manager.test.js.map +0 -1
- package/dist/core/identity.d.ts +0 -47
- package/dist/core/identity.d.ts.map +0 -1
- package/dist/core/identity.js +0 -130
- package/dist/core/identity.js.map +0 -1
- package/dist/core/identity.test.d.ts +0 -2
- package/dist/core/identity.test.d.ts.map +0 -1
- package/dist/core/identity.test.js +0 -43
- package/dist/core/identity.test.js.map +0 -1
- package/dist/core/serverless.d.ts +0 -155
- package/dist/core/serverless.d.ts.map +0 -1
- package/dist/core/serverless.js +0 -615
- package/dist/core/serverless.js.map +0 -1
- package/dist/daemon/webhook.test.d.ts +0 -2
- package/dist/daemon/webhook.test.d.ts.map +0 -1
- package/dist/daemon/webhook.test.js +0 -24
- package/dist/daemon/webhook.test.js.map +0 -1
- package/dist/protocol/messages.d.ts +0 -739
- package/dist/protocol/messages.d.ts.map +0 -1
- package/dist/protocol/messages.js +0 -188
- package/dist/protocol/messages.js.map +0 -1
- package/dist/protocol/messages.test.d.ts +0 -2
- package/dist/protocol/messages.test.d.ts.map +0 -1
- package/dist/protocol/messages.test.js +0 -55
- package/dist/protocol/messages.test.js.map +0 -1
- package/docs/F2A-PROTOCOL.md +0 -61
- package/docs/MOBILE_BOOTSTRAP_DESIGN.md +0 -126
- package/docs/a2a-lessons.md +0 -316
- package/docs/middleware-guide.md +0 -448
- package/docs/readme-update-checklist.md +0 -90
- package/docs/reputation-guide.md +0 -396
- package/docs/rfcs/001-reputation-system.md +0 -712
- package/docs/security-design.md +0 -247
- package/install.sh +0 -231
- package/packages/openclaw-adapter/README.md +0 -510
- package/packages/openclaw-adapter/openclaw.plugin.json +0 -106
- package/packages/openclaw-adapter/package.json +0 -40
- package/packages/openclaw-adapter/src/announcement-queue.test.ts +0 -449
- package/packages/openclaw-adapter/src/announcement-queue.ts +0 -403
- package/packages/openclaw-adapter/src/capability-detector.test.ts +0 -99
- package/packages/openclaw-adapter/src/capability-detector.ts +0 -183
- package/packages/openclaw-adapter/src/claim-handlers.test.ts +0 -974
- package/packages/openclaw-adapter/src/claim-handlers.ts +0 -482
- package/packages/openclaw-adapter/src/connector.business.test.ts +0 -583
- package/packages/openclaw-adapter/src/connector.ts +0 -795
- package/packages/openclaw-adapter/src/index.test.ts +0 -82
- package/packages/openclaw-adapter/src/index.ts +0 -18
- package/packages/openclaw-adapter/src/integration.e2e.test.ts +0 -829
- package/packages/openclaw-adapter/src/logger.ts +0 -51
- package/packages/openclaw-adapter/src/network-client.test.ts +0 -266
- package/packages/openclaw-adapter/src/network-client.ts +0 -251
- package/packages/openclaw-adapter/src/network-recovery.test.ts +0 -465
- package/packages/openclaw-adapter/src/node-manager.test.ts +0 -136
- package/packages/openclaw-adapter/src/node-manager.ts +0 -429
- package/packages/openclaw-adapter/src/plugin.test.ts +0 -439
- package/packages/openclaw-adapter/src/plugin.ts +0 -104
- package/packages/openclaw-adapter/src/reputation.test.ts +0 -221
- package/packages/openclaw-adapter/src/reputation.ts +0 -368
- package/packages/openclaw-adapter/src/task-guard.test.ts +0 -502
- package/packages/openclaw-adapter/src/task-guard.ts +0 -860
- package/packages/openclaw-adapter/src/task-queue.concurrency.test.ts +0 -462
- package/packages/openclaw-adapter/src/task-queue.edge-cases.test.ts +0 -284
- package/packages/openclaw-adapter/src/task-queue.persistence.test.ts +0 -408
- package/packages/openclaw-adapter/src/task-queue.ts +0 -668
- package/packages/openclaw-adapter/src/tool-handlers.test.ts +0 -906
- package/packages/openclaw-adapter/src/tool-handlers.ts +0 -574
- package/packages/openclaw-adapter/src/types.ts +0 -361
- package/packages/openclaw-adapter/src/webhook-pusher.test.ts +0 -188
- package/packages/openclaw-adapter/src/webhook-pusher.ts +0 -220
- package/packages/openclaw-adapter/src/webhook-server.test.ts +0 -580
- package/packages/openclaw-adapter/src/webhook-server.ts +0 -202
- package/packages/openclaw-adapter/tsconfig.json +0 -20
- package/src/cli/commands.test.ts +0 -157
- package/src/cli/commands.ts +0 -129
- package/src/cli/index.test.ts +0 -77
- package/src/cli/index.ts +0 -234
- package/src/core/autonomous-economy.test.ts +0 -291
- package/src/core/autonomous-economy.ts +0 -428
- package/src/core/e2ee-crypto.test.ts +0 -125
- package/src/core/e2ee-crypto.ts +0 -246
- package/src/core/f2a.test.ts +0 -269
- package/src/core/f2a.ts +0 -618
- package/src/core/p2p-network.test.ts +0 -199
- package/src/core/p2p-network.ts +0 -1432
- package/src/core/reputation-security.test.ts +0 -403
- package/src/core/reputation-security.ts +0 -562
- package/src/core/reputation.test.ts +0 -260
- package/src/core/reputation.ts +0 -576
- package/src/core/review-committee.test.ts +0 -380
- package/src/core/review-committee.ts +0 -401
- package/src/core/token-manager.test.ts +0 -133
- package/src/core/token-manager.ts +0 -140
- package/src/daemon/control-server.test.ts +0 -216
- package/src/daemon/control-server.ts +0 -292
- package/src/daemon/index.test.ts +0 -85
- package/src/daemon/index.ts +0 -89
- package/src/daemon/main.ts +0 -44
- package/src/daemon/start.ts +0 -29
- package/src/daemon/webhook.test.ts +0 -68
- package/src/daemon/webhook.ts +0 -105
- package/src/index.test.ts +0 -436
- package/src/index.ts +0 -72
- package/src/types/index.test.ts +0 -87
- package/src/types/index.ts +0 -341
- package/src/types/result.ts +0 -68
- package/src/utils/benchmark.ts +0 -237
- package/src/utils/logger.ts +0 -331
- package/src/utils/middleware.ts +0 -229
- package/src/utils/rate-limiter.ts +0 -207
- package/src/utils/signature.ts +0 -136
- package/src/utils/validation.ts +0 -186
- package/tests/docker/Dockerfile.node +0 -23
- package/tests/docker/Dockerfile.runner +0 -18
- package/tests/docker/docker-compose.test.yml +0 -73
- package/tests/integration/message-passing.test.ts +0 -109
- package/tests/integration/multi-node.test.ts +0 -92
- package/tests/integration/p2p-connection.test.ts +0 -83
- package/tests/integration/test-config.ts +0 -32
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -26
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* F2A 评审系统
|
|
3
|
-
* Phase 2: 评审机制
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Logger } from '../utils/logger.js';
|
|
7
|
-
import { ReputationManager } from './reputation.js';
|
|
8
|
-
|
|
9
|
-
// ============================================================================
|
|
10
|
-
// 类型定义
|
|
11
|
-
// ============================================================================
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 评审维度
|
|
15
|
-
*/
|
|
16
|
-
export interface ReviewDimensions {
|
|
17
|
-
/** 工作量评估 (0-100) */
|
|
18
|
-
workload: number;
|
|
19
|
-
/** 价值分 (-100 ~ 100) */
|
|
20
|
-
value: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 风险标记
|
|
25
|
-
*/
|
|
26
|
-
export type RiskFlag = 'dangerous' | 'malicious' | 'spam' | 'invalid';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* 任务评审
|
|
30
|
-
*/
|
|
31
|
-
export interface TaskReview {
|
|
32
|
-
taskId: string;
|
|
33
|
-
reviewerId: string;
|
|
34
|
-
dimensions: ReviewDimensions;
|
|
35
|
-
riskFlags?: RiskFlag[];
|
|
36
|
-
comment?: string;
|
|
37
|
-
timestamp: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 评审结果
|
|
42
|
-
*/
|
|
43
|
-
export interface ReviewResult {
|
|
44
|
-
taskId: string;
|
|
45
|
-
requesterId: string;
|
|
46
|
-
executorId?: string;
|
|
47
|
-
finalWorkload: number;
|
|
48
|
-
finalValue: number;
|
|
49
|
-
reviews: TaskReview[];
|
|
50
|
-
outliers: TaskReview[];
|
|
51
|
-
timestamp: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 评审委员会配置
|
|
56
|
-
*/
|
|
57
|
-
export interface ReviewCommitteeConfig {
|
|
58
|
-
/** 最小评审人数 */
|
|
59
|
-
minReviewers: number;
|
|
60
|
-
/** 最大评审人数 */
|
|
61
|
-
maxReviewers: number;
|
|
62
|
-
/** 评审资格最低信誉分 */
|
|
63
|
-
minReputation: number;
|
|
64
|
-
/** 评审超时(毫秒) */
|
|
65
|
-
reviewTimeout: number;
|
|
66
|
-
/** 偏离检测阈值(标准差倍数) */
|
|
67
|
-
outlierThreshold: number;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 待评审任务
|
|
72
|
-
*/
|
|
73
|
-
export interface PendingReview {
|
|
74
|
-
taskId: string;
|
|
75
|
-
requesterId: string;
|
|
76
|
-
executorId?: string;
|
|
77
|
-
taskDescription: string;
|
|
78
|
-
taskParameters?: Record<string, unknown>;
|
|
79
|
-
createdAt: number;
|
|
80
|
-
reviews: TaskReview[];
|
|
81
|
-
requiredReviewers: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ============================================================================
|
|
85
|
-
// 默认配置
|
|
86
|
-
// ============================================================================
|
|
87
|
-
|
|
88
|
-
const DEFAULT_COMMITTEE_CONFIG: ReviewCommitteeConfig = {
|
|
89
|
-
minReviewers: 1,
|
|
90
|
-
maxReviewers: 7,
|
|
91
|
-
minReputation: 50,
|
|
92
|
-
reviewTimeout: 5 * 60 * 1000, // 5 分钟
|
|
93
|
-
outlierThreshold: 2, // 2 个标准差
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// ============================================================================
|
|
97
|
-
// 评审委员会
|
|
98
|
-
// ============================================================================
|
|
99
|
-
|
|
100
|
-
export class ReviewCommittee {
|
|
101
|
-
private config: ReviewCommitteeConfig;
|
|
102
|
-
private reputationManager: ReputationManager;
|
|
103
|
-
private pendingReviews: Map<string, PendingReview> = new Map();
|
|
104
|
-
private logger: Logger;
|
|
105
|
-
|
|
106
|
-
constructor(
|
|
107
|
-
reputationManager: ReputationManager,
|
|
108
|
-
config: Partial<ReviewCommitteeConfig> = {}
|
|
109
|
-
) {
|
|
110
|
-
this.reputationManager = reputationManager;
|
|
111
|
-
this.config = { ...DEFAULT_COMMITTEE_CONFIG, ...config };
|
|
112
|
-
this.logger = new Logger({ component: 'ReviewCommittee' });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 根据网络规模计算需要的评审人数
|
|
117
|
-
*/
|
|
118
|
-
getRequiredReviewers(networkSize: number): number {
|
|
119
|
-
if (networkSize < 10) return 1;
|
|
120
|
-
if (networkSize < 50) return 3;
|
|
121
|
-
return 5;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* 获取可用的评审者
|
|
126
|
-
*/
|
|
127
|
-
getAvailableReviewers(excludeIds: string[] = []): string[] {
|
|
128
|
-
const highRepNodes = this.reputationManager.getHighReputationNodes(
|
|
129
|
-
this.config.minReputation
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
return highRepNodes
|
|
133
|
-
.map(e => e.peerId)
|
|
134
|
-
.filter(id => !excludeIds.includes(id));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 提交任务进行评审
|
|
139
|
-
*/
|
|
140
|
-
submitForReview(
|
|
141
|
-
taskId: string,
|
|
142
|
-
requesterId: string,
|
|
143
|
-
taskDescription: string,
|
|
144
|
-
taskParameters?: Record<string, unknown>,
|
|
145
|
-
executorId?: string
|
|
146
|
-
): PendingReview {
|
|
147
|
-
const networkSize = this.reputationManager.getAllReputations().length;
|
|
148
|
-
const requiredReviewers = Math.min(
|
|
149
|
-
this.config.maxReviewers,
|
|
150
|
-
Math.max(this.config.minReviewers, this.getRequiredReviewers(networkSize))
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const pending: PendingReview = {
|
|
154
|
-
taskId,
|
|
155
|
-
requesterId,
|
|
156
|
-
executorId,
|
|
157
|
-
taskDescription,
|
|
158
|
-
taskParameters,
|
|
159
|
-
createdAt: Date.now(),
|
|
160
|
-
reviews: [],
|
|
161
|
-
requiredReviewers,
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
this.pendingReviews.set(taskId, pending);
|
|
165
|
-
|
|
166
|
-
this.logger.info('Task submitted for review', {
|
|
167
|
-
taskId,
|
|
168
|
-
requesterId: requesterId.slice(0, 16),
|
|
169
|
-
requiredReviewers,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
return pending;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* 提交评审
|
|
177
|
-
*/
|
|
178
|
-
submitReview(review: TaskReview): { success: boolean; message: string } {
|
|
179
|
-
const pending = this.pendingReviews.get(review.taskId);
|
|
180
|
-
|
|
181
|
-
if (!pending) {
|
|
182
|
-
return { success: false, message: 'Task not found or already completed' };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// 验证评审者资格
|
|
186
|
-
if (!this.reputationManager.hasPermission(review.reviewerId, 'review')) {
|
|
187
|
-
return { success: false, message: 'Reviewer does not have permission' };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// 验证评审者不是请求者或执行者
|
|
191
|
-
if (review.reviewerId === pending.requesterId ||
|
|
192
|
-
review.reviewerId === pending.executorId) {
|
|
193
|
-
return { success: false, message: 'Cannot review own task' };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 检查是否已经评审过
|
|
197
|
-
if (pending.reviews.some(r => r.reviewerId === review.reviewerId)) {
|
|
198
|
-
return { success: false, message: 'Already reviewed this task' };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// 验证评审维度
|
|
202
|
-
if (!this.validateReviewDimensions(review.dimensions)) {
|
|
203
|
-
return { success: false, message: 'Invalid review dimensions' };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
pending.reviews.push(review);
|
|
207
|
-
|
|
208
|
-
this.logger.info('Review submitted', {
|
|
209
|
-
taskId: review.taskId,
|
|
210
|
-
reviewerId: review.reviewerId.slice(0, 16),
|
|
211
|
-
workload: review.dimensions.workload,
|
|
212
|
-
value: review.dimensions.value,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
return { success: true, message: 'Review submitted' };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* 检查评审是否完成
|
|
220
|
-
*/
|
|
221
|
-
isReviewComplete(taskId: string): boolean {
|
|
222
|
-
const pending = this.pendingReviews.get(taskId);
|
|
223
|
-
if (!pending) return false;
|
|
224
|
-
return pending.reviews.length >= pending.requiredReviewers;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 结算评审
|
|
229
|
-
*/
|
|
230
|
-
finalizeReview(taskId: string): ReviewResult | null {
|
|
231
|
-
const pending = this.pendingReviews.get(taskId);
|
|
232
|
-
if (!pending) return null;
|
|
233
|
-
|
|
234
|
-
if (pending.reviews.length < pending.requiredReviewers) {
|
|
235
|
-
this.logger.warn('Not enough reviews', {
|
|
236
|
-
taskId,
|
|
237
|
-
current: pending.reviews.length,
|
|
238
|
-
required: pending.requiredReviewers,
|
|
239
|
-
});
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const { finalWorkload, finalValue, outliers } = this.aggregateReviews(
|
|
244
|
-
pending.reviews
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
const result: ReviewResult = {
|
|
248
|
-
taskId,
|
|
249
|
-
requesterId: pending.requesterId,
|
|
250
|
-
executorId: pending.executorId,
|
|
251
|
-
finalWorkload,
|
|
252
|
-
finalValue,
|
|
253
|
-
reviews: pending.reviews,
|
|
254
|
-
outliers,
|
|
255
|
-
timestamp: Date.now(),
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
// 移除待评审任务
|
|
259
|
-
this.pendingReviews.delete(taskId);
|
|
260
|
-
|
|
261
|
-
// 更新评审者信誉
|
|
262
|
-
this.updateReviewerReputations(pending.reviews, outliers);
|
|
263
|
-
|
|
264
|
-
this.logger.info('Review finalized', {
|
|
265
|
-
taskId,
|
|
266
|
-
finalWorkload,
|
|
267
|
-
finalValue,
|
|
268
|
-
outliers: outliers.length,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* 聚合评审结果
|
|
276
|
-
*/
|
|
277
|
-
aggregateReviews(reviews: TaskReview[]): {
|
|
278
|
-
finalWorkload: number;
|
|
279
|
-
finalValue: number;
|
|
280
|
-
outliers: TaskReview[];
|
|
281
|
-
} {
|
|
282
|
-
if (reviews.length === 1) {
|
|
283
|
-
return {
|
|
284
|
-
finalWorkload: reviews[0].dimensions.workload,
|
|
285
|
-
finalValue: reviews[0].dimensions.value,
|
|
286
|
-
outliers: [],
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// 计算平均值和标准差
|
|
291
|
-
const workloads = reviews.map(r => r.dimensions.workload);
|
|
292
|
-
const values = reviews.map(r => r.dimensions.value);
|
|
293
|
-
|
|
294
|
-
const avgWorkload = this.average(workloads);
|
|
295
|
-
const avgValue = this.average(values);
|
|
296
|
-
const stdDevWorkload = this.stdDev(workloads, avgWorkload);
|
|
297
|
-
const stdDevValue = this.stdDev(values, avgValue);
|
|
298
|
-
|
|
299
|
-
// 去掉最高和最低
|
|
300
|
-
const sortedWorkloads = [...workloads].sort((a, b) => a - b);
|
|
301
|
-
const sortedValues = [...values].sort((a, b) => a - b);
|
|
302
|
-
|
|
303
|
-
const trimmedWorkloads = sortedWorkloads.slice(1, -1);
|
|
304
|
-
const trimmedValues = sortedValues.slice(1, -1);
|
|
305
|
-
|
|
306
|
-
const finalWorkload = this.average(trimmedWorkloads);
|
|
307
|
-
const finalValue = this.average(trimmedValues);
|
|
308
|
-
|
|
309
|
-
// 识别偏离者
|
|
310
|
-
const outliers = reviews.filter(r =>
|
|
311
|
-
Math.abs(r.dimensions.workload - avgWorkload) > this.config.outlierThreshold * stdDevWorkload ||
|
|
312
|
-
Math.abs(r.dimensions.value - avgValue) > this.config.outlierThreshold * stdDevValue
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
return { finalWorkload, finalValue, outliers };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* 获取待评审任务列表
|
|
320
|
-
*/
|
|
321
|
-
getPendingReviews(): PendingReview[] {
|
|
322
|
-
return Array.from(this.pendingReviews.values());
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* 获取特定任务的评审状态
|
|
327
|
-
*/
|
|
328
|
-
getReviewStatus(taskId: string): PendingReview | null {
|
|
329
|
-
return this.pendingReviews.get(taskId) || null;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* 清理超时的待评审任务
|
|
334
|
-
*/
|
|
335
|
-
cleanupExpiredReviews(): string[] {
|
|
336
|
-
const now = Date.now();
|
|
337
|
-
const expired: string[] = [];
|
|
338
|
-
|
|
339
|
-
for (const [taskId, pending] of this.pendingReviews) {
|
|
340
|
-
if (now - pending.createdAt > this.config.reviewTimeout) {
|
|
341
|
-
expired.push(taskId);
|
|
342
|
-
this.pendingReviews.delete(taskId);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (expired.length > 0) {
|
|
347
|
-
this.logger.info('Cleaned up expired reviews', { count: expired.length });
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return expired;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ============================================================================
|
|
354
|
-
// 私有方法
|
|
355
|
-
// ============================================================================
|
|
356
|
-
|
|
357
|
-
private validateReviewDimensions(dimensions: ReviewDimensions): boolean {
|
|
358
|
-
const { workload, value } = dimensions;
|
|
359
|
-
|
|
360
|
-
// 工作量范围检查
|
|
361
|
-
if (workload < 0 || workload > 100) return false;
|
|
362
|
-
|
|
363
|
-
// 价值分范围检查
|
|
364
|
-
if (value < -100 || value > 100) return false;
|
|
365
|
-
|
|
366
|
-
return true;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
private updateReviewerReputations(
|
|
370
|
-
reviews: TaskReview[],
|
|
371
|
-
outliers: TaskReview[]
|
|
372
|
-
): void {
|
|
373
|
-
for (const review of reviews) {
|
|
374
|
-
if (outliers.includes(review)) {
|
|
375
|
-
// 偏离评审 → 惩罚
|
|
376
|
-
this.reputationManager.recordReviewPenalty(
|
|
377
|
-
review.reviewerId,
|
|
378
|
-
-5,
|
|
379
|
-
'Outlier review'
|
|
380
|
-
);
|
|
381
|
-
} else {
|
|
382
|
-
// 正常评审 → 奖励
|
|
383
|
-
this.reputationManager.recordReviewReward(review.reviewerId, 3);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
private average(values: number[]): number {
|
|
389
|
-
if (values.length === 0) return 0;
|
|
390
|
-
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
private stdDev(values: number[], mean: number): number {
|
|
394
|
-
if (values.length === 0) return 0;
|
|
395
|
-
const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
|
|
396
|
-
return Math.sqrt(this.average(squaredDiffs));
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// 默认导出
|
|
401
|
-
export default ReviewCommittee;
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { TokenManager } from './token-manager.js';
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { tmpdir } from 'os';
|
|
6
|
-
|
|
7
|
-
describe('TokenManager', () => {
|
|
8
|
-
let tempDir: string;
|
|
9
|
-
let tokenManager: TokenManager;
|
|
10
|
-
let originalEnv: string | undefined;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
// 创建临时目录
|
|
14
|
-
tempDir = join(tmpdir(), `f2a-test-${Date.now()}`);
|
|
15
|
-
|
|
16
|
-
// 保存原始环境变量
|
|
17
|
-
originalEnv = process.env.F2A_CONTROL_TOKEN;
|
|
18
|
-
delete process.env.F2A_CONTROL_TOKEN;
|
|
19
|
-
|
|
20
|
-
// 创建新的 TokenManager
|
|
21
|
-
tokenManager = new TokenManager(tempDir);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
// 恢复环境变量
|
|
26
|
-
if (originalEnv !== undefined) {
|
|
27
|
-
process.env.F2A_CONTROL_TOKEN = originalEnv;
|
|
28
|
-
} else {
|
|
29
|
-
delete process.env.F2A_CONTROL_TOKEN;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// 清理临时目录
|
|
33
|
-
try {
|
|
34
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
35
|
-
} catch {}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('getToken', () => {
|
|
39
|
-
it('should generate new token when none exists', () => {
|
|
40
|
-
const token = tokenManager.getToken();
|
|
41
|
-
|
|
42
|
-
expect(token).toBeDefined();
|
|
43
|
-
expect(token.startsWith('f2a-')).toBe(true);
|
|
44
|
-
expect(token.length).toBeGreaterThan(40); // f2a- + 64 hex chars
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should return same token on subsequent calls', () => {
|
|
48
|
-
const token1 = tokenManager.getToken();
|
|
49
|
-
const token2 = tokenManager.getToken();
|
|
50
|
-
|
|
51
|
-
expect(token1).toBe(token2);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should use environment variable if set', () => {
|
|
55
|
-
const envToken = 'custom-env-token-123';
|
|
56
|
-
process.env.F2A_CONTROL_TOKEN = envToken;
|
|
57
|
-
|
|
58
|
-
const token = tokenManager.getToken();
|
|
59
|
-
expect(token).toBe(envToken);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should load token from file if exists', () => {
|
|
63
|
-
// 先获取一个 token(会保存到文件)
|
|
64
|
-
const token1 = tokenManager.getToken();
|
|
65
|
-
|
|
66
|
-
// 创建新的 TokenManager 实例(相同目录)
|
|
67
|
-
const newManager = new TokenManager(tempDir);
|
|
68
|
-
const token2 = newManager.getToken();
|
|
69
|
-
|
|
70
|
-
expect(token2).toBe(token1);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should reject insecure default token', () => {
|
|
74
|
-
process.env.F2A_CONTROL_TOKEN = 'f2a-default-token';
|
|
75
|
-
|
|
76
|
-
// 应该抛出错误
|
|
77
|
-
expect(() => tokenManager.getToken()).toThrow('Insecure token detected');
|
|
78
|
-
|
|
79
|
-
delete process.env.F2A_CONTROL_TOKEN;
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('verifyToken', () => {
|
|
84
|
-
it('should return true for valid token', () => {
|
|
85
|
-
const token = tokenManager.getToken();
|
|
86
|
-
|
|
87
|
-
expect(tokenManager.verifyToken(token)).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should return false for invalid token', () => {
|
|
91
|
-
tokenManager.getToken(); // 确保有 token
|
|
92
|
-
|
|
93
|
-
expect(tokenManager.verifyToken('wrong-token')).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should return false for undefined token', () => {
|
|
97
|
-
tokenManager.getToken();
|
|
98
|
-
|
|
99
|
-
expect(tokenManager.verifyToken(undefined)).toBe(false);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('should return false for empty string', () => {
|
|
103
|
-
tokenManager.getToken();
|
|
104
|
-
|
|
105
|
-
expect(tokenManager.verifyToken('')).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('getTokenPath', () => {
|
|
110
|
-
it('should return correct path', () => {
|
|
111
|
-
const path = tokenManager.getTokenPath();
|
|
112
|
-
|
|
113
|
-
expect(path).toContain('control-token');
|
|
114
|
-
expect(path).toBe(join(tempDir, 'control-token'));
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('token format', () => {
|
|
119
|
-
it('should generate token with correct format', () => {
|
|
120
|
-
const token = tokenManager.getToken();
|
|
121
|
-
|
|
122
|
-
// 格式: f2a-[64 hex chars]
|
|
123
|
-
expect(token).toMatch(/^f2a-[a-f0-9]{64}$/);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should generate unique tokens for different instances', () => {
|
|
127
|
-
const token1 = new TokenManager(join(tempDir, 'a')).getToken();
|
|
128
|
-
const token2 = new TokenManager(join(tempDir, 'b')).getToken();
|
|
129
|
-
|
|
130
|
-
expect(token1).not.toBe(token2);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
|
|
2
|
-
import { join, dirname } from 'path';
|
|
3
|
-
import { randomBytes, timingSafeEqual } from 'crypto';
|
|
4
|
-
import { homedir } from 'os';
|
|
5
|
-
import { Logger } from '../utils/logger.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Token 管理器
|
|
9
|
-
* 负责生成、存储和验证 F2A 控制 Token
|
|
10
|
-
*/
|
|
11
|
-
export class TokenManager {
|
|
12
|
-
private tokenPath: string;
|
|
13
|
-
private token: string | null = null;
|
|
14
|
-
private logger: Logger;
|
|
15
|
-
|
|
16
|
-
constructor(dataDir?: string) {
|
|
17
|
-
this.logger = new Logger({ component: 'TokenManager' });
|
|
18
|
-
// 默认存储在用户主目录的 .f2a 文件夹
|
|
19
|
-
const baseDir = dataDir || join(homedir(), '.f2a');
|
|
20
|
-
this.tokenPath = join(baseDir, 'control-token');
|
|
21
|
-
|
|
22
|
-
// 确保目录存在
|
|
23
|
-
const dir = join(baseDir);
|
|
24
|
-
if (!existsSync(dir)) {
|
|
25
|
-
mkdirSync(dir, { recursive: true });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 获取或生成 Token
|
|
31
|
-
* 优先从环境变量读取,其次从文件读取,最后生成新的
|
|
32
|
-
*/
|
|
33
|
-
getToken(): string {
|
|
34
|
-
// 1. 优先使用环境变量
|
|
35
|
-
const envToken = process.env.F2A_CONTROL_TOKEN;
|
|
36
|
-
if (envToken) {
|
|
37
|
-
// 检查是否为不安全默认值
|
|
38
|
-
if (envToken === 'f2a-default-token') {
|
|
39
|
-
this.logger.error('F2A_CONTROL_TOKEN is using the insecure default value!');
|
|
40
|
-
this.logger.error('Please set a secure token: export F2A_CONTROL_TOKEN=$(openssl rand -hex 32)');
|
|
41
|
-
throw new Error(
|
|
42
|
-
'Insecure token detected. F2A_CONTROL_TOKEN cannot use the default value "f2a-default-token". ' +
|
|
43
|
-
'Please set a secure token: export F2A_CONTROL_TOKEN=$(openssl rand -hex 32)'
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
this.token = envToken;
|
|
47
|
-
return envToken;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 2. 从文件读取
|
|
51
|
-
if (existsSync(this.tokenPath)) {
|
|
52
|
-
const fileToken = readFileSync(this.tokenPath, 'utf-8').trim();
|
|
53
|
-
if (fileToken) {
|
|
54
|
-
this.token = fileToken;
|
|
55
|
-
return fileToken;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 3. 生成新的随机 Token
|
|
60
|
-
const newToken = this.generateSecureToken();
|
|
61
|
-
this.saveToken(newToken);
|
|
62
|
-
this.token = newToken;
|
|
63
|
-
|
|
64
|
-
this.logger.info('Generated new control token', { path: this.tokenPath });
|
|
65
|
-
this.logger.info('To use a custom token, set F2A_CONTROL_TOKEN environment variable');
|
|
66
|
-
|
|
67
|
-
return newToken;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 验证 Token 是否有效
|
|
72
|
-
* 使用 timingSafeEqual 防止时序攻击
|
|
73
|
-
*/
|
|
74
|
-
verifyToken(token: string | undefined): boolean {
|
|
75
|
-
if (!token) return false;
|
|
76
|
-
|
|
77
|
-
const expectedToken = this.getToken();
|
|
78
|
-
|
|
79
|
-
// 两个 token 长度必须相同
|
|
80
|
-
if (token.length !== expectedToken.length) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 使用 timingSafeEqual 防止时序攻击
|
|
85
|
-
try {
|
|
86
|
-
return timingSafeEqual(
|
|
87
|
-
Buffer.from(token, 'utf-8'),
|
|
88
|
-
Buffer.from(expectedToken, 'utf-8')
|
|
89
|
-
);
|
|
90
|
-
} catch {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 记录 Token 使用审计日志
|
|
97
|
-
*/
|
|
98
|
-
logTokenUsage(clientInfo: { ip?: string; action?: string; success: boolean }): void {
|
|
99
|
-
const auditPath = join(dirname(this.tokenPath), 'token-audit.log');
|
|
100
|
-
const entry = {
|
|
101
|
-
timestamp: new Date().toISOString(),
|
|
102
|
-
...clientInfo
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
appendFileSync(auditPath, JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
107
|
-
} catch (error) {
|
|
108
|
-
this.logger.error('Failed to write audit log', { error });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 生成安全的随机 Token
|
|
114
|
-
*/
|
|
115
|
-
private generateSecureToken(): string {
|
|
116
|
-
// 生成 32 字节 (64 字符) 的十六进制随机字符串
|
|
117
|
-
return 'f2a-' + randomBytes(32).toString('hex');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* 保存 Token 到文件
|
|
122
|
-
*/
|
|
123
|
-
private saveToken(token: string): void {
|
|
124
|
-
try {
|
|
125
|
-
writeFileSync(this.tokenPath, token, { mode: 0o600 }); // 仅所有者可读写
|
|
126
|
-
} catch (error) {
|
|
127
|
-
this.logger.error('Failed to save token', { error });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* 获取 Token 文件路径
|
|
133
|
-
*/
|
|
134
|
-
getTokenPath(): string {
|
|
135
|
-
return this.tokenPath;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 单例导出
|
|
140
|
-
export const defaultTokenManager = new TokenManager();
|