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.
- package/README.md +8 -6
- package/dist/background/realtime-monitor.worker.d.ts +1 -1
- package/dist/background/realtime-monitor.worker.js +3 -4
- package/dist/bootstrap.js +9 -0
- package/dist/dev/run-monitor-loop.js +0 -1
- package/dist/plugin-commands.js +27 -0
- package/dist/prompts/analysis/pre-market-brief-prompt.d.ts +1 -1
- package/dist/prompts/analysis/pre-market-brief-prompt.js +4 -3
- package/dist/services/alert-service.js +34 -4
- package/dist/services/monitor-service.d.ts +1 -1
- package/dist/services/monitor-service.js +18 -9
- package/dist/services/mx-search-service.d.ts +8 -1
- package/dist/services/mx-search-service.js +400 -10
- package/dist/services/pre-market-brief-service.js +343 -39
- package/dist/services/watchlist-service.d.ts +5 -1
- package/dist/services/watchlist-service.js +8 -3
- package/dist/tools/eastmoney-watchlist.tool.d.ts +31 -0
- package/dist/tools/eastmoney-watchlist.tool.js +294 -0
- package/dist/tools/mx-data.tool.d.ts +8 -0
- package/dist/tools/mx-data.tool.js +94 -0
- package/dist/tools/mx-select-stock.tool.js +6 -2
- package/dist/tools/screen-stock-candidates.tool.d.ts +34 -0
- package/dist/tools/screen-stock-candidates.tool.js +477 -0
- package/dist/types/mx-data.d.ts +23 -0
- package/dist/types/mx-data.js +1 -0
- package/dist/types/mx-select-stock.d.ts +1 -0
- package/dist/types/mx-self-select.d.ts +30 -0
- package/dist/types/mx-self-select.js +1 -0
- package/openclaw.plugin.json +143 -24
- package/package.json +9 -9
- package/skills/stock-analysis/SKILL.md +31 -2
- package/skills/usage-help/SKILL.md +33 -0
|
@@ -33,6 +33,76 @@ const RISK_KEYWORDS = [
|
|
|
33
33
|
"制裁",
|
|
34
34
|
"关税",
|
|
35
35
|
];
|
|
36
|
+
const FALLBACK_MAJOR_NEWS_LIMIT = 5;
|
|
37
|
+
const FALLBACK_TOPIC_KEY_POINT_LIMIT = 6;
|
|
38
|
+
const TOPIC_RULES = [
|
|
39
|
+
{
|
|
40
|
+
category: "地缘与能源",
|
|
41
|
+
keywords: [
|
|
42
|
+
"伊朗",
|
|
43
|
+
"以色列",
|
|
44
|
+
"中东",
|
|
45
|
+
"海湾",
|
|
46
|
+
"霍尔木兹",
|
|
47
|
+
"俄乌",
|
|
48
|
+
"俄罗斯",
|
|
49
|
+
"乌克兰",
|
|
50
|
+
"原油",
|
|
51
|
+
"石油",
|
|
52
|
+
"油轮",
|
|
53
|
+
"EIA",
|
|
54
|
+
"能源",
|
|
55
|
+
"侵略",
|
|
56
|
+
"制裁",
|
|
57
|
+
"开战",
|
|
58
|
+
],
|
|
59
|
+
score: 8,
|
|
60
|
+
macroImplication: "地缘风险与能源价格预期可能影响开盘风险偏好",
|
|
61
|
+
riskImplication: "海外扰动容易传导到原油、航运、防务及周期品情绪",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
category: "科技产业",
|
|
65
|
+
keywords: [
|
|
66
|
+
"AI",
|
|
67
|
+
"人工智能",
|
|
68
|
+
"大模型",
|
|
69
|
+
"算力",
|
|
70
|
+
"芯片",
|
|
71
|
+
"半导体",
|
|
72
|
+
"台积电",
|
|
73
|
+
"量子",
|
|
74
|
+
"机器人",
|
|
75
|
+
"开源",
|
|
76
|
+
"3D生成",
|
|
77
|
+
],
|
|
78
|
+
score: 7,
|
|
79
|
+
macroImplication: "科技成长方向的产业催化可能影响题材活跃度",
|
|
80
|
+
opportunityImplication: "产业链、算力、应用和设备端是否出现扩散",
|
|
81
|
+
riskImplication: "高热度题材若只停留在消息刺激,开盘后容易高开分化",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
category: "政策与支付互联",
|
|
85
|
+
keywords: ["政策", "微信支付", "二维码", "跨境", "互联互通", "试点", "监管", "关税"],
|
|
86
|
+
score: 6,
|
|
87
|
+
macroImplication: "政策或跨境互联线索可能改变相关行业预期",
|
|
88
|
+
opportunityImplication: "支付、出海、消费场景和平台生态是否获得资金确认",
|
|
89
|
+
riskImplication: "监管或外部政策变化可能压制相关板块估值与情绪",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
category: "产业与订单",
|
|
93
|
+
keywords: ["订单", "中标", "业绩", "回购", "增持", "涨价", "并购", "并购重组", "发布", "恢复"],
|
|
94
|
+
score: 5,
|
|
95
|
+
macroImplication: "产业事件或公司行为可能提供结构性交易线索",
|
|
96
|
+
opportunityImplication: "订单、业绩、价格和资本运作线索是否带来板块联动",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
category: "资金与宏观",
|
|
100
|
+
keywords: ["美联储", "利率", "美元", "人民币", "股市", "出口", "进口", "产量", "库存"],
|
|
101
|
+
score: 5,
|
|
102
|
+
macroImplication: "宏观和资金面变化可能影响开盘定价与风险偏好",
|
|
103
|
+
riskImplication: "宏观数据变化若与市场预期背离,可能造成高开低走或避险交易",
|
|
104
|
+
},
|
|
105
|
+
];
|
|
36
106
|
export class PreMarketBriefService {
|
|
37
107
|
watchlistService;
|
|
38
108
|
jin10McpService;
|
|
@@ -185,13 +255,8 @@ function matchesPreMarketBrief(record) {
|
|
|
185
255
|
function findMatchedItems(flash, watchlist) {
|
|
186
256
|
const normalizedContent = normalizeText(flash.content);
|
|
187
257
|
return watchlist.filter((item) => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.map((keyword) => keyword.replace(/\s+/g, "").trim())
|
|
191
|
-
.filter((keyword) => keyword.length >= 2);
|
|
192
|
-
return [...directKeywords, ...boardKeywords]
|
|
193
|
-
.map((keyword) => normalizeText(keyword))
|
|
194
|
-
.some((keyword) => keyword && normalizedContent.includes(keyword));
|
|
258
|
+
return buildWatchlistKeywordEntries(item)
|
|
259
|
+
.some((entry) => normalizedContent.includes(entry.normalized));
|
|
195
260
|
});
|
|
196
261
|
}
|
|
197
262
|
function buildFlashMatchContext(flash, watchlist) {
|
|
@@ -205,64 +270,290 @@ function buildFlashMatchContext(flash, watchlist) {
|
|
|
205
270
|
};
|
|
206
271
|
}
|
|
207
272
|
function buildFallbackSummary(matchContexts) {
|
|
208
|
-
const
|
|
209
|
-
const
|
|
273
|
+
const topics = buildFlashTopics(matchContexts);
|
|
274
|
+
const opportunityTopics = topics.filter(isOpportunityTopic);
|
|
275
|
+
const riskTopics = topics.filter(isRiskTopic);
|
|
210
276
|
return [
|
|
211
277
|
formatSectionTitle("🧭", "重大要闻"),
|
|
212
|
-
|
|
278
|
+
formatMajorNewsBullets(topics),
|
|
213
279
|
"",
|
|
214
280
|
formatSectionTitle("🎯", "自选相关"),
|
|
215
|
-
formatMatchedBullets(matchContexts, 5),
|
|
281
|
+
formatMatchedBullets(matchContexts, topics, 5),
|
|
216
282
|
"",
|
|
217
283
|
formatSectionTitle("💡", "潜在机会"),
|
|
218
|
-
|
|
219
|
-
?
|
|
284
|
+
opportunityTopics.length > 0
|
|
285
|
+
? formatOpportunityBullets(opportunityTopics, 4)
|
|
220
286
|
: "• 未发现基于当前整理快讯可直接确认的新增机会方向。",
|
|
221
287
|
"",
|
|
222
288
|
formatSectionTitle("⚠️", "风险提示"),
|
|
223
|
-
|
|
224
|
-
?
|
|
289
|
+
riskTopics.length > 0
|
|
290
|
+
? formatRiskBullets(riskTopics, 4)
|
|
225
291
|
: "• 当前整理快讯中未发现特别突出的新增风险,但仍需留意开盘后的情绪变化。",
|
|
226
292
|
"",
|
|
227
293
|
formatSectionTitle("📌", "开盘前关注清单"),
|
|
228
|
-
buildFocusBullets(matchContexts),
|
|
294
|
+
buildFocusBullets(topics, matchContexts),
|
|
229
295
|
].join("\n");
|
|
230
296
|
}
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
297
|
+
function buildFlashTopics(contexts) {
|
|
298
|
+
const seen = new Set();
|
|
299
|
+
const topics = [];
|
|
300
|
+
for (const context of contexts) {
|
|
301
|
+
const texts = context.keyPoints.length > 0
|
|
302
|
+
? context.keyPoints
|
|
303
|
+
: [formatContextSummary(context)];
|
|
304
|
+
const limitedTexts = texts.slice(0, FALLBACK_TOPIC_KEY_POINT_LIMIT);
|
|
305
|
+
for (const text of limitedTexts) {
|
|
306
|
+
const normalizedText = normalizeDigestText(text);
|
|
307
|
+
if (!normalizedText || seen.has(normalizedText)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
seen.add(normalizedText);
|
|
311
|
+
const rule = inferTopicRule(`${context.headline} ${text}`);
|
|
312
|
+
topics.push({
|
|
313
|
+
context,
|
|
314
|
+
text,
|
|
315
|
+
time: context.flash.published_at.slice(11, 16),
|
|
316
|
+
rule,
|
|
317
|
+
score: scoreTopic(text, context, rule),
|
|
318
|
+
matchedItems: context.matchedItems.filter((item) => topicMatchesWatchlistItem(text, item)),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (topics.length > 0) {
|
|
323
|
+
return topics;
|
|
324
|
+
}
|
|
325
|
+
return contexts.map((context) => {
|
|
326
|
+
const text = formatContextSummary(context);
|
|
327
|
+
const rule = inferTopicRule(`${context.headline} ${text}`);
|
|
328
|
+
return {
|
|
329
|
+
context,
|
|
330
|
+
text,
|
|
331
|
+
time: context.flash.published_at.slice(11, 16),
|
|
332
|
+
rule,
|
|
333
|
+
score: scoreTopic(text, context, rule),
|
|
334
|
+
matchedItems: context.matchedItems,
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function formatMajorNewsBullets(topics) {
|
|
339
|
+
const selected = selectDiverseTopics(topics, FALLBACK_MAJOR_NEWS_LIMIT);
|
|
340
|
+
if (selected.length === 0) {
|
|
341
|
+
return "• 未提取到足够正文细节,今日重大要闻暂只能按标题级线索观察。";
|
|
342
|
+
}
|
|
343
|
+
return selected
|
|
344
|
+
.map((topic) => `• [${topic.time}] ${topic.rule.category}: ${topic.text};${topic.rule.macroImplication}。`)
|
|
345
|
+
.join("\n");
|
|
346
|
+
}
|
|
347
|
+
function formatOpportunityBullets(topics, limit) {
|
|
348
|
+
return selectDiverseTopics(topics, limit)
|
|
349
|
+
.map((topic) => {
|
|
350
|
+
const implication = topic.rule.opportunityImplication ?? "相关方向是否获得资金确认";
|
|
351
|
+
return `• [${topic.time}] ${topic.rule.category}: ${topic.text};观察${implication}。`;
|
|
237
352
|
})
|
|
238
353
|
.join("\n");
|
|
239
354
|
}
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
355
|
+
function formatRiskBullets(topics, limit) {
|
|
356
|
+
return selectDiverseTopics(topics, limit)
|
|
357
|
+
.map((topic) => {
|
|
358
|
+
const implication = topic.rule.riskImplication ?? "消息不确定性对开盘情绪的扰动";
|
|
359
|
+
return `• [${topic.time}] ${topic.rule.category}: ${topic.text};风险点在于${implication}。`;
|
|
360
|
+
})
|
|
361
|
+
.join("\n");
|
|
362
|
+
}
|
|
363
|
+
function formatMatchedBullets(contexts, topics, limit) {
|
|
364
|
+
const matchedBySymbol = new Map();
|
|
365
|
+
for (const topic of topics) {
|
|
366
|
+
for (const item of topic.matchedItems) {
|
|
367
|
+
addWatchlistCue(matchedBySymbol, item, {
|
|
368
|
+
text: topic.text,
|
|
369
|
+
reason: describeWatchlistMatch(item, topic.text),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const context of contexts) {
|
|
374
|
+
for (const item of context.matchedItems) {
|
|
375
|
+
if (matchedBySymbol.has(item.symbol)) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const cue = findBestWatchlistCue(context, item);
|
|
379
|
+
addWatchlistCue(matchedBySymbol, item, {
|
|
380
|
+
text: cue,
|
|
381
|
+
reason: describeWatchlistMatch(item, cue),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const entries = [...matchedBySymbol.values()].slice(0, limit);
|
|
386
|
+
if (entries.length === 0) {
|
|
243
387
|
return "• 未发现直接命中自选股、行业或题材的盘前整理快讯。";
|
|
244
388
|
}
|
|
245
|
-
return
|
|
246
|
-
const
|
|
247
|
-
|
|
389
|
+
return entries.map(({ item, cues }) => {
|
|
390
|
+
const text = cues
|
|
391
|
+
.slice(0, 2)
|
|
392
|
+
.map((cue) => `${cue.reason}: ${cue.text}`)
|
|
393
|
+
.join(";");
|
|
394
|
+
return `• ${item.name}(${item.symbol}): ${text}`;
|
|
248
395
|
}).join("\n");
|
|
249
396
|
}
|
|
250
|
-
function buildFocusBullets(contexts) {
|
|
397
|
+
function buildFocusBullets(topics, contexts) {
|
|
251
398
|
const bullets = [];
|
|
252
|
-
const
|
|
253
|
-
for (const
|
|
254
|
-
const labels =
|
|
255
|
-
bullets
|
|
399
|
+
const matchedTopics = topics.filter((topic) => topic.matchedItems.length > 0);
|
|
400
|
+
for (const topic of selectDiverseTopics(matchedTopics, 2)) {
|
|
401
|
+
const labels = topic.matchedItems.map((item) => item.name).join("、");
|
|
402
|
+
addUniqueBullet(bullets, `• 关注 ${labels} 与“${formatFocusCueText(topic.text)}”的联动强度,开盘看竞价、放量承接和回落后的资金回流。`);
|
|
403
|
+
}
|
|
404
|
+
for (const topic of selectDiverseTopics(topics.filter(isRiskTopic), 2)) {
|
|
405
|
+
addUniqueBullet(bullets, `• 观察 ${topic.rule.category} 是否压制风险偏好,重点看“${formatFocusCueText(topic.text)}”有无后续快讯确认。`);
|
|
406
|
+
}
|
|
407
|
+
for (const topic of selectDiverseTopics(topics.filter(isOpportunityTopic), 2)) {
|
|
408
|
+
addUniqueBullet(bullets, `• 观察 ${topic.rule.category} 主题是否扩散,重点看“${formatFocusCueText(topic.text)}”对应方向高开后的承接而非单点冲高。`);
|
|
256
409
|
}
|
|
257
410
|
if (bullets.length < 3) {
|
|
258
|
-
for (const context of contexts
|
|
259
|
-
bullets
|
|
411
|
+
for (const context of contexts) {
|
|
412
|
+
addUniqueBullet(bullets, `• 复核“${formatFocusCue(context)}”是否有后续消息或竞价强化,避免仅按标题级线索追高。`);
|
|
413
|
+
if (bullets.length >= 3) {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
260
416
|
}
|
|
261
417
|
}
|
|
262
418
|
return bullets.slice(0, 5).join("\n");
|
|
263
419
|
}
|
|
420
|
+
function selectDiverseTopics(topics, limit) {
|
|
421
|
+
const sorted = [...topics].sort(compareTopicsByImportance);
|
|
422
|
+
const selected = [];
|
|
423
|
+
const selectedCategories = new Set();
|
|
424
|
+
const selectedTexts = new Set();
|
|
425
|
+
for (const topic of sorted) {
|
|
426
|
+
if (selected.length >= limit) {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
const normalizedText = normalizeDigestText(topic.text);
|
|
430
|
+
if (selectedTexts.has(normalizedText) || selectedCategories.has(topic.rule.category)) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
selected.push(topic);
|
|
434
|
+
selectedTexts.add(normalizedText);
|
|
435
|
+
selectedCategories.add(topic.rule.category);
|
|
436
|
+
}
|
|
437
|
+
for (const topic of sorted) {
|
|
438
|
+
if (selected.length >= limit) {
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
const normalizedText = normalizeDigestText(topic.text);
|
|
442
|
+
if (selectedTexts.has(normalizedText)) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
selected.push(topic);
|
|
446
|
+
selectedTexts.add(normalizedText);
|
|
447
|
+
}
|
|
448
|
+
return selected.sort((left, right) => left.context.flash.published_ts - right.context.flash.published_ts);
|
|
449
|
+
}
|
|
450
|
+
function compareTopicsByImportance(left, right) {
|
|
451
|
+
return right.score - left.score || left.context.flash.published_ts - right.context.flash.published_ts;
|
|
452
|
+
}
|
|
453
|
+
function inferTopicRule(text) {
|
|
454
|
+
return TOPIC_RULES
|
|
455
|
+
.filter((rule) => containsAnyKeyword(text, rule.keywords))
|
|
456
|
+
.sort((left, right) => right.score - left.score)[0]
|
|
457
|
+
?? {
|
|
458
|
+
category: "市场情绪",
|
|
459
|
+
keywords: [],
|
|
460
|
+
score: 3,
|
|
461
|
+
macroImplication: "该线索可能影响局部题材情绪,需结合竞价强弱确认",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function scoreTopic(text, context, rule) {
|
|
465
|
+
let score = rule.score;
|
|
466
|
+
if (containsAnyKeyword(text, OPPORTUNITY_KEYWORDS)) {
|
|
467
|
+
score += 2;
|
|
468
|
+
}
|
|
469
|
+
if (containsAnyKeyword(text, RISK_KEYWORDS)) {
|
|
470
|
+
score += 2;
|
|
471
|
+
}
|
|
472
|
+
if (context.matchedItems.length > 0) {
|
|
473
|
+
score += 1;
|
|
474
|
+
}
|
|
475
|
+
if (text.length >= 18) {
|
|
476
|
+
score += 1;
|
|
477
|
+
}
|
|
478
|
+
return score;
|
|
479
|
+
}
|
|
480
|
+
function isOpportunityTopic(topic) {
|
|
481
|
+
return Boolean(topic.rule.opportunityImplication) || containsAnyKeyword(topic.text, OPPORTUNITY_KEYWORDS);
|
|
482
|
+
}
|
|
483
|
+
function isRiskTopic(topic) {
|
|
484
|
+
return Boolean(topic.rule.riskImplication) || containsAnyKeyword(topic.text, RISK_KEYWORDS);
|
|
485
|
+
}
|
|
486
|
+
function addWatchlistCue(matchedBySymbol, item, cue) {
|
|
487
|
+
const entry = matchedBySymbol.get(item.symbol) ?? { item, cues: [] };
|
|
488
|
+
const normalizedCue = normalizeDigestText(cue.text);
|
|
489
|
+
if (!entry.cues.some((existing) => normalizeDigestText(existing.text) === normalizedCue)) {
|
|
490
|
+
entry.cues.push(cue);
|
|
491
|
+
}
|
|
492
|
+
matchedBySymbol.set(item.symbol, entry);
|
|
493
|
+
}
|
|
494
|
+
function findBestWatchlistCue(context, item) {
|
|
495
|
+
const exactCue = context.keyPoints.find((point) => topicMatchesWatchlistItem(point, item));
|
|
496
|
+
return exactCue ?? formatContextSummary(context);
|
|
497
|
+
}
|
|
498
|
+
function topicMatchesWatchlistItem(text, item) {
|
|
499
|
+
const normalizedText = normalizeText(text);
|
|
500
|
+
return buildWatchlistKeywordEntries(item)
|
|
501
|
+
.some((entry) => normalizedText.includes(entry.normalized));
|
|
502
|
+
}
|
|
503
|
+
function describeWatchlistMatch(item, text) {
|
|
504
|
+
const normalizedText = normalizeText(text);
|
|
505
|
+
const entries = buildWatchlistKeywordEntries(item);
|
|
506
|
+
const directMatch = entries
|
|
507
|
+
.find((entry) => entry.kind === "direct" && normalizedText.includes(entry.normalized));
|
|
508
|
+
if (directMatch) {
|
|
509
|
+
return "直接命中";
|
|
510
|
+
}
|
|
511
|
+
const themeMatch = entries
|
|
512
|
+
.find((entry) => entry.kind === "theme" && normalizedText.includes(entry.normalized));
|
|
513
|
+
if (themeMatch) {
|
|
514
|
+
return `题材${themeMatch.keyword}`;
|
|
515
|
+
}
|
|
516
|
+
const sectorMatch = entries
|
|
517
|
+
.find((entry) => entry.kind === "sector" && normalizedText.includes(entry.normalized));
|
|
518
|
+
if (sectorMatch) {
|
|
519
|
+
return `行业${sectorMatch.keyword}`;
|
|
520
|
+
}
|
|
521
|
+
return "规则命中";
|
|
522
|
+
}
|
|
523
|
+
function buildWatchlistKeywordEntries(item) {
|
|
524
|
+
const entries = [
|
|
525
|
+
...[item.symbol, item.symbol.slice(0, 6), item.name].map((keyword) => ({ keyword, kind: "direct" })),
|
|
526
|
+
...extractSectorKeywords(item.sector).map((keyword) => ({ keyword, kind: "sector" })),
|
|
527
|
+
...item.themes.map((keyword) => ({ keyword, kind: "theme" })),
|
|
528
|
+
];
|
|
529
|
+
const seen = new Set();
|
|
530
|
+
return entries
|
|
531
|
+
.map((entry) => ({
|
|
532
|
+
...entry,
|
|
533
|
+
keyword: entry.keyword.replace(/\s+/g, "").trim(),
|
|
534
|
+
normalized: normalizeText(entry.keyword),
|
|
535
|
+
}))
|
|
536
|
+
.filter((entry) => entry.normalized.length >= 2)
|
|
537
|
+
.filter((entry) => {
|
|
538
|
+
if (seen.has(entry.normalized)) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
seen.add(entry.normalized);
|
|
542
|
+
return true;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
function addUniqueBullet(bullets, bullet) {
|
|
546
|
+
const normalizedBullet = normalizeDigestText(bullet);
|
|
547
|
+
if (!bullets.some((existing) => normalizeDigestText(existing) === normalizedBullet)) {
|
|
548
|
+
bullets.push(bullet);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function formatFocusCueText(text) {
|
|
552
|
+
return truncateText(text, 34);
|
|
553
|
+
}
|
|
264
554
|
function containsAnyKeyword(content, keywords) {
|
|
265
|
-
|
|
555
|
+
const normalizedContent = normalizeText(content);
|
|
556
|
+
return keywords.some((keyword) => normalizedContent.includes(normalizeText(keyword)));
|
|
266
557
|
}
|
|
267
558
|
function normalizeText(value) {
|
|
268
559
|
return value.toLowerCase().replace(/\s+/g, "");
|
|
@@ -330,7 +621,7 @@ function extractFlashKeyPoints(content, headline) {
|
|
|
330
621
|
}
|
|
331
622
|
deduped.add(normalizedSegment);
|
|
332
623
|
keyPoints.push(truncateText(segment, 88));
|
|
333
|
-
if (keyPoints.length >=
|
|
624
|
+
if (keyPoints.length >= FALLBACK_TOPIC_KEY_POINT_LIMIT) {
|
|
334
625
|
break;
|
|
335
626
|
}
|
|
336
627
|
}
|
|
@@ -341,14 +632,27 @@ function stripHeadline(content, headline) {
|
|
|
341
632
|
if (!headline) {
|
|
342
633
|
return trimmed;
|
|
343
634
|
}
|
|
344
|
-
|
|
635
|
+
const isTruncatedHeadline = headline.endsWith("...");
|
|
636
|
+
if (!isTruncatedHeadline && trimmed.startsWith(headline)) {
|
|
637
|
+
return trimmed.slice(headline.length).replace(/^[::。;,、\s-]+/, "").trim();
|
|
638
|
+
}
|
|
639
|
+
const withoutDigestPrefix = trimmed
|
|
640
|
+
.replace(/^【?金十数据整理[::]\s*/, "")
|
|
641
|
+
.replace(/^】\s*/, "")
|
|
642
|
+
.trim();
|
|
643
|
+
if (withoutDigestPrefix !== trimmed) {
|
|
644
|
+
return withoutDigestPrefix;
|
|
645
|
+
}
|
|
646
|
+
if (isTruncatedHeadline) {
|
|
345
647
|
return trimmed;
|
|
346
648
|
}
|
|
347
|
-
return trimmed
|
|
649
|
+
return trimmed;
|
|
348
650
|
}
|
|
349
651
|
function cleanFlashSegment(segment) {
|
|
350
652
|
return segment
|
|
351
653
|
.trim()
|
|
654
|
+
.replace(/^【?金十数据整理[::]\s*/, "")
|
|
655
|
+
.replace(/^([^】]{2,40})】\s*/, "")
|
|
352
656
|
.replace(/^[-•●▪◦]\s*/, "")
|
|
353
657
|
.replace(/^\d+\s*[、..))]\s*/, "")
|
|
354
658
|
.replace(/^[((]?\d+[))]\s*/, "")
|
|
@@ -5,6 +5,10 @@ import { WatchlistProfileService } from "./watchlist-profile-service.js";
|
|
|
5
5
|
interface GetWatchlistItemOptions {
|
|
6
6
|
refreshConceptBoards?: boolean;
|
|
7
7
|
}
|
|
8
|
+
interface AddWatchlistOptions {
|
|
9
|
+
enrichProfile?: boolean;
|
|
10
|
+
name?: string;
|
|
11
|
+
}
|
|
8
12
|
export interface AddWatchlistResult {
|
|
9
13
|
item: WatchlistItem;
|
|
10
14
|
profileError: string | null;
|
|
@@ -29,7 +33,7 @@ export declare class WatchlistService {
|
|
|
29
33
|
private readonly instrumentService;
|
|
30
34
|
private readonly watchlistProfileService;
|
|
31
35
|
constructor(repository: WatchlistRepository, instrumentService: InstrumentService, watchlistProfileService?: WatchlistProfileService | null);
|
|
32
|
-
add(symbolInput: string, costPrice?: number | null): Promise<AddWatchlistResult>;
|
|
36
|
+
add(symbolInput: string, costPrice?: number | null, options?: AddWatchlistOptions): Promise<AddWatchlistResult>;
|
|
33
37
|
list(): Promise<WatchlistItem[]>;
|
|
34
38
|
remove(symbolInput: string): Promise<boolean>;
|
|
35
39
|
getBySymbol(symbolInput: string, options?: GetWatchlistItemOptions): Promise<WatchlistItem | null>;
|
|
@@ -12,11 +12,11 @@ export class WatchlistService {
|
|
|
12
12
|
this.instrumentService = instrumentService;
|
|
13
13
|
this.watchlistProfileService = watchlistProfileService;
|
|
14
14
|
}
|
|
15
|
-
async add(symbolInput, costPrice = null) {
|
|
15
|
+
async add(symbolInput, costPrice = null, options = {}) {
|
|
16
16
|
const symbol = normalizeSymbol(symbolInput);
|
|
17
17
|
const existing = await this.getBySymbol(symbol);
|
|
18
18
|
const normalizedCostPrice = normalizeCostPrice(costPrice);
|
|
19
|
-
const name = await this.instrumentService.resolveName(symbol);
|
|
19
|
+
const name = normalizeOptionalName(options.name) ?? await this.instrumentService.resolveName(symbol);
|
|
20
20
|
const addedAt = formatChinaDateTime();
|
|
21
21
|
const fallbackProfile = {
|
|
22
22
|
sector: existing?.sector ?? null,
|
|
@@ -26,7 +26,8 @@ export class WatchlistService {
|
|
|
26
26
|
};
|
|
27
27
|
let profile = fallbackProfile;
|
|
28
28
|
let profileError = null;
|
|
29
|
-
|
|
29
|
+
const enrichProfile = options.enrichProfile ?? true;
|
|
30
|
+
if (enrichProfile && this.watchlistProfileService) {
|
|
30
31
|
try {
|
|
31
32
|
const resolved = await this.watchlistProfileService.resolve(symbol, name, addedAt);
|
|
32
33
|
profile = {
|
|
@@ -199,6 +200,10 @@ function sanitizeName(name, symbol) {
|
|
|
199
200
|
}
|
|
200
201
|
return trimmed;
|
|
201
202
|
}
|
|
203
|
+
function normalizeOptionalName(value) {
|
|
204
|
+
const normalized = sanitizeName(String(value ?? ""), "");
|
|
205
|
+
return normalized || null;
|
|
206
|
+
}
|
|
202
207
|
function toErrorMessage(error) {
|
|
203
208
|
return error instanceof Error ? error.message : String(error);
|
|
204
209
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { MxApiService } from "../services/mx-search-service.js";
|
|
2
|
+
import { WatchlistService } from "../services/watchlist-service.js";
|
|
3
|
+
export declare function listEastmoneyWatchlistTool(mxApiService: MxApiService): {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
run(): Promise<string>;
|
|
7
|
+
};
|
|
8
|
+
export declare function syncEastmoneyWatchlistTool(mxApiService: MxApiService, watchlistService: WatchlistService): {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
optional: boolean;
|
|
12
|
+
run({ rawInput }: {
|
|
13
|
+
rawInput?: unknown;
|
|
14
|
+
}): Promise<string>;
|
|
15
|
+
};
|
|
16
|
+
export declare function pushEastmoneyWatchlistTool(mxApiService: MxApiService, watchlistService: WatchlistService): {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
optional: boolean;
|
|
20
|
+
run({ rawInput }: {
|
|
21
|
+
rawInput?: unknown;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
};
|
|
24
|
+
export declare function removeEastmoneyWatchlistTool(mxApiService: MxApiService): {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
optional: boolean;
|
|
28
|
+
run({ rawInput }: {
|
|
29
|
+
rawInput?: unknown;
|
|
30
|
+
}): Promise<string>;
|
|
31
|
+
};
|