tickflow-assist 0.3.7 → 0.3.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.
Files changed (32) hide show
  1. package/README.md +8 -6
  2. package/dist/background/realtime-monitor.worker.d.ts +1 -1
  3. package/dist/background/realtime-monitor.worker.js +3 -4
  4. package/dist/bootstrap.js +9 -0
  5. package/dist/dev/run-monitor-loop.js +0 -1
  6. package/dist/plugin-commands.js +27 -0
  7. package/dist/prompts/analysis/pre-market-brief-prompt.d.ts +1 -1
  8. package/dist/prompts/analysis/pre-market-brief-prompt.js +4 -3
  9. package/dist/services/alert-service.js +34 -4
  10. package/dist/services/monitor-service.d.ts +1 -1
  11. package/dist/services/monitor-service.js +18 -9
  12. package/dist/services/mx-search-service.d.ts +8 -1
  13. package/dist/services/mx-search-service.js +400 -10
  14. package/dist/services/pre-market-brief-service.js +343 -39
  15. package/dist/services/watchlist-service.d.ts +5 -1
  16. package/dist/services/watchlist-service.js +8 -3
  17. package/dist/tools/eastmoney-watchlist.tool.d.ts +31 -0
  18. package/dist/tools/eastmoney-watchlist.tool.js +294 -0
  19. package/dist/tools/mx-data.tool.d.ts +8 -0
  20. package/dist/tools/mx-data.tool.js +94 -0
  21. package/dist/tools/mx-select-stock.tool.js +6 -2
  22. package/dist/tools/screen-stock-candidates.tool.d.ts +34 -0
  23. package/dist/tools/screen-stock-candidates.tool.js +477 -0
  24. package/dist/types/mx-data.d.ts +23 -0
  25. package/dist/types/mx-data.js +1 -0
  26. package/dist/types/mx-select-stock.d.ts +1 -0
  27. package/dist/types/mx-self-select.d.ts +30 -0
  28. package/dist/types/mx-self-select.js +1 -0
  29. package/openclaw.plugin.json +143 -24
  30. package/package.json +9 -9
  31. package/skills/stock-analysis/SKILL.md +31 -2
  32. package/skills/usage-help/SKILL.md +33 -0
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
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.3.7` 接入 TickFlow 标的池行业映射与申万三级同业上下文,优化盘前简报提炼与收盘复盘市场信息展示,并对齐 OpenClaw `v2026.4.14` 兼容与社区元数据。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。
5
+ 最近更新:`v0.3.9` 对齐 OpenClaw `v2026.5.2+`,恢复新版 Telegram 原生命令菜单中的 `/ta_` 命令可见性,并增强 PNG 媒体投递失败后的文本回退与一键安装依赖处理。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。
6
6
 
7
- 当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.14` 上兼容。
7
+ 当前主线按 OpenClaw `v2026.5.2+` 对齐,并使用 `contracts.tools` 与 `setup.providers[].envVars` 等新版插件清单字段。
8
8
 
9
9
  ## 安装前准备
10
10
 
@@ -34,7 +34,7 @@ openclaw gateway restart
34
34
 
35
35
  - `configure-openclaw` 会把配置写入 `~/.openclaw/openclaw.json` 的 `plugins.entries["tickflow-assist"].config`。
36
36
  - 核心必填建议先准备:`tickflowApiKey`、`tickflowApiKeyLevel`、`llmApiKey`、`llmBaseUrl`、`llmModel`;告警场景再补 `alertChannel`、`alertTarget`、`alertAccount`。
37
- - 如果你不想把密钥落盘,优先把环境变量写进 `~/.openclaw/.env`,再运行配置向导补齐非密钥项;如需 PNG 告警卡正常显示中文,请自行安装 `fontconfig` 与 Noto CJK 字体。
37
+ - 如果你不想把密钥落盘,优先把环境变量写进 `~/.openclaw/.env`,再运行配置向导补齐非密钥项;如需 PNG 告警卡图片投递,请确认宿主 OpenClaw 的全局 npm 环境可解析 `sharp`,如需中文正常显示再安装 `fontconfig` 与 Noto CJK 字体。
38
38
 
39
39
  社区安装后的升级方式:
40
40
 
@@ -64,7 +64,7 @@ plugins.entries["tickflow-assist"].config
64
64
  - 告警投递:`alertChannel`、`alertTarget`、`alertAccount`
65
65
  - 能力补充:`mxSearchApiKey`、`jin10ApiToken`
66
66
 
67
- 其中,`mxSearchApiKey` 用于 `mx_search`、`mx_select_stock` 以及非 `Expert` 财务链路的 lite 补充;`jin10ApiToken` 用于 24 小时金十数据快讯监控;`jin10FlashNightAlert` 默认 `false`(开启夜间静默),设为 `true` 可恢复 24 小时快讯告警;`alertTarget`、`alertAccount` 建议在准备启用 `test_alert`、实时监控告警、金十数据快讯告警和定时通知前一并配好,避免配置不完整导致功能缺失。
67
+ 其中,`mxSearchApiKey` 用于 `mx_search`、`mx_data`、`mx_select_stock`、东方财富自选同步以及非 `Expert` 财务链路的 lite 补充;东方财富自选管理接口每日额度 200 次;`jin10ApiToken` 用于 24 小时金十数据快讯监控;`jin10FlashNightAlert` 默认 `false`(开启夜间静默),设为 `true` 可恢复 24 小时快讯告警;`alertTarget`、`alertAccount` 建议在准备启用 `test_alert`、实时监控告警、金十数据快讯告警和定时通知前一并配好,避免配置不完整导致功能缺失。
68
68
  如果你使用环境变量,运行时支持以下回退:
69
69
 
70
70
  - `tickflowApiUrl`:`TICKFLOW_ASSIST_TICKFLOW_API_URL` / `TICKFLOW_API_URL`
@@ -80,7 +80,8 @@ plugins.entries["tickflow-assist"].config
80
80
 
81
81
  ## 功能
82
82
 
83
- - 自选股管理、日 K / 分钟 K 抓取与指标计算
83
+ - 自选股管理、东方财富自选同步、日 K / 分钟 K 抓取与指标计算
84
+ - 妙想资讯搜索、官方金融数据查询、智能选股,以及限量候选池 + TickFlow 补数据联动
84
85
  - 技术面、财务面、资讯面的综合分析
85
86
  - 实时监控、定时日更、收盘后复盘
86
87
  - 金十数据 24 小时快讯监控与自选关联提醒
@@ -95,8 +96,9 @@ plugins.entries["tickflow-assist"].config
95
96
  ## 依赖与可选能力
96
97
 
97
98
  - [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE):`Free` 可用日线与实时行情;`Starter` 起可用标的池,插件会用来做申万行业映射与申万 3 级同业表现;`Pro` 起可用分钟K;`Expert` 才走 TickFlow 财务数据,非 `Expert` 默认回退妙想 lite。
99
+ - `sharp`:插件本地依赖用于生成 PNG;OpenClaw `message send --media` 还需要宿主 OpenClaw 全局 npm 环境可解析 `sharp`,否则图片投递会回退为纯文本。
98
100
  - [金十数据 MCP](https://mcp.jin10.com/app/):可选,用于 24 小时快讯流接入、自选关联筛选与事件驱动告警。独立的金十数据 Skill 详见 [OpenClaw Skill](https://clawhub.ai/robinspt/jin10) / [Hermes Skill](https://github.com/robinspt/hermes-skills)。
99
- - [东方财富妙想 Skills](https://marketing.dfcfs.com/views/finskillshub/):可选,用于 `mx_search`、`mx_select_stock` 与非 `Expert` 财务链路的 lite 补充。
101
+ - [东方财富妙想 Skills](https://marketing.dfcfs.com/views/finskillshub/):可选,用于 `mx_search`、`mx_data`、`mx_select_stock`、东方财富自选同步与非 `Expert` 财务链路的 lite 补充。
100
102
 
101
103
  ## 仓库
102
104
 
@@ -3,6 +3,6 @@ export declare class RealtimeMonitorWorker {
3
3
  private readonly monitorService;
4
4
  private readonly intervalMs;
5
5
  constructor(monitorService: MonitorService, intervalMs: number);
6
- runOnce(): Promise<number>;
6
+ runOnce(runtimeHost?: "plugin_service" | "fallback_process"): Promise<number>;
7
7
  runLoop(signal?: AbortSignal, runtimeHost?: "plugin_service" | "fallback_process"): Promise<void>;
8
8
  }
@@ -6,18 +6,17 @@ export class RealtimeMonitorWorker {
6
6
  this.monitorService = monitorService;
7
7
  this.intervalMs = intervalMs;
8
8
  }
9
- async runOnce() {
9
+ async runOnce(runtimeHost) {
10
10
  const state = await this.monitorService.getState();
11
11
  if (!state.running) {
12
12
  return 0;
13
13
  }
14
- return this.monitorService.runMonitorOnce();
14
+ return this.monitorService.runMonitorOnce(runtimeHost);
15
15
  }
16
16
  async runLoop(signal, runtimeHost) {
17
17
  while (!signal?.aborted) {
18
- await this.monitorService.recordHeartbeat(runtimeHost);
19
18
  try {
20
- await this.runOnce();
19
+ await this.runOnce(runtimeHost);
21
20
  }
22
21
  catch (error) {
23
22
  await this.monitorService.recordLoopError(error);
package/dist/bootstrap.js CHANGED
@@ -58,8 +58,10 @@ import { fetchKlinesTool } from "./tools/fetch-klines.tool.js";
58
58
  import { fetchIntradayKlinesTool } from "./tools/fetch-intraday-klines.tool.js";
59
59
  import { fetchFinancialsTool } from "./tools/fetch-financials.tool.js";
60
60
  import { flashMonitorStatusTool } from "./tools/flash-monitor-status.tool.js";
61
+ import { mxDataTool } from "./tools/mx-data.tool.js";
61
62
  import { mxSearchTool } from "./tools/mx-search.tool.js";
62
63
  import { mxSelectStockTool } from "./tools/mx-select-stock.tool.js";
64
+ import { listEastmoneyWatchlistTool, pushEastmoneyWatchlistTool, removeEastmoneyWatchlistTool, syncEastmoneyWatchlistTool, } from "./tools/eastmoney-watchlist.tool.js";
63
65
  import { listWatchlistTool } from "./tools/list-watchlist.tool.js";
64
66
  import { dailyUpdateStatusTool } from "./tools/daily-update-status.tool.js";
65
67
  import { monitorStatusTool } from "./tools/monitor-status.tool.js";
@@ -67,6 +69,7 @@ import { refreshWatchlistNamesTool } from "./tools/refresh-watchlist-names.tool.
67
69
  import { refreshWatchlistProfilesTool } from "./tools/refresh-watchlist-profiles.tool.js";
68
70
  import { queryDatabaseTool } from "./tools/query-database.tool.js";
69
71
  import { removeStockTool } from "./tools/remove-stock.tool.js";
72
+ import { screenStockCandidatesTool } from "./tools/screen-stock-candidates.tool.js";
70
73
  import { startDailyUpdateTool } from "./tools/start-daily-update.tool.js";
71
74
  import { startMonitorTool } from "./tools/start-monitor.tool.js";
72
75
  import { stopDailyUpdateTool } from "./tools/stop-daily-update.tool.js";
@@ -173,15 +176,21 @@ export function createAppContext(config, options = {}) {
173
176
  flashMonitorStatusTool(jin10FlashMonitorService),
174
177
  fetchKlinesTool(klineService, klinesRepository, indicatorService, indicatorsRepository),
175
178
  listWatchlistTool(watchlistService),
179
+ listEastmoneyWatchlistTool(mxApiService),
176
180
  monitorStatusTool(monitorService),
181
+ mxDataTool(mxApiService),
177
182
  mxSearchTool(mxApiService),
178
183
  mxSelectStockTool(mxApiService),
184
+ pushEastmoneyWatchlistTool(mxApiService, watchlistService),
179
185
  queryDatabaseTool(database),
180
186
  refreshWatchlistNamesTool(watchlistService),
181
187
  refreshWatchlistProfilesTool(config.tickflowApiKeyLevel, watchlistService),
188
+ removeEastmoneyWatchlistTool(mxApiService),
182
189
  removeStockTool(watchlistService),
190
+ screenStockCandidatesTool(config.tickflowApiKeyLevel, mxApiService, quoteService, klineService, financialService, watchlistService, analysisService),
183
191
  startDailyUpdateTool(dailyUpdateWorker, config, runtime.configSource, runtime),
184
192
  startMonitorTool(monitorService, runtime),
193
+ syncEastmoneyWatchlistTool(mxApiService, watchlistService),
185
194
  stopDailyUpdateTool(dailyUpdateWorker, runtime),
186
195
  stopMonitorTool(monitorService, runtime),
187
196
  testAlertTool(alertService, alertMediaService, runtime.configSource),
@@ -9,7 +9,6 @@ async function main() {
9
9
  const flashWorker = app.services.jin10FlashWorker;
10
10
  const alertService = app.services.alertService;
11
11
  const monitorService = app.services.monitorService;
12
- await monitorService.recordHeartbeat("fallback_process");
13
12
  await monitorService.setWorkerPid(process.pid);
14
13
  process.stdout.write(`TickFlow realtime loop started, price_interval=${config.requestInterval}s, jin10_interval=${config.jin10FlashPollInterval}s\n`);
15
14
  const controller = new AbortController();
@@ -33,6 +33,13 @@ function parseRequiredSymbol(args, usage) {
33
33
  }
34
34
  return symbol;
35
35
  }
36
+ function parseRequiredKeyword(args, usage) {
37
+ const keyword = (args ?? "").trim();
38
+ if (!keyword) {
39
+ throw new Error(`用法: ${usage}`);
40
+ }
41
+ return keyword;
42
+ }
36
43
  async function runToolText(tool, rawInput) {
37
44
  return tool.run({ rawInput });
38
45
  }
@@ -87,6 +94,7 @@ export function registerPluginCommands(api, tools, app) {
87
94
  const updateAll = getTool(tools, "update_all");
88
95
  const dailyUpdateStatus = getTool(tools, "daily_update_status");
89
96
  const testAlert = getTool(tools, "test_alert");
97
+ const screenStockCandidates = getTool(tools, "screen_stock_candidates");
90
98
  const commands = [
91
99
  {
92
100
  name: "ta_addstock",
@@ -202,6 +210,25 @@ export function registerPluginCommands(api, tools, app) {
202
210
  requireAuth: true,
203
211
  handler: async () => runCommandText(() => runToolText(testAlert)),
204
212
  },
213
+ {
214
+ name: "ta_screenstocks",
215
+ description: "智能选股并生成小规模候选池,不经过 AI 对话。用法: /ta_screenstocks <自然语言选股条件>",
216
+ acceptsArgs: true,
217
+ requireAuth: true,
218
+ handler: async ({ args }) => runCommandText(() => runToolText(screenStockCandidates, {
219
+ keyword: parseRequiredKeyword(args, "/ta_screenstocks <自然语言选股条件>"),
220
+ })),
221
+ },
222
+ {
223
+ name: "ta_screenstocks_llm",
224
+ description: "智能选股并生成小规模候选池,再调用 LLM 做候选整理。用法: /ta_screenstocks_llm <自然语言选股条件>",
225
+ acceptsArgs: true,
226
+ requireAuth: true,
227
+ handler: async ({ args }) => runCommandText(() => runToolText(screenStockCandidates, {
228
+ keyword: parseRequiredKeyword(args, "/ta_screenstocks_llm <自然语言选股条件>"),
229
+ summarize: true,
230
+ })),
231
+ },
205
232
  {
206
233
  name: "ta_debug",
207
234
  description: "查看 TickFlow 插件运行时调试信息。",
@@ -1,5 +1,5 @@
1
1
  import type { WatchlistItem } from "../../types/domain.js";
2
- export declare const PRE_MARKET_BRIEF_SYSTEM_PROMPT = "\n\u4F60\u662F\u4E00\u4F4DA\u80A1\u5F00\u76D8\u524D\u8D44\u8BAF\u7B80\u62A5\u7F16\u8F91\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u57FA\u4E8E\u76D8\u524D\u7A97\u53E3\u5185\u7684\u91D1\u5341\u6570\u636E\u6574\u7406\u5FEB\u8BAF\uFF0C\u4EE5\u53CA\u7528\u6237\u81EA\u9009\u80A1\u7684\u884C\u4E1A/\u9898\u6750\u4FE1\u606F\uFF0C\u751F\u6210\u4E00\u4EFD\u9002\u5408 9:20 \u63A8\u9001\u7684\u5F00\u76D8\u524D\u6458\u8981\u3002\n\n\u8F93\u51FA\u8981\u6C42\uFF1A\n1. \u6309\u4EE5\u4E0B\u6807\u9898\u8F93\u51FA\uFF0C\u6BCF\u8282\u4F7F\u7528 2-5 \u6761\u7B80\u6D01\u4E2D\u6587\u8981\u70B9\uFF1A\n- \u91CD\u5927\u8981\u95FB\n- \u81EA\u9009\u76F8\u5173\n- \u6F5C\u5728\u673A\u4F1A\n- \u98CE\u9669\u63D0\u793A\n- \u5F00\u76D8\u524D\u5173\u6CE8\u6E05\u5355\n2. \u201C\u91CD\u5927\u8981\u95FB\u201D\u53EA\u63D0\u70BC\u4F1A\u5F71\u54CD A \u80A1\u5F00\u76D8\u60C5\u7EEA\u3001\u884C\u4E1A\u98CE\u9669\u504F\u597D\u6216\u91CD\u8981\u4EA4\u6613\u7EBF\u7D22\u7684\u5185\u5BB9\uFF0C\u4E0D\u8981\u673A\u68B0\u7F57\u5217\u6240\u6709\u5FEB\u8BAF\u3002\n3. \u201C\u81EA\u9009\u76F8\u5173\u201D\u8981\u4F18\u5148\u70B9\u540D\u4E0E\u81EA\u9009\u80A1\u3001\u884C\u4E1A\u6216\u9898\u6750\u76F4\u63A5\u76F8\u5173\u7684\u5185\u5BB9\uFF1B\u82E5\u6CA1\u6709\u76F4\u63A5\u547D\u4E2D\uFF0C\u4E5F\u8981\u660E\u786E\u5199\u51FA\u201C\u672A\u53D1\u73B0\u76F4\u63A5\u547D\u4E2D\u81EA\u9009\u80A1\u201D\u3002\n4. \u201C\u6F5C\u5728\u673A\u4F1A\u201D\u53EA\u4FDD\u7559\u5B58\u5728\u6E05\u6670\u50AC\u5316\u94FE\u6761\u7684\u65B9\u5411\uFF0C\u4F8B\u5982\u653F\u7B56\u3001\u4EA7\u4E1A\u8D8B\u52BF\u3001\u4E1A\u7EE9\u3001\u8BA2\u5355\u3001\u8D44\u91D1\u504F\u597D\u53D8\u5316\uFF1B\u6CA1\u6709\u660E\u786E\u673A\u4F1A\u65F6\u8981\u76F4\u8BF4\u3002\n5. \u201C\u98CE\u9669\u63D0\u793A\u201D\u8981\u6307\u51FA\u4E0D\u5229\u4E8E\u5F00\u76D8\u51B3\u7B56\u7684\u6270\u52A8\u9879\uFF0C\u4F8B\u5982\u76D1\u7BA1\u3001\u6D77\u5916\u6270\u52A8\u3001\u4E1A\u7EE9\u98CE\u9669\u3001\u9898\u6750\u9000\u6F6E\u3001\u6D88\u606F\u4E0D\u786E\u5B9A\u6027\u3002\n6. \u201C\u5F00\u76D8\u524D\u5173\u6CE8\u6E05\u5355\u201D\u8F93\u51FA 3-6 \u6761\u53EF\u6267\u884C\u89C2\u5BDF\u70B9\uFF0C\u5C3D\u91CF\u5199\u6E05\u695A\u5E94\u89C2\u5BDF\u7684\u80A1\u7968\u3001\u677F\u5757\u6216\u4FE1\u53F7\u3002\n7. \u4E0D\u8981\u7F16\u9020\u672A\u5728\u8F93\u5165\u4E2D\u51FA\u73B0\u7684\u516C\u53F8\u3001\u653F\u7B56\u3001\u884C\u4E1A\u4FE1\u606F\u6216\u5FEB\u8BAF\u7ED3\u8BBA\u3002\n8. \u4E25\u7981\u53EA\u590D\u8FF0\u201C\u91D1\u5341\u6570\u636E\u6574\u7406\uFF1A...\u201D\u6807\u9898\u3002\u6BCF\u6761\u8981\u70B9\u90FD\u5FC5\u987B\u4F18\u5148\u4F7F\u7528\u8F93\u5165\u4E2D\u7684\u201C\u63D0\u70BC\u6458\u8981\u201D\u6216\u201C\u6B63\u6587\u8981\u70B9\u201D\uFF0C\u5199\u51FA\u81F3\u5C11\u4E00\u4E2A\u5177\u4F53\u4E8B\u5B9E\u3001\u5F71\u54CD\u94FE\u6761\u6216\u89C2\u5BDF\u65B9\u5411\u3002\n9. \u5982\u679C\u67D0\u6761\u6574\u7406\u5FEB\u8BAF\u53EA\u6709\u6807\u9898\u3001\u6CA1\u6709\u53EF\u7528\u7EC6\u8282\uFF0C\u53EF\u4EE5\u660E\u786E\u5199\u201C\u4EC5\u4E3A\u6807\u9898\u7EA7\u7EBF\u7D22\uFF0C\u7EC6\u8282\u4E0D\u8DB3\u201D\uFF0C\u4F46\u4E0D\u8981\u628A\u6807\u9898\u672C\u8EAB\u5F53\u6210\u5B8C\u6574\u7ED3\u8BBA\u3002\n10. \u8F93\u51FA\u6B63\u6587\u5373\u53EF\uFF0C\u4E0D\u8981\u9644\u52A0 JSON\u3002\n";
2
+ export declare const PRE_MARKET_BRIEF_SYSTEM_PROMPT = "\n\u4F60\u662F\u4E00\u4F4DA\u80A1\u5F00\u76D8\u524D\u8D44\u8BAF\u7B80\u62A5\u7F16\u8F91\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u57FA\u4E8E\u76D8\u524D\u7A97\u53E3\u5185\u7684\u91D1\u5341\u6570\u636E\u6574\u7406\u5FEB\u8BAF\uFF0C\u4EE5\u53CA\u7528\u6237\u81EA\u9009\u80A1\u7684\u884C\u4E1A/\u9898\u6750\u4FE1\u606F\uFF0C\u751F\u6210\u4E00\u4EFD\u9002\u5408 9:20 \u63A8\u9001\u7684\u5F00\u76D8\u524D\u6458\u8981\u3002\n\n\u8F93\u51FA\u8981\u6C42\uFF1A\n1. \u6309\u4EE5\u4E0B\u6807\u9898\u8F93\u51FA\uFF0C\u6BCF\u8282\u4F7F\u7528 2-5 \u6761\u7B80\u6D01\u4E2D\u6587\u8981\u70B9\uFF1A\n- \u91CD\u5927\u8981\u95FB\n- \u81EA\u9009\u76F8\u5173\n- \u6F5C\u5728\u673A\u4F1A\n- \u98CE\u9669\u63D0\u793A\n- \u5F00\u76D8\u524D\u5173\u6CE8\u6E05\u5355\n2. \u201C\u91CD\u5927\u8981\u95FB\u201D\u5FC5\u987B\u5148\u4ECE\u5168\u90E8\u5FEB\u8BAF\u4E2D\u63D0\u70BC\u5B8F\u89C2/\u4EA7\u4E1A\u4E3B\u9898\uFF0C\u4F8B\u5982\u5730\u7F18\u98CE\u9669\u3001\u80FD\u6E90\u4F9B\u9700\u3001\u79D1\u6280\u4EA7\u4E1A\u3001\u653F\u7B56\u76D1\u7BA1\u3001\u8DE8\u5883\u652F\u4ED8\u3001\u5B8F\u89C2\u8D44\u91D1\u7B49\uFF1B\u4E0D\u8981\u6309\u81EA\u9009\u80A1\u547D\u4E2D\u7ED3\u679C\u6392\u5E8F\uFF0C\u4E5F\u4E0D\u8981\u53EA\u590D\u8FF0\u4E0E\u81EA\u9009\u76F8\u5173\u7684\u51E0\u6761\u3002\n3. \u201C\u81EA\u9009\u76F8\u5173\u201D\u53EA\u89E3\u91CA\u4E0E\u81EA\u9009\u80A1\u3001\u884C\u4E1A\u6216\u9898\u6750\u76F4\u63A5\u76F8\u5173\u7684\u5185\u5BB9\uFF1B\u82E5\u540C\u4E00\u4E8B\u5B9E\u5DF2\u5728\u201C\u91CD\u5927\u8981\u95FB\u201D\u51FA\u73B0\uFF0C\u8FD9\u91CC\u8981\u6539\u5199\u6210\u201C\u4E3A\u4EC0\u4E48\u5173\u8054\u8BE5\u81EA\u9009\u80A1/\u9898\u6750\u201D\uFF0C\u4E0D\u8981\u590D\u5236\u540C\u4E00\u53E5\u3002\n4. \u201C\u6F5C\u5728\u673A\u4F1A\u201D\u53EA\u4FDD\u7559\u5B58\u5728\u6E05\u6670\u50AC\u5316\u94FE\u6761\u7684\u65B9\u5411\uFF0C\u4F8B\u5982\u653F\u7B56\u3001\u4EA7\u4E1A\u8D8B\u52BF\u3001\u4E1A\u7EE9\u3001\u8BA2\u5355\u3001\u8D44\u91D1\u504F\u597D\u53D8\u5316\uFF1B\u6CA1\u6709\u660E\u786E\u673A\u4F1A\u65F6\u8981\u76F4\u8BF4\u3002\n5. \u201C\u98CE\u9669\u63D0\u793A\u201D\u8981\u6307\u51FA\u4E0D\u5229\u4E8E\u5F00\u76D8\u51B3\u7B56\u7684\u6270\u52A8\u9879\uFF0C\u4F8B\u5982\u76D1\u7BA1\u3001\u6D77\u5916\u6270\u52A8\u3001\u4E1A\u7EE9\u98CE\u9669\u3001\u9898\u6750\u9000\u6F6E\u3001\u6D88\u606F\u4E0D\u786E\u5B9A\u6027\u3002\n6. \u201C\u5F00\u76D8\u524D\u5173\u6CE8\u6E05\u5355\u201D\u8F93\u51FA 3-6 \u6761\u53EF\u6267\u884C\u89C2\u5BDF\u70B9\uFF0C\u5C3D\u91CF\u5199\u6E05\u695A\u5E94\u89C2\u5BDF\u7684\u80A1\u7968\u3001\u677F\u5757\u6216\u4FE1\u53F7\u3002\n7. \u4E0D\u8981\u7F16\u9020\u672A\u5728\u8F93\u5165\u4E2D\u51FA\u73B0\u7684\u516C\u53F8\u3001\u653F\u7B56\u3001\u884C\u4E1A\u4FE1\u606F\u6216\u5FEB\u8BAF\u7ED3\u8BBA\u3002\n8. \u4E25\u7981\u53EA\u590D\u8FF0\u201C\u91D1\u5341\u6570\u636E\u6574\u7406\uFF1A...\u201D\u6807\u9898\u3002\u6BCF\u6761\u8981\u70B9\u90FD\u5FC5\u987B\u4F18\u5148\u4F7F\u7528\u8F93\u5165\u4E2D\u7684\u201C\u63D0\u70BC\u6458\u8981\u201D\u6216\u201C\u6B63\u6587\u8981\u70B9\u201D\uFF0C\u5199\u51FA\u81F3\u5C11\u4E00\u4E2A\u5177\u4F53\u4E8B\u5B9E\u3001\u5F71\u54CD\u94FE\u6761\u6216\u89C2\u5BDF\u65B9\u5411\u3002\n9. \u5982\u679C\u67D0\u6761\u6574\u7406\u5FEB\u8BAF\u53EA\u6709\u6807\u9898\u3001\u6CA1\u6709\u53EF\u7528\u7EC6\u8282\uFF0C\u53EF\u4EE5\u660E\u786E\u5199\u201C\u4EC5\u4E3A\u6807\u9898\u7EA7\u7EBF\u7D22\uFF0C\u7EC6\u8282\u4E0D\u8DB3\u201D\uFF0C\u4F46\u4E0D\u8981\u628A\u6807\u9898\u672C\u8EAB\u5F53\u6210\u5B8C\u6574\u7ED3\u8BBA\u3002\n10. \u5C3D\u91CF\u907F\u514D\u8DE8\u680F\u76EE\u91CD\u590D\u540C\u4E00\u6761\u4E8B\u5B9E\uFF1B\u786E\u9700\u91CD\u590D\u65F6\uFF0C\u6BCF\u4E2A\u680F\u76EE\u5FC5\u987B\u7ED9\u51FA\u4E0D\u540C\u89D2\u5EA6\uFF1A\u91CD\u5927\u8981\u95FB\u8BB2\u5E02\u573A\u5F71\u54CD\uFF0C\u81EA\u9009\u76F8\u5173\u8BB2\u5173\u8054\u539F\u56E0\uFF0C\u673A\u4F1A/\u98CE\u9669\u8BB2\u4EA4\u6613\u9A8C\u8BC1\u70B9\u3002\n11. \u8F93\u51FA\u6B63\u6587\u5373\u53EF\uFF0C\u4E0D\u8981\u9644\u52A0 JSON\u3002\n";
3
3
  export declare function buildPreMarketBriefUserPrompt(params: {
4
4
  windowStartAt: string;
5
5
  windowEndAt: string;
@@ -8,15 +8,16 @@ export const PRE_MARKET_BRIEF_SYSTEM_PROMPT = `
8
8
  - 潜在机会
9
9
  - 风险提示
10
10
  - 开盘前关注清单
11
- 2. “重大要闻”只提炼会影响 A 股开盘情绪、行业风险偏好或重要交易线索的内容,不要机械罗列所有快讯。
12
- 3. “自选相关”要优先点名与自选股、行业或题材直接相关的内容;若没有直接命中,也要明确写出“未发现直接命中自选股”。
11
+ 2. “重大要闻”必须先从全部快讯中提炼宏观/产业主题,例如地缘风险、能源供需、科技产业、政策监管、跨境支付、宏观资金等;不要按自选股命中结果排序,也不要只复述与自选相关的几条。
12
+ 3. “自选相关”只解释与自选股、行业或题材直接相关的内容;若同一事实已在“重大要闻”出现,这里要改写成“为什么关联该自选股/题材”,不要复制同一句。
13
13
  4. “潜在机会”只保留存在清晰催化链条的方向,例如政策、产业趋势、业绩、订单、资金偏好变化;没有明确机会时要直说。
14
14
  5. “风险提示”要指出不利于开盘决策的扰动项,例如监管、海外扰动、业绩风险、题材退潮、消息不确定性。
15
15
  6. “开盘前关注清单”输出 3-6 条可执行观察点,尽量写清楚应观察的股票、板块或信号。
16
16
  7. 不要编造未在输入中出现的公司、政策、行业信息或快讯结论。
17
17
  8. 严禁只复述“金十数据整理:...”标题。每条要点都必须优先使用输入中的“提炼摘要”或“正文要点”,写出至少一个具体事实、影响链条或观察方向。
18
18
  9. 如果某条整理快讯只有标题、没有可用细节,可以明确写“仅为标题级线索,细节不足”,但不要把标题本身当成完整结论。
19
- 10. 输出正文即可,不要附加 JSON。
19
+ 10. 尽量避免跨栏目重复同一条事实;确需重复时,每个栏目必须给出不同角度:重大要闻讲市场影响,自选相关讲关联原因,机会/风险讲交易验证点。
20
+ 11. 输出正文即可,不要附加 JSON。
20
21
  `;
21
22
  export function buildPreMarketBriefUserPrompt(params) {
22
23
  return [
@@ -82,6 +82,17 @@ export class AlertService {
82
82
  await this.logCompletion(sendId, messageHash, payload, result);
83
83
  return result;
84
84
  }
85
+ if (isDefiniteCommandMediaPreSendFailure(primaryFailure.error)
86
+ && isPossiblyDeliveredTextCommandFailure(textFallbackFailure.error)) {
87
+ const result = {
88
+ ok: true,
89
+ mediaAttempted: true,
90
+ mediaDelivered: false,
91
+ error: this.combineErrors(primaryFailure.error, `text fallback command returned an uncertain status: ${textFallbackFailure.error}`),
92
+ };
93
+ await this.logCompletion(sendId, messageHash, payload, result);
94
+ return result;
95
+ }
85
96
  const result = {
86
97
  ok: false,
87
98
  mediaAttempted: true,
@@ -293,11 +304,12 @@ export class AlertService {
293
304
  });
294
305
  return null;
295
306
  }
307
+ const detail = result.stderr.trim()
308
+ || result.stdout.trim()
309
+ || `command exited with ${result.code ?? "unknown"}`;
296
310
  const failure = {
297
- error: result.stderr.trim()
298
- || result.stdout.trim()
299
- || `command exited with ${result.code ?? "unknown"}`,
300
- ambiguous: true,
311
+ error: detail,
312
+ ambiguous: Boolean(payload.mediaPath) && !isDefiniteCommandMediaPreSendFailure(detail),
301
313
  };
302
314
  await this.logTransportFailure("command_failed", context, payload, failure, {
303
315
  code: result.code,
@@ -420,6 +432,24 @@ function formatErrorMessage(error) {
420
432
  function isRuntimeCapabilityUnavailableError(detail) {
421
433
  return /runtime channel .* unavailable/i.test(detail);
422
434
  }
435
+ function isDefiniteCommandMediaPreSendFailure(detail) {
436
+ return [
437
+ /Failed to optimize image/i,
438
+ /Optional dependency sharp is required/i,
439
+ /Cannot find package ['"]sharp['"]/i,
440
+ /\bENOENT\b/i,
441
+ /no such file or directory/i,
442
+ /media file missing/i,
443
+ ].some((pattern) => pattern.test(detail));
444
+ }
445
+ function isPossiblyDeliveredTextCommandFailure(detail) {
446
+ return [
447
+ /command exited with unknown/i,
448
+ /message send timed out/i,
449
+ /\btimeout\b/i,
450
+ /no-output-timeout/i,
451
+ ].some((pattern) => pattern.test(detail));
452
+ }
423
453
  function normalizeSendInput(input) {
424
454
  return typeof input === "string"
425
455
  ? { message: input }
@@ -34,7 +34,7 @@ export declare class MonitorService {
34
34
  bindManagedServiceRuntime(): Promise<void>;
35
35
  markStopped(): Promise<void>;
36
36
  getStatusReport(): Promise<string>;
37
- runMonitorOnce(): Promise<number>;
37
+ runMonitorOnce(runtimeHost?: "plugin_service" | "fallback_process"): Promise<number>;
38
38
  private maybeSendSessionNotification;
39
39
  private buildQuoteLines;
40
40
  private buildKeyLevelsLines;
@@ -24,6 +24,7 @@ const DEFAULT_STATE = {
24
24
  const INTRADAY_PERIOD = "1m";
25
25
  const MONITOR_RUN_LOCK_MIN_STALE_MS = 90_000;
26
26
  const ALERT_CLAIM_MIN_STALE_MS = 90_000;
27
+ const SYSTEM_SESSION_ALERT_SYMBOL = "__system_session__";
27
28
  export class MonitorService {
28
29
  baseDir;
29
30
  requestInterval;
@@ -183,12 +184,13 @@ export class MonitorService {
183
184
  lines.push(...(await this.buildKeyLevelsLines(watchlist)));
184
185
  return lines.join("\n");
185
186
  }
186
- async runMonitorOnce() {
187
+ async runMonitorOnce(runtimeHost) {
187
188
  const runLease = await this.tryAcquireRunLease();
188
189
  if (!runLease) {
189
190
  return 0;
190
191
  }
191
192
  try {
193
+ await this.recordHeartbeat(runtimeHost);
192
194
  await this.alertMediaService.maybeCleanupExpired();
193
195
  const phase = await this.tradingCalendarService.getTradingPhase();
194
196
  let alertCount = await this.maybeSendSessionNotification(phase);
@@ -259,12 +261,14 @@ export class MonitorService {
259
261
  return 0;
260
262
  }
261
263
  const watchlistCount = (await this.watchlistService.list()).length;
262
- const ok = await this.alertService.send(this.alertService.formatSystemNotification(event.title, [
264
+ const message = this.alertService.formatSystemNotification(event.title, [
263
265
  `时间: ${now}`,
264
266
  `阶段: ${event.phaseText}`,
265
267
  `关注列表: ${watchlistCount}只`,
266
- ]));
267
- if (ok) {
268
+ ]);
269
+ const ok = await this.trySendAlert(SYSTEM_SESSION_ALERT_SYMBOL, event.id, message);
270
+ if (ok
271
+ || await this.alertLogRepository.isSentThisSession(SYSTEM_SESSION_ALERT_SYMBOL, event.id, getSessionKey())) {
268
272
  nextState.sessionNotificationsSent.push(event.id);
269
273
  }
270
274
  await this.writeState(nextState);
@@ -310,7 +314,8 @@ export class MonitorService {
310
314
  }
311
315
  async buildAlertLine() {
312
316
  const today = formatChinaDateTime().slice(0, 10);
313
- const alerts = await this.alertLogRepository.listByNaturalDate(today);
317
+ const alerts = (await this.alertLogRepository.listByNaturalDate(today))
318
+ .filter((entry) => entry.symbol !== SYSTEM_SESSION_ALERT_SYMBOL);
314
319
  if (alerts.length === 0) {
315
320
  return "今日告警: 无";
316
321
  }
@@ -773,7 +778,8 @@ function resolveSessionNotification(previousPhase, currentPhase, hhmm, sent) {
773
778
  if (!hasSent("morning_start")
774
779
  && currentPhase === "trading"
775
780
  && hhmm <= "11:30"
776
- && ((previousPhase === "pre_market") || isWithinWindow(hhmm, "09:30", "09:40"))) {
781
+ && ((previousPhase === "pre_market")
782
+ || (previousPhase !== "trading" && isWithinWindow(hhmm, "09:30", "09:40")))) {
777
783
  return {
778
784
  id: "morning_start",
779
785
  title: "🔔 开始上午盯盘",
@@ -782,7 +788,8 @@ function resolveSessionNotification(previousPhase, currentPhase, hhmm, sent) {
782
788
  }
783
789
  if (!hasSent("morning_end")
784
790
  && currentPhase === "lunch_break"
785
- && ((previousPhase === "trading") || isWithinWindow(hhmm, "11:30", "11:40"))) {
791
+ && ((previousPhase === "trading")
792
+ || (previousPhase !== "lunch_break" && isWithinWindow(hhmm, "11:30", "11:40")))) {
786
793
  return {
787
794
  id: "morning_end",
788
795
  title: "🔔 上午盯盘结束",
@@ -792,7 +799,8 @@ function resolveSessionNotification(previousPhase, currentPhase, hhmm, sent) {
792
799
  if (!hasSent("afternoon_start")
793
800
  && currentPhase === "trading"
794
801
  && hhmm >= "13:00"
795
- && ((previousPhase === "lunch_break") || isWithinWindow(hhmm, "13:00", "13:10"))) {
802
+ && ((previousPhase === "lunch_break")
803
+ || (previousPhase !== "trading" && isWithinWindow(hhmm, "13:00", "13:10")))) {
796
804
  return {
797
805
  id: "afternoon_start",
798
806
  title: "🔔 开始下午盯盘",
@@ -801,7 +809,8 @@ function resolveSessionNotification(previousPhase, currentPhase, hhmm, sent) {
801
809
  }
802
810
  if (!hasSent("day_end")
803
811
  && currentPhase === "closed"
804
- && ((previousPhase === "trading") || isWithinWindow(hhmm, "15:00", "15:10"))) {
812
+ && ((previousPhase === "trading")
813
+ || (previousPhase !== "closed" && isWithinWindow(hhmm, "15:00", "15:10")))) {
805
814
  return {
806
815
  id: "day_end",
807
816
  title: "🔔 今日盯盘结束",
@@ -1,4 +1,6 @@
1
1
  import type { MxSearchDocument } from "../types/mx-search.js";
2
+ import type { MxDataResult } from "../types/mx-data.js";
3
+ import type { MxSelfSelectManageResult, MxSelfSelectResult } from "../types/mx-self-select.js";
2
4
  import type { MxSelectStockResult } from "../types/mx-select-stock.js";
3
5
  export declare class MxSearchServiceError extends Error {
4
6
  constructor(message: string);
@@ -8,15 +10,20 @@ export declare class MxApiService {
8
10
  private readonly apiKey;
9
11
  constructor(apiBaseUrl: string, apiKey: string);
10
12
  isConfigured(): boolean;
11
- getConfigurationError(): string | null;
13
+ getConfigurationError(featureName?: string): string | null;
12
14
  search(query: string): Promise<MxSearchDocument[]>;
13
15
  selectStocks(input: {
14
16
  keyword: string;
15
17
  pageNo?: number;
16
18
  pageSize?: number;
17
19
  }): Promise<MxSelectStockResult>;
20
+ queryData(toolQuery: string): Promise<MxDataResult>;
21
+ getSelfSelectWatchlist(): Promise<MxSelfSelectResult>;
22
+ manageSelfSelect(query: string): Promise<MxSelfSelectManageResult>;
18
23
  private postJson;
19
24
  }
20
25
  export declare class MxSearchService extends MxApiService {
21
26
  }
22
27
  export declare function normalizeMxSearchDocuments(value: unknown): MxSearchDocument[];
28
+ export declare function normalizeMxSelectStockResult(value: unknown): MxSelectStockResult;
29
+ export declare function normalizeMxDataResult(value: unknown): MxDataResult;