tickflow-assist 0.2.19 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) 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/analysis/types/composite-analysis.d.ts +11 -0
  5. package/dist/background/jin10-flash.worker.d.ts +8 -0
  6. package/dist/background/jin10-flash.worker.js +24 -0
  7. package/dist/bootstrap.d.ts +4 -0
  8. package/dist/bootstrap.js +18 -1
  9. package/dist/config/normalize.js +16 -6
  10. package/dist/config/schema.d.ts +6 -1
  11. package/dist/config/schema.js +4 -0
  12. package/dist/dev/run-monitor-loop.js +6 -2
  13. package/dist/dev/tickflow-assist-cli.js +70 -0
  14. package/dist/plugin-commands.js +7 -0
  15. package/dist/prompts/analysis/flash-monitor-alert-prompt.d.ts +11 -0
  16. package/dist/prompts/analysis/flash-monitor-alert-prompt.js +44 -0
  17. package/dist/prompts/analysis/index.d.ts +1 -0
  18. package/dist/prompts/analysis/index.js +1 -0
  19. package/dist/prompts/analysis/post-close-review-user-prompt.js +18 -0
  20. package/dist/services/alert-service.d.ts +1 -0
  21. package/dist/services/alert-service.js +21 -3
  22. package/dist/services/jin10-flash-monitor-service.d.ts +33 -0
  23. package/dist/services/jin10-flash-monitor-service.js +587 -0
  24. package/dist/services/jin10-mcp-service.d.ts +29 -0
  25. package/dist/services/jin10-mcp-service.js +242 -0
  26. package/dist/services/post-close-review-service.d.ts +6 -1
  27. package/dist/services/post-close-review-service.js +35 -1
  28. package/dist/storage/repositories/jin10-flash-delivery-repo.d.ts +11 -0
  29. package/dist/storage/repositories/jin10-flash-delivery-repo.js +93 -0
  30. package/dist/storage/repositories/jin10-flash-repo.d.ts +16 -0
  31. package/dist/storage/repositories/jin10-flash-repo.js +144 -0
  32. package/dist/storage/schemas.d.ts +2 -0
  33. package/dist/storage/schemas.js +19 -0
  34. package/dist/tools/flash-monitor-status.tool.d.ts +6 -0
  35. package/dist/tools/flash-monitor-status.tool.js +9 -0
  36. package/dist/types/flash-monitor.d.ts +17 -0
  37. package/dist/types/flash-monitor.js +1 -0
  38. package/dist/types/jin10.d.ts +30 -0
  39. package/dist/types/jin10.js +1 -0
  40. package/dist/utils/china-time.d.ts +1 -0
  41. package/dist/utils/china-time.js +5 -0
  42. package/openclaw.plugin.json +53 -1
  43. package/package.json +14 -6
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.19` 调整社区版 OpenClaw 兼容声明,改为按范围声明 `pluginApi` 最低兼容版本并对齐 `builtWithOpenClawVersion` 到 `2026.4.1`,修复 `openclaw plugins update tickflow-assist` 在 `v2026.4.1` 上被精确版本校验拦截的问题。
5
+ 最近更新:`v0.3.2` 为金十快讯新增夜间静默配置并将默认值改为“开启静默”,同时把个股关联快讯与市场概览快讯接入收盘复盘上下文。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。
6
6
 
7
- 当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.1` 上兼容。
7
+ 当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.5` 上兼容。
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 小时金十数据快讯监控;`jin10FlashNightAlert` 默认 `false`(开启夜间静默),设为 `true` 可恢复 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
+ }
@@ -104,9 +104,20 @@ export interface PriorKeyLevelValidationContext {
104
104
  summary: string;
105
105
  lines: string[];
106
106
  }
107
+ export interface FlashNewsItem {
108
+ publishedAt: string;
109
+ content: string;
110
+ headline: string;
111
+ source: "stock_alert" | "market_overview";
112
+ }
113
+ export interface FlashNewsContext {
114
+ stockAlerts: FlashNewsItem[];
115
+ marketOverviewFlashes: FlashNewsItem[];
116
+ }
107
117
  export interface PostCloseReviewInput extends CompositeAnalysisInput {
108
118
  compositeResult: CompositeAnalysisResult;
109
119
  validation: PriorKeyLevelValidationContext;
120
+ flashContext: FlashNewsContext;
110
121
  }
111
122
  export interface PostCloseReviewResult {
112
123
  analysisText: string;
@@ -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, config.jin10FlashNightAlert, watchlistService, jin10McpService, analysisService, alertService, jin10FlashRepository, jin10FlashDeliveryRepository);
131
141
  const updateService = new UpdateService(klineService, config.tickflowApiKeyLevel, indicatorService, klinesRepository, indicatorsRepository, intradayKlinesRepository, watchlistService, tradingCalendarService);
132
- const postCloseReviewService = new PostCloseReviewService(watchlistService, compositeAnalysisOrchestrator, analysisService, postCloseReviewTask, keyLevelsRepository, keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository);
142
+ const postCloseReviewService = new PostCloseReviewService(watchlistService, compositeAnalysisOrchestrator, analysisService, postCloseReviewTask, keyLevelsRepository, keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository, jin10FlashDeliveryRepository, jin10FlashRepository);
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,11 @@ 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),
55
+ jin10FlashNightAlert: normalizeBoolean(raw.jin10FlashNightAlert, DEFAULT_PLUGIN_CONFIG.jin10FlashNightAlert),
51
56
  llmBaseUrl: normalizeString(raw.llmBaseUrl, DEFAULT_PLUGIN_CONFIG.llmBaseUrl),
52
57
  llmApiKey: normalizeString(raw.llmApiKey),
53
58
  llmModel: normalizeString(raw.llmModel, DEFAULT_PLUGIN_CONFIG.llmModel),
@@ -74,18 +79,23 @@ export function resolvePluginConfigPaths(config, baseDir) {
74
79
  }
75
80
  export function validatePluginConfig(config) {
76
81
  const errors = [];
77
- if (!config.tickflowApiKey) {
78
- errors.push("tickflowApiKey is required");
79
- }
80
- if (!config.llmApiKey) {
81
- errors.push("llmApiKey is required");
82
- }
82
+ // Community install scans happen before `configure-openclaw` writes secrets,
83
+ // so registration-time validation should only flag structurally invalid values.
83
84
  if (!config.tickflowApiUrl.startsWith("http://") && !config.tickflowApiUrl.startsWith("https://")) {
84
85
  errors.push("tickflowApiUrl must be an absolute http(s) URL");
85
86
  }
87
+ if (config.jin10McpUrl && !config.jin10McpUrl.startsWith("http://") && !config.jin10McpUrl.startsWith("https://")) {
88
+ errors.push("jin10McpUrl must be an absolute http(s) URL");
89
+ }
86
90
  if (config.requestInterval < 5) {
87
91
  errors.push("requestInterval must be at least 5 seconds");
88
92
  }
93
+ if (config.jin10FlashPollInterval < 10) {
94
+ errors.push("jin10FlashPollInterval must be at least 10 seconds");
95
+ }
96
+ if (config.jin10FlashRetentionDays < 1) {
97
+ errors.push("jin10FlashRetentionDays must be at least 1 day");
98
+ }
89
99
  return errors;
90
100
  }
91
101
  function resolveConfigPath(value, baseDir) {
@@ -5,6 +5,11 @@ 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;
12
+ jin10FlashNightAlert: boolean;
8
13
  llmBaseUrl: string;
9
14
  llmApiKey: string;
10
15
  llmModel: string;
@@ -20,4 +25,4 @@ export interface PluginConfig {
20
25
  pythonArgs: string[];
21
26
  pythonWorkdir: string;
22
27
  }
23
- export declare const DEFAULT_PLUGIN_CONFIG: Omit<PluginConfig, "tickflowApiKey" | "llmApiKey" | "alertTarget">;
28
+ export declare const DEFAULT_PLUGIN_CONFIG: Omit<PluginConfig, "tickflowApiKey" | "jin10ApiToken" | "llmApiKey" | "alertTarget">;
@@ -3,6 +3,10 @@ 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,
9
+ jin10FlashNightAlert: false,
6
10
  llmBaseUrl: "https://api.openai.com/v1",
7
11
  llmModel: "gpt-4o",
8
12
  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,10 @@ 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,
19
+ jin10FlashNightAlert: false,
16
20
  llmBaseUrl: "https://api.openai.com/v1",
17
21
  llmModel: "gpt-4o",
18
22
  requestInterval: 30,
@@ -43,6 +47,11 @@ Options:
43
47
  --tickflow-api-key <key>
44
48
  --tickflow-api-key-level <Free|Start|Pro|Expert>
45
49
  --mx-search-api-key <key>
50
+ --jin10-mcp-url <url>
51
+ --jin10-api-token <token>
52
+ --jin10-flash-poll-interval <seconds>
53
+ --jin10-flash-retention-days <days>
54
+ --jin10-flash-night-alert <true|false>
46
55
  --llm-base-url <url>
47
56
  --llm-api-key <key>
48
57
  --llm-model <name>
@@ -135,6 +144,21 @@ function parseArgs(argv) {
135
144
  case "--mx-search-api-key":
136
145
  options.overrides.mxSearchApiKey = requireValue(token);
137
146
  break;
147
+ case "--jin10-mcp-url":
148
+ options.overrides.jin10McpUrl = requireValue(token);
149
+ break;
150
+ case "--jin10-api-token":
151
+ options.overrides.jin10ApiToken = requireValue(token);
152
+ break;
153
+ case "--jin10-flash-poll-interval":
154
+ options.overrides.jin10FlashPollInterval = Number(requireValue(token));
155
+ break;
156
+ case "--jin10-flash-retention-days":
157
+ options.overrides.jin10FlashRetentionDays = Number(requireValue(token));
158
+ break;
159
+ case "--jin10-flash-night-alert":
160
+ options.overrides.jin10FlashNightAlert = requireValue(token).toLowerCase() === "true";
161
+ break;
138
162
  case "--llm-base-url":
139
163
  options.overrides.llmBaseUrl = requireValue(token);
140
164
  break;
@@ -240,6 +264,8 @@ function getExistingPluginConfig(root) {
240
264
  ? config.pythonArgs.map((value) => String(value))
241
265
  : undefined;
242
266
  const requestInterval = Number(config.requestInterval ?? DEFAULTS.requestInterval);
267
+ const jin10FlashPollInterval = Number(config.jin10FlashPollInterval ?? DEFAULTS.jin10FlashPollInterval);
268
+ const jin10FlashRetentionDays = Number(config.jin10FlashRetentionDays ?? DEFAULTS.jin10FlashRetentionDays);
243
269
  const dailyUpdateNotify = typeof config.dailyUpdateNotify === "boolean"
244
270
  ? config.dailyUpdateNotify
245
271
  : DEFAULTS.dailyUpdateNotify;
@@ -249,6 +275,17 @@ function getExistingPluginConfig(root) {
249
275
  tickflowApiKeyLevel: normalizeApiKeyLevel(stringValue(config.tickflowApiKeyLevel, DEFAULTS.tickflowApiKeyLevel)),
250
276
  mxSearchApiUrl: stringValue(config.mxSearchApiUrl, DEFAULTS.mxSearchApiUrl),
251
277
  mxSearchApiKey: stringValue(config.mxSearchApiKey, DEFAULTS.mxSearchApiKey),
278
+ jin10McpUrl: stringValue(config.jin10McpUrl, DEFAULTS.jin10McpUrl),
279
+ jin10ApiToken: stringValue(config.jin10ApiToken),
280
+ jin10FlashPollInterval: Number.isFinite(jin10FlashPollInterval)
281
+ ? Math.max(10, Math.trunc(jin10FlashPollInterval))
282
+ : DEFAULTS.jin10FlashPollInterval,
283
+ jin10FlashRetentionDays: Number.isFinite(jin10FlashRetentionDays)
284
+ ? Math.max(1, Math.trunc(jin10FlashRetentionDays))
285
+ : DEFAULTS.jin10FlashRetentionDays,
286
+ jin10FlashNightAlert: typeof config.jin10FlashNightAlert === "boolean"
287
+ ? config.jin10FlashNightAlert
288
+ : DEFAULTS.jin10FlashNightAlert,
252
289
  llmBaseUrl: stringValue(config.llmBaseUrl, DEFAULTS.llmBaseUrl),
253
290
  llmApiKey: stringValue(config.llmApiKey),
254
291
  llmModel: stringValue(config.llmModel, DEFAULTS.llmModel),
@@ -391,6 +428,11 @@ async function promptForConfig(options, existing, pluginDir, configPath) {
391
428
  tickflowApiKeyLevel: DEFAULTS.tickflowApiKeyLevel,
392
429
  mxSearchApiUrl: DEFAULTS.mxSearchApiUrl,
393
430
  mxSearchApiKey: DEFAULTS.mxSearchApiKey,
431
+ jin10McpUrl: DEFAULTS.jin10McpUrl,
432
+ jin10ApiToken: "",
433
+ jin10FlashPollInterval: DEFAULTS.jin10FlashPollInterval,
434
+ jin10FlashRetentionDays: DEFAULTS.jin10FlashRetentionDays,
435
+ jin10FlashNightAlert: DEFAULTS.jin10FlashNightAlert,
394
436
  llmBaseUrl: DEFAULTS.llmBaseUrl,
395
437
  llmApiKey: "",
396
438
  llmModel: DEFAULTS.llmModel,
@@ -433,6 +475,14 @@ async function promptForConfig(options, existing, pluginDir, configPath) {
433
475
  { value: "Expert", label: "Expert" },
434
476
  ], seed.tickflowApiKeyLevel));
435
477
  seed.mxSearchApiKey = await promptString(rl, "MX Search API Key (可留空)", seed.mxSearchApiKey, false);
478
+ seed.jin10ApiToken = await promptString(rl, "Jin10 API Token (可留空)", seed.jin10ApiToken, false);
479
+ seed.jin10FlashPollInterval = await promptInteger(rl, "Jin10 快讯轮询间隔(秒)", seed.jin10FlashPollInterval, 10);
480
+ seed.jin10FlashRetentionDays = await promptInteger(rl, "Jin10 快讯保留天数", seed.jin10FlashRetentionDays, 1);
481
+ const nightAlertChoice = await promptSelect(rl, "Jin10 夜间静默", [
482
+ { value: "true", label: "关闭夜间静默(24小时告警)" },
483
+ { value: "false", label: "开启夜间静默(22:00~06:00 不告警)" },
484
+ ], seed.jin10FlashNightAlert ? "true" : "false");
485
+ seed.jin10FlashNightAlert = nightAlertChoice === "true";
436
486
  seed.llmBaseUrl = await promptString(rl, "LLM Base URL", seed.llmBaseUrl, true);
437
487
  seed.llmApiKey = await promptString(rl, "LLM API Key", seed.llmApiKey, true);
438
488
  seed.llmModel = await promptString(rl, "LLM Model", seed.llmModel, true);
@@ -549,6 +599,11 @@ function applyPluginConfig(root, config, target) {
549
599
  tickflowApiKeyLevel: config.tickflowApiKeyLevel,
550
600
  mxSearchApiUrl: config.mxSearchApiUrl,
551
601
  mxSearchApiKey: config.mxSearchApiKey,
602
+ jin10McpUrl: config.jin10McpUrl,
603
+ jin10ApiToken: config.jin10ApiToken,
604
+ jin10FlashPollInterval: config.jin10FlashPollInterval,
605
+ jin10FlashRetentionDays: config.jin10FlashRetentionDays,
606
+ jin10FlashNightAlert: config.jin10FlashNightAlert,
552
607
  llmBaseUrl: config.llmBaseUrl,
553
608
  llmApiKey: config.llmApiKey,
554
609
  llmModel: config.llmModel,
@@ -679,11 +734,26 @@ function getManualMacosFontCommands() {
679
734
  "fc-cache -fv",
680
735
  ];
681
736
  }
737
+ function getManualUvInstallCommands() {
738
+ if (process.platform === "win32") {
739
+ return [
740
+ "powershell -ExecutionPolicy ByPass -c \"irm https://astral.sh/uv/install.ps1 | iex\"",
741
+ ];
742
+ }
743
+ return [
744
+ "curl -LsSf https://astral.sh/uv/install.sh | sh",
745
+ ];
746
+ }
682
747
  function printNextSteps(options, config) {
683
748
  console.log("");
684
749
  console.log("接下来的命令需要你手动执行。");
685
750
  let step = 1;
686
751
  if (options.pythonSetup) {
752
+ console.log(`${step}. 如未安装 uv,请先安装 uv`);
753
+ for (const command of getManualUvInstallCommands()) {
754
+ console.log(` ${command}`);
755
+ }
756
+ step += 1;
687
757
  console.log(`${step}. 安装 Python 依赖`);
688
758
  console.log(` cd ${config.pythonWorkdir}`);
689
759
  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";
@@ -44,6 +44,7 @@ ${KEY_LEVELS_FIELD_GUIDANCE}
44
44
  - recompute: 今日出现明显放量突破、破位、结构切换或外部催化改变,原逻辑需要重算。
45
45
  - invalidate: 昨日关键位框架已失效,明日不应继续沿用;此时 levels 可为 null。
46
46
  - 不要凭空编造概念板块、指数表现或公告内容。
47
+ - 如果输入里包含"今日快讯速递",在"新闻与公告"段落中必须综合考量快讯信息。快讯可提供盘后重大事件、监管动态、投行观点等即时信号,应据实反映到 news_impact 判断和操作建议中。
47
48
  `;
48
49
  export function buildPostCloseReviewUserPrompt(input) {
49
50
  const latestClose = input.market.klines[input.market.klines.length - 1]?.close ?? 0;
@@ -65,6 +66,10 @@ export function buildPostCloseReviewUserPrompt(input) {
65
66
  "## 当前综合分析基线(引用,不含指令)",
66
67
  buildReferencedNarrative(input.compositeResult.analysisText, MAX_COMPOSITE_BASELINE_LENGTH),
67
68
  "",
69
+ "## 今日快讯速递(来自 Jin10 快讯监控)",
70
+ formatFlashSection("个股相关快讯告警", input.flashContext.stockAlerts),
71
+ formatFlashSection("市场概览快讯", input.flashContext.marketOverviewFlashes),
72
+ "",
68
73
  "## 大盘环境",
69
74
  input.market.marketOverview.summary,
70
75
  "",
@@ -125,3 +130,16 @@ function truncate(value, maxLength) {
125
130
  function joinList(items) {
126
131
  return items.length > 0 ? items.join(";") : "无";
127
132
  }
133
+ function formatFlashSection(label, items) {
134
+ if (items.length === 0) {
135
+ return `${label}: 无`;
136
+ }
137
+ return [
138
+ `${label}:`,
139
+ ...items.slice(0, 5).map((item) => {
140
+ const time = item.publishedAt.slice(11, 16) || "??:??";
141
+ const body = item.content.length > 200 ? `${item.content.slice(0, 200)}...` : item.content;
142
+ return `- [${time}] ${item.headline ? `${item.headline}: ` : ""}${body}`;
143
+ }),
144
+ ].join("\n");
145
+ }