@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.
Files changed (136) hide show
  1. package/package.json +8 -1
  2. package/.github/workflows/ci.yml +0 -113
  3. package/.github/workflows/publish.yml +0 -60
  4. package/MONOREPO.md +0 -58
  5. package/SKILL.md +0 -137
  6. package/dist/adapters/openclaw.d.ts +0 -103
  7. package/dist/adapters/openclaw.d.ts.map +0 -1
  8. package/dist/adapters/openclaw.js +0 -297
  9. package/dist/adapters/openclaw.js.map +0 -1
  10. package/dist/core/connection-manager.d.ts +0 -80
  11. package/dist/core/connection-manager.d.ts.map +0 -1
  12. package/dist/core/connection-manager.js +0 -235
  13. package/dist/core/connection-manager.js.map +0 -1
  14. package/dist/core/connection-manager.test.d.ts +0 -2
  15. package/dist/core/connection-manager.test.d.ts.map +0 -1
  16. package/dist/core/connection-manager.test.js +0 -52
  17. package/dist/core/connection-manager.test.js.map +0 -1
  18. package/dist/core/identity.d.ts +0 -47
  19. package/dist/core/identity.d.ts.map +0 -1
  20. package/dist/core/identity.js +0 -130
  21. package/dist/core/identity.js.map +0 -1
  22. package/dist/core/identity.test.d.ts +0 -2
  23. package/dist/core/identity.test.d.ts.map +0 -1
  24. package/dist/core/identity.test.js +0 -43
  25. package/dist/core/identity.test.js.map +0 -1
  26. package/dist/core/serverless.d.ts +0 -155
  27. package/dist/core/serverless.d.ts.map +0 -1
  28. package/dist/core/serverless.js +0 -615
  29. package/dist/core/serverless.js.map +0 -1
  30. package/dist/daemon/webhook.test.d.ts +0 -2
  31. package/dist/daemon/webhook.test.d.ts.map +0 -1
  32. package/dist/daemon/webhook.test.js +0 -24
  33. package/dist/daemon/webhook.test.js.map +0 -1
  34. package/dist/protocol/messages.d.ts +0 -739
  35. package/dist/protocol/messages.d.ts.map +0 -1
  36. package/dist/protocol/messages.js +0 -188
  37. package/dist/protocol/messages.js.map +0 -1
  38. package/dist/protocol/messages.test.d.ts +0 -2
  39. package/dist/protocol/messages.test.d.ts.map +0 -1
  40. package/dist/protocol/messages.test.js +0 -55
  41. package/dist/protocol/messages.test.js.map +0 -1
  42. package/docs/F2A-PROTOCOL.md +0 -61
  43. package/docs/MOBILE_BOOTSTRAP_DESIGN.md +0 -126
  44. package/docs/a2a-lessons.md +0 -316
  45. package/docs/middleware-guide.md +0 -448
  46. package/docs/readme-update-checklist.md +0 -90
  47. package/docs/reputation-guide.md +0 -396
  48. package/docs/rfcs/001-reputation-system.md +0 -712
  49. package/docs/security-design.md +0 -247
  50. package/install.sh +0 -231
  51. package/packages/openclaw-adapter/README.md +0 -510
  52. package/packages/openclaw-adapter/openclaw.plugin.json +0 -106
  53. package/packages/openclaw-adapter/package.json +0 -40
  54. package/packages/openclaw-adapter/src/announcement-queue.test.ts +0 -449
  55. package/packages/openclaw-adapter/src/announcement-queue.ts +0 -403
  56. package/packages/openclaw-adapter/src/capability-detector.test.ts +0 -99
  57. package/packages/openclaw-adapter/src/capability-detector.ts +0 -183
  58. package/packages/openclaw-adapter/src/claim-handlers.test.ts +0 -974
  59. package/packages/openclaw-adapter/src/claim-handlers.ts +0 -482
  60. package/packages/openclaw-adapter/src/connector.business.test.ts +0 -583
  61. package/packages/openclaw-adapter/src/connector.ts +0 -795
  62. package/packages/openclaw-adapter/src/index.test.ts +0 -82
  63. package/packages/openclaw-adapter/src/index.ts +0 -18
  64. package/packages/openclaw-adapter/src/integration.e2e.test.ts +0 -829
  65. package/packages/openclaw-adapter/src/logger.ts +0 -51
  66. package/packages/openclaw-adapter/src/network-client.test.ts +0 -266
  67. package/packages/openclaw-adapter/src/network-client.ts +0 -251
  68. package/packages/openclaw-adapter/src/network-recovery.test.ts +0 -465
  69. package/packages/openclaw-adapter/src/node-manager.test.ts +0 -136
  70. package/packages/openclaw-adapter/src/node-manager.ts +0 -429
  71. package/packages/openclaw-adapter/src/plugin.test.ts +0 -439
  72. package/packages/openclaw-adapter/src/plugin.ts +0 -104
  73. package/packages/openclaw-adapter/src/reputation.test.ts +0 -221
  74. package/packages/openclaw-adapter/src/reputation.ts +0 -368
  75. package/packages/openclaw-adapter/src/task-guard.test.ts +0 -502
  76. package/packages/openclaw-adapter/src/task-guard.ts +0 -860
  77. package/packages/openclaw-adapter/src/task-queue.concurrency.test.ts +0 -462
  78. package/packages/openclaw-adapter/src/task-queue.edge-cases.test.ts +0 -284
  79. package/packages/openclaw-adapter/src/task-queue.persistence.test.ts +0 -408
  80. package/packages/openclaw-adapter/src/task-queue.ts +0 -668
  81. package/packages/openclaw-adapter/src/tool-handlers.test.ts +0 -906
  82. package/packages/openclaw-adapter/src/tool-handlers.ts +0 -574
  83. package/packages/openclaw-adapter/src/types.ts +0 -361
  84. package/packages/openclaw-adapter/src/webhook-pusher.test.ts +0 -188
  85. package/packages/openclaw-adapter/src/webhook-pusher.ts +0 -220
  86. package/packages/openclaw-adapter/src/webhook-server.test.ts +0 -580
  87. package/packages/openclaw-adapter/src/webhook-server.ts +0 -202
  88. package/packages/openclaw-adapter/tsconfig.json +0 -20
  89. package/src/cli/commands.test.ts +0 -157
  90. package/src/cli/commands.ts +0 -129
  91. package/src/cli/index.test.ts +0 -77
  92. package/src/cli/index.ts +0 -234
  93. package/src/core/autonomous-economy.test.ts +0 -291
  94. package/src/core/autonomous-economy.ts +0 -428
  95. package/src/core/e2ee-crypto.test.ts +0 -125
  96. package/src/core/e2ee-crypto.ts +0 -246
  97. package/src/core/f2a.test.ts +0 -269
  98. package/src/core/f2a.ts +0 -618
  99. package/src/core/p2p-network.test.ts +0 -199
  100. package/src/core/p2p-network.ts +0 -1432
  101. package/src/core/reputation-security.test.ts +0 -403
  102. package/src/core/reputation-security.ts +0 -562
  103. package/src/core/reputation.test.ts +0 -260
  104. package/src/core/reputation.ts +0 -576
  105. package/src/core/review-committee.test.ts +0 -380
  106. package/src/core/review-committee.ts +0 -401
  107. package/src/core/token-manager.test.ts +0 -133
  108. package/src/core/token-manager.ts +0 -140
  109. package/src/daemon/control-server.test.ts +0 -216
  110. package/src/daemon/control-server.ts +0 -292
  111. package/src/daemon/index.test.ts +0 -85
  112. package/src/daemon/index.ts +0 -89
  113. package/src/daemon/main.ts +0 -44
  114. package/src/daemon/start.ts +0 -29
  115. package/src/daemon/webhook.test.ts +0 -68
  116. package/src/daemon/webhook.ts +0 -105
  117. package/src/index.test.ts +0 -436
  118. package/src/index.ts +0 -72
  119. package/src/types/index.test.ts +0 -87
  120. package/src/types/index.ts +0 -341
  121. package/src/types/result.ts +0 -68
  122. package/src/utils/benchmark.ts +0 -237
  123. package/src/utils/logger.ts +0 -331
  124. package/src/utils/middleware.ts +0 -229
  125. package/src/utils/rate-limiter.ts +0 -207
  126. package/src/utils/signature.ts +0 -136
  127. package/src/utils/validation.ts +0 -186
  128. package/tests/docker/Dockerfile.node +0 -23
  129. package/tests/docker/Dockerfile.runner +0 -18
  130. package/tests/docker/docker-compose.test.yml +0 -73
  131. package/tests/integration/message-passing.test.ts +0 -109
  132. package/tests/integration/multi-node.test.ts +0 -92
  133. package/tests/integration/p2p-connection.test.ts +0 -83
  134. package/tests/integration/test-config.ts +0 -32
  135. package/tsconfig.json +0 -21
  136. 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
- }