@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,668 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* F2A Task Queue with SQLite Persistence
|
|
3
|
-
* 任务队列 + SQLite 持久化
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { TaskRequest, TaskResponse } from './types.js';
|
|
7
|
-
import Database from 'better-sqlite3';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import fs from 'fs';
|
|
10
|
-
import { randomUUID } from 'crypto';
|
|
11
|
-
import { queueLogger as logger } from './logger.js';
|
|
12
|
-
|
|
13
|
-
export interface QueuedTask extends TaskRequest {
|
|
14
|
-
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
15
|
-
createdAt: number;
|
|
16
|
-
updatedAt?: number;
|
|
17
|
-
result?: unknown;
|
|
18
|
-
error?: string;
|
|
19
|
-
latency?: number;
|
|
20
|
-
webhookPushed?: boolean; // 是否已通过 webhook 推送
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface TaskQueueStats {
|
|
24
|
-
pending: number;
|
|
25
|
-
processing: number;
|
|
26
|
-
completed: number;
|
|
27
|
-
failed: number;
|
|
28
|
-
total: number;
|
|
29
|
-
webhookPending: number; // 待 webhook 推送的任务数
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface TaskQueueOptions {
|
|
33
|
-
maxSize?: number;
|
|
34
|
-
maxAgeMs?: number;
|
|
35
|
-
persistDir?: string; // 持久化目录
|
|
36
|
-
persistEnabled?: boolean;
|
|
37
|
-
/** 清理阈值比例(当队列大小超过 maxSize * cleanupThreshold 时触发清理,默认 0.8) */
|
|
38
|
-
cleanupThreshold?: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** 数据库行类型 */
|
|
42
|
-
interface TaskRow {
|
|
43
|
-
id: string;
|
|
44
|
-
task_type: string | null;
|
|
45
|
-
description: string | null;
|
|
46
|
-
parameters: string | null;
|
|
47
|
-
status: string;
|
|
48
|
-
created_at: number;
|
|
49
|
-
updated_at: number | null;
|
|
50
|
-
result: string | null;
|
|
51
|
-
error: string | null;
|
|
52
|
-
latency: number | null;
|
|
53
|
-
webhook_pushed: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** 默认超时时间(毫秒) */
|
|
57
|
-
const DEFAULT_TIMEOUT_MS = 30000;
|
|
58
|
-
/** 最小超时时间(毫秒) */
|
|
59
|
-
const MIN_TIMEOUT_MS = 1000;
|
|
60
|
-
/** 最大超时时间(毫秒) - 24小时 */
|
|
61
|
-
const MAX_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
62
|
-
|
|
63
|
-
export class TaskQueue {
|
|
64
|
-
private tasks = new Map<string, QueuedTask>();
|
|
65
|
-
private maxSize: number;
|
|
66
|
-
private maxAgeMs: number;
|
|
67
|
-
private persistEnabled: boolean;
|
|
68
|
-
private cleanupThreshold: number;
|
|
69
|
-
private lastCleanupTime: number = 0;
|
|
70
|
-
private cleanupIntervalMs: number;
|
|
71
|
-
private db?: Database.Database;
|
|
72
|
-
|
|
73
|
-
constructor(options?: TaskQueueOptions) {
|
|
74
|
-
this.maxSize = options?.maxSize || 1000;
|
|
75
|
-
this.maxAgeMs = options?.maxAgeMs || 24 * 60 * 60 * 1000; // 24小时
|
|
76
|
-
this.persistEnabled = options?.persistEnabled ?? true;
|
|
77
|
-
this.cleanupThreshold = options?.cleanupThreshold || 0.8; // 默认 80% 触发清理
|
|
78
|
-
// 清理间隔:至少每 maxAgeMs/10 时间执行一次清理检查
|
|
79
|
-
this.cleanupIntervalMs = Math.min(this.maxAgeMs / 10, 60000); // 最多 1 分钟
|
|
80
|
-
|
|
81
|
-
if (this.persistEnabled && options?.persistDir) {
|
|
82
|
-
this.initPersistence(options.persistDir);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* 初始化 SQLite 持久化
|
|
88
|
-
*/
|
|
89
|
-
private initPersistence(persistDir: string): void {
|
|
90
|
-
// 确保目录存在
|
|
91
|
-
if (!fs.existsSync(persistDir)) {
|
|
92
|
-
fs.mkdirSync(persistDir, { recursive: true });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const dbPath = path.join(persistDir, 'task-queue.db');
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
this.db = new Database(dbPath);
|
|
99
|
-
|
|
100
|
-
// 创建表
|
|
101
|
-
this.db.exec(`
|
|
102
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
103
|
-
id TEXT PRIMARY KEY,
|
|
104
|
-
task_type TEXT,
|
|
105
|
-
description TEXT,
|
|
106
|
-
parameters TEXT,
|
|
107
|
-
status TEXT NOT NULL,
|
|
108
|
-
created_at INTEGER NOT NULL,
|
|
109
|
-
updated_at INTEGER,
|
|
110
|
-
result TEXT,
|
|
111
|
-
error TEXT,
|
|
112
|
-
latency INTEGER,
|
|
113
|
-
webhook_pushed INTEGER DEFAULT 0
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
CREATE INDEX IF NOT EXISTS idx_status ON tasks(status);
|
|
117
|
-
CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at);
|
|
118
|
-
`);
|
|
119
|
-
|
|
120
|
-
// 恢复未完成的任务到内存
|
|
121
|
-
this.restore();
|
|
122
|
-
} catch (e) {
|
|
123
|
-
// 数据库损坏或无法访问,删除并重新创建
|
|
124
|
-
logger.warn('数据库初始化失败,将重建: error=%s', e);
|
|
125
|
-
try {
|
|
126
|
-
if (this.db) {
|
|
127
|
-
this.db.close();
|
|
128
|
-
}
|
|
129
|
-
} catch (closeErr) {
|
|
130
|
-
// 忽略关闭错误,继续重建
|
|
131
|
-
logger.warn('关闭数据库时出错: error=%s', closeErr);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 删除损坏的数据库文件
|
|
135
|
-
if (fs.existsSync(dbPath)) {
|
|
136
|
-
fs.unlinkSync(dbPath);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 重新创建
|
|
140
|
-
this.db = new Database(dbPath);
|
|
141
|
-
this.db.exec(`
|
|
142
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
143
|
-
id TEXT PRIMARY KEY,
|
|
144
|
-
task_type TEXT,
|
|
145
|
-
description TEXT,
|
|
146
|
-
parameters TEXT,
|
|
147
|
-
status TEXT NOT NULL,
|
|
148
|
-
created_at INTEGER NOT NULL,
|
|
149
|
-
updated_at INTEGER,
|
|
150
|
-
result TEXT,
|
|
151
|
-
error TEXT,
|
|
152
|
-
latency INTEGER,
|
|
153
|
-
webhook_pushed INTEGER DEFAULT 0
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
CREATE INDEX IF NOT EXISTS idx_status ON tasks(status);
|
|
157
|
-
CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at);
|
|
158
|
-
`);
|
|
159
|
-
|
|
160
|
-
logger.info('数据库已重建');
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* 从数据库恢复任务
|
|
166
|
-
*
|
|
167
|
-
* P1 修复:尝试逐条恢复有效数据,避免数据库损坏时丢失所有任务
|
|
168
|
-
*/
|
|
169
|
-
private restore(): void {
|
|
170
|
-
if (!this.db) return;
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
// 恢复时将 processing 状态的任务重置为 pending,避免僵尸任务
|
|
174
|
-
// 这些任务在崩溃前正在处理,但未完成,需要重新执行
|
|
175
|
-
this.db.exec(`
|
|
176
|
-
UPDATE tasks SET status = 'pending' WHERE status = 'processing'
|
|
177
|
-
`);
|
|
178
|
-
|
|
179
|
-
const rows = this.db.prepare(`
|
|
180
|
-
SELECT * FROM tasks
|
|
181
|
-
WHERE status IN ('pending', 'processing')
|
|
182
|
-
ORDER BY created_at ASC
|
|
183
|
-
`).all() as TaskRow[];
|
|
184
|
-
|
|
185
|
-
let recoveredCount = 0;
|
|
186
|
-
let skippedCount = 0;
|
|
187
|
-
|
|
188
|
-
for (const row of rows) {
|
|
189
|
-
try {
|
|
190
|
-
// 验证必要字段
|
|
191
|
-
if (!row.id || typeof row.id !== 'string') {
|
|
192
|
-
logger.warn('跳过无效记录: 缺少 id');
|
|
193
|
-
skippedCount++;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (!row.created_at || typeof row.created_at !== 'number') {
|
|
198
|
-
logger.warn('跳过无效记录: id=%s, 缺少 created_at', row.id);
|
|
199
|
-
skippedCount++;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// 检查任务是否过期
|
|
204
|
-
const age = Date.now() - row.created_at;
|
|
205
|
-
if (age > this.maxAgeMs) {
|
|
206
|
-
// 删除过期任务
|
|
207
|
-
this.db.prepare('DELETE FROM tasks WHERE id = ?').run(row.id);
|
|
208
|
-
skippedCount++;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const task: QueuedTask = {
|
|
213
|
-
taskId: row.id,
|
|
214
|
-
taskType: row.task_type || undefined,
|
|
215
|
-
description: row.description || undefined,
|
|
216
|
-
parameters: this.safeJsonParse(row.parameters),
|
|
217
|
-
status: row.status || 'pending',
|
|
218
|
-
createdAt: row.created_at,
|
|
219
|
-
updatedAt: row.updated_at || undefined,
|
|
220
|
-
result: this.safeJsonParse(row.result),
|
|
221
|
-
error: row.error || undefined,
|
|
222
|
-
latency: row.latency || undefined,
|
|
223
|
-
webhookPushed: row.webhook_pushed === 1
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
this.tasks.set(task.taskId, task);
|
|
227
|
-
recoveredCount++;
|
|
228
|
-
} catch (e) {
|
|
229
|
-
logger.warn('跳过无效任务记录: id=%s, error=%s', row.id, e);
|
|
230
|
-
skippedCount++;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (skippedCount > 0) {
|
|
235
|
-
logger.info('恢复完成: recovered=%d, skipped=%d', recoveredCount, skippedCount);
|
|
236
|
-
} else {
|
|
237
|
-
logger.info('从持久化恢复任务: count=%d', this.tasks.size);
|
|
238
|
-
}
|
|
239
|
-
} catch (e) {
|
|
240
|
-
// P1 修复:数据库损坏时不清空所有任务,而是尝试逐条恢复
|
|
241
|
-
logger.warn('批量恢复失败,尝试逐条恢复: error=%s', e);
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
// 尝试逐条读取并恢复
|
|
245
|
-
const rows = this.db.prepare(`SELECT * FROM tasks WHERE status IN ('pending', 'processing')`).all() as TaskRow[];
|
|
246
|
-
|
|
247
|
-
for (const row of rows) {
|
|
248
|
-
try {
|
|
249
|
-
if (row.id && row.created_at) {
|
|
250
|
-
const task: QueuedTask = {
|
|
251
|
-
taskId: row.id,
|
|
252
|
-
taskType: row.task_type || undefined,
|
|
253
|
-
description: row.description || undefined,
|
|
254
|
-
parameters: this.safeJsonParse(row.parameters),
|
|
255
|
-
status: 'pending', // 恢复时默认设为 pending
|
|
256
|
-
createdAt: row.created_at,
|
|
257
|
-
updatedAt: row.updated_at || undefined,
|
|
258
|
-
result: this.safeJsonParse(row.result),
|
|
259
|
-
error: row.error || undefined,
|
|
260
|
-
latency: row.latency || undefined,
|
|
261
|
-
webhookPushed: row.webhook_pushed === 1
|
|
262
|
-
};
|
|
263
|
-
this.tasks.set(task.taskId, task);
|
|
264
|
-
}
|
|
265
|
-
} catch (rowError) {
|
|
266
|
-
// 单条记录恢复失败,跳过并继续
|
|
267
|
-
logger.warn('单条记录恢复失败: id=%s, error=%s', row?.id, rowError);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
logger.info('逐条恢复完成: count=%d', this.tasks.size);
|
|
272
|
-
} catch (recoverError) {
|
|
273
|
-
// 所有恢复尝试都失败,记录错误但不清空内存
|
|
274
|
-
logger.error('所有恢复尝试失败,将使用空内存队列: error=%s', recoverError);
|
|
275
|
-
// 注意:这里不清空 this.tasks,保留任何可能已部分恢复的数据
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* 安全解析 JSON,失败返回 undefined
|
|
282
|
-
*/
|
|
283
|
-
private safeJsonParse(json: string | null | undefined): unknown {
|
|
284
|
-
if (!json) return undefined;
|
|
285
|
-
try {
|
|
286
|
-
return JSON.parse(json);
|
|
287
|
-
} catch {
|
|
288
|
-
logger.warn('JSON 解析失败,跳过: json=%s...', json.slice(0, 50));
|
|
289
|
-
return undefined;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* 添加新任务到队列
|
|
295
|
-
*/
|
|
296
|
-
add(request: TaskRequest): QueuedTask {
|
|
297
|
-
// ========== 输入验证(带默认值)==========
|
|
298
|
-
|
|
299
|
-
// taskId 验证(必须)
|
|
300
|
-
if (!request.taskId || typeof request.taskId !== 'string' || request.taskId.trim() === '') {
|
|
301
|
-
throw new Error('taskId must be a non-empty string');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// from 验证(可选,默认 'unknown')
|
|
305
|
-
const from = request.from && typeof request.from === 'string' && request.from.trim()
|
|
306
|
-
? request.from.trim()
|
|
307
|
-
: 'unknown';
|
|
308
|
-
|
|
309
|
-
// timestamp 验证(可选,默认当前时间)
|
|
310
|
-
let timestamp: number;
|
|
311
|
-
if (request.timestamp !== undefined) {
|
|
312
|
-
if (typeof request.timestamp !== 'number' || !Number.isFinite(request.timestamp)) {
|
|
313
|
-
throw new Error('timestamp must be a finite number if provided');
|
|
314
|
-
}
|
|
315
|
-
if (request.timestamp <= 0) {
|
|
316
|
-
throw new Error('timestamp must be positive');
|
|
317
|
-
}
|
|
318
|
-
// timestamp 不能是未来时间(允许 5 分钟的时钟偏差)
|
|
319
|
-
const maxFutureTime = Date.now() + 5 * 60 * 1000;
|
|
320
|
-
if (request.timestamp > maxFutureTime) {
|
|
321
|
-
throw new Error('timestamp cannot be in the future');
|
|
322
|
-
}
|
|
323
|
-
timestamp = request.timestamp;
|
|
324
|
-
} else {
|
|
325
|
-
timestamp = Date.now();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// timeout 验证(可选,默认 DEFAULT_TIMEOUT_MS)
|
|
329
|
-
let timeout: number;
|
|
330
|
-
if (request.timeout !== undefined) {
|
|
331
|
-
if (typeof request.timeout !== 'number' || !Number.isFinite(request.timeout)) {
|
|
332
|
-
throw new Error('timeout must be a finite number if provided');
|
|
333
|
-
}
|
|
334
|
-
if (request.timeout < MIN_TIMEOUT_MS) {
|
|
335
|
-
throw new Error(`timeout must be >= ${MIN_TIMEOUT_MS}ms`);
|
|
336
|
-
}
|
|
337
|
-
if (request.timeout > MAX_TIMEOUT_MS) {
|
|
338
|
-
throw new Error(`timeout cannot exceed ${MAX_TIMEOUT_MS}ms (24 hours)`);
|
|
339
|
-
}
|
|
340
|
-
timeout = request.timeout;
|
|
341
|
-
} else {
|
|
342
|
-
timeout = DEFAULT_TIMEOUT_MS;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// ========== 队列容量管理 ==========
|
|
346
|
-
|
|
347
|
-
// 智能清理策略:
|
|
348
|
-
// 1. 队列大小超过阈值时触发完整清理
|
|
349
|
-
// 2. 距离上次清理超过清理间隔时触发清理
|
|
350
|
-
const now = Date.now();
|
|
351
|
-
const cleanupTriggerSize = Math.floor(this.maxSize * this.cleanupThreshold);
|
|
352
|
-
const shouldCleanup =
|
|
353
|
-
this.tasks.size >= cleanupTriggerSize ||
|
|
354
|
-
(now - this.lastCleanupTime) > this.cleanupIntervalMs;
|
|
355
|
-
|
|
356
|
-
if (shouldCleanup) {
|
|
357
|
-
this.cleanup();
|
|
358
|
-
this.lastCleanupTime = now;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// 检查是否为重复添加(保留原 createdAt)
|
|
362
|
-
const existingTask = this.tasks.get(request.taskId);
|
|
363
|
-
const preservedCreatedAt = existingTask?.createdAt ?? Date.now();
|
|
364
|
-
|
|
365
|
-
// 使用事务确保原子性(解决竞态条件)
|
|
366
|
-
if (this.db) {
|
|
367
|
-
const insertTask = this.db!.transaction(() => {
|
|
368
|
-
// 在事务内检查队列大小(原子操作)
|
|
369
|
-
const count = this.db!.prepare('SELECT COUNT(*) as count FROM tasks').get() as { count: number };
|
|
370
|
-
if (count.count >= this.maxSize && !existingTask) {
|
|
371
|
-
throw new Error('Task queue is full');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const task: QueuedTask = {
|
|
375
|
-
...request,
|
|
376
|
-
from,
|
|
377
|
-
timestamp,
|
|
378
|
-
timeout,
|
|
379
|
-
status: 'pending',
|
|
380
|
-
createdAt: preservedCreatedAt,
|
|
381
|
-
webhookPushed: false
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
this.db!.prepare(`
|
|
385
|
-
INSERT OR REPLACE INTO tasks
|
|
386
|
-
(id, task_type, description, parameters, status, created_at, webhook_pushed)
|
|
387
|
-
VALUES (?, ?, ?, ?, ?, ?, 0)
|
|
388
|
-
`).run(
|
|
389
|
-
task.taskId,
|
|
390
|
-
task.taskType || null,
|
|
391
|
-
task.description || null,
|
|
392
|
-
task.parameters ? JSON.stringify(task.parameters) : null,
|
|
393
|
-
task.status,
|
|
394
|
-
task.createdAt
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
return task;
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const task = insertTask();
|
|
401
|
-
// 同步内存状态
|
|
402
|
-
this.tasks.set(request.taskId, task);
|
|
403
|
-
return task;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// 无 DB 时,在内存中检查队列是否已满
|
|
407
|
-
// 注意:对于新任务才检查容量限制
|
|
408
|
-
if (!existingTask && this.tasks.size >= this.maxSize) {
|
|
409
|
-
throw new Error('Task queue is full');
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const task: QueuedTask = {
|
|
413
|
-
...request,
|
|
414
|
-
from,
|
|
415
|
-
timestamp,
|
|
416
|
-
timeout,
|
|
417
|
-
status: 'pending',
|
|
418
|
-
createdAt: preservedCreatedAt,
|
|
419
|
-
webhookPushed: false
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
this.tasks.set(request.taskId, task);
|
|
423
|
-
return task;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* 获取待处理任务
|
|
428
|
-
*/
|
|
429
|
-
getPending(limit: number = 10): QueuedTask[] {
|
|
430
|
-
return Array.from(this.tasks.values())
|
|
431
|
-
.filter(t => t.status === 'pending')
|
|
432
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
433
|
-
.slice(0, limit);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* 获取待 webhook 推送的任务
|
|
438
|
-
*/
|
|
439
|
-
getWebhookPending(): QueuedTask[] {
|
|
440
|
-
return Array.from(this.tasks.values())
|
|
441
|
-
.filter(t => t.status === 'pending' && !t.webhookPushed)
|
|
442
|
-
.sort((a, b) => a.createdAt - b.createdAt);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* 标记任务为已推送 webhook
|
|
447
|
-
*/
|
|
448
|
-
markWebhookPushed(taskId: string): void {
|
|
449
|
-
const task = this.tasks.get(taskId);
|
|
450
|
-
if (!task) return;
|
|
451
|
-
|
|
452
|
-
task.webhookPushed = true;
|
|
453
|
-
task.updatedAt = Date.now();
|
|
454
|
-
|
|
455
|
-
if (this.db) {
|
|
456
|
-
this.db!.prepare(`
|
|
457
|
-
UPDATE tasks SET webhook_pushed = 1, updated_at = ? WHERE id = ?
|
|
458
|
-
`).run(task.updatedAt, taskId);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* 标记任务为处理中
|
|
464
|
-
*/
|
|
465
|
-
markProcessing(taskId: string): QueuedTask | undefined {
|
|
466
|
-
const task = this.tasks.get(taskId);
|
|
467
|
-
if (!task) return undefined;
|
|
468
|
-
|
|
469
|
-
task.status = 'processing';
|
|
470
|
-
task.updatedAt = Date.now();
|
|
471
|
-
|
|
472
|
-
if (this.db) {
|
|
473
|
-
// 使用事务确保内存和数据库状态一致
|
|
474
|
-
const updateTransaction = this.db!.transaction(() => {
|
|
475
|
-
this.db!.prepare(`
|
|
476
|
-
UPDATE tasks SET status = 'processing', updated_at = ? WHERE id = ?
|
|
477
|
-
`).run(task.updatedAt, taskId);
|
|
478
|
-
});
|
|
479
|
-
updateTransaction();
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return task;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* P1 修复:重置 processing 任务为 pending
|
|
487
|
-
* 用于处理因异常导致的僵尸任务
|
|
488
|
-
*/
|
|
489
|
-
resetProcessingTask(taskId: string): QueuedTask | undefined {
|
|
490
|
-
const task = this.tasks.get(taskId);
|
|
491
|
-
if (!task || task.status !== 'processing') return undefined;
|
|
492
|
-
|
|
493
|
-
task.status = 'pending';
|
|
494
|
-
task.updatedAt = Date.now();
|
|
495
|
-
|
|
496
|
-
if (this.db) {
|
|
497
|
-
const updateTransaction = this.db!.transaction(() => {
|
|
498
|
-
this.db!.prepare(`
|
|
499
|
-
UPDATE tasks SET status = 'pending', updated_at = ? WHERE id = ?
|
|
500
|
-
`).run(task.updatedAt, taskId);
|
|
501
|
-
});
|
|
502
|
-
updateTransaction();
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return task;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* 完成任务
|
|
510
|
-
*/
|
|
511
|
-
complete(taskId: string, response: TaskResponse): QueuedTask | undefined {
|
|
512
|
-
const task = this.tasks.get(taskId);
|
|
513
|
-
if (!task) return undefined;
|
|
514
|
-
|
|
515
|
-
if (response.status === 'success') {
|
|
516
|
-
task.status = 'completed';
|
|
517
|
-
task.result = response.result;
|
|
518
|
-
} else {
|
|
519
|
-
task.status = 'failed';
|
|
520
|
-
task.error = response.error;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
task.latency = response.latency;
|
|
524
|
-
task.updatedAt = Date.now();
|
|
525
|
-
|
|
526
|
-
if (this.db) {
|
|
527
|
-
// 使用事务确保内存和数据库状态一致
|
|
528
|
-
const updateTransaction = this.db!.transaction(() => {
|
|
529
|
-
this.db!.prepare(`
|
|
530
|
-
UPDATE tasks
|
|
531
|
-
SET status = ?, result = ?, error = ?, latency = ?, updated_at = ?
|
|
532
|
-
WHERE id = ?
|
|
533
|
-
`).run(
|
|
534
|
-
task.status,
|
|
535
|
-
task.result ? JSON.stringify(task.result) : null,
|
|
536
|
-
task.error || null,
|
|
537
|
-
task.latency || null,
|
|
538
|
-
task.updatedAt,
|
|
539
|
-
taskId
|
|
540
|
-
);
|
|
541
|
-
});
|
|
542
|
-
updateTransaction();
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
return task;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
* 获取任务
|
|
550
|
-
*/
|
|
551
|
-
get(taskId: string): QueuedTask | undefined {
|
|
552
|
-
return this.tasks.get(taskId);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* 删除任务
|
|
557
|
-
*/
|
|
558
|
-
delete(taskId: string): boolean {
|
|
559
|
-
const deleted = this.tasks.delete(taskId);
|
|
560
|
-
|
|
561
|
-
if (deleted && this.db) {
|
|
562
|
-
this.db!.prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
return deleted;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* 获取队列统计
|
|
570
|
-
*/
|
|
571
|
-
getStats(): TaskQueueStats {
|
|
572
|
-
const tasks = Array.from(this.tasks.values());
|
|
573
|
-
return {
|
|
574
|
-
pending: tasks.filter(t => t.status === 'pending').length,
|
|
575
|
-
processing: tasks.filter(t => t.status === 'processing').length,
|
|
576
|
-
completed: tasks.filter(t => t.status === 'completed').length,
|
|
577
|
-
failed: tasks.filter(t => t.status === 'failed').length,
|
|
578
|
-
total: tasks.length,
|
|
579
|
-
webhookPending: tasks.filter(t => t.status === 'pending' && !t.webhookPushed).length
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* 获取所有任务
|
|
585
|
-
*/
|
|
586
|
-
getAll(): QueuedTask[] {
|
|
587
|
-
return Array.from(this.tasks.values());
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* 清理过期任务
|
|
592
|
-
* 同时清理已完成/失败的任务以腾出空间
|
|
593
|
-
*/
|
|
594
|
-
cleanup(): void {
|
|
595
|
-
const now = Date.now();
|
|
596
|
-
const toDelete: string[] = [];
|
|
597
|
-
|
|
598
|
-
for (const [id, task] of this.tasks) {
|
|
599
|
-
const age = now - task.createdAt;
|
|
600
|
-
|
|
601
|
-
// 清理过期任务
|
|
602
|
-
if (age > this.maxAgeMs) {
|
|
603
|
-
toDelete.push(id);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
for (const id of toDelete) {
|
|
608
|
-
this.tasks.delete(id);
|
|
609
|
-
if (this.db) {
|
|
610
|
-
this.db!.prepare('DELETE FROM tasks WHERE id = ?').run(id);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// 如果队列接近满,清理已完成和失败的任务
|
|
615
|
-
const highWatermark = Math.floor(this.maxSize * 0.9);
|
|
616
|
-
if (this.tasks.size >= highWatermark) {
|
|
617
|
-
const completedAndFailed: string[] = [];
|
|
618
|
-
|
|
619
|
-
for (const [id, task] of this.tasks) {
|
|
620
|
-
if (task.status === 'completed' || task.status === 'failed') {
|
|
621
|
-
completedAndFailed.push(id);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// 按更新时间排序,优先删除旧的已完成/失败任务
|
|
626
|
-
completedAndFailed.sort((a, b) => {
|
|
627
|
-
const taskA = this.tasks.get(a);
|
|
628
|
-
const taskB = this.tasks.get(b);
|
|
629
|
-
return (taskA?.updatedAt || taskA?.createdAt || 0) - (taskB?.updatedAt || taskB?.createdAt || 0);
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
// 删除足够多的任务以腾出空间
|
|
633
|
-
const targetSize = Math.floor(this.maxSize * 0.7);
|
|
634
|
-
const toRemove = completedAndFailed.slice(0, Math.max(0, this.tasks.size - targetSize));
|
|
635
|
-
|
|
636
|
-
for (const id of toRemove) {
|
|
637
|
-
this.tasks.delete(id);
|
|
638
|
-
if (this.db) {
|
|
639
|
-
this.db!.prepare('DELETE FROM tasks WHERE id = ?').run(id);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
if (toRemove.length > 0) {
|
|
644
|
-
logger.info('清理已完成/失败任务以释放空间: count=%d', toRemove.length);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* 清空队列
|
|
651
|
-
*/
|
|
652
|
-
clear(): void {
|
|
653
|
-
this.tasks.clear();
|
|
654
|
-
if (this.db) {
|
|
655
|
-
this.db!.exec('DELETE FROM tasks');
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* 关闭数据库连接
|
|
661
|
-
*/
|
|
662
|
-
close(): void {
|
|
663
|
-
if (this.db) {
|
|
664
|
-
this.db.close();
|
|
665
|
-
this.db = undefined;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|