@openfinclaw/openfinclaw-strategy 2026.3.310 → 2026.4.9
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 +8 -3
- package/openclaw.plugin.json +12 -2
- package/package.json +1 -1
- package/src/config.ts +27 -1
- package/src/db/db.ts +3 -1
- package/src/db/repositories.ts +15 -10
- package/src/db/schema.ts +17 -0
- package/src/scheduler/cron-setup.ts +93 -26
- package/src/types.ts +6 -0
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";
|
|
@@ -82,6 +82,11 @@ export default definePluginEntry({
|
|
|
82
82
|
prependSystemContext: `${OPENFINCLAW_AGENT_GUIDANCE}\n\n${tournamentPrompt}`,
|
|
83
83
|
}));
|
|
84
84
|
|
|
85
|
+
// Graceful shutdown: close SQLite connection
|
|
86
|
+
process.once("beforeExit", () => {
|
|
87
|
+
closeDb();
|
|
88
|
+
});
|
|
89
|
+
|
|
85
90
|
// ── Gateway Cron registration ──
|
|
86
91
|
// Write cron jobs directly to ~/.openclaw/cron/jobs.json during register().
|
|
87
92
|
// This ensures jobs are available immediately on both gateway startup AND
|
|
@@ -90,9 +95,9 @@ export default definePluginEntry({
|
|
|
90
95
|
if (config.schedulerEnabled) {
|
|
91
96
|
setupOpenfinclawCronJobs(config)
|
|
92
97
|
.then((result) => {
|
|
93
|
-
if (result.created > 0) {
|
|
98
|
+
if (result.created > 0 || result.migrated > 0) {
|
|
94
99
|
api.logger.info(
|
|
95
|
-
`[OpenFinClaw] Cron jobs
|
|
100
|
+
`[OpenFinClaw] Cron jobs: ${result.created} created, ${result.migrated} migrated, ${result.existing} existing`,
|
|
96
101
|
);
|
|
97
102
|
}
|
|
98
103
|
})
|
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.9",
|
|
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/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
|
@@ -141,6 +141,23 @@ export function ensureSchema(db: DatabaseSync): void {
|
|
|
141
141
|
// ── migrations (add columns to existing databases) ──────────────────────
|
|
142
142
|
}
|
|
143
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
|
+
|
|
144
161
|
/** Add a column if it doesn't already exist (safe for repeated calls). */
|
|
145
162
|
function ensureColumn(db: DatabaseSync, table: string, column: string, definition: string): void {
|
|
146
163
|
const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
@@ -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) */
|