@openfinclaw/openfinclaw-strategy 2026.3.310 → 2026.4.2
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/index.ts +3 -25
- package/openclaw.plugin.json +12 -2
- package/package.json +1 -1
- package/src/config.ts +27 -1
- package/src/db/schema.ts +0 -4
- package/src/scheduler/cron-setup.ts +93 -26
- package/src/types.ts +6 -0
- package/src/tournament/cron-setup.ts +0 -102
- package/src/tournament/db.test.ts +0 -222
- package/src/tournament/db.ts +0 -286
- package/src/tournament/orchestrator.test.ts +0 -232
- package/src/tournament/orchestrator.ts +0 -238
- package/src/tournament/prompts.ts +0 -65
- package/src/tournament/tools.test.ts +0 -221
- package/src/tournament/tools.ts +0 -192
package/index.ts
CHANGED
|
@@ -20,10 +20,6 @@ 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";
|
|
27
23
|
|
|
28
24
|
export default definePluginEntry({
|
|
29
25
|
id: "openfinclaw-strategy",
|
|
@@ -72,14 +68,9 @@ export default definePluginEntry({
|
|
|
72
68
|
handler: createOpenFinclawGatewayProxy({ port: config.httpPort, logger: api.logger }),
|
|
73
69
|
});
|
|
74
70
|
|
|
75
|
-
// Register tournament tools (tournament_pick, tournament_leaderboard, tournament_result)
|
|
76
|
-
const getTournamentDb = () => new TournamentDb(getDb());
|
|
77
|
-
registerTournamentTools(api.registerTool.bind(api), getTournamentDb);
|
|
78
|
-
|
|
79
71
|
// Inject agent system prompt: prioritise tool calls so data lands in SQLite
|
|
80
|
-
const tournamentPrompt = buildOrchestratorPrompt();
|
|
81
72
|
api.on("before_prompt_build", async () => ({
|
|
82
|
-
prependSystemContext:
|
|
73
|
+
prependSystemContext: OPENFINCLAW_AGENT_GUIDANCE,
|
|
83
74
|
}));
|
|
84
75
|
|
|
85
76
|
// ── Gateway Cron registration ──
|
|
@@ -90,9 +81,9 @@ export default definePluginEntry({
|
|
|
90
81
|
if (config.schedulerEnabled) {
|
|
91
82
|
setupOpenfinclawCronJobs(config)
|
|
92
83
|
.then((result) => {
|
|
93
|
-
if (result.created > 0) {
|
|
84
|
+
if (result.created > 0 || result.migrated > 0) {
|
|
94
85
|
api.logger.info(
|
|
95
|
-
`[OpenFinClaw] Cron jobs
|
|
86
|
+
`[OpenFinClaw] Cron jobs: ${result.created} created, ${result.migrated} migrated, ${result.existing} existing`,
|
|
96
87
|
);
|
|
97
88
|
}
|
|
98
89
|
})
|
|
@@ -101,19 +92,6 @@ export default definePluginEntry({
|
|
|
101
92
|
`[OpenFinClaw] Cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
102
93
|
);
|
|
103
94
|
});
|
|
104
|
-
|
|
105
|
-
// Register tournament cron job
|
|
106
|
-
setupTournamentCronJob()
|
|
107
|
-
.then((result) => {
|
|
108
|
-
if (result.created) {
|
|
109
|
-
api.logger.info("[OpenFinClaw] Tournament cron job registered");
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
.catch((err) => {
|
|
113
|
-
api.logger.info(
|
|
114
|
-
`[OpenFinClaw] Tournament cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
95
|
}
|
|
118
96
|
},
|
|
119
97
|
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"datahubGatewayUrl": {
|
|
24
24
|
"type": "string",
|
|
25
25
|
"description": "DataHub Gateway URL for market data",
|
|
26
|
-
"default": "
|
|
26
|
+
"default": "https://datahub.openfinclaw.ai"
|
|
27
27
|
},
|
|
28
28
|
"requestTimeoutMs": {
|
|
29
29
|
"type": "number",
|
|
@@ -69,6 +69,16 @@
|
|
|
69
69
|
"default": "Asia/Shanghai",
|
|
70
70
|
"description": "Timezone for scheduled tasks"
|
|
71
71
|
},
|
|
72
|
+
"cronSessionTarget": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"default": "isolated",
|
|
75
|
+
"description": "Cron job session mode: 'isolated' (own session, won't block main chat) or 'main' (legacy, runs in main conversation)"
|
|
76
|
+
},
|
|
77
|
+
"cronDeliveryMode": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"default": "announce",
|
|
80
|
+
"description": "Cron result delivery: 'announce' (push to user's last active channel) or 'none' (silent)"
|
|
81
|
+
},
|
|
72
82
|
"newsApiKey": {
|
|
73
83
|
"type": "string",
|
|
74
84
|
"description": "Optional API key for Finnhub or NewsAPI news provider",
|
|
@@ -101,7 +111,7 @@
|
|
|
101
111
|
},
|
|
102
112
|
"datahubGatewayUrl": {
|
|
103
113
|
"label": "DataHub Gateway URL",
|
|
104
|
-
"placeholder": "
|
|
114
|
+
"placeholder": "https://datahub.openfinclaw.ai"
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfinclaw/openfinclaw-strategy",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.4.2",
|
|
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/config.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
|
6
6
|
import type { NewsProviderType, UnifiedPluginConfig } from "./types.js";
|
|
7
7
|
|
|
8
8
|
const DEFAULT_HUB_API_URL = "https://hub.openfinclaw.ai";
|
|
9
|
-
const DEFAULT_DATAHUB_GATEWAY_URL = "
|
|
9
|
+
const DEFAULT_DATAHUB_GATEWAY_URL = "https://datahub.openfinclaw.ai";
|
|
10
10
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
11
11
|
|
|
12
12
|
const DEFAULT_SCAN_CRON = "0 8 * * *";
|
|
@@ -92,6 +92,29 @@ export function resolvePluginConfig(api: OpenClawPluginApi): UnifiedPluginConfig
|
|
|
92
92
|
readEnv(["OPENFINCLAW_SCAN_TZ"]) ??
|
|
93
93
|
DEFAULT_SCAN_TZ;
|
|
94
94
|
|
|
95
|
+
const cronAgentIdRaw =
|
|
96
|
+
(typeof raw?.cronAgentId === "string" ? raw.cronAgentId : undefined) ??
|
|
97
|
+
readEnv(["OPENFINCLAW_CRON_AGENT_ID"]);
|
|
98
|
+
const cronAgentId = cronAgentIdRaw?.trim() || undefined;
|
|
99
|
+
|
|
100
|
+
const VALID_SESSION_TARGETS = new Set(["isolated", "main"]);
|
|
101
|
+
const cronSessionTargetRaw =
|
|
102
|
+
(typeof raw?.cronSessionTarget === "string" ? raw.cronSessionTarget : undefined) ??
|
|
103
|
+
readEnv(["OPENFINCLAW_CRON_SESSION_TARGET"]) ??
|
|
104
|
+
"isolated";
|
|
105
|
+
const cronSessionTarget = VALID_SESSION_TARGETS.has(cronSessionTargetRaw)
|
|
106
|
+
? cronSessionTargetRaw
|
|
107
|
+
: "isolated";
|
|
108
|
+
|
|
109
|
+
const VALID_DELIVERY_MODES = new Set(["announce", "none"]);
|
|
110
|
+
const cronDeliveryModeRaw =
|
|
111
|
+
(typeof raw?.cronDeliveryMode === "string" ? raw.cronDeliveryMode : undefined) ??
|
|
112
|
+
readEnv(["OPENFINCLAW_CRON_DELIVERY_MODE"]) ??
|
|
113
|
+
"announce";
|
|
114
|
+
const cronDeliveryMode = VALID_DELIVERY_MODES.has(cronDeliveryModeRaw)
|
|
115
|
+
? cronDeliveryModeRaw
|
|
116
|
+
: "announce";
|
|
117
|
+
|
|
95
118
|
// ── News config ──
|
|
96
119
|
const newsApiKey =
|
|
97
120
|
(typeof raw?.newsApiKey === "string" ? raw.newsApiKey : undefined) ??
|
|
@@ -124,6 +147,9 @@ export function resolvePluginConfig(api: OpenClawPluginApi): UnifiedPluginConfig
|
|
|
124
147
|
weeklyReportCronExpr,
|
|
125
148
|
monthlyReportCronExpr,
|
|
126
149
|
scanTimezone,
|
|
150
|
+
cronAgentId,
|
|
151
|
+
cronSessionTarget,
|
|
152
|
+
cronDeliveryMode,
|
|
127
153
|
newsApiKey: newsApiKey && newsApiKey.length > 0 ? newsApiKey : undefined,
|
|
128
154
|
newsProvider,
|
|
129
155
|
priceAlertThreshold,
|
package/src/db/schema.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
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";
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Create (or migrate) all MVP tables.
|
|
@@ -135,9 +134,6 @@ export function ensureSchema(db: DatabaseSync): void {
|
|
|
135
134
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_price_alerts_created ON price_alerts(created_at);`);
|
|
136
135
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_price_alerts_strategy ON price_alerts(strategy_id);`);
|
|
137
136
|
|
|
138
|
-
// ── tournament tables ──────────────────────────────────────────────────
|
|
139
|
-
ensureTournamentSchema(db);
|
|
140
|
-
|
|
141
137
|
// ── migrations (add columns to existing databases) ──────────────────────
|
|
142
138
|
}
|
|
143
139
|
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* The running CronService watches the store file and picks up new jobs
|
|
9
9
|
* on its next tick.
|
|
10
10
|
*
|
|
11
|
-
* Idempotent:
|
|
11
|
+
* Idempotent: creates missing jobs and migrates legacy "main" session jobs
|
|
12
|
+
* to "isolated" so cron tasks run in their own session without blocking the
|
|
13
|
+
* user's active conversation.
|
|
12
14
|
*
|
|
13
15
|
* Jobs:
|
|
14
16
|
* - openfinclaw:daily-scan (0 8 * * *) 每日策略扫描
|
|
@@ -23,16 +25,21 @@ import type { UnifiedPluginConfig } from "../types.js";
|
|
|
23
25
|
|
|
24
26
|
// ── Cron store types (mirrors src/cron/types.ts subset) ──────────────────
|
|
25
27
|
|
|
28
|
+
type CronPayload =
|
|
29
|
+
| { kind: "systemEvent"; text: string }
|
|
30
|
+
| { kind: "agentTurn"; message: string };
|
|
31
|
+
|
|
26
32
|
/** Minimal stored cron job shape. */
|
|
27
33
|
interface StoredCronJob {
|
|
28
34
|
id: string;
|
|
29
35
|
name: string;
|
|
36
|
+
agentId?: string;
|
|
30
37
|
enabled: boolean;
|
|
31
38
|
schedule: { kind: "cron"; expr: string; tz?: string };
|
|
32
|
-
payload:
|
|
39
|
+
payload: CronPayload;
|
|
33
40
|
sessionTarget: string;
|
|
34
41
|
wakeMode: string;
|
|
35
|
-
delivery: { mode: string };
|
|
42
|
+
delivery: { mode: string; channel?: string; to?: string };
|
|
36
43
|
createdAtMs: number;
|
|
37
44
|
updatedAtMs: number;
|
|
38
45
|
state: Record<string, unknown>;
|
|
@@ -43,6 +50,14 @@ interface CronStoreFile {
|
|
|
43
50
|
jobs: StoredCronJob[];
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
/** Names managed by this plugin — used for migration detection. */
|
|
54
|
+
const OPENFINCLAW_JOB_NAMES = new Set([
|
|
55
|
+
"openfinclaw:daily-scan",
|
|
56
|
+
"openfinclaw:price-monitor",
|
|
57
|
+
"openfinclaw:weekly-report",
|
|
58
|
+
"openfinclaw:monthly-report",
|
|
59
|
+
]);
|
|
60
|
+
|
|
46
61
|
// ── File I/O ──────────────────────────────────────────────────────────────
|
|
47
62
|
|
|
48
63
|
/** Resolve default cron store path: ~/.openclaw/cron/jobs.json */
|
|
@@ -82,15 +97,26 @@ async function saveStore(storePath: string, store: CronStoreFile): Promise<void>
|
|
|
82
97
|
function buildCronJobDefs(config: UnifiedPluginConfig): Array<{
|
|
83
98
|
name: string;
|
|
84
99
|
schedule: { kind: "cron"; expr: string; tz?: string };
|
|
85
|
-
payload:
|
|
100
|
+
payload: CronPayload;
|
|
101
|
+
sessionTarget: string;
|
|
102
|
+
delivery: { mode: string };
|
|
86
103
|
}> {
|
|
104
|
+
const sessionTarget = config.cronSessionTarget;
|
|
105
|
+
const delivery = { mode: config.cronDeliveryMode };
|
|
106
|
+
const useAgentTurn = sessionTarget !== "main";
|
|
107
|
+
|
|
108
|
+
function makePayload(text: string): CronPayload {
|
|
109
|
+
return useAgentTurn ? { kind: "agentTurn", message: text } : { kind: "systemEvent", text };
|
|
110
|
+
}
|
|
111
|
+
|
|
87
112
|
return [
|
|
88
113
|
{
|
|
89
114
|
name: "openfinclaw:daily-scan",
|
|
90
115
|
schedule: { kind: "cron", expr: config.scanCronExpr, tz: config.scanTimezone },
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
116
|
+
sessionTarget,
|
|
117
|
+
delivery,
|
|
118
|
+
payload: makePayload(
|
|
119
|
+
[
|
|
94
120
|
"[openfinclaw-strategy 每日扫描]",
|
|
95
121
|
"1. 调用 strategy_daily_scan 获取策略扫描报告",
|
|
96
122
|
"2. 分析每条新闻对策略的影响(利好/利空/中性)",
|
|
@@ -98,56 +124,94 @@ function buildCronJobDefs(config: UnifiedPluginConfig): Array<{
|
|
|
98
124
|
"4. 用 skill_publish_verify 确认回测完成",
|
|
99
125
|
"5. 将分析报告和建议操作发送给用户",
|
|
100
126
|
].join("\n"),
|
|
101
|
-
|
|
127
|
+
),
|
|
102
128
|
},
|
|
103
129
|
{
|
|
104
130
|
name: "openfinclaw:price-monitor",
|
|
105
131
|
schedule: { kind: "cron", expr: config.monitorCronExpr, tz: config.scanTimezone },
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
sessionTarget,
|
|
133
|
+
delivery,
|
|
134
|
+
payload: makePayload(
|
|
135
|
+
[
|
|
109
136
|
"[openfinclaw-strategy 价格监控]",
|
|
110
137
|
"调用 strategy_price_monitor 检查所有策略标的的价格异动。",
|
|
111
138
|
"如有告警(涨跌幅超阈值),分析可能的原因并通知用户。",
|
|
112
139
|
].join("\n"),
|
|
113
|
-
|
|
140
|
+
),
|
|
114
141
|
},
|
|
115
142
|
{
|
|
116
143
|
name: "openfinclaw:weekly-report",
|
|
117
144
|
schedule: { kind: "cron", expr: config.weeklyReportCronExpr, tz: config.scanTimezone },
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
sessionTarget,
|
|
146
|
+
delivery,
|
|
147
|
+
payload: makePayload(
|
|
148
|
+
[
|
|
121
149
|
"[openfinclaw-strategy 周报]",
|
|
122
150
|
'调用 strategy_periodic_report(period="weekly") 生成策略绩效周报。',
|
|
123
151
|
"汇总本周回测结果、价格告警和扫描记录,发送给用户。",
|
|
124
152
|
].join("\n"),
|
|
125
|
-
|
|
153
|
+
),
|
|
126
154
|
},
|
|
127
155
|
{
|
|
128
156
|
name: "openfinclaw:monthly-report",
|
|
129
157
|
schedule: { kind: "cron", expr: config.monthlyReportCronExpr, tz: config.scanTimezone },
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
158
|
+
sessionTarget,
|
|
159
|
+
delivery,
|
|
160
|
+
payload: makePayload(
|
|
161
|
+
[
|
|
133
162
|
"[openfinclaw-strategy 月报]",
|
|
134
163
|
'调用 strategy_periodic_report(period="monthly") 生成策略绩效月报。',
|
|
135
164
|
"汇总本月回测结果、价格告警和扫描记录,发送给用户。",
|
|
136
165
|
].join("\n"),
|
|
137
|
-
|
|
166
|
+
),
|
|
138
167
|
},
|
|
139
168
|
];
|
|
140
169
|
}
|
|
141
170
|
|
|
171
|
+
// ── Migration ─────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Migrate legacy openfinclaw jobs that still use sessionTarget "main" +
|
|
175
|
+
* systemEvent payload to the new isolated agentTurn pattern.
|
|
176
|
+
* Returns the number of jobs migrated.
|
|
177
|
+
*/
|
|
178
|
+
function migrateLegacyJobs(
|
|
179
|
+
store: CronStoreFile,
|
|
180
|
+
defs: ReturnType<typeof buildCronJobDefs>,
|
|
181
|
+
): number {
|
|
182
|
+
const defsByName = new Map(defs.map((d) => [d.name, d]));
|
|
183
|
+
let migrated = 0;
|
|
184
|
+
|
|
185
|
+
for (const job of store.jobs) {
|
|
186
|
+
if (!OPENFINCLAW_JOB_NAMES.has(job.name)) continue;
|
|
187
|
+
if (job.sessionTarget !== "main") continue;
|
|
188
|
+
|
|
189
|
+
const def = defsByName.get(job.name);
|
|
190
|
+
if (!def) continue;
|
|
191
|
+
|
|
192
|
+
job.payload = def.payload;
|
|
193
|
+
job.sessionTarget = def.sessionTarget;
|
|
194
|
+
job.delivery = def.delivery;
|
|
195
|
+
job.updatedAtMs = Date.now();
|
|
196
|
+
migrated++;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return migrated;
|
|
200
|
+
}
|
|
201
|
+
|
|
142
202
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
143
203
|
|
|
144
204
|
/**
|
|
145
|
-
*
|
|
205
|
+
* Register and migrate openfinclaw cron jobs by writing to the
|
|
146
206
|
* Gateway cron store file. Safe to call at plugin startup.
|
|
207
|
+
*
|
|
208
|
+
* - Creates missing jobs with the current config (isolated session by default).
|
|
209
|
+
* - Migrates existing legacy "main" session jobs to "isolated" + "agentTurn"
|
|
210
|
+
* so cron tasks no longer block the user's active conversation.
|
|
147
211
|
*/
|
|
148
212
|
export async function setupOpenfinclawCronJobs(
|
|
149
213
|
config: UnifiedPluginConfig,
|
|
150
|
-
): Promise<{ ok: boolean; created: number; existing: number }> {
|
|
214
|
+
): Promise<{ ok: boolean; created: number; existing: number; migrated: number }> {
|
|
151
215
|
const storePath = defaultStorePath();
|
|
152
216
|
const store = await loadStore(storePath);
|
|
153
217
|
const existingNames = new Set(store.jobs.map((j) => j.name));
|
|
@@ -160,12 +224,13 @@ export async function setupOpenfinclawCronJobs(
|
|
|
160
224
|
store.jobs.push({
|
|
161
225
|
id: randomUUID(),
|
|
162
226
|
name: def.name,
|
|
227
|
+
agentId: config.cronAgentId,
|
|
163
228
|
enabled: true,
|
|
164
229
|
schedule: def.schedule,
|
|
165
230
|
payload: def.payload,
|
|
166
|
-
sessionTarget:
|
|
231
|
+
sessionTarget: def.sessionTarget,
|
|
167
232
|
wakeMode: "now",
|
|
168
|
-
delivery:
|
|
233
|
+
delivery: def.delivery,
|
|
169
234
|
createdAtMs: now,
|
|
170
235
|
updatedAtMs: now,
|
|
171
236
|
state: {},
|
|
@@ -173,9 +238,11 @@ export async function setupOpenfinclawCronJobs(
|
|
|
173
238
|
created++;
|
|
174
239
|
}
|
|
175
240
|
|
|
176
|
-
|
|
241
|
+
const migrated = migrateLegacyJobs(store, defs);
|
|
242
|
+
|
|
243
|
+
if (created > 0 || migrated > 0) {
|
|
177
244
|
await saveStore(storePath, store);
|
|
178
245
|
}
|
|
179
246
|
|
|
180
|
-
return { ok: true, created, existing: existingNames.size };
|
|
247
|
+
return { ok: true, created, existing: existingNames.size, migrated };
|
|
181
248
|
}
|
package/src/types.ts
CHANGED
|
@@ -552,6 +552,12 @@ export interface UnifiedPluginConfig {
|
|
|
552
552
|
monthlyReportCronExpr: string;
|
|
553
553
|
/** 调度时区 */
|
|
554
554
|
scanTimezone: string;
|
|
555
|
+
/** 定时任务绑定的专用 agent id;未设置时沿用默认 agent */
|
|
556
|
+
cronAgentId: string | undefined;
|
|
557
|
+
/** Cron session 模式:"isolated"(独立 session,不阻塞主对话)或 "main"(旧行为) */
|
|
558
|
+
cronSessionTarget: string;
|
|
559
|
+
/** Cron 投递模式:"announce"(推送到用户最近活跃频道)或 "none" */
|
|
560
|
+
cronDeliveryMode: string;
|
|
555
561
|
|
|
556
562
|
// ── 新闻源配置 ────────────────────────────────────────────
|
|
557
563
|
/** 外部新闻 API Key(Finnhub 或 NewsAPI) */
|
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
}
|