tickflow-assist 0.3.3 → 0.3.5

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.
@@ -0,0 +1,289 @@
1
+ import { createHash } from "node:crypto";
2
+ import { PRE_MARKET_BRIEF_SYSTEM_PROMPT, buildPreMarketBriefUserPrompt, } from "../prompts/analysis/index.js";
3
+ import { formatChinaDateTime } from "../utils/china-time.js";
4
+ const PRE_MARKET_BRIEF_KEYWORD = "金十数据整理";
5
+ const PRE_MARKET_READY_TIME = "09:20:00";
6
+ const PRE_MARKET_SYNC_MAX_PAGES = 12;
7
+ const OPPORTUNITY_KEYWORDS = [
8
+ "政策",
9
+ "订单",
10
+ "中标",
11
+ "业绩",
12
+ "回购",
13
+ "增持",
14
+ "涨价",
15
+ "算力",
16
+ "并购",
17
+ "并购重组",
18
+ "AI",
19
+ "人工智能",
20
+ "机器人",
21
+ ];
22
+ const RISK_KEYWORDS = [
23
+ "减持",
24
+ "监管",
25
+ "问询",
26
+ "处罚",
27
+ "停牌",
28
+ "复牌",
29
+ "下调",
30
+ "风险",
31
+ "不确定",
32
+ "制裁",
33
+ "关税",
34
+ ];
35
+ export class PreMarketBriefService {
36
+ watchlistService;
37
+ jin10McpService;
38
+ flashRepository;
39
+ analysisService;
40
+ constructor(watchlistService, jin10McpService, flashRepository, analysisService) {
41
+ this.watchlistService = watchlistService;
42
+ this.jin10McpService = jin10McpService;
43
+ this.flashRepository = flashRepository;
44
+ this.analysisService = analysisService;
45
+ }
46
+ async run(now = new Date()) {
47
+ const watchlist = await this.watchlistService.list();
48
+ if (watchlist.length === 0) {
49
+ return {
50
+ resultType: "skipped",
51
+ message: "🚫 开盘前资讯简报已跳过:关注列表为空。",
52
+ sourceCount: 0,
53
+ matchedWatchlistCount: 0,
54
+ };
55
+ }
56
+ const configError = this.jin10McpService.getConfigurationError();
57
+ if (configError) {
58
+ return {
59
+ resultType: "skipped",
60
+ message: `🚫 开盘前资讯简报已跳过:${configError}`,
61
+ sourceCount: 0,
62
+ matchedWatchlistCount: 0,
63
+ };
64
+ }
65
+ const window = buildPreMarketWindow(now);
66
+ await this.syncWindow(window);
67
+ const flashes = (await this.flashRepository.listByPublishedRange(window.startTs, window.endTs))
68
+ .filter((record) => matchesPreMarketBrief(record));
69
+ if (flashes.length === 0) {
70
+ return {
71
+ resultType: "success",
72
+ message: [
73
+ `**🌅 开盘前资讯简报|${window.endAt.slice(0, 10)}**`,
74
+ `信息窗口: ${window.startAt} ~ ${window.endAt}`,
75
+ `整理快讯: 0 条 | 自选: ${watchlist.length} 只`,
76
+ "",
77
+ `本窗口未检索到标题含“${PRE_MARKET_BRIEF_KEYWORD}”的快讯,今日无新增盘前整理摘要。`,
78
+ ].join("\n"),
79
+ sourceCount: 0,
80
+ matchedWatchlistCount: 0,
81
+ };
82
+ }
83
+ const matchContexts = flashes.map((flash) => ({
84
+ flash,
85
+ matchedItems: findMatchedItems(flash, watchlist),
86
+ }));
87
+ const matchedWatchlistCount = new Set(matchContexts.flatMap((context) => context.matchedItems.map((item) => item.symbol))).size;
88
+ const summary = await this.buildSummary(window, watchlist, matchContexts);
89
+ return {
90
+ resultType: "success",
91
+ message: [
92
+ `**🌅 开盘前资讯简报|${window.endAt.slice(0, 10)}**`,
93
+ `信息窗口: ${window.startAt} ~ ${window.endAt}`,
94
+ `整理快讯: ${flashes.length} 条 | 自选: ${watchlist.length} 只 | 规则命中: ${matchedWatchlistCount} 只`,
95
+ "",
96
+ summary.trim(),
97
+ ].join("\n"),
98
+ sourceCount: flashes.length,
99
+ matchedWatchlistCount,
100
+ };
101
+ }
102
+ async syncWindow(window) {
103
+ let cursor;
104
+ const collected = [];
105
+ for (let pageIndex = 0; pageIndex < PRE_MARKET_SYNC_MAX_PAGES; pageIndex += 1) {
106
+ const page = await this.jin10McpService.listFlash(cursor);
107
+ const records = page.items
108
+ .map((item) => toFlashRecord(item))
109
+ .filter((item) => item != null);
110
+ if (records.length === 0) {
111
+ break;
112
+ }
113
+ collected.push(...records);
114
+ const oldestPublishedTs = records[records.length - 1]?.published_ts ?? Number.MAX_SAFE_INTEGER;
115
+ if (oldestPublishedTs < window.startTs || !page.hasMore || !page.nextCursor) {
116
+ break;
117
+ }
118
+ cursor = page.nextCursor;
119
+ }
120
+ if (collected.length > 0) {
121
+ await this.flashRepository.saveAll(collected);
122
+ }
123
+ }
124
+ async buildSummary(window, watchlist, matchContexts) {
125
+ const promptInput = {
126
+ windowStartAt: window.startAt,
127
+ windowEndAt: window.endAt,
128
+ watchlist,
129
+ flashes: matchContexts.map((context) => ({
130
+ publishedAt: context.flash.published_at,
131
+ headline: extractHeadlineFromContent(context.flash.content),
132
+ content: context.flash.content,
133
+ url: context.flash.url,
134
+ matchedSymbols: context.matchedItems.map((item) => item.symbol),
135
+ })),
136
+ };
137
+ if (this.analysisService.isConfigured()) {
138
+ try {
139
+ return await this.analysisService.generateText(PRE_MARKET_BRIEF_SYSTEM_PROMPT, buildPreMarketBriefUserPrompt(promptInput), {
140
+ maxTokens: 1600,
141
+ temperature: 0.2,
142
+ });
143
+ }
144
+ catch {
145
+ // Fall through to deterministic fallback so the scheduled push still lands.
146
+ }
147
+ }
148
+ return buildFallbackSummary(matchContexts);
149
+ }
150
+ }
151
+ function buildPreMarketWindow(now) {
152
+ const chinaToday = formatChinaDate(now);
153
+ const previousDay = formatChinaDate(new Date(toChinaTimestamp(`${chinaToday} ${PRE_MARKET_READY_TIME}`) - 24 * 60 * 60 * 1000));
154
+ const startAt = `${previousDay} 17:00:00`;
155
+ const endAt = `${chinaToday} ${PRE_MARKET_READY_TIME}`;
156
+ return {
157
+ startAt,
158
+ endAt,
159
+ startTs: toChinaTimestamp(startAt),
160
+ endTs: toChinaTimestamp(endAt),
161
+ };
162
+ }
163
+ function toChinaTimestamp(value) {
164
+ return new Date(`${value.replace(" ", "T")}+08:00`).getTime();
165
+ }
166
+ function formatChinaDate(date) {
167
+ const formatter = new Intl.DateTimeFormat("zh-CN", {
168
+ timeZone: "Asia/Shanghai",
169
+ year: "numeric",
170
+ month: "2-digit",
171
+ day: "2-digit",
172
+ });
173
+ const map = Object.fromEntries(formatter
174
+ .formatToParts(date)
175
+ .filter((part) => part.type !== "literal")
176
+ .map((part) => [part.type, part.value]));
177
+ return `${map.year}-${map.month}-${map.day}`;
178
+ }
179
+ function matchesPreMarketBrief(record) {
180
+ return extractHeadlineText(record.content).includes(PRE_MARKET_BRIEF_KEYWORD);
181
+ }
182
+ function findMatchedItems(flash, watchlist) {
183
+ const normalizedContent = normalizeText(flash.content);
184
+ return watchlist.filter((item) => {
185
+ const directKeywords = [item.symbol, item.symbol.slice(0, 6), item.name];
186
+ const boardKeywords = [item.sector ?? "", ...item.themes]
187
+ .map((keyword) => keyword.replace(/\s+/g, "").trim())
188
+ .filter((keyword) => keyword.length >= 2);
189
+ return [...directKeywords, ...boardKeywords]
190
+ .map((keyword) => normalizeText(keyword))
191
+ .some((keyword) => keyword && normalizedContent.includes(keyword));
192
+ });
193
+ }
194
+ function buildFallbackSummary(matchContexts) {
195
+ const opportunityContexts = matchContexts.filter((context) => containsAnyKeyword(context.flash.content, OPPORTUNITY_KEYWORDS));
196
+ const riskContexts = matchContexts.filter((context) => containsAnyKeyword(context.flash.content, RISK_KEYWORDS));
197
+ return [
198
+ formatSectionTitle("🧭", "重大要闻"),
199
+ formatFlashBullets(matchContexts, 5),
200
+ "",
201
+ formatSectionTitle("🎯", "自选相关"),
202
+ formatMatchedBullets(matchContexts, 5),
203
+ "",
204
+ formatSectionTitle("💡", "潜在机会"),
205
+ opportunityContexts.length > 0
206
+ ? formatFlashBullets(opportunityContexts, 4)
207
+ : "• 未发现基于当前整理快讯可直接确认的新增机会方向。",
208
+ "",
209
+ formatSectionTitle("⚠️", "风险提示"),
210
+ riskContexts.length > 0
211
+ ? formatFlashBullets(riskContexts, 4)
212
+ : "• 当前整理快讯中未发现特别突出的新增风险,但仍需留意开盘后的情绪变化。",
213
+ "",
214
+ formatSectionTitle("📌", "开盘前关注清单"),
215
+ buildFocusBullets(matchContexts),
216
+ ].join("\n");
217
+ }
218
+ function formatFlashBullets(contexts, limit) {
219
+ return contexts
220
+ .slice(0, limit)
221
+ .map((context) => {
222
+ const time = context.flash.published_at.slice(11, 16);
223
+ return `• [${time}] ${extractHeadlineFromContent(context.flash.content)}`;
224
+ })
225
+ .join("\n");
226
+ }
227
+ function formatMatchedBullets(contexts, limit) {
228
+ const matched = contexts.filter((context) => context.matchedItems.length > 0).slice(0, limit);
229
+ if (matched.length === 0) {
230
+ return "• 未发现直接命中自选股、行业或题材的盘前整理快讯。";
231
+ }
232
+ return matched.map((context) => {
233
+ const labels = context.matchedItems.map((item) => `${item.name}(${item.symbol})`).join("、");
234
+ return `• ${labels}: ${extractHeadlineFromContent(context.flash.content)}`;
235
+ }).join("\n");
236
+ }
237
+ function buildFocusBullets(contexts) {
238
+ const bullets = [];
239
+ const matchedContexts = contexts.filter((context) => context.matchedItems.length > 0);
240
+ for (const context of matchedContexts.slice(0, 3)) {
241
+ const labels = context.matchedItems.map((item) => item.name).join("、");
242
+ bullets.push(`• 关注 ${labels} 开盘后的量价反馈,核实“${extractHeadlineFromContent(context.flash.content)}”是否继续发酵。`);
243
+ }
244
+ if (bullets.length < 3) {
245
+ for (const context of contexts.slice(0, 3 - bullets.length)) {
246
+ bullets.push(`• 关注“${extractHeadlineFromContent(context.flash.content)}”对应板块是否出现竞价强化或高开分歧。`);
247
+ }
248
+ }
249
+ return bullets.slice(0, 5).join("\n");
250
+ }
251
+ function containsAnyKeyword(content, keywords) {
252
+ return keywords.some((keyword) => content.includes(keyword));
253
+ }
254
+ function normalizeText(value) {
255
+ return value.toLowerCase().replace(/\s+/g, "");
256
+ }
257
+ function formatSectionTitle(icon, title) {
258
+ return `**【${icon} ${title}】**`;
259
+ }
260
+ function extractHeadlineFromContent(content) {
261
+ const firstLine = extractHeadlineText(content);
262
+ return firstLine.length > 72 ? `${firstLine.slice(0, 72)}...` : firstLine;
263
+ }
264
+ function extractHeadlineText(content) {
265
+ return content.split(/[\n。!!]/)[0]?.trim() ?? "";
266
+ }
267
+ function toFlashRecord(item) {
268
+ const published = new Date(item.time);
269
+ if (Number.isNaN(published.getTime())) {
270
+ return null;
271
+ }
272
+ return {
273
+ flash_key: buildFlashKey(item.url, item.time, item.content),
274
+ published_at: formatChinaDateTime(published),
275
+ published_ts: published.getTime(),
276
+ content: item.content.trim(),
277
+ url: item.url.trim(),
278
+ ingested_at: formatChinaDateTime(),
279
+ raw: item.raw,
280
+ };
281
+ }
282
+ function buildFlashKey(url, time, content) {
283
+ if (url.trim()) {
284
+ return url.trim();
285
+ }
286
+ return createHash("sha1")
287
+ .update(`${time}\n${content}`)
288
+ .digest("hex");
289
+ }
@@ -13,6 +13,10 @@ export declare class TradingCalendarService {
13
13
  ok: boolean;
14
14
  reason: string;
15
15
  }>;
16
+ canRunPreMarketBrief(date?: Date): Promise<{
17
+ ok: boolean;
18
+ reason: string;
19
+ }>;
16
20
  getRecentTradingDays(limit: number, endDate?: Date): Promise<string[]>;
17
21
  private loadDays;
18
22
  private toChinaDate;
@@ -51,6 +51,17 @@ export class TradingCalendarService {
51
51
  }
52
52
  return { ok: true, reason: "交易日已完成收盘且达到复盘时间" };
53
53
  }
54
+ async canRunPreMarketBrief(date = new Date()) {
55
+ if (!(await this.isTradingDay(date))) {
56
+ return { ok: false, reason: `${this.toChinaDate(date)} 非交易日` };
57
+ }
58
+ const china = this.toChinaParts(date);
59
+ const hhmm = `${china.hour}:${china.minute}`;
60
+ if (hhmm < "09:20") {
61
+ return { ok: false, reason: `当前 ${hhmm},须等到 09:20 后执行盘前资讯简报` };
62
+ }
63
+ return { ok: true, reason: "交易日且达到盘前资讯简报时间" };
64
+ }
54
65
  async getRecentTradingDays(limit, endDate = new Date()) {
55
66
  if (!Number.isInteger(limit) || limit <= 0) {
56
67
  throw new Error(`limit must be a positive integer, got ${limit}`);
@@ -10,6 +10,7 @@ export declare class Jin10FlashRepository {
10
10
  }>;
11
11
  getLatest(): Promise<Jin10FlashRecord | null>;
12
12
  countSincePublishedTs(publishedTs: number): Promise<number>;
13
+ listByPublishedRange(startPublishedTs: number, endPublishedTs: number): Promise<Jin10FlashRecord[]>;
13
14
  searchByContentKeywords(keywords: string[], datePrefix: string): Promise<Jin10FlashRecord[]>;
14
15
  pruneOlderThanPublishedTs(publishedTs: number): Promise<void>;
15
16
  private listExistingKeys;
@@ -45,7 +45,16 @@ export class Jin10FlashRepository {
45
45
  if (rows.length === 0) {
46
46
  return null;
47
47
  }
48
- return fromFlashRow(rows[rows.length - 1]);
48
+ let latestRow = rows[0];
49
+ let latestTs = Number(rows[0]?.published_ts ?? 0);
50
+ for (const row of rows.slice(1)) {
51
+ const publishedTs = Number(row.published_ts ?? 0);
52
+ if (publishedTs >= latestTs) {
53
+ latestRow = row;
54
+ latestTs = publishedTs;
55
+ }
56
+ }
57
+ return latestRow ? fromFlashRow(latestRow) : null;
49
58
  }
50
59
  async countSincePublishedTs(publishedTs) {
51
60
  if (!(await this.db.hasTable(JIN10_FLASH_TABLE))) {
@@ -54,6 +63,19 @@ export class Jin10FlashRepository {
54
63
  const table = await this.db.openTable(JIN10_FLASH_TABLE);
55
64
  return table.countRows(`published_ts >= ${Math.trunc(publishedTs)}`);
56
65
  }
66
+ async listByPublishedRange(startPublishedTs, endPublishedTs) {
67
+ if (!(await this.db.hasTable(JIN10_FLASH_TABLE))) {
68
+ return [];
69
+ }
70
+ const table = await this.db.openTable(JIN10_FLASH_TABLE);
71
+ const rows = (await table
72
+ .query()
73
+ .where(`published_ts >= ${Math.trunc(startPublishedTs)} AND published_ts <= ${Math.trunc(endPublishedTs)}`)
74
+ .toArray());
75
+ return rows
76
+ .map((row) => fromFlashRow(row))
77
+ .sort((left, right) => left.published_ts - right.published_ts);
78
+ }
57
79
  async searchByContentKeywords(keywords, datePrefix) {
58
80
  if (keywords.length === 0 || !(await this.db.hasTable(JIN10_FLASH_TABLE))) {
59
81
  return [];
@@ -23,4 +23,11 @@ export interface DailyUpdateState {
23
23
  lastReviewResultType: DailyUpdateResultType | null;
24
24
  lastReviewResultSummary: string | null;
25
25
  reviewConsecutiveFailures: number;
26
+ lastPreMarketAttemptAt: string | null;
27
+ lastPreMarketAttemptDate: string | null;
28
+ lastPreMarketSuccessAt: string | null;
29
+ lastPreMarketSuccessDate: string | null;
30
+ lastPreMarketResultType: DailyUpdateResultType | null;
31
+ lastPreMarketResultSummary: string | null;
32
+ preMarketConsecutiveFailures: number;
26
33
  }
@@ -1,11 +1,39 @@
1
1
  {
2
2
  "id": "tickflow-assist",
3
3
  "name": "TickFlow Assist",
4
- "version": "0.3.3",
4
+ "version": "0.3.5",
5
5
  "description": "A-share watchlist analysis, monitoring, and alert delivery powered by TickFlow and OpenClaw.",
6
6
  "skills": [
7
7
  "skills"
8
8
  ],
9
+ "activation": {
10
+ "onCapabilities": ["tool", "hook"]
11
+ },
12
+ "setup": {
13
+ "requiresRuntime": true
14
+ },
15
+ "configContracts": {
16
+ "secretInputs": {
17
+ "paths": [
18
+ {
19
+ "path": "tickflowApiKey",
20
+ "expected": "string"
21
+ },
22
+ {
23
+ "path": "llmApiKey",
24
+ "expected": "string"
25
+ },
26
+ {
27
+ "path": "mxSearchApiKey",
28
+ "expected": "string"
29
+ },
30
+ {
31
+ "path": "jin10ApiToken",
32
+ "expected": "string"
33
+ }
34
+ ]
35
+ }
36
+ },
9
37
  "configSchema": {
10
38
  "type": "object",
11
39
  "additionalProperties": false,
@@ -13,38 +41,38 @@
13
41
  "tickflowApiUrl": {
14
42
  "type": "string",
15
43
  "default": "https://api.tickflow.org",
16
- "description": "TickFlow API base URL."
44
+ "description": "TickFlow API base URL. Leave empty to use TICKFLOW_ASSIST_TICKFLOW_API_URL or TICKFLOW_API_URL."
17
45
  },
18
46
  "tickflowApiKey": {
19
47
  "type": "string",
20
48
  "minLength": 1,
21
- "description": "TickFlow API key used for market and financial data. Required before using the plugin."
49
+ "description": "TickFlow API key used for market and financial data. Leave empty to use TICKFLOW_ASSIST_TICKFLOW_API_KEY or TICKFLOW_API_KEY."
22
50
  },
23
51
  "tickflowApiKeyLevel": {
24
52
  "type": "string",
25
53
  "enum": ["Free", "Start", "Pro", "Expert"],
26
54
  "default": "Free",
27
- "description": "TickFlow subscription level. Pro and Expert enable intraday K-line fetching."
55
+ "description": "TickFlow subscription level. Pro and Expert enable intraday K-line fetching. Can also come from TICKFLOW_ASSIST_TICKFLOW_API_KEY_LEVEL or TICKFLOW_API_KEY_LEVEL."
28
56
  },
29
57
  "mxSearchApiUrl": {
30
58
  "type": "string",
31
59
  "default": "https://mkapi2.dfcfs.com/finskillshub/api/claw",
32
- "description": "MX Search API base URL."
60
+ "description": "MX Search API base URL. Leave empty to use TICKFLOW_ASSIST_MX_SEARCH_API_URL or MX_SEARCH_API_URL."
33
61
  },
34
62
  "mxSearchApiKey": {
35
63
  "type": "string",
36
64
  "default": "",
37
- "description": "Optional. Enables mx_search, mx_select_stock, and non-Expert financial fallback."
65
+ "description": "Configure this during setup if you want mx_search, mx_select_stock, and non-Expert financial fallback. Leave empty to use TICKFLOW_ASSIST_MX_SEARCH_API_KEY, MX_SEARCH_API_KEY, or MX_APIKEY."
38
66
  },
39
67
  "jin10McpUrl": {
40
68
  "type": "string",
41
69
  "default": "https://mcp.jin10.com/mcp",
42
- "description": "Jin10 MCP server URL used for realtime flash monitoring."
70
+ "description": "Jin10 MCP server URL used for realtime flash monitoring. Leave empty to use TICKFLOW_ASSIST_JIN10_MCP_URL or JIN10_MCP_URL."
43
71
  },
44
72
  "jin10ApiToken": {
45
73
  "type": "string",
46
74
  "default": "",
47
- "description": "Optional. Enables 24/7 Jin10 realtime flash monitoring when configured."
75
+ "description": "Configure this during setup to keep the managed Jin10 realtime flash monitor fully available. Leave empty to use TICKFLOW_ASSIST_JIN10_API_TOKEN or JIN10_API_TOKEN."
48
76
  },
49
77
  "jin10FlashPollInterval": {
50
78
  "type": "integer",
@@ -66,17 +94,17 @@
66
94
  "llmBaseUrl": {
67
95
  "type": "string",
68
96
  "default": "https://api.openai.com/v1",
69
- "description": "OpenAI-compatible LLM API base URL."
97
+ "description": "OpenAI-compatible LLM API base URL. Leave empty to use TICKFLOW_ASSIST_LLM_BASE_URL or LLM_BASE_URL."
70
98
  },
71
99
  "llmApiKey": {
72
100
  "type": "string",
73
101
  "minLength": 1,
74
- "description": "API key for the analysis model endpoint configured in llmBaseUrl. Required before using analysis features."
102
+ "description": "API key for the analysis model endpoint configured in llmBaseUrl. Leave empty to use TICKFLOW_ASSIST_LLM_API_KEY or LLM_API_KEY."
75
103
  },
76
104
  "llmModel": {
77
105
  "type": "string",
78
106
  "default": "gpt-4o",
79
- "description": "Model name used for analysis."
107
+ "description": "Model name used for analysis. Leave empty to use TICKFLOW_ASSIST_LLM_MODEL or LLM_MODEL."
80
108
  },
81
109
  "databasePath": {
82
110
  "type": "string",
@@ -101,7 +129,7 @@
101
129
  },
102
130
  "alertChannel": {
103
131
  "type": "string",
104
- "description": "Alert delivery channel, e.g. telegram, qqbot, wecom",
132
+ "description": "Configure this during setup for alert delivery, e.g. telegram, qqbot, wecom.",
105
133
  "default": "telegram"
106
134
  },
107
135
  "openclawCliBin": {
@@ -114,7 +142,7 @@
114
142
  },
115
143
  "alertTarget": {
116
144
  "type": "string",
117
- "description": "Optional. Channel target for test_alert, monitoring alerts, and scheduled notifications.",
145
+ "description": "Configure this during setup for test_alert, monitoring alerts, and scheduled notifications.",
118
146
  "default": ""
119
147
  },
120
148
  "pythonBin": {
@@ -140,26 +168,26 @@
140
168
  "uiHints": {
141
169
  "tickflowApiKey": {
142
170
  "label": "TickFlow API Key",
143
- "help": "Required before using TickFlow market and financial data.",
171
+ "help": "Used for TickFlow market and financial data. Leave empty to use TICKFLOW_ASSIST_TICKFLOW_API_KEY or TICKFLOW_API_KEY.",
144
172
  "sensitive": true
145
173
  },
146
174
  "tickflowApiKeyLevel": {
147
175
  "label": "TickFlow API Key Level",
148
- "help": "Set this to your actual TickFlow subscription level."
176
+ "help": "Set this to your actual TickFlow subscription level, or provide it via TICKFLOW_ASSIST_TICKFLOW_API_KEY_LEVEL / TICKFLOW_API_KEY_LEVEL."
149
177
  },
150
178
  "mxSearchApiKey": {
151
179
  "label": "MX Search API Key",
152
- "help": "Optional. Enables mx_search, mx_select_stock, and lite financial fallback.",
180
+ "help": "Configure this during setup if you want mx_search, mx_select_stock, and lite financial fallback, or use TICKFLOW_ASSIST_MX_SEARCH_API_KEY / MX_SEARCH_API_KEY / MX_APIKEY.",
153
181
  "sensitive": true
154
182
  },
155
183
  "jin10McpUrl": {
156
184
  "label": "Jin10 MCP URL",
157
- "help": "Optional. MCP endpoint used by the 24/7 Jin10 flash monitor.",
185
+ "help": "Configure this together with the Jin10 token for the managed flash monitor, or use TICKFLOW_ASSIST_JIN10_MCP_URL / JIN10_MCP_URL.",
158
186
  "advanced": true
159
187
  },
160
188
  "jin10ApiToken": {
161
189
  "label": "Jin10 API Token",
162
- "help": "Optional. Enables Jin10 realtime flash monitoring.",
190
+ "help": "Configure this during setup to keep the managed Jin10 flash monitor fully available, or use TICKFLOW_ASSIST_JIN10_API_TOKEN / JIN10_API_TOKEN.",
163
191
  "sensitive": true
164
192
  },
165
193
  "jin10FlashPollInterval": {
@@ -179,24 +207,24 @@
179
207
  },
180
208
  "llmBaseUrl": {
181
209
  "label": "LLM Base URL",
182
- "help": "OpenAI-compatible analysis endpoint."
210
+ "help": "OpenAI-compatible analysis endpoint. Leave empty to use TICKFLOW_ASSIST_LLM_BASE_URL or LLM_BASE_URL."
183
211
  },
184
212
  "llmApiKey": {
185
213
  "label": "LLM API Key",
186
- "help": "Required before using analysis model requests.",
214
+ "help": "Used for analysis model requests. Leave empty to use TICKFLOW_ASSIST_LLM_API_KEY or LLM_API_KEY.",
187
215
  "sensitive": true
188
216
  },
189
217
  "llmModel": {
190
218
  "label": "LLM Model",
191
- "help": "Model name used for stock analysis."
219
+ "help": "Model name used for stock analysis. Leave empty to use TICKFLOW_ASSIST_LLM_MODEL or LLM_MODEL."
192
220
  },
193
221
  "alertChannel": {
194
222
  "label": "Alert Channel",
195
- "help": "Default delivery channel for alert messages."
223
+ "help": "Configure this during setup before enabling alert delivery."
196
224
  },
197
225
  "alertTarget": {
198
226
  "label": "Alert Target",
199
- "help": "Optional. Configure this if you want test_alert, monitoring alerts, or scheduled notifications."
227
+ "help": "Configure this during setup for test_alert, monitoring alerts, and scheduled notifications."
200
228
  },
201
229
  "databasePath": {
202
230
  "label": "Database Path",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickflow-assist",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "面向 A 股投资与盯盘场景的 OpenClaw 智能股票插件,基于 TickFlow API 提供实时监控、收盘后复盘、多维综合分析、关键价位跟踪与告警能力。OpenClaw smart stock plugin for A-share investing and watchlist workflows, powered by TickFlow API for realtime monitoring, post-close review, multi-dimensional analysis, key level tracking, and alerts.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "dev": "tsc -p tsconfig.json --watch",
38
38
  "prepack": "npm run build && node ./scripts/prepare-package-assets.mjs",
39
39
  "postpack": "node ./scripts/restore-package-assets.mjs",
40
- "test": "npm run build && node --test dist/plugin-registration.test.js dist/tools/test-alert.tool.test.js dist/services/jin10-mcp-service.test.js dist/services/monitor-service.test.js",
40
+ "test": "npm run build && node --test dist/plugin-registration.test.js dist/tools/test-alert.tool.test.js dist/services/jin10-mcp-service.test.js dist/services/jin10-flash-monitor-service.test.js dist/services/pre-market-brief-service.test.js dist/services/monitor-service.test.js",
41
41
  "community-setup": "node dist/dev/tickflow-assist-cli.js configure-openclaw",
42
42
  "tool": "node dist/dev/run-tool.js",
43
43
  "monitor-loop": "node dist/dev/run-monitor-loop.js",
@@ -46,12 +46,12 @@
46
46
  },
47
47
  "openclaw": {
48
48
  "build": {
49
- "openclawVersion": "2026.4.5"
49
+ "openclawVersion": "2026.4.11"
50
50
  },
51
51
  "compat": {
52
52
  "pluginApi": ">=2026.3.31",
53
53
  "minGatewayVersion": "2026.3.31",
54
- "builtWithOpenClawVersion": "2026.4.5"
54
+ "builtWithOpenClawVersion": "2026.4.11"
55
55
  },
56
56
  "extensions": [
57
57
  "dist/plugin.js"
@@ -69,9 +69,9 @@
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^22.13.11",
72
- "openclaw": "^2026.4.5",
72
+ "openclaw": "^2026.4.11",
73
73
  "typescript": "^5.8.2"
74
74
  },
75
- "readme": "# TickFlow Assist\n\n基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE) 获取行情与财务数据,并可选接入 [金十数据 MCP](https://mcp.jin10.com/app/) 快讯流,结合 LLM 生成技术面、基本面、资讯面的综合判断,并把结果持久化到本地 LanceDB。\n\n最近更新:`v0.3.3` 修复监控告警图片在去重前高频生成与重复告警风险,并收敛价格告警触发噪声。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。\n\n当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.5` 上兼容。\n\n## 安装\n\n社区安装:\n\n```bash\nopenclaw plugins install tickflow-assist\nnpx -y tickflow-assist configure-openclaw\ncd ~/.openclaw/extensions/tickflow-assist/python && uv sync\nopenclaw plugins enable tickflow-assist\nopenclaw config validate\nopenclaw gateway restart\n```\n\n安装阶段允许先落插件,再通过第二条命令写入 `tickflowApiKey`、`llmApiKey` 等正式配置。\n`configure-openclaw` 会写入 `~/.openclaw/openclaw.json` 中的 `plugins.entries[\"tickflow-assist\"].config`,并打印后续建议执行的命令;它不再自动执行 `openclaw`、`uv` 或系统包安装命令。\n如果检测到 `plugins.installs[\"tickflow-assist\"]` 来自 `clawhub`,向导还会把被旧版本钉死的 `spec` 归一化为 `clawhub:tickflow-assist`,避免后续升级继续锁在旧版本。\n\n如果你希望先审阅配置,再只打印最少的后续步骤,可使用:\n\n```bash\nnpx -y tickflow-assist configure-openclaw --no-enable --no-restart\n```\n\n如果你在 Linux 或 macOS 上需要 PNG 告警卡正常显示中文,请额外手动安装 `fontconfig` 与 Noto CJK 一类中文字体,例如:\n\n```bash\n# Debian / Ubuntu\nsudo apt-get update\nsudo apt-get install -y fontconfig fonts-noto-cjk\nfc-cache -fv\n\n# RHEL / Fedora / Rocky / AlmaLinux\nsudo dnf install -y fontconfig google-noto-sans-cjk-ttc-fonts\nfc-cache -fv\n\n# Arch / Manjaro\nsudo pacman -Sy --noconfirm fontconfig noto-fonts-cjk\nfc-cache -fv\n\n# Alpine\nsudo apk add fontconfig font-noto-cjk\nfc-cache -fv\n\n# macOS (Homebrew)\nbrew install fontconfig\nbrew install --cask font-noto-sans-cjk\nfc-cache -fv\n```\n\n社区安装后的升级方式:\n\n```bash\nopenclaw plugins update tickflow-assist\nopenclaw gateway restart\n```\n\n## 配置\n\n插件正式运行读取:\n\n```text\n~/.openclaw/openclaw.json\n```\n\n配置路径:\n\n```text\nplugins.entries[\"tickflow-assist\"].config\n```\n\n建议按完整功能显式填写以下字段,不要只填 API Key:\n\n- 核心运行:`tickflowApiKey`、`llmApiKey`、`llmBaseUrl`、`llmModel`\n- 本地数据:`databasePath`、`calendarFile`\n- 告警投递:`alertChannel`、`alertTarget`、`alertAccount`\n- 能力补充:`mxSearchApiKey`、`jin10ApiToken`\n\n其中,`mxSearchApiKey` 用于 `mx_search`、`mx_select_stock` 以及非 `Expert` 财务链路的 lite 补充;`jin10ApiToken` 用于 24 小时金十数据快讯监控;`jin10FlashNightAlert` 默认 `false`(开启夜间静默),设为 `true` 可恢复 24 小时快讯告警;`alertTarget`、`alertAccount` 建议在准备启用 `test_alert`、实时监控告警、金十数据快讯告警和定时通知前一并配好,避免配置不完整导致功能缺失。\n\n## 功能\n\n- 自选股管理、日 K / 分钟 K 抓取与指标计算\n- 技术面、财务面、资讯面的综合分析\n- 实时监控、定时日更、收盘后复盘\n- 金十数据 24 小时快讯监控与自选关联提醒\n- 本地 LanceDB 数据留痕与分析结果查看\n\n## 运行说明\n\n- 插件会在本地 `databasePath` 下持久化 LanceDB 数据。\n- 后台服务会按配置执行定时日更、实时监控与金十数据快讯监控。\n- Python 子模块仅用于技术指标计算,不承担主业务流程。\n\n## 依赖与可选能力\n\n- [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE):提供日线、分钟线、实时行情与财务数据接口。\n- [金十数据 MCP](https://mcp.jin10.com/app/):可选,用于 24 小时快讯流接入、自选关联筛选与事件驱动告警。\n- [东方财富妙想 Skills](https://marketing.dfcfs.com/views/finskillshub/):可选,用于 `mx_search`、`mx_select_stock` 与非 `Expert` 财务链路的 lite 补充。\n\n## 仓库\n\n- GitHub: <https://github.com/robinspt/tickflow-assist>\n",
75
+ "readme": "# TickFlow Assist\n\n基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE) 获取行情与财务数据,并可选接入 [金十数据 MCP](https://mcp.jin10.com/app/) 快讯流,结合 LLM 生成技术面、基本面、资讯面的综合判断,并把结果持久化到本地 LanceDB。\n\n最近更新:`v0.3.5` 对齐 OpenClaw `2026.4.11` metadata 与社区安装提示,修复源码升级时本地链接扫描 `node_modules` 失败的问题,并支持通过环境变量回退 TickFlow / LLM / MX / Jin10 配置。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。\n\n当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.11` 上兼容。\n\n## 安装前准备\n\n在执行社区安装前,建议先确认你已经准备好以下配置:\n\n- 核心必需:`tickflowApiKey`、`llmApiKey`、`llmBaseUrl`、`llmModel`\n- 告警投递:`alertChannel`、`alertTarget`、`alertAccount`\n- 可选增强:`mxSearchApiKey`、`jin10ApiToken`\n\n其中,`configure-openclaw` 会把上述配置写入 `~/.openclaw/openclaw.json` 的 `plugins.entries[\"tickflow-assist\"].config`,插件启用后会在本地 `databasePath` 下持久化 LanceDB 数据,并运行监控 / 日更等后台服务。\n如果你不想把密钥写进配置文件,运行时也支持环境变量回退,优先级是 `openclaw.json / local.config.json` > 环境变量 > 默认值。\n常用环境变量:`TICKFLOW_ASSIST_TICKFLOW_API_KEY` / `TICKFLOW_API_KEY`、`TICKFLOW_ASSIST_LLM_API_KEY` / `LLM_API_KEY`、`TICKFLOW_ASSIST_LLM_BASE_URL` / `LLM_BASE_URL`、`TICKFLOW_ASSIST_LLM_MODEL` / `LLM_MODEL`、`TICKFLOW_ASSIST_MX_SEARCH_API_KEY` / `MX_SEARCH_API_KEY` / `MX_APIKEY`、`TICKFLOW_ASSIST_JIN10_API_TOKEN` / `JIN10_API_TOKEN`。\n\n## 安装\n\n社区安装:\n\n```bash\nopenclaw plugins install tickflow-assist\nnpx -y tickflow-assist configure-openclaw\ncd ~/.openclaw/extensions/tickflow-assist/python && uv sync\nopenclaw plugins enable tickflow-assist\nopenclaw config validate\nopenclaw gateway restart\n```\n\n安装阶段允许先落插件,再通过第二条命令写入 `tickflowApiKey`、`llmApiKey`、`llmBaseUrl`、`llmModel` 等正式配置。\n`configure-openclaw` 会写入 `~/.openclaw/openclaw.json` 中的 `plugins.entries[\"tickflow-assist\"].config`,并打印后续建议执行的命令;它不再自动执行 `openclaw`、`uv` 或系统包安装命令,也不会重新执行插件安装;如果你已经设置了环境变量,密钥项可留空,输入 `-` 可主动清空已有配置并切回环境变量。\n如果检测到 `plugins.installs[\"tickflow-assist\"]` 来自 `clawhub`,向导还会把被旧版本钉死的 `spec` 归一化为 `clawhub:tickflow-assist`,避免后续升级继续锁在旧版本。\n\n如果你希望先审阅配置,再只打印最少的后续步骤,可使用:\n\n```bash\nnpx -y tickflow-assist configure-openclaw --no-enable --no-restart\n```\n\n如果你在 Linux 或 macOS 上需要 PNG 告警卡正常显示中文,请额外手动安装 `fontconfig` 与 Noto CJK 一类中文字体,例如:\n\n```bash\n# Debian / Ubuntu\nsudo apt-get update\nsudo apt-get install -y fontconfig fonts-noto-cjk\nfc-cache -fv\n\n# RHEL / Fedora / Rocky / AlmaLinux\nsudo dnf install -y fontconfig google-noto-sans-cjk-ttc-fonts\nfc-cache -fv\n\n# Arch / Manjaro\nsudo pacman -Sy --noconfirm fontconfig noto-fonts-cjk\nfc-cache -fv\n\n# Alpine\nsudo apk add fontconfig font-noto-cjk\nfc-cache -fv\n\n# macOS (Homebrew)\nbrew install fontconfig\nbrew install --cask font-noto-sans-cjk\nfc-cache -fv\n```\n\n社区安装后的升级方式:\n\n```bash\nopenclaw plugins update tickflow-assist\nopenclaw gateway restart\n```\n\n## 配置\n\n插件正式运行读取:\n\n```text\n~/.openclaw/openclaw.json\n```\n\n配置路径:\n\n```text\nplugins.entries[\"tickflow-assist\"].config\n```\n\n建议按完整功能显式填写以下字段,不要只填 API Key:\n\n- 核心运行:`tickflowApiKey`、`llmApiKey`、`llmBaseUrl`、`llmModel`\n- 本地数据:`databasePath`、`calendarFile`\n- 告警投递:`alertChannel`、`alertTarget`、`alertAccount`\n- 能力补充:`mxSearchApiKey`、`jin10ApiToken`\n\n其中,`mxSearchApiKey` 用于 `mx_search`、`mx_select_stock` 以及非 `Expert` 财务链路的 lite 补充;`jin10ApiToken` 用于 24 小时金十数据快讯监控;`jin10FlashNightAlert` 默认 `false`(开启夜间静默),设为 `true` 可恢复 24 小时快讯告警;`alertTarget`、`alertAccount` 建议在准备启用 `test_alert`、实时监控告警、金十数据快讯告警和定时通知前一并配好,避免配置不完整导致功能缺失。\n如果你使用环境变量,运行时支持以下回退:\n\n- `tickflowApiUrl`:`TICKFLOW_ASSIST_TICKFLOW_API_URL` / `TICKFLOW_API_URL`\n- `tickflowApiKey`:`TICKFLOW_ASSIST_TICKFLOW_API_KEY` / `TICKFLOW_API_KEY`\n- `tickflowApiKeyLevel`:`TICKFLOW_ASSIST_TICKFLOW_API_KEY_LEVEL` / `TICKFLOW_API_KEY_LEVEL`\n- `llmBaseUrl`:`TICKFLOW_ASSIST_LLM_BASE_URL` / `LLM_BASE_URL`\n- `llmApiKey`:`TICKFLOW_ASSIST_LLM_API_KEY` / `LLM_API_KEY`\n- `llmModel`:`TICKFLOW_ASSIST_LLM_MODEL` / `LLM_MODEL`\n- `mxSearchApiUrl`:`TICKFLOW_ASSIST_MX_SEARCH_API_URL` / `MX_SEARCH_API_URL`\n- `mxSearchApiKey`:`TICKFLOW_ASSIST_MX_SEARCH_API_KEY` / `MX_SEARCH_API_KEY` / `MX_APIKEY`\n- `jin10McpUrl`:`TICKFLOW_ASSIST_JIN10_MCP_URL` / `JIN10_MCP_URL`\n- `jin10ApiToken`:`TICKFLOW_ASSIST_JIN10_API_TOKEN` / `JIN10_API_TOKEN`\n\n## 功能\n\n- 自选股管理、日 K / 分钟 K 抓取与指标计算\n- 技术面、财务面、资讯面的综合分析\n- 实时监控、定时日更、收盘后复盘\n- 金十数据 24 小时快讯监控与自选关联提醒\n- 本地 LanceDB 数据留痕与分析结果查看\n\n## 运行说明\n\n- 插件会在本地 `databasePath` 下持久化 LanceDB 数据。\n- 后台服务会按配置执行定时日更、实时监控与金十数据快讯监控。\n- Python 子模块仅用于技术指标计算,不承担主业务流程。\n\n## 依赖与可选能力\n\n- [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE):提供日线、分钟线、实时行情与财务数据接口。\n- [金十数据 MCP](https://mcp.jin10.com/app/):可选,用于 24 小时快讯流接入、自选关联筛选与事件驱动告警。\n- [东方财富妙想 Skills](https://marketing.dfcfs.com/views/finskillshub/):可选,用于 `mx_search`、`mx_select_stock` 与非 `Expert` 财务链路的 lite 补充。\n\n## 仓库\n\n- GitHub: [robinspt/tickflow-assist](https://github.com/robinspt/tickflow-assist)\n",
76
76
  "readmeFilename": "README.md"
77
77
  }