tickflow-assist 0.2.18 → 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.
Files changed (37) hide show
  1. package/README.md +13 -6
  2. package/dist/analysis/parsers/flash-alert-decision.parser.d.ts +8 -0
  3. package/dist/analysis/parsers/flash-alert-decision.parser.js +34 -0
  4. package/dist/background/jin10-flash.worker.d.ts +8 -0
  5. package/dist/background/jin10-flash.worker.js +24 -0
  6. package/dist/bootstrap.d.ts +4 -0
  7. package/dist/bootstrap.js +17 -0
  8. package/dist/config/normalize.js +15 -6
  9. package/dist/config/schema.d.ts +5 -1
  10. package/dist/config/schema.js +3 -0
  11. package/dist/dev/run-monitor-loop.js +6 -2
  12. package/dist/dev/tickflow-assist-cli.js +45 -0
  13. package/dist/plugin-commands.js +7 -0
  14. package/dist/prompts/analysis/flash-monitor-alert-prompt.d.ts +11 -0
  15. package/dist/prompts/analysis/flash-monitor-alert-prompt.js +44 -0
  16. package/dist/prompts/analysis/index.d.ts +1 -0
  17. package/dist/prompts/analysis/index.js +1 -0
  18. package/dist/services/alert-service.d.ts +1 -0
  19. package/dist/services/alert-service.js +21 -3
  20. package/dist/services/jin10-flash-monitor-service.d.ts +32 -0
  21. package/dist/services/jin10-flash-monitor-service.js +578 -0
  22. package/dist/services/jin10-mcp-service.d.ts +29 -0
  23. package/dist/services/jin10-mcp-service.js +242 -0
  24. package/dist/storage/repositories/jin10-flash-delivery-repo.d.ts +10 -0
  25. package/dist/storage/repositories/jin10-flash-delivery-repo.js +57 -0
  26. package/dist/storage/repositories/jin10-flash-repo.d.ts +15 -0
  27. package/dist/storage/repositories/jin10-flash-repo.js +126 -0
  28. package/dist/storage/schemas.d.ts +2 -0
  29. package/dist/storage/schemas.js +19 -0
  30. package/dist/tools/flash-monitor-status.tool.d.ts +6 -0
  31. package/dist/tools/flash-monitor-status.tool.js +9 -0
  32. package/dist/types/flash-monitor.d.ts +17 -0
  33. package/dist/types/flash-monitor.js +1 -0
  34. package/dist/types/jin10.d.ts +30 -0
  35. package/dist/types/jin10.js +1 -0
  36. package/openclaw.plugin.json +43 -1
  37. package/package.json +15 -7
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # TickFlow Assist
2
2
 
3
- 基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE) 获取行情与财务数据,结合 LLM 生成技术面、基本面、资讯面的综合判断,并把结果持久化到本地 LanceDB。
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.2.18` 调整 PNG 告警卡的日内时间轴与午间衔接逻辑,修复测试图与 demo 图的时间展示不一致;同时修正社区版配置字段说明,并补强 npm 打包脚本对包页 README 元数据的处理。
5
+ 最近更新:`v0.3.1` 修复 GitHub Actions 自动发布 npm 包时的 provenance 仓库元数据校验失败,并将金十数据 MCP 握手里的 `clientInfo` 调整为中性固定值。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。
6
6
 
7
- 当前主线按 OpenClaw `v2026.3.31+` 对齐。
7
+ 当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.1` 上兼容。
8
8
 
9
9
  ## 安装
10
10
 
@@ -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,8 @@
1
+ export interface FlashAlertDecision {
2
+ alert: boolean;
3
+ importance: "high" | "medium" | "low";
4
+ relevantSymbols: string[];
5
+ headline: string;
6
+ reason: string;
7
+ }
8
+ export declare function parseFlashAlertDecision(responseText: string): FlashAlertDecision;
@@ -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
+ }
@@ -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,
@@ -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
- if (!config.tickflowApiKey) {
78
- errors.push("tickflowApiKey is required");
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) {
@@ -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">;
@@ -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 monitor loop started, interval=${config.requestInterval}s\n`);
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 worker.runLoop(controller.signal, "fallback_process");
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");
@@ -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";
@@ -60,6 +60,7 @@ export declare class AlertService {
60
60
  private combineErrors;
61
61
  private trySendPayload;
62
62
  private trySendViaRuntime;
63
+ private invokeRuntimeChannelSend;
63
64
  private trySendViaCommand;
64
65
  private buildCliArgs;
65
66
  }
@@ -153,20 +153,20 @@ export class AlertService {
153
153
  try {
154
154
  switch (this.channel) {
155
155
  case "discord":
156
- await runtimeContext.runtime.channel.discord.sendMessageDiscord(this.options.target, payload.message, {
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.slack.sendMessageSlack(this.options.target, payload.message, {
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.signal.sendMessageSignal(this.options.target, payload.message, baseOptions);
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":