tickflow-assist 0.2.19 → 0.3.1
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 -5
- package/dist/analysis/parsers/flash-alert-decision.parser.d.ts +8 -0
- package/dist/analysis/parsers/flash-alert-decision.parser.js +34 -0
- package/dist/background/jin10-flash.worker.d.ts +8 -0
- package/dist/background/jin10-flash.worker.js +24 -0
- package/dist/bootstrap.d.ts +4 -0
- package/dist/bootstrap.js +17 -0
- package/dist/config/normalize.js +15 -6
- package/dist/config/schema.d.ts +5 -1
- package/dist/config/schema.js +3 -0
- package/dist/dev/run-monitor-loop.js +6 -2
- package/dist/dev/tickflow-assist-cli.js +45 -0
- package/dist/plugin-commands.js +7 -0
- package/dist/prompts/analysis/flash-monitor-alert-prompt.d.ts +11 -0
- package/dist/prompts/analysis/flash-monitor-alert-prompt.js +44 -0
- package/dist/prompts/analysis/index.d.ts +1 -0
- package/dist/prompts/analysis/index.js +1 -0
- package/dist/services/alert-service.d.ts +1 -0
- package/dist/services/alert-service.js +21 -3
- package/dist/services/jin10-flash-monitor-service.d.ts +32 -0
- package/dist/services/jin10-flash-monitor-service.js +578 -0
- package/dist/services/jin10-mcp-service.d.ts +29 -0
- package/dist/services/jin10-mcp-service.js +242 -0
- package/dist/storage/repositories/jin10-flash-delivery-repo.d.ts +10 -0
- package/dist/storage/repositories/jin10-flash-delivery-repo.js +57 -0
- package/dist/storage/repositories/jin10-flash-repo.d.ts +15 -0
- package/dist/storage/repositories/jin10-flash-repo.js +126 -0
- package/dist/storage/schemas.d.ts +2 -0
- package/dist/storage/schemas.js +19 -0
- package/dist/tools/flash-monitor-status.tool.d.ts +6 -0
- package/dist/tools/flash-monitor-status.tool.js +9 -0
- package/dist/types/flash-monitor.d.ts +17 -0
- package/dist/types/flash-monitor.js +1 -0
- package/dist/types/jin10.d.ts +30 -0
- package/dist/types/jin10.js +1 -0
- package/openclaw.plugin.json +43 -1
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# TickFlow Assist
|
|
2
2
|
|
|
3
|
-
基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE)
|
|
3
|
+
基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE) 获取行情与财务数据,并可选接入 [金十数据 MCP](https://mcp.jin10.com/app/) 快讯流,结合 LLM 生成技术面、基本面、资讯面的综合判断,并把结果持久化到本地 LanceDB。
|
|
4
4
|
|
|
5
|
-
最近更新:`v0.
|
|
5
|
+
最近更新:`v0.3.1` 修复 GitHub Actions 自动发布 npm 包时的 provenance 仓库元数据校验失败,并将金十数据 MCP 握手里的 `clientInfo` 调整为中性固定值。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。
|
|
6
6
|
|
|
7
7
|
当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.1` 上兼容。
|
|
8
8
|
|
|
@@ -81,23 +81,30 @@ plugins.entries["tickflow-assist"].config
|
|
|
81
81
|
- 核心运行:`tickflowApiKey`、`llmApiKey`、`llmBaseUrl`、`llmModel`
|
|
82
82
|
- 本地数据:`databasePath`、`calendarFile`
|
|
83
83
|
- 告警投递:`alertChannel`、`alertTarget`、`alertAccount`
|
|
84
|
-
- 能力补充:`mxSearchApiKey`
|
|
84
|
+
- 能力补充:`mxSearchApiKey`、`jin10ApiToken`
|
|
85
85
|
|
|
86
|
-
其中,`mxSearchApiKey` 用于 `mx_search`、`mx_select_stock` 以及非 `Expert` 财务链路的 lite 补充;`alertTarget`、`alertAccount` 建议在准备启用 `test_alert
|
|
86
|
+
其中,`mxSearchApiKey` 用于 `mx_search`、`mx_select_stock` 以及非 `Expert` 财务链路的 lite 补充;`jin10ApiToken` 用于 24 小时金十数据快讯监控;`alertTarget`、`alertAccount` 建议在准备启用 `test_alert`、实时监控告警、金十数据快讯告警和定时通知前一并配好,避免配置不完整导致功能缺失。
|
|
87
87
|
|
|
88
88
|
## 功能
|
|
89
89
|
|
|
90
90
|
- 自选股管理、日 K / 分钟 K 抓取与指标计算
|
|
91
91
|
- 技术面、财务面、资讯面的综合分析
|
|
92
92
|
- 实时监控、定时日更、收盘后复盘
|
|
93
|
+
- 金十数据 24 小时快讯监控与自选关联提醒
|
|
93
94
|
- 本地 LanceDB 数据留痕与分析结果查看
|
|
94
95
|
|
|
95
96
|
## 运行说明
|
|
96
97
|
|
|
97
98
|
- 插件会在本地 `databasePath` 下持久化 LanceDB 数据。
|
|
98
|
-
-
|
|
99
|
+
- 后台服务会按配置执行定时日更、实时监控与金十数据快讯监控。
|
|
99
100
|
- Python 子模块仅用于技术指标计算,不承担主业务流程。
|
|
100
101
|
|
|
102
|
+
## 依赖与可选能力
|
|
103
|
+
|
|
104
|
+
- [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE):提供日线、分钟线、实时行情与财务数据接口。
|
|
105
|
+
- [金十数据 MCP](https://mcp.jin10.com/app/):可选,用于 24 小时快讯流接入、自选关联筛选与事件驱动告警。
|
|
106
|
+
- [东方财富妙想 Skills](https://marketing.dfcfs.com/views/finskillshub/):可选,用于 `mx_search`、`mx_select_stock` 与非 `Expert` 财务链路的 lite 补充。
|
|
107
|
+
|
|
101
108
|
## 仓库
|
|
102
109
|
|
|
103
110
|
- GitHub: <https://github.com/robinspt/tickflow-assist>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { parseJsonBlock } from "./json-block.parser.js";
|
|
2
|
+
export function parseFlashAlertDecision(responseText) {
|
|
3
|
+
const parsed = parseJsonBlock(responseText, {
|
|
4
|
+
requiredKeys: ["alert", "relevant_symbols"],
|
|
5
|
+
});
|
|
6
|
+
return {
|
|
7
|
+
alert: normalizeBoolean(parsed?.alert),
|
|
8
|
+
importance: normalizeImportance(parsed?.importance),
|
|
9
|
+
relevantSymbols: normalizeSymbols(parsed?.relevant_symbols),
|
|
10
|
+
headline: normalizeText(parsed?.headline),
|
|
11
|
+
reason: normalizeText(parsed?.reason),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function normalizeBoolean(value) {
|
|
15
|
+
return value === true;
|
|
16
|
+
}
|
|
17
|
+
function normalizeImportance(value) {
|
|
18
|
+
if (value === "high" || value === "low") {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
return "medium";
|
|
22
|
+
}
|
|
23
|
+
function normalizeSymbols(value) {
|
|
24
|
+
if (!Array.isArray(value)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return value
|
|
28
|
+
.map((item) => String(item ?? "").trim())
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.slice(0, 10);
|
|
31
|
+
}
|
|
32
|
+
function normalizeText(value) {
|
|
33
|
+
return typeof value === "string" ? value.trim() : "";
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Jin10FlashMonitorService } from "../services/jin10-flash-monitor-service.js";
|
|
2
|
+
export declare class Jin10FlashWorker {
|
|
3
|
+
private readonly monitorService;
|
|
4
|
+
private readonly intervalMs;
|
|
5
|
+
constructor(monitorService: Jin10FlashMonitorService, intervalMs: number);
|
|
6
|
+
runOnce(): Promise<number>;
|
|
7
|
+
runLoop(signal?: AbortSignal, runtimeHost?: "plugin_service" | "fallback_process"): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { sleepWithAbort } from "../utils/abortable-sleep.js";
|
|
2
|
+
export class Jin10FlashWorker {
|
|
3
|
+
monitorService;
|
|
4
|
+
intervalMs;
|
|
5
|
+
constructor(monitorService, intervalMs) {
|
|
6
|
+
this.monitorService = monitorService;
|
|
7
|
+
this.intervalMs = intervalMs;
|
|
8
|
+
}
|
|
9
|
+
async runOnce() {
|
|
10
|
+
return this.monitorService.runMonitorOnce();
|
|
11
|
+
}
|
|
12
|
+
async runLoop(signal, runtimeHost) {
|
|
13
|
+
while (!signal?.aborted) {
|
|
14
|
+
await this.monitorService.recordHeartbeat(runtimeHost);
|
|
15
|
+
try {
|
|
16
|
+
await this.runOnce();
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
await this.monitorService.recordLoopError(error);
|
|
20
|
+
}
|
|
21
|
+
await sleepWithAbort(this.intervalMs, signal);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/bootstrap.d.ts
CHANGED
|
@@ -2,11 +2,13 @@ import type { PluginConfig } from "./config/schema.js";
|
|
|
2
2
|
import { Database } from "./storage/db.js";
|
|
3
3
|
import { WatchlistService } from "./services/watchlist-service.js";
|
|
4
4
|
import { MonitorService } from "./services/monitor-service.js";
|
|
5
|
+
import { Jin10FlashMonitorService } from "./services/jin10-flash-monitor-service.js";
|
|
5
6
|
import { AlertService } from "./services/alert-service.js";
|
|
6
7
|
import { AlertMediaService } from "./services/alert-media-service.js";
|
|
7
8
|
import type { LocalTool, OpenClawPluginConfig, OpenClawPluginRuntime, RegisteredService } from "./runtime/plugin-api.js";
|
|
8
9
|
import { RealtimeMonitorWorker } from "./background/realtime-monitor.worker.js";
|
|
9
10
|
import { DailyUpdateWorker } from "./background/daily-update.worker.js";
|
|
11
|
+
import { Jin10FlashWorker } from "./background/jin10-flash.worker.js";
|
|
10
12
|
import type { WatchlistItem } from "./types/domain.js";
|
|
11
13
|
export interface AppContext {
|
|
12
14
|
config: PluginConfig;
|
|
@@ -22,7 +24,9 @@ export interface AppContext {
|
|
|
22
24
|
alertService: AlertService;
|
|
23
25
|
alertMediaService: AlertMediaService;
|
|
24
26
|
monitorService: MonitorService;
|
|
27
|
+
jin10FlashMonitorService: Jin10FlashMonitorService;
|
|
25
28
|
realtimeMonitorWorker: RealtimeMonitorWorker;
|
|
29
|
+
jin10FlashWorker: Jin10FlashWorker;
|
|
26
30
|
dailyUpdateWorker: DailyUpdateWorker;
|
|
27
31
|
watchlistService: WatchlistService;
|
|
28
32
|
database: Database;
|
package/dist/bootstrap.js
CHANGED
|
@@ -6,6 +6,7 @@ import { IndicatorService } from "./services/indicator-service.js";
|
|
|
6
6
|
import { FinancialService } from "./services/financial-service.js";
|
|
7
7
|
import { FinancialLiteService } from "./services/financial-lite-service.js";
|
|
8
8
|
import { MxApiService } from "./services/mx-search-service.js";
|
|
9
|
+
import { Jin10McpService } from "./services/jin10-mcp-service.js";
|
|
9
10
|
import { Database } from "./storage/db.js";
|
|
10
11
|
import { WatchlistRepository } from "./storage/repositories/watchlist-repo.js";
|
|
11
12
|
import { KlinesRepository } from "./storage/repositories/klines-repo.js";
|
|
@@ -19,6 +20,8 @@ import { TechnicalAnalysisRepository } from "./storage/repositories/technical-an
|
|
|
19
20
|
import { FinancialAnalysisRepository } from "./storage/repositories/financial-analysis-repo.js";
|
|
20
21
|
import { NewsAnalysisRepository } from "./storage/repositories/news-analysis-repo.js";
|
|
21
22
|
import { CompositeAnalysisRepository } from "./storage/repositories/composite-analysis-repo.js";
|
|
23
|
+
import { Jin10FlashRepository } from "./storage/repositories/jin10-flash-repo.js";
|
|
24
|
+
import { Jin10FlashDeliveryRepository } from "./storage/repositories/jin10-flash-delivery-repo.js";
|
|
22
25
|
import { WatchlistService } from "./services/watchlist-service.js";
|
|
23
26
|
import { WatchlistProfileService } from "./services/watchlist-profile-service.js";
|
|
24
27
|
import { AnalysisService } from "./services/analysis-service.js";
|
|
@@ -26,6 +29,7 @@ import { AnalysisViewService } from "./services/analysis-view-service.js";
|
|
|
26
29
|
import { QuoteService } from "./services/quote-service.js";
|
|
27
30
|
import { TradingCalendarService } from "./services/trading-calendar-service.js";
|
|
28
31
|
import { MonitorService } from "./services/monitor-service.js";
|
|
32
|
+
import { Jin10FlashMonitorService } from "./services/jin10-flash-monitor-service.js";
|
|
29
33
|
import { AlertService } from "./services/alert-service.js";
|
|
30
34
|
import { AlertMediaService } from "./services/alert-media-service.js";
|
|
31
35
|
import { UpdateService } from "./services/update-service.js";
|
|
@@ -47,6 +51,7 @@ import { analyzeTool } from "./tools/analyze.tool.js";
|
|
|
47
51
|
import { fetchKlinesTool } from "./tools/fetch-klines.tool.js";
|
|
48
52
|
import { fetchIntradayKlinesTool } from "./tools/fetch-intraday-klines.tool.js";
|
|
49
53
|
import { fetchFinancialsTool } from "./tools/fetch-financials.tool.js";
|
|
54
|
+
import { flashMonitorStatusTool } from "./tools/flash-monitor-status.tool.js";
|
|
50
55
|
import { mxSearchTool } from "./tools/mx-search.tool.js";
|
|
51
56
|
import { mxSelectStockTool } from "./tools/mx-select-stock.tool.js";
|
|
52
57
|
import { listWatchlistTool } from "./tools/list-watchlist.tool.js";
|
|
@@ -68,6 +73,7 @@ import { createCommandRunner } from "./runtime/command-runner.js";
|
|
|
68
73
|
import { resolvePreferredOpenClawTmpDir } from "./runtime/openclaw-temp-dir.js";
|
|
69
74
|
import { RealtimeMonitorWorker } from "./background/realtime-monitor.worker.js";
|
|
70
75
|
import { DailyUpdateWorker } from "./background/daily-update.worker.js";
|
|
76
|
+
import { Jin10FlashWorker } from "./background/jin10-flash.worker.js";
|
|
71
77
|
export function createAppContext(config, options = {}) {
|
|
72
78
|
const runtime = {
|
|
73
79
|
configSource: options.configSource ?? "local_config",
|
|
@@ -90,11 +96,14 @@ export function createAppContext(config, options = {}) {
|
|
|
90
96
|
const financialAnalysisRepository = new FinancialAnalysisRepository(database);
|
|
91
97
|
const newsAnalysisRepository = new NewsAnalysisRepository(database);
|
|
92
98
|
const compositeAnalysisRepository = new CompositeAnalysisRepository(database);
|
|
99
|
+
const jin10FlashRepository = new Jin10FlashRepository(database);
|
|
100
|
+
const jin10FlashDeliveryRepository = new Jin10FlashDeliveryRepository(database);
|
|
93
101
|
const instrumentService = new InstrumentService(tickflowClient);
|
|
94
102
|
const klineService = new KlineService(tickflowClient);
|
|
95
103
|
const quoteService = new QuoteService(tickflowClient);
|
|
96
104
|
const financialService = new FinancialService(tickflowClient);
|
|
97
105
|
const mxApiService = new MxApiService(config.mxSearchApiUrl, config.mxSearchApiKey);
|
|
106
|
+
const jin10McpService = new Jin10McpService(config.jin10McpUrl, config.jin10ApiToken);
|
|
98
107
|
const financialLiteService = new FinancialLiteService(mxApiService);
|
|
99
108
|
const analysisService = new AnalysisService(config.llmBaseUrl, config.llmApiKey, config.llmModel, analysisLogRepository);
|
|
100
109
|
const watchlistProfileService = new WatchlistProfileService(mxApiService, analysisService);
|
|
@@ -128,9 +137,11 @@ export function createAppContext(config, options = {}) {
|
|
|
128
137
|
const compositeStockAnalysisTask = new CompositeStockAnalysisTask(keyLevelsRepository, analysisLogRepository);
|
|
129
138
|
const compositeAnalysisOrchestrator = new CompositeAnalysisOrchestrator(analysisService, marketAnalysisProvider, financialAnalysisProvider, newsAnalysisProvider, klineTechnicalSignalTask, financialFundamentalTask, financialFundamentalLiteTask, newsCatalystTask, compositeStockAnalysisTask, technicalAnalysisRepository, financialAnalysisRepository, newsAnalysisRepository, compositeAnalysisRepository);
|
|
130
139
|
const monitorService = new MonitorService(config.databasePath, config.requestInterval, config.alertChannel, watchlistService, quoteService, tradingCalendarService, keyLevelsRepository, alertLogRepository, klinesRepository, intradayKlinesRepository, klineService, alertService, alertMediaService);
|
|
140
|
+
const jin10FlashMonitorService = new Jin10FlashMonitorService(config.databasePath, config.jin10FlashPollInterval, config.jin10FlashRetentionDays, watchlistService, jin10McpService, analysisService, alertService, jin10FlashRepository, jin10FlashDeliveryRepository);
|
|
131
141
|
const updateService = new UpdateService(klineService, config.tickflowApiKeyLevel, indicatorService, klinesRepository, indicatorsRepository, intradayKlinesRepository, watchlistService, tradingCalendarService);
|
|
132
142
|
const postCloseReviewService = new PostCloseReviewService(watchlistService, compositeAnalysisOrchestrator, analysisService, postCloseReviewTask, keyLevelsRepository, keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository);
|
|
133
143
|
const realtimeMonitorWorker = new RealtimeMonitorWorker(monitorService, config.requestInterval * 1000);
|
|
144
|
+
const jin10FlashWorker = new Jin10FlashWorker(jin10FlashMonitorService, config.jin10FlashPollInterval * 1000);
|
|
134
145
|
const dailyUpdateWorker = new DailyUpdateWorker(updateService, postCloseReviewService, tradingCalendarService, config.databasePath, alertService, config.dailyUpdateNotify, runtime.configSource);
|
|
135
146
|
let managedLoopAbortController = null;
|
|
136
147
|
let managedLoopPromise = null;
|
|
@@ -143,6 +154,7 @@ export function createAppContext(config, options = {}) {
|
|
|
143
154
|
dailyUpdateStatusTool(dailyUpdateWorker, runtime.configSource),
|
|
144
155
|
fetchIntradayKlinesTool(config.tickflowApiKeyLevel, klineService, intradayKlinesRepository, tradingCalendarService),
|
|
145
156
|
fetchFinancialsTool(financialService),
|
|
157
|
+
flashMonitorStatusTool(jin10FlashMonitorService),
|
|
146
158
|
fetchKlinesTool(klineService, klinesRepository, indicatorService, indicatorsRepository),
|
|
147
159
|
listWatchlistTool(watchlistService),
|
|
148
160
|
monitorStatusTool(monitorService),
|
|
@@ -175,6 +187,9 @@ export function createAppContext(config, options = {}) {
|
|
|
175
187
|
dailyUpdateWorker
|
|
176
188
|
.runLoop(abortController.signal, "plugin_service", runtime.configSource)
|
|
177
189
|
.catch(() => { }),
|
|
190
|
+
jin10FlashWorker
|
|
191
|
+
.runLoop(abortController.signal, "plugin_service")
|
|
192
|
+
.catch(() => { }),
|
|
178
193
|
realtimeMonitorWorker
|
|
179
194
|
.runLoop(abortController.signal, "plugin_service")
|
|
180
195
|
.catch(() => { }),
|
|
@@ -198,7 +213,9 @@ export function createAppContext(config, options = {}) {
|
|
|
198
213
|
alertService,
|
|
199
214
|
alertMediaService,
|
|
200
215
|
monitorService,
|
|
216
|
+
jin10FlashMonitorService,
|
|
201
217
|
realtimeMonitorWorker,
|
|
218
|
+
jin10FlashWorker,
|
|
202
219
|
dailyUpdateWorker,
|
|
203
220
|
watchlistService,
|
|
204
221
|
database,
|
package/dist/config/normalize.js
CHANGED
|
@@ -48,6 +48,10 @@ export function normalizePluginConfig(input) {
|
|
|
48
48
|
tickflowApiKeyLevel: normalizeTickflowApiKeyLevel(raw.tickflowApiKeyLevel, DEFAULT_PLUGIN_CONFIG.tickflowApiKeyLevel),
|
|
49
49
|
mxSearchApiUrl: normalizeString(raw.mxSearchApiUrl, envMxSearchApiUrl || DEFAULT_PLUGIN_CONFIG.mxSearchApiUrl),
|
|
50
50
|
mxSearchApiKey: normalizeString(raw.mxSearchApiKey, envMxSearchApiKey || DEFAULT_PLUGIN_CONFIG.mxSearchApiKey),
|
|
51
|
+
jin10McpUrl: normalizeString(raw.jin10McpUrl, DEFAULT_PLUGIN_CONFIG.jin10McpUrl),
|
|
52
|
+
jin10ApiToken: normalizeString(raw.jin10ApiToken),
|
|
53
|
+
jin10FlashPollInterval: normalizeInteger(raw.jin10FlashPollInterval, DEFAULT_PLUGIN_CONFIG.jin10FlashPollInterval),
|
|
54
|
+
jin10FlashRetentionDays: normalizeInteger(raw.jin10FlashRetentionDays, DEFAULT_PLUGIN_CONFIG.jin10FlashRetentionDays),
|
|
51
55
|
llmBaseUrl: normalizeString(raw.llmBaseUrl, DEFAULT_PLUGIN_CONFIG.llmBaseUrl),
|
|
52
56
|
llmApiKey: normalizeString(raw.llmApiKey),
|
|
53
57
|
llmModel: normalizeString(raw.llmModel, DEFAULT_PLUGIN_CONFIG.llmModel),
|
|
@@ -74,18 +78,23 @@ export function resolvePluginConfigPaths(config, baseDir) {
|
|
|
74
78
|
}
|
|
75
79
|
export function validatePluginConfig(config) {
|
|
76
80
|
const errors = [];
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
if (!config.llmApiKey) {
|
|
81
|
-
errors.push("llmApiKey is required");
|
|
82
|
-
}
|
|
81
|
+
// Community install scans happen before `configure-openclaw` writes secrets,
|
|
82
|
+
// so registration-time validation should only flag structurally invalid values.
|
|
83
83
|
if (!config.tickflowApiUrl.startsWith("http://") && !config.tickflowApiUrl.startsWith("https://")) {
|
|
84
84
|
errors.push("tickflowApiUrl must be an absolute http(s) URL");
|
|
85
85
|
}
|
|
86
|
+
if (config.jin10McpUrl && !config.jin10McpUrl.startsWith("http://") && !config.jin10McpUrl.startsWith("https://")) {
|
|
87
|
+
errors.push("jin10McpUrl must be an absolute http(s) URL");
|
|
88
|
+
}
|
|
86
89
|
if (config.requestInterval < 5) {
|
|
87
90
|
errors.push("requestInterval must be at least 5 seconds");
|
|
88
91
|
}
|
|
92
|
+
if (config.jin10FlashPollInterval < 10) {
|
|
93
|
+
errors.push("jin10FlashPollInterval must be at least 10 seconds");
|
|
94
|
+
}
|
|
95
|
+
if (config.jin10FlashRetentionDays < 1) {
|
|
96
|
+
errors.push("jin10FlashRetentionDays must be at least 1 day");
|
|
97
|
+
}
|
|
89
98
|
return errors;
|
|
90
99
|
}
|
|
91
100
|
function resolveConfigPath(value, baseDir) {
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export interface PluginConfig {
|
|
|
5
5
|
tickflowApiKeyLevel: TickflowApiKeyLevel;
|
|
6
6
|
mxSearchApiUrl: string;
|
|
7
7
|
mxSearchApiKey: string;
|
|
8
|
+
jin10McpUrl: string;
|
|
9
|
+
jin10ApiToken: string;
|
|
10
|
+
jin10FlashPollInterval: number;
|
|
11
|
+
jin10FlashRetentionDays: number;
|
|
8
12
|
llmBaseUrl: string;
|
|
9
13
|
llmApiKey: string;
|
|
10
14
|
llmModel: string;
|
|
@@ -20,4 +24,4 @@ export interface PluginConfig {
|
|
|
20
24
|
pythonArgs: string[];
|
|
21
25
|
pythonWorkdir: string;
|
|
22
26
|
}
|
|
23
|
-
export declare const DEFAULT_PLUGIN_CONFIG: Omit<PluginConfig, "tickflowApiKey" | "llmApiKey" | "alertTarget">;
|
|
27
|
+
export declare const DEFAULT_PLUGIN_CONFIG: Omit<PluginConfig, "tickflowApiKey" | "jin10ApiToken" | "llmApiKey" | "alertTarget">;
|
package/dist/config/schema.js
CHANGED
|
@@ -3,6 +3,9 @@ export const DEFAULT_PLUGIN_CONFIG = {
|
|
|
3
3
|
tickflowApiKeyLevel: "free",
|
|
4
4
|
mxSearchApiUrl: "https://mkapi2.dfcfs.com/finskillshub/api/claw",
|
|
5
5
|
mxSearchApiKey: "",
|
|
6
|
+
jin10McpUrl: "https://mcp.jin10.com/mcp",
|
|
7
|
+
jin10FlashPollInterval: 300,
|
|
8
|
+
jin10FlashRetentionDays: 7,
|
|
6
9
|
llmBaseUrl: "https://api.openai.com/v1",
|
|
7
10
|
llmModel: "gpt-4o",
|
|
8
11
|
databasePath: "./data/lancedb",
|
|
@@ -6,11 +6,12 @@ async function main() {
|
|
|
6
6
|
const config = await loadLocalConfig();
|
|
7
7
|
const app = createAppContext(config, { configSource: "local_config" });
|
|
8
8
|
const worker = app.services.realtimeMonitorWorker;
|
|
9
|
+
const flashWorker = app.services.jin10FlashWorker;
|
|
9
10
|
const alertService = app.services.alertService;
|
|
10
11
|
const monitorService = app.services.monitorService;
|
|
11
12
|
await monitorService.recordHeartbeat("fallback_process");
|
|
12
13
|
await monitorService.setWorkerPid(process.pid);
|
|
13
|
-
process.stdout.write(`TickFlow
|
|
14
|
+
process.stdout.write(`TickFlow realtime loop started, price_interval=${config.requestInterval}s, jin10_interval=${config.jin10FlashPollInterval}s\n`);
|
|
14
15
|
const controller = new AbortController();
|
|
15
16
|
const shutdown = async (signal) => {
|
|
16
17
|
controller.abort();
|
|
@@ -29,7 +30,10 @@ async function main() {
|
|
|
29
30
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
30
31
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
31
32
|
try {
|
|
32
|
-
await
|
|
33
|
+
await Promise.all([
|
|
34
|
+
worker.runLoop(controller.signal, "fallback_process"),
|
|
35
|
+
flashWorker.runLoop(controller.signal, "fallback_process"),
|
|
36
|
+
]);
|
|
33
37
|
}
|
|
34
38
|
catch (error) {
|
|
35
39
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -13,6 +13,9 @@ const DEFAULTS = {
|
|
|
13
13
|
tickflowApiKeyLevel: "Free",
|
|
14
14
|
mxSearchApiUrl: "https://mkapi2.dfcfs.com/finskillshub/api/claw",
|
|
15
15
|
mxSearchApiKey: "",
|
|
16
|
+
jin10McpUrl: "https://mcp.jin10.com/mcp",
|
|
17
|
+
jin10FlashPollInterval: 300,
|
|
18
|
+
jin10FlashRetentionDays: 7,
|
|
16
19
|
llmBaseUrl: "https://api.openai.com/v1",
|
|
17
20
|
llmModel: "gpt-4o",
|
|
18
21
|
requestInterval: 30,
|
|
@@ -43,6 +46,8 @@ Options:
|
|
|
43
46
|
--tickflow-api-key <key>
|
|
44
47
|
--tickflow-api-key-level <Free|Start|Pro|Expert>
|
|
45
48
|
--mx-search-api-key <key>
|
|
49
|
+
--jin10-mcp-url <url>
|
|
50
|
+
--jin10-api-token <token>
|
|
46
51
|
--llm-base-url <url>
|
|
47
52
|
--llm-api-key <key>
|
|
48
53
|
--llm-model <name>
|
|
@@ -135,6 +140,12 @@ function parseArgs(argv) {
|
|
|
135
140
|
case "--mx-search-api-key":
|
|
136
141
|
options.overrides.mxSearchApiKey = requireValue(token);
|
|
137
142
|
break;
|
|
143
|
+
case "--jin10-mcp-url":
|
|
144
|
+
options.overrides.jin10McpUrl = requireValue(token);
|
|
145
|
+
break;
|
|
146
|
+
case "--jin10-api-token":
|
|
147
|
+
options.overrides.jin10ApiToken = requireValue(token);
|
|
148
|
+
break;
|
|
138
149
|
case "--llm-base-url":
|
|
139
150
|
options.overrides.llmBaseUrl = requireValue(token);
|
|
140
151
|
break;
|
|
@@ -240,6 +251,8 @@ function getExistingPluginConfig(root) {
|
|
|
240
251
|
? config.pythonArgs.map((value) => String(value))
|
|
241
252
|
: undefined;
|
|
242
253
|
const requestInterval = Number(config.requestInterval ?? DEFAULTS.requestInterval);
|
|
254
|
+
const jin10FlashPollInterval = Number(config.jin10FlashPollInterval ?? DEFAULTS.jin10FlashPollInterval);
|
|
255
|
+
const jin10FlashRetentionDays = Number(config.jin10FlashRetentionDays ?? DEFAULTS.jin10FlashRetentionDays);
|
|
243
256
|
const dailyUpdateNotify = typeof config.dailyUpdateNotify === "boolean"
|
|
244
257
|
? config.dailyUpdateNotify
|
|
245
258
|
: DEFAULTS.dailyUpdateNotify;
|
|
@@ -249,6 +262,14 @@ function getExistingPluginConfig(root) {
|
|
|
249
262
|
tickflowApiKeyLevel: normalizeApiKeyLevel(stringValue(config.tickflowApiKeyLevel, DEFAULTS.tickflowApiKeyLevel)),
|
|
250
263
|
mxSearchApiUrl: stringValue(config.mxSearchApiUrl, DEFAULTS.mxSearchApiUrl),
|
|
251
264
|
mxSearchApiKey: stringValue(config.mxSearchApiKey, DEFAULTS.mxSearchApiKey),
|
|
265
|
+
jin10McpUrl: stringValue(config.jin10McpUrl, DEFAULTS.jin10McpUrl),
|
|
266
|
+
jin10ApiToken: stringValue(config.jin10ApiToken),
|
|
267
|
+
jin10FlashPollInterval: Number.isFinite(jin10FlashPollInterval)
|
|
268
|
+
? Math.max(10, Math.trunc(jin10FlashPollInterval))
|
|
269
|
+
: DEFAULTS.jin10FlashPollInterval,
|
|
270
|
+
jin10FlashRetentionDays: Number.isFinite(jin10FlashRetentionDays)
|
|
271
|
+
? Math.max(1, Math.trunc(jin10FlashRetentionDays))
|
|
272
|
+
: DEFAULTS.jin10FlashRetentionDays,
|
|
252
273
|
llmBaseUrl: stringValue(config.llmBaseUrl, DEFAULTS.llmBaseUrl),
|
|
253
274
|
llmApiKey: stringValue(config.llmApiKey),
|
|
254
275
|
llmModel: stringValue(config.llmModel, DEFAULTS.llmModel),
|
|
@@ -391,6 +412,10 @@ async function promptForConfig(options, existing, pluginDir, configPath) {
|
|
|
391
412
|
tickflowApiKeyLevel: DEFAULTS.tickflowApiKeyLevel,
|
|
392
413
|
mxSearchApiUrl: DEFAULTS.mxSearchApiUrl,
|
|
393
414
|
mxSearchApiKey: DEFAULTS.mxSearchApiKey,
|
|
415
|
+
jin10McpUrl: DEFAULTS.jin10McpUrl,
|
|
416
|
+
jin10ApiToken: "",
|
|
417
|
+
jin10FlashPollInterval: DEFAULTS.jin10FlashPollInterval,
|
|
418
|
+
jin10FlashRetentionDays: DEFAULTS.jin10FlashRetentionDays,
|
|
394
419
|
llmBaseUrl: DEFAULTS.llmBaseUrl,
|
|
395
420
|
llmApiKey: "",
|
|
396
421
|
llmModel: DEFAULTS.llmModel,
|
|
@@ -433,6 +458,7 @@ async function promptForConfig(options, existing, pluginDir, configPath) {
|
|
|
433
458
|
{ value: "Expert", label: "Expert" },
|
|
434
459
|
], seed.tickflowApiKeyLevel));
|
|
435
460
|
seed.mxSearchApiKey = await promptString(rl, "MX Search API Key (可留空)", seed.mxSearchApiKey, false);
|
|
461
|
+
seed.jin10ApiToken = await promptString(rl, "Jin10 API Token (可留空)", seed.jin10ApiToken, false);
|
|
436
462
|
seed.llmBaseUrl = await promptString(rl, "LLM Base URL", seed.llmBaseUrl, true);
|
|
437
463
|
seed.llmApiKey = await promptString(rl, "LLM API Key", seed.llmApiKey, true);
|
|
438
464
|
seed.llmModel = await promptString(rl, "LLM Model", seed.llmModel, true);
|
|
@@ -549,6 +575,10 @@ function applyPluginConfig(root, config, target) {
|
|
|
549
575
|
tickflowApiKeyLevel: config.tickflowApiKeyLevel,
|
|
550
576
|
mxSearchApiUrl: config.mxSearchApiUrl,
|
|
551
577
|
mxSearchApiKey: config.mxSearchApiKey,
|
|
578
|
+
jin10McpUrl: config.jin10McpUrl,
|
|
579
|
+
jin10ApiToken: config.jin10ApiToken,
|
|
580
|
+
jin10FlashPollInterval: config.jin10FlashPollInterval,
|
|
581
|
+
jin10FlashRetentionDays: config.jin10FlashRetentionDays,
|
|
552
582
|
llmBaseUrl: config.llmBaseUrl,
|
|
553
583
|
llmApiKey: config.llmApiKey,
|
|
554
584
|
llmModel: config.llmModel,
|
|
@@ -679,11 +709,26 @@ function getManualMacosFontCommands() {
|
|
|
679
709
|
"fc-cache -fv",
|
|
680
710
|
];
|
|
681
711
|
}
|
|
712
|
+
function getManualUvInstallCommands() {
|
|
713
|
+
if (process.platform === "win32") {
|
|
714
|
+
return [
|
|
715
|
+
"powershell -ExecutionPolicy ByPass -c \"irm https://astral.sh/uv/install.ps1 | iex\"",
|
|
716
|
+
];
|
|
717
|
+
}
|
|
718
|
+
return [
|
|
719
|
+
"curl -LsSf https://astral.sh/uv/install.sh | sh",
|
|
720
|
+
];
|
|
721
|
+
}
|
|
682
722
|
function printNextSteps(options, config) {
|
|
683
723
|
console.log("");
|
|
684
724
|
console.log("接下来的命令需要你手动执行。");
|
|
685
725
|
let step = 1;
|
|
686
726
|
if (options.pythonSetup) {
|
|
727
|
+
console.log(`${step}. 如未安装 uv,请先安装 uv`);
|
|
728
|
+
for (const command of getManualUvInstallCommands()) {
|
|
729
|
+
console.log(` ${command}`);
|
|
730
|
+
}
|
|
731
|
+
step += 1;
|
|
687
732
|
console.log(`${step}. 安装 Python 依赖`);
|
|
688
733
|
console.log(` cd ${config.pythonWorkdir}`);
|
|
689
734
|
console.log(" uv sync");
|
package/dist/plugin-commands.js
CHANGED
|
@@ -81,6 +81,7 @@ export function registerPluginCommands(api, tools, app) {
|
|
|
81
81
|
const startMonitor = getTool(tools, "start_monitor");
|
|
82
82
|
const stopMonitor = getTool(tools, "stop_monitor");
|
|
83
83
|
const monitorStatus = getTool(tools, "monitor_status");
|
|
84
|
+
const flashMonitorStatus = getTool(tools, "flash_monitor_status");
|
|
84
85
|
const startDailyUpdate = getTool(tools, "start_daily_update");
|
|
85
86
|
const stopDailyUpdate = getTool(tools, "stop_daily_update");
|
|
86
87
|
const updateAll = getTool(tools, "update_all");
|
|
@@ -165,6 +166,12 @@ export function registerPluginCommands(api, tools, app) {
|
|
|
165
166
|
requireAuth: true,
|
|
166
167
|
handler: async () => runCommandText(() => runToolText(monitorStatus)),
|
|
167
168
|
},
|
|
169
|
+
{
|
|
170
|
+
name: "ta_flashstatus",
|
|
171
|
+
description: "查看 Jin10 快讯监控状态,不经过 AI 对话。",
|
|
172
|
+
requireAuth: true,
|
|
173
|
+
handler: async () => runCommandText(() => runToolText(flashMonitorStatus)),
|
|
174
|
+
},
|
|
168
175
|
{
|
|
169
176
|
name: "ta_startdailyupdate",
|
|
170
177
|
description: "启动定时日更任务,不经过 AI 对话。",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WatchlistItem } from "../../types/domain.js";
|
|
2
|
+
import type { Jin10FlashRecord } from "../../types/jin10.js";
|
|
3
|
+
export declare const FLASH_MONITOR_ALERT_SYSTEM_PROMPT = "\n\u4F60\u662F\u4E00\u4F4DA\u80A1\u76D8\u4E2D/\u76D8\u540E\u5FEB\u8BAF\u544A\u8B66\u7B5B\u9009\u5668\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u5224\u65AD\u4E00\u6761\u91D1\u5341\u5FEB\u8BAF\uFF0C\u662F\u5426\u503C\u5F97\u5BF9\u5F53\u524D\u5173\u6CE8\u5217\u8868\u53D1\u9001\u4E00\u6B21\u544A\u8B66\u3002\n\n\u5224\u65AD\u89C4\u5219\uFF1A\n1. \u53EA\u6709\u5728\u5FEB\u8BAF\u4E0E\u5173\u6CE8\u80A1\u7968\u672C\u8EAB\uFF0C\u6216\u4E0E\u5176\u884C\u4E1A/\u9898\u6750\u5B58\u5728\u660E\u786E\u3001\u53EF\u6267\u884C\u3001\u77ED\u671F\u53EF\u80FD\u5F71\u54CD\u98CE\u9669\u504F\u597D\u7684\u5173\u8054\u65F6\uFF0C\u624D\u8FD4\u56DE alert=true\u3002\n2. \u76F4\u63A5\u70B9\u540D\u516C\u53F8/\u80A1\u7968\u4EE3\u7801\u7684\u516C\u544A\u3001\u8BA2\u5355\u3001\u4E2D\u6807\u3001\u51CF\u6301\u3001\u589E\u6301\u3001\u4E1A\u7EE9\u9884\u544A\u3001\u91CD\u7EC4\u3001\u76D1\u7BA1\u3001\u505C\u590D\u724C\u3001\u91CD\u5927\u4EA7\u54C1/\u9879\u76EE\u3001\u91CD\u8981\u884C\u4E1A\u653F\u7B56\uFF0C\u4F18\u5148\u7EA7\u6700\u9AD8\u3002\n3. \u7EAF\u6D77\u5916\u5B8F\u89C2\u3001\u5730\u7F18\u3001\u5546\u54C1\u62A5\u4EF7\u3001\u76F4\u64AD\u63A8\u5E7F\u3001\u56FE\u793A\u64AD\u62A5\u3001\u4E0EA\u80A1\u5019\u9009\u6807\u7684\u7F3A\u4E4F\u6E05\u6670\u4F20\u5BFC\u8DEF\u5F84\u7684\u5185\u5BB9\uFF0C\u4E0D\u8981\u89E6\u53D1\u544A\u8B66\u3002\n4. \u884C\u4E1A/\u9898\u6750\u7EA7\u5FEB\u8BAF\u53EA\u6709\u5728\u786E\u5B9E\u4F1A\u5F71\u54CD\u5019\u9009\u677F\u5757\u98CE\u9669\u504F\u597D\u65F6\uFF0C\u624D\u53EF\u89E6\u53D1\uFF1B\u6CDB\u6CDB\u800C\u8C08\u7684\u884C\u4E1A\u65B0\u95FB\u4E0D\u8981\u89E6\u53D1\u3002\n5. \u8F93\u51FA\u5FC5\u987B\u53EA\u6709\u4E00\u4E2A ```json \u4EE3\u7801\u5757\uFF0C\u7ED3\u6784\u5982\u4E0B\uFF1A\n{\n \"alert\": boolean,\n \"importance\": \"high\" | \"medium\" | \"low\",\n \"relevant_symbols\": [\"000001\", \"600519\"],\n \"headline\": \"\u7B80\u77ED\u544A\u8B66\u6807\u9898\",\n \"reason\": \"20-50\u5B57\u4E2D\u6587\u7406\u7531\"\n}\n";
|
|
4
|
+
export declare function buildFlashMonitorAlertUserPrompt(params: {
|
|
5
|
+
flash: Jin10FlashRecord;
|
|
6
|
+
candidates: Array<{
|
|
7
|
+
item: WatchlistItem;
|
|
8
|
+
directKeywords: string[];
|
|
9
|
+
boardKeywords: string[];
|
|
10
|
+
}>;
|
|
11
|
+
}): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const FLASH_MONITOR_ALERT_SYSTEM_PROMPT = `
|
|
2
|
+
你是一位A股盘中/盘后快讯告警筛选器。你的任务是判断一条金十快讯,是否值得对当前关注列表发送一次告警。
|
|
3
|
+
|
|
4
|
+
判断规则:
|
|
5
|
+
1. 只有在快讯与关注股票本身,或与其行业/题材存在明确、可执行、短期可能影响风险偏好的关联时,才返回 alert=true。
|
|
6
|
+
2. 直接点名公司/股票代码的公告、订单、中标、减持、增持、业绩预告、重组、监管、停复牌、重大产品/项目、重要行业政策,优先级最高。
|
|
7
|
+
3. 纯海外宏观、地缘、商品报价、直播推广、图示播报、与A股候选标的缺乏清晰传导路径的内容,不要触发告警。
|
|
8
|
+
4. 行业/题材级快讯只有在确实会影响候选板块风险偏好时,才可触发;泛泛而谈的行业新闻不要触发。
|
|
9
|
+
5. 输出必须只有一个 \`\`\`json 代码块,结构如下:
|
|
10
|
+
{
|
|
11
|
+
"alert": boolean,
|
|
12
|
+
"importance": "high" | "medium" | "low",
|
|
13
|
+
"relevant_symbols": ["000001", "600519"],
|
|
14
|
+
"headline": "简短告警标题",
|
|
15
|
+
"reason": "20-50字中文理由"
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
export function buildFlashMonitorAlertUserPrompt(params) {
|
|
19
|
+
return [
|
|
20
|
+
"请判断以下金十快讯是否值得触发一次A股自选告警。",
|
|
21
|
+
"",
|
|
22
|
+
"## 快讯",
|
|
23
|
+
`时间: ${params.flash.published_at}`,
|
|
24
|
+
`链接: ${params.flash.url}`,
|
|
25
|
+
`正文: ${params.flash.content}`,
|
|
26
|
+
"",
|
|
27
|
+
"## 一阶段候选命中",
|
|
28
|
+
...params.candidates.map((candidate, index) => formatCandidate(index + 1, candidate.item, candidate.directKeywords, candidate.boardKeywords)),
|
|
29
|
+
"",
|
|
30
|
+
"请特别警惕误报:如果只是宽泛宏观信息、海外事件或商品行情,且没有明确传导到候选股票/行业,就不要发告警。",
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
function formatCandidate(index, item, directKeywords, boardKeywords) {
|
|
34
|
+
return [
|
|
35
|
+
`${index}. ${item.name}(${item.symbol})`,
|
|
36
|
+
` 直接命中: ${formatKeywords(directKeywords)}`,
|
|
37
|
+
` 行业/题材命中: ${formatKeywords(boardKeywords)}`,
|
|
38
|
+
` 行业: ${item.sector ?? "未知"}`,
|
|
39
|
+
` 题材: ${item.themes.length > 0 ? item.themes.join("、") : "无"}`,
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
function formatKeywords(items) {
|
|
43
|
+
return items.length > 0 ? items.join("、") : "无";
|
|
44
|
+
}
|
|
@@ -6,3 +6,4 @@ export { NEWS_ANALYSIS_SYSTEM_PROMPT, buildNewsAnalysisUserPrompt, } from "./new
|
|
|
6
6
|
export { COMPOSITE_ANALYSIS_SYSTEM_PROMPT, buildCompositeAnalysisUserPrompt, } from "./composite-analysis-user-prompt.js";
|
|
7
7
|
export { POST_CLOSE_REVIEW_SYSTEM_PROMPT, buildPostCloseReviewUserPrompt, } from "./post-close-review-user-prompt.js";
|
|
8
8
|
export { WATCHLIST_PROFILE_EXTRACTION_SYSTEM_PROMPT, buildWatchlistProfileExtractionUserPrompt, } from "./watchlist-profile-extraction-prompt.js";
|
|
9
|
+
export { FLASH_MONITOR_ALERT_SYSTEM_PROMPT, buildFlashMonitorAlertUserPrompt, } from "./flash-monitor-alert-prompt.js";
|
|
@@ -6,3 +6,4 @@ export { NEWS_ANALYSIS_SYSTEM_PROMPT, buildNewsAnalysisUserPrompt, } from "./new
|
|
|
6
6
|
export { COMPOSITE_ANALYSIS_SYSTEM_PROMPT, buildCompositeAnalysisUserPrompt, } from "./composite-analysis-user-prompt.js";
|
|
7
7
|
export { POST_CLOSE_REVIEW_SYSTEM_PROMPT, buildPostCloseReviewUserPrompt, } from "./post-close-review-user-prompt.js";
|
|
8
8
|
export { WATCHLIST_PROFILE_EXTRACTION_SYSTEM_PROMPT, buildWatchlistProfileExtractionUserPrompt, } from "./watchlist-profile-extraction-prompt.js";
|
|
9
|
+
export { FLASH_MONITOR_ALERT_SYSTEM_PROMPT, buildFlashMonitorAlertUserPrompt, } from "./flash-monitor-alert-prompt.js";
|
|
@@ -153,20 +153,20 @@ export class AlertService {
|
|
|
153
153
|
try {
|
|
154
154
|
switch (this.channel) {
|
|
155
155
|
case "discord":
|
|
156
|
-
await runtimeContext.runtime.channel
|
|
156
|
+
await this.invokeRuntimeChannelSend(runtimeContext.runtime.channel, "discord", "sendMessageDiscord", this.options.target, payload.message, {
|
|
157
157
|
...baseOptions,
|
|
158
158
|
filename: payload.filename,
|
|
159
159
|
});
|
|
160
160
|
return null;
|
|
161
161
|
case "slack":
|
|
162
|
-
await runtimeContext.runtime.channel
|
|
162
|
+
await this.invokeRuntimeChannelSend(runtimeContext.runtime.channel, "slack", "sendMessageSlack", this.options.target, payload.message, {
|
|
163
163
|
...baseOptions,
|
|
164
164
|
uploadFileName: payload.filename,
|
|
165
165
|
uploadTitle: payload.filename,
|
|
166
166
|
});
|
|
167
167
|
return null;
|
|
168
168
|
case "signal":
|
|
169
|
-
await runtimeContext.runtime.channel
|
|
169
|
+
await this.invokeRuntimeChannelSend(runtimeContext.runtime.channel, "signal", "sendMessageSignal", this.options.target, payload.message, baseOptions);
|
|
170
170
|
return null;
|
|
171
171
|
default:
|
|
172
172
|
// OpenClaw 2026.3.31 narrows the typed runtime channel surface.
|
|
@@ -178,6 +178,14 @@ export class AlertService {
|
|
|
178
178
|
return `runtime delivery failed: ${formatErrorMessage(error)}`;
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
+
async invokeRuntimeChannelSend(runtimeChannel, channelName, methodName, target, message, options) {
|
|
182
|
+
const channelApi = getRuntimeChannelApi(runtimeChannel, channelName);
|
|
183
|
+
const method = channelApi?.[methodName];
|
|
184
|
+
if (typeof method !== "function") {
|
|
185
|
+
throw new Error(`runtime channel ${channelName}.${methodName} unavailable`);
|
|
186
|
+
}
|
|
187
|
+
await method.call(channelApi, target, message, options);
|
|
188
|
+
}
|
|
181
189
|
async trySendViaCommand(payload) {
|
|
182
190
|
try {
|
|
183
191
|
const result = await this.runCommandWithTimeout(this.buildCliArgs(payload), { timeoutMs: 15_000 });
|
|
@@ -234,6 +242,16 @@ function normalizeSendInput(input) {
|
|
|
234
242
|
? { message: input }
|
|
235
243
|
: input;
|
|
236
244
|
}
|
|
245
|
+
function getRuntimeChannelApi(runtimeChannel, channelName) {
|
|
246
|
+
if (!isRecord(runtimeChannel)) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
const channelApi = runtimeChannel[channelName];
|
|
250
|
+
return isRecord(channelApi) ? channelApi : null;
|
|
251
|
+
}
|
|
252
|
+
function isRecord(value) {
|
|
253
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
254
|
+
}
|
|
237
255
|
function getAlertStyle(ruleCode, fallbackTitle) {
|
|
238
256
|
switch (ruleCode) {
|
|
239
257
|
case "stop_loss_hit":
|