@openfinclaw/openfinclaw-strategy 2026.4.10 → 2026.4.12
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 +12 -12
- package/SKILL.md +9 -9
- package/index.ts +29 -2
- package/package.json +1 -1
- package/src/cli.ts +22 -6
- package/src/db/db.ts +3 -1
- package/src/db/repositories.ts +15 -10
- package/src/db/schema.ts +21 -0
- package/src/tournament/cron-setup.ts +102 -0
- package/src/tournament/db.test.ts +222 -0
- package/src/tournament/db.ts +286 -0
- package/src/tournament/orchestrator.test.ts +232 -0
- package/src/tournament/orchestrator.ts +238 -0
- package/src/tournament/prompts.ts +65 -0
- package/src/tournament/tools.test.ts +221 -0
- package/src/tournament/tools.ts +192 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ openclaw plugins install @openfinclaw/openfinclaw-strategy
|
|
|
14
14
|
## 快速开始
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
#
|
|
17
|
+
# 查看策略排行榜
|
|
18
18
|
openclaw strategy leaderboard
|
|
19
19
|
|
|
20
20
|
# 查看收益榜 Top 10
|
|
@@ -45,24 +45,24 @@ openclaw strategy show <strategy-id> --remote
|
|
|
45
45
|
|
|
46
46
|
### 策略工具
|
|
47
47
|
|
|
48
|
-
| 工具名 | 说明 | API Key
|
|
49
|
-
| ---------------------- | -------------------------- |
|
|
50
|
-
| `skill_leaderboard` | 查询排行榜 |
|
|
51
|
-
| `skill_get_info` | 获取 Hub 策略公开详情 |
|
|
52
|
-
| `skill_validate` | 本地验证策略包(FEP v2.0) |
|
|
53
|
-
| `skill_list_local` | 列出本地策略 |
|
|
54
|
-
| `skill_fork` | 从 Hub 下载策略到本地 |
|
|
55
|
-
| `skill_publish` | 发布策略 ZIP 到 Hub |
|
|
56
|
-
| `skill_publish_verify` | 查询发布状态和回测报告 |
|
|
48
|
+
| 工具名 | 说明 | API Key |
|
|
49
|
+
| ---------------------- | -------------------------- | ------- |
|
|
50
|
+
| `skill_leaderboard` | 查询排行榜 | 需要 |
|
|
51
|
+
| `skill_get_info` | 获取 Hub 策略公开详情 | 需要 |
|
|
52
|
+
| `skill_validate` | 本地验证策略包(FEP v2.0) | 需要 |
|
|
53
|
+
| `skill_list_local` | 列出本地策略 | 需要 |
|
|
54
|
+
| `skill_fork` | 从 Hub 下载策略到本地 | 需要 |
|
|
55
|
+
| `skill_publish` | 发布策略 ZIP 到 Hub | 需要 |
|
|
56
|
+
| `skill_publish_verify` | 查询发布状态和回测报告 | 需要 |
|
|
57
57
|
|
|
58
58
|
## CLI 命令
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
|
-
#
|
|
61
|
+
# 查看排行榜
|
|
62
62
|
openclaw strategy leaderboard
|
|
63
63
|
openclaw strategy leaderboard returns --limit 10
|
|
64
64
|
|
|
65
|
-
# 从 Hub Fork
|
|
65
|
+
# 从 Hub Fork 策略
|
|
66
66
|
openclaw strategy fork <strategy-id>
|
|
67
67
|
|
|
68
68
|
# 列出本地策略
|
package/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ metadata:
|
|
|
12
12
|
|
|
13
13
|
统一金融工具平台,一个 API Key 即可使用所有功能:
|
|
14
14
|
|
|
15
|
-
- **行情数据**:
|
|
15
|
+
- **行情数据**: 价格查询、K线、加密市场数据、多资产对比、代码搜索
|
|
16
16
|
- **策略工具**: 创建、验证、发布、Fork 策略
|
|
17
17
|
- **统一认证**: 一个 API Key 访问 Hub 和 DataHub
|
|
18
18
|
|
|
@@ -54,13 +54,13 @@ Agent: 使用 fin_compare 对比收益
|
|
|
54
54
|
### 策略管理
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
#
|
|
57
|
+
# 查看排行榜
|
|
58
58
|
openclaw strategy leaderboard
|
|
59
59
|
|
|
60
60
|
# 查看收益榜 Top 10
|
|
61
61
|
openclaw strategy leaderboard returns --limit 10
|
|
62
62
|
|
|
63
|
-
#
|
|
63
|
+
# 查看策略详情
|
|
64
64
|
openclaw strategy show 550e8400-e29b-41d4-a716-446655440001 --remote
|
|
65
65
|
```
|
|
66
66
|
|
|
@@ -134,10 +134,10 @@ export OPENFINCLAW_API_KEY=YOUR_API_KEY
|
|
|
134
134
|
|
|
135
135
|
| 工具名 | 用途 | 需要 API Key |
|
|
136
136
|
| ---------------------- | -------------------------------------- | ------------ |
|
|
137
|
-
| `skill_leaderboard` | 查询排行榜(综合/收益/风控/人气/新星) |
|
|
138
|
-
| `skill_get_info` | 获取 Hub 策略公开详情 |
|
|
139
|
-
| `skill_validate` | 本地验证策略包格式(FEP v2.0) |
|
|
140
|
-
| `skill_list_local` | 列出本地已下载的策略 |
|
|
137
|
+
| `skill_leaderboard` | 查询排行榜(综合/收益/风控/人气/新星) | **是** |
|
|
138
|
+
| `skill_get_info` | 获取 Hub 策略公开详情 | **是** |
|
|
139
|
+
| `skill_validate` | 本地验证策略包格式(FEP v2.0) | **是** |
|
|
140
|
+
| `skill_list_local` | 列出本地已下载的策略 | **是** |
|
|
141
141
|
| `skill_fork` | 从 Hub 下载公开策略到本地 | **是** |
|
|
142
142
|
| `skill_publish` | 发布策略 ZIP 到 Hub,自动触发回测 | **是** |
|
|
143
143
|
| `skill_publish_verify` | 查询发布状态和回测报告 | **是** |
|
|
@@ -172,7 +172,7 @@ export OPENFINCLAW_API_KEY=YOUR_API_KEY
|
|
|
172
172
|
|
|
173
173
|
### strategy leaderboard
|
|
174
174
|
|
|
175
|
-
查看 Hub
|
|
175
|
+
查看 Hub 排行榜:
|
|
176
176
|
|
|
177
177
|
```bash
|
|
178
178
|
# 综合榜 Top 20(默认)
|
|
@@ -223,7 +223,7 @@ openclaw strategy list
|
|
|
223
223
|
# 查看本地策略
|
|
224
224
|
openclaw strategy show btc-adaptive-dca-34a5792f
|
|
225
225
|
|
|
226
|
-
# 从 Hub
|
|
226
|
+
# 从 Hub 获取最新信息
|
|
227
227
|
openclaw strategy show 550e8400-e29b-41d4-a716-446655440001 --remote
|
|
228
228
|
```
|
|
229
229
|
|
package/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
|
12
12
|
import { registerStrategyCli } from "./src/cli.js";
|
|
13
13
|
import { resolvePluginConfig } from "./src/config.js";
|
|
14
14
|
import { registerDatahubTools } from "./src/datahub/tools.js";
|
|
15
|
-
import { getDb } from "./src/db/db.js";
|
|
15
|
+
import { closeDb, getDb } from "./src/db/db.js";
|
|
16
16
|
import { createOpenFinclawGatewayProxy } from "./src/http/gateway-proxy.js";
|
|
17
17
|
import { startHttpServer } from "./src/http/server.js";
|
|
18
18
|
import { OPENFINCLAW_AGENT_GUIDANCE } from "./src/prompt-guidance.js";
|
|
@@ -20,6 +20,10 @@ import { setupOpenfinclawCronJobs } from "./src/scheduler/cron-setup.js";
|
|
|
20
20
|
import { AggregatedNewsProvider, createNewsProviders } from "./src/scheduler/news-provider.js";
|
|
21
21
|
import { registerSchedulerTools } from "./src/scheduler/tools.js";
|
|
22
22
|
import { registerStrategyTools } from "./src/strategy/tools.js";
|
|
23
|
+
import { setupTournamentCronJob } from "./src/tournament/cron-setup.js";
|
|
24
|
+
import { TournamentDb } from "./src/tournament/db.js";
|
|
25
|
+
import { buildOrchestratorPrompt } from "./src/tournament/prompts.js";
|
|
26
|
+
import { registerTournamentTools } from "./src/tournament/tools.js";
|
|
23
27
|
|
|
24
28
|
export default definePluginEntry({
|
|
25
29
|
id: "openfinclaw-strategy",
|
|
@@ -68,11 +72,21 @@ export default definePluginEntry({
|
|
|
68
72
|
handler: createOpenFinclawGatewayProxy({ port: config.httpPort, logger: api.logger }),
|
|
69
73
|
});
|
|
70
74
|
|
|
75
|
+
// Register tournament tools (tournament_pick, tournament_leaderboard, tournament_result)
|
|
76
|
+
const getTournamentDb = () => new TournamentDb(getDb());
|
|
77
|
+
registerTournamentTools(api.registerTool.bind(api), getTournamentDb);
|
|
78
|
+
|
|
71
79
|
// Inject agent system prompt: prioritise tool calls so data lands in SQLite
|
|
80
|
+
const tournamentPrompt = buildOrchestratorPrompt();
|
|
72
81
|
api.on("before_prompt_build", async () => ({
|
|
73
|
-
prependSystemContext: OPENFINCLAW_AGENT_GUIDANCE
|
|
82
|
+
prependSystemContext: `${OPENFINCLAW_AGENT_GUIDANCE}\n\n${tournamentPrompt}`,
|
|
74
83
|
}));
|
|
75
84
|
|
|
85
|
+
// Graceful shutdown: close SQLite connection
|
|
86
|
+
process.once("beforeExit", () => {
|
|
87
|
+
closeDb();
|
|
88
|
+
});
|
|
89
|
+
|
|
76
90
|
// ── Gateway Cron registration ──
|
|
77
91
|
// Write cron jobs directly to ~/.openclaw/cron/jobs.json during register().
|
|
78
92
|
// This ensures jobs are available immediately on both gateway startup AND
|
|
@@ -92,6 +106,19 @@ export default definePluginEntry({
|
|
|
92
106
|
`[OpenFinClaw] Cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
93
107
|
);
|
|
94
108
|
});
|
|
109
|
+
|
|
110
|
+
// Register tournament cron job
|
|
111
|
+
setupTournamentCronJob()
|
|
112
|
+
.then((result) => {
|
|
113
|
+
if (result.created) {
|
|
114
|
+
api.logger.info("[OpenFinClaw] Tournament cron job registered");
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.catch((err) => {
|
|
118
|
+
api.logger.info(
|
|
119
|
+
`[OpenFinClaw] Tournament cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
95
122
|
}
|
|
96
123
|
},
|
|
97
124
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfinclaw/openfinclaw-strategy",
|
|
3
|
-
"version": "2026.4.
|
|
3
|
+
"version": "2026.4.12",
|
|
4
4
|
"description": "OpenFinClaw - Unified financial tools: market data (price/K-line/crypto/compare/search), strategy publishing, fork, and validation. Single API key for Hub and DataHub.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"backtest",
|
package/src/cli.ts
CHANGED
|
@@ -50,7 +50,7 @@ export function registerStrategyCli(params: {
|
|
|
50
50
|
// ── strategy leaderboard ──
|
|
51
51
|
root
|
|
52
52
|
.command("leaderboard [boardType]")
|
|
53
|
-
.description("Query strategy leaderboard from Hub
|
|
53
|
+
.description("Query strategy leaderboard from Hub")
|
|
54
54
|
.option("-l, --limit <number>", "Number of results (max 100)", "20")
|
|
55
55
|
.option("-o, --offset <number>", "Offset for pagination", "0")
|
|
56
56
|
.action(
|
|
@@ -63,10 +63,18 @@ export function registerStrategyCli(params: {
|
|
|
63
63
|
url.searchParams.set("limit", String(limit));
|
|
64
64
|
url.searchParams.set("offset", String(offset));
|
|
65
65
|
|
|
66
|
+
if (!config.apiKey) {
|
|
67
|
+
console.error(
|
|
68
|
+
"✗ API key not configured. Set apiKey in plugin config or OPENFINCLAW_API_KEY env var.",
|
|
69
|
+
);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
try {
|
|
67
75
|
const response = await fetch(url.toString(), {
|
|
68
76
|
method: "GET",
|
|
69
|
-
headers: { Accept: "application/json" },
|
|
77
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${config.apiKey}` },
|
|
70
78
|
signal: AbortSignal.timeout(config.requestTimeoutMs),
|
|
71
79
|
});
|
|
72
80
|
|
|
@@ -344,10 +352,18 @@ function printStrategyInfo(
|
|
|
344
352
|
console.log("Hub:");
|
|
345
353
|
console.log(` ID: ${hub.id}`);
|
|
346
354
|
console.log(` 名称: ${hub.name}`);
|
|
347
|
-
if (hub.version)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (hub.
|
|
355
|
+
if (hub.version) {
|
|
356
|
+
console.log(` 版本: ${hub.version}`);
|
|
357
|
+
}
|
|
358
|
+
if (hub.author?.displayName) {
|
|
359
|
+
console.log(` 作者: ${hub.author.displayName}`);
|
|
360
|
+
}
|
|
361
|
+
if (hub.market) {
|
|
362
|
+
console.log(` 市场: ${hub.market}`);
|
|
363
|
+
}
|
|
364
|
+
if (hub.description) {
|
|
365
|
+
console.log(` 描述: ${hub.description}`);
|
|
366
|
+
}
|
|
351
367
|
|
|
352
368
|
if (hub.backtestResult) {
|
|
353
369
|
console.log("");
|
package/src/db/db.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { mkdirSync, unlinkSync } from "node:fs";
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { DatabaseSync } from "node:sqlite";
|
|
15
|
-
import { ensureSchema } from "./schema.js";
|
|
15
|
+
import { cleanupOldRows, ensureSchema } from "./schema.js";
|
|
16
16
|
|
|
17
17
|
/** globalThis key — survives module hot-reloads within the same process. */
|
|
18
18
|
const GLOBAL_DB_KEY = "__openfinclaw_db__" as const;
|
|
@@ -52,6 +52,8 @@ export function getDb(): DatabaseSync {
|
|
|
52
52
|
ensureSchema(db);
|
|
53
53
|
}
|
|
54
54
|
(globalThis as Record<string, unknown>)[GLOBAL_DB_KEY] = db;
|
|
55
|
+
// Best-effort cleanup of old rows on first access each process lifecycle
|
|
56
|
+
cleanupOldRows(db);
|
|
55
57
|
return db;
|
|
56
58
|
}
|
|
57
59
|
|
package/src/db/repositories.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Data access helpers for OpenFinClaw plugin SQLite tables.
|
|
3
3
|
*/
|
|
4
|
-
import type { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import type { DatabaseSync, SQLInputValue } from "node:sqlite";
|
|
5
|
+
|
|
6
|
+
/** Cast unknown[] to SQLInputValue[] for db.prepare().run() spread. */
|
|
7
|
+
function sqlParams(values: unknown[]): SQLInputValue[] {
|
|
8
|
+
return values as SQLInputValue[];
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
// ── Row types ─────────────────────────────────────────────────────────────
|
|
7
12
|
|
|
@@ -262,7 +267,7 @@ export function updateBacktestResult(
|
|
|
262
267
|
}
|
|
263
268
|
if (sets.length === 0) return;
|
|
264
269
|
values.push(id);
|
|
265
|
-
db.prepare(`UPDATE backtest_results SET ${sets.join(", ")} WHERE id = ?`).run(...values);
|
|
270
|
+
db.prepare(`UPDATE backtest_results SET ${sets.join(", ")} WHERE id = ?`).run(...sqlParams(values));
|
|
266
271
|
} catch {
|
|
267
272
|
// Logging must never crash the calling tool
|
|
268
273
|
}
|
|
@@ -347,7 +352,7 @@ export function updateScanHistory(
|
|
|
347
352
|
}
|
|
348
353
|
if (sets.length === 0) return;
|
|
349
354
|
values.push(id);
|
|
350
|
-
db.prepare(`UPDATE scan_history SET ${sets.join(", ")} WHERE id = ?`).run(...values);
|
|
355
|
+
db.prepare(`UPDATE scan_history SET ${sets.join(", ")} WHERE id = ?`).run(...sqlParams(values));
|
|
351
356
|
} catch {
|
|
352
357
|
// Logging must never crash the calling tool
|
|
353
358
|
}
|
|
@@ -378,7 +383,7 @@ export function queryScanHistory(
|
|
|
378
383
|
params.push(limit, offset);
|
|
379
384
|
return db
|
|
380
385
|
.prepare(`SELECT * FROM scan_history ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?`)
|
|
381
|
-
.all(...params) as ScanHistoryEntry[];
|
|
386
|
+
.all(...sqlParams(params)) as unknown as ScanHistoryEntry[];
|
|
382
387
|
}
|
|
383
388
|
|
|
384
389
|
/**
|
|
@@ -462,7 +467,7 @@ export function queryPriceAlerts(
|
|
|
462
467
|
params.push(limit, offset);
|
|
463
468
|
return db
|
|
464
469
|
.prepare(`SELECT * FROM price_alerts ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
|
|
465
|
-
.all(...params) as PriceAlertEntry[];
|
|
470
|
+
.all(...sqlParams(params)) as unknown as PriceAlertEntry[];
|
|
466
471
|
}
|
|
467
472
|
|
|
468
473
|
/** Count price_alerts rows since an inclusive time bound (ISO 8601 created_at). */
|
|
@@ -488,19 +493,19 @@ export function acknowledgePriceAlert(db: DatabaseSync, id: string): void {
|
|
|
488
493
|
export function queryActivityLog(db: DatabaseSync, limit = 50, offset = 0): ActivityLogEntry[] {
|
|
489
494
|
return db
|
|
490
495
|
.prepare(`SELECT * FROM agent_activity_log ORDER BY timestamp DESC LIMIT ? OFFSET ?`)
|
|
491
|
-
.all(limit, offset) as ActivityLogEntry[];
|
|
496
|
+
.all(limit, offset) as unknown as ActivityLogEntry[];
|
|
492
497
|
}
|
|
493
498
|
|
|
494
499
|
/** Query agent_events, newest first. */
|
|
495
500
|
export function queryAgentEvents(db: DatabaseSync, limit = 50, offset = 0): AgentEventEntry[] {
|
|
496
501
|
return db
|
|
497
502
|
.prepare(`SELECT * FROM agent_events ORDER BY timestamp DESC LIMIT ? OFFSET ?`)
|
|
498
|
-
.all(limit, offset) as AgentEventEntry[];
|
|
503
|
+
.all(limit, offset) as unknown as AgentEventEntry[];
|
|
499
504
|
}
|
|
500
505
|
|
|
501
506
|
/** Query all strategies, newest first. */
|
|
502
507
|
export function queryStrategies(db: DatabaseSync): StrategyRow[] {
|
|
503
|
-
return db.prepare(`SELECT * FROM strategies ORDER BY updated_at DESC`).all() as StrategyRow[];
|
|
508
|
+
return db.prepare(`SELECT * FROM strategies ORDER BY updated_at DESC`).all() as unknown as StrategyRow[];
|
|
504
509
|
}
|
|
505
510
|
|
|
506
511
|
/** Query backtest results, optionally filtered by strategy_id. */
|
|
@@ -508,9 +513,9 @@ export function queryBacktestResults(db: DatabaseSync, strategyId?: string): Bac
|
|
|
508
513
|
if (strategyId) {
|
|
509
514
|
return db
|
|
510
515
|
.prepare(`SELECT * FROM backtest_results WHERE strategy_id = ? ORDER BY created_at DESC`)
|
|
511
|
-
.all(strategyId) as BacktestRow[];
|
|
516
|
+
.all(strategyId) as unknown as BacktestRow[];
|
|
512
517
|
}
|
|
513
518
|
return db
|
|
514
519
|
.prepare(`SELECT * FROM backtest_results ORDER BY created_at DESC`)
|
|
515
|
-
.all() as BacktestRow[];
|
|
520
|
+
.all() as unknown as BacktestRow[];
|
|
516
521
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Based on ER diagram v0.3.
|
|
4
4
|
*/
|
|
5
5
|
import type { DatabaseSync } from "node:sqlite";
|
|
6
|
+
import { ensureTournamentSchema } from "../tournament/db.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Create (or migrate) all MVP tables.
|
|
@@ -134,9 +135,29 @@ export function ensureSchema(db: DatabaseSync): void {
|
|
|
134
135
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_price_alerts_created ON price_alerts(created_at);`);
|
|
135
136
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_price_alerts_strategy ON price_alerts(strategy_id);`);
|
|
136
137
|
|
|
138
|
+
// ── tournament tables ──────────────────────────────────────────────────
|
|
139
|
+
ensureTournamentSchema(db);
|
|
140
|
+
|
|
137
141
|
// ── migrations (add columns to existing databases) ──────────────────────
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Purge old rows from accumulating tables.
|
|
146
|
+
* Safe to call periodically (e.g. on plugin register or via cron).
|
|
147
|
+
* Keeps 90 days of activity logs, scan history, and acknowledged price alerts.
|
|
148
|
+
*/
|
|
149
|
+
export function cleanupOldRows(db: DatabaseSync): void {
|
|
150
|
+
const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString();
|
|
151
|
+
try {
|
|
152
|
+
db.exec(`DELETE FROM agent_activity_log WHERE timestamp < '${cutoff}'`);
|
|
153
|
+
db.exec(`DELETE FROM scan_history WHERE started_at < '${cutoff}'`);
|
|
154
|
+
db.exec(`DELETE FROM price_alerts WHERE acknowledged = 1 AND created_at < '${cutoff}'`);
|
|
155
|
+
db.exec(`DELETE FROM agent_events WHERE timestamp < '${cutoff}'`);
|
|
156
|
+
} catch {
|
|
157
|
+
// Cleanup is best-effort — never crash the plugin
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
140
161
|
/** Add a column if it doesn't already exist (safe for repeated calls). */
|
|
141
162
|
function ensureColumn(db: DatabaseSync, table: string, column: string, definition: string): void {
|
|
142
163
|
const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tournament cron job registration.
|
|
3
|
+
* Follows the same file-based pattern as scheduler/cron-setup.ts.
|
|
4
|
+
* @module openfinclaw/tournament/cron-setup
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TOURNAMENT_CRON = "0 9 * * 1-5"; // 9 AM weekdays
|
|
11
|
+
const DEFAULT_TOURNAMENT_TZ = "Asia/Shanghai";
|
|
12
|
+
|
|
13
|
+
interface StoredCronJob {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
schedule: { kind: "cron"; expr: string; tz?: string };
|
|
18
|
+
payload: { kind: "systemEvent"; text: string };
|
|
19
|
+
sessionTarget: string;
|
|
20
|
+
wakeMode: string;
|
|
21
|
+
delivery: { mode: string };
|
|
22
|
+
createdAtMs: number;
|
|
23
|
+
updatedAtMs: number;
|
|
24
|
+
state: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface CronStoreFile {
|
|
28
|
+
version: 1;
|
|
29
|
+
jobs: StoredCronJob[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function defaultStorePath(): string {
|
|
33
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
34
|
+
return path.join(home, ".openclaw", "cron", "jobs.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function loadStore(storePath: string): Promise<CronStoreFile> {
|
|
38
|
+
try {
|
|
39
|
+
const raw = await fs.promises.readFile(storePath, "utf-8");
|
|
40
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
41
|
+
const jobs = Array.isArray(parsed.jobs) ? (parsed.jobs as StoredCronJob[]) : [];
|
|
42
|
+
return { version: 1, jobs };
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if ((err as { code?: string }).code === "ENOENT") {
|
|
45
|
+
return { version: 1, jobs: [] };
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function saveStore(storePath: string, store: CronStoreFile): Promise<void> {
|
|
52
|
+
const dir = path.dirname(storePath);
|
|
53
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
54
|
+
const json = JSON.stringify(store, null, 2);
|
|
55
|
+
const tmp = `${storePath}.${process.pid}.${Date.now()}.tmp`;
|
|
56
|
+
await fs.promises.writeFile(tmp, json, "utf-8");
|
|
57
|
+
await fs.promises.rename(tmp, storePath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const TOURNAMENT_JOB_NAME = "openfinclaw:tournament";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register the daily tournament cron job.
|
|
64
|
+
* Idempotent: skips if job already exists by name.
|
|
65
|
+
*/
|
|
66
|
+
export async function setupTournamentCronJob(config?: {
|
|
67
|
+
cronExpr?: string;
|
|
68
|
+
timezone?: string;
|
|
69
|
+
}): Promise<{ ok: boolean; created: boolean }> {
|
|
70
|
+
const storePath = defaultStorePath();
|
|
71
|
+
const store = await loadStore(storePath);
|
|
72
|
+
|
|
73
|
+
const existingNames = new Set(store.jobs.map((j) => j.name));
|
|
74
|
+
if (existingNames.has(TOURNAMENT_JOB_NAME)) {
|
|
75
|
+
return { ok: true, created: false };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const cronExpr =
|
|
79
|
+
config?.cronExpr ?? process.env.OPENFINCLAW_TOURNAMENT_CRON ?? DEFAULT_TOURNAMENT_CRON;
|
|
80
|
+
const timezone = config?.timezone ?? DEFAULT_TOURNAMENT_TZ;
|
|
81
|
+
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
store.jobs.push({
|
|
84
|
+
id: randomUUID(),
|
|
85
|
+
name: TOURNAMENT_JOB_NAME,
|
|
86
|
+
enabled: true,
|
|
87
|
+
schedule: { kind: "cron", expr: cronExpr, tz: timezone },
|
|
88
|
+
payload: {
|
|
89
|
+
kind: "systemEvent",
|
|
90
|
+
text: "[openfinclaw:tournament] 每日策略锦标赛触发。请执行今日锦标赛流程。",
|
|
91
|
+
},
|
|
92
|
+
sessionTarget: "main",
|
|
93
|
+
wakeMode: "now",
|
|
94
|
+
delivery: { mode: "none" },
|
|
95
|
+
createdAtMs: now,
|
|
96
|
+
updatedAtMs: now,
|
|
97
|
+
state: {},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await saveStore(storePath, store);
|
|
101
|
+
return { ok: true, created: true };
|
|
102
|
+
}
|