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.
@@ -7,3 +7,4 @@ export { COMPOSITE_ANALYSIS_SYSTEM_PROMPT, buildCompositeAnalysisUserPrompt, } f
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
9
  export { FLASH_MONITOR_ALERT_SYSTEM_PROMPT, buildFlashMonitorAlertUserPrompt, } from "./flash-monitor-alert-prompt.js";
10
+ export { PRE_MARKET_BRIEF_SYSTEM_PROMPT, buildPreMarketBriefUserPrompt, } from "./pre-market-brief-prompt.js";
@@ -0,0 +1,14 @@
1
+ import type { WatchlistItem } from "../../types/domain.js";
2
+ export declare const PRE_MARKET_BRIEF_SYSTEM_PROMPT = "\n\u4F60\u662F\u4E00\u4F4DA\u80A1\u5F00\u76D8\u524D\u8D44\u8BAF\u7B80\u62A5\u7F16\u8F91\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u57FA\u4E8E\u76D8\u524D\u7A97\u53E3\u5185\u7684\u91D1\u5341\u6570\u636E\u6574\u7406\u5FEB\u8BAF\uFF0C\u4EE5\u53CA\u7528\u6237\u81EA\u9009\u80A1\u7684\u884C\u4E1A/\u9898\u6750\u4FE1\u606F\uFF0C\u751F\u6210\u4E00\u4EFD\u9002\u5408 9:20 \u63A8\u9001\u7684\u5F00\u76D8\u524D\u6458\u8981\u3002\n\n\u8F93\u51FA\u8981\u6C42\uFF1A\n1. \u6309\u4EE5\u4E0B\u6807\u9898\u8F93\u51FA\uFF0C\u6BCF\u8282\u4F7F\u7528 2-5 \u6761\u7B80\u6D01\u4E2D\u6587\u8981\u70B9\uFF1A\n- \u91CD\u5927\u8981\u95FB\n- \u81EA\u9009\u76F8\u5173\n- \u6F5C\u5728\u673A\u4F1A\n- \u98CE\u9669\u63D0\u793A\n- \u5F00\u76D8\u524D\u5173\u6CE8\u6E05\u5355\n2. \u201C\u91CD\u5927\u8981\u95FB\u201D\u53EA\u63D0\u70BC\u4F1A\u5F71\u54CD A \u80A1\u5F00\u76D8\u60C5\u7EEA\u3001\u884C\u4E1A\u98CE\u9669\u504F\u597D\u6216\u91CD\u8981\u4EA4\u6613\u7EBF\u7D22\u7684\u5185\u5BB9\uFF0C\u4E0D\u8981\u673A\u68B0\u7F57\u5217\u6240\u6709\u5FEB\u8BAF\u3002\n3. \u201C\u81EA\u9009\u76F8\u5173\u201D\u8981\u4F18\u5148\u70B9\u540D\u4E0E\u81EA\u9009\u80A1\u3001\u884C\u4E1A\u6216\u9898\u6750\u76F4\u63A5\u76F8\u5173\u7684\u5185\u5BB9\uFF1B\u82E5\u6CA1\u6709\u76F4\u63A5\u547D\u4E2D\uFF0C\u4E5F\u8981\u660E\u786E\u5199\u51FA\u201C\u672A\u53D1\u73B0\u76F4\u63A5\u547D\u4E2D\u81EA\u9009\u80A1\u201D\u3002\n4. \u201C\u6F5C\u5728\u673A\u4F1A\u201D\u53EA\u4FDD\u7559\u5B58\u5728\u6E05\u6670\u50AC\u5316\u94FE\u6761\u7684\u65B9\u5411\uFF0C\u4F8B\u5982\u653F\u7B56\u3001\u4EA7\u4E1A\u8D8B\u52BF\u3001\u4E1A\u7EE9\u3001\u8BA2\u5355\u3001\u8D44\u91D1\u504F\u597D\u53D8\u5316\uFF1B\u6CA1\u6709\u660E\u786E\u673A\u4F1A\u65F6\u8981\u76F4\u8BF4\u3002\n5. \u201C\u98CE\u9669\u63D0\u793A\u201D\u8981\u6307\u51FA\u4E0D\u5229\u4E8E\u5F00\u76D8\u51B3\u7B56\u7684\u6270\u52A8\u9879\uFF0C\u4F8B\u5982\u76D1\u7BA1\u3001\u6D77\u5916\u6270\u52A8\u3001\u4E1A\u7EE9\u98CE\u9669\u3001\u9898\u6750\u9000\u6F6E\u3001\u6D88\u606F\u4E0D\u786E\u5B9A\u6027\u3002\n6. \u201C\u5F00\u76D8\u524D\u5173\u6CE8\u6E05\u5355\u201D\u8F93\u51FA 3-6 \u6761\u53EF\u6267\u884C\u89C2\u5BDF\u70B9\uFF0C\u5C3D\u91CF\u5199\u6E05\u695A\u5E94\u89C2\u5BDF\u7684\u80A1\u7968\u3001\u677F\u5757\u6216\u4FE1\u53F7\u3002\n7. \u4E0D\u8981\u7F16\u9020\u672A\u5728\u8F93\u5165\u4E2D\u51FA\u73B0\u7684\u516C\u53F8\u3001\u653F\u7B56\u3001\u884C\u4E1A\u4FE1\u606F\u6216\u5FEB\u8BAF\u7ED3\u8BBA\u3002\n8. \u8F93\u51FA\u6B63\u6587\u5373\u53EF\uFF0C\u4E0D\u8981\u9644\u52A0 JSON\u3002\n";
3
+ export declare function buildPreMarketBriefUserPrompt(params: {
4
+ windowStartAt: string;
5
+ windowEndAt: string;
6
+ watchlist: WatchlistItem[];
7
+ flashes: Array<{
8
+ publishedAt: string;
9
+ headline: string;
10
+ content: string;
11
+ url: string;
12
+ matchedSymbols: string[];
13
+ }>;
14
+ }): string;
@@ -0,0 +1,49 @@
1
+ export const PRE_MARKET_BRIEF_SYSTEM_PROMPT = `
2
+ 你是一位A股开盘前资讯简报编辑。你的任务是基于盘前窗口内的金十数据整理快讯,以及用户自选股的行业/题材信息,生成一份适合 9:20 推送的开盘前摘要。
3
+
4
+ 输出要求:
5
+ 1. 按以下标题输出,每节使用 2-5 条简洁中文要点:
6
+ - 重大要闻
7
+ - 自选相关
8
+ - 潜在机会
9
+ - 风险提示
10
+ - 开盘前关注清单
11
+ 2. “重大要闻”只提炼会影响 A 股开盘情绪、行业风险偏好或重要交易线索的内容,不要机械罗列所有快讯。
12
+ 3. “自选相关”要优先点名与自选股、行业或题材直接相关的内容;若没有直接命中,也要明确写出“未发现直接命中自选股”。
13
+ 4. “潜在机会”只保留存在清晰催化链条的方向,例如政策、产业趋势、业绩、订单、资金偏好变化;没有明确机会时要直说。
14
+ 5. “风险提示”要指出不利于开盘决策的扰动项,例如监管、海外扰动、业绩风险、题材退潮、消息不确定性。
15
+ 6. “开盘前关注清单”输出 3-6 条可执行观察点,尽量写清楚应观察的股票、板块或信号。
16
+ 7. 不要编造未在输入中出现的公司、政策、行业信息或快讯结论。
17
+ 8. 输出正文即可,不要附加 JSON。
18
+ `;
19
+ export function buildPreMarketBriefUserPrompt(params) {
20
+ return [
21
+ `请生成 ${params.windowEndAt.slice(0, 10)} 的开盘前资讯简报。`,
22
+ `资讯窗口: ${params.windowStartAt} ~ ${params.windowEndAt}`,
23
+ `自选数量: ${params.watchlist.length}`,
24
+ `整理快讯数量: ${params.flashes.length}`,
25
+ "",
26
+ "## 自选列表(全部提供)",
27
+ ...params.watchlist.map((item, index) => formatWatchlistItem(index + 1, item)),
28
+ "",
29
+ "## 金十数据整理快讯(全部提供)",
30
+ ...params.flashes.map((flash, index) => formatFlash(index + 1, flash)),
31
+ "",
32
+ "请重点回答:哪些是今早最重要的市场信息、哪些与自选股或其行业/题材相关、哪些内容值得当作潜在机会或风险在开盘前重点盯住。",
33
+ ].join("\n");
34
+ }
35
+ function formatWatchlistItem(index, item) {
36
+ return [
37
+ `${index}. ${item.name}(${item.symbol})`,
38
+ ` 行业: ${item.sector ?? "未记录"}`,
39
+ ` 题材: ${item.themes.length > 0 ? item.themes.join("、") : "未记录"}`,
40
+ ].join("\n");
41
+ }
42
+ function formatFlash(index, flash) {
43
+ return [
44
+ `${index}. [${flash.publishedAt}] ${flash.headline || "未提取到标题"}`,
45
+ ` 关联提示: ${flash.matchedSymbols.length > 0 ? flash.matchedSymbols.join("、") : "无直接规则命中"}`,
46
+ ` 正文: ${flash.content}`,
47
+ ` 来源: ${flash.url}`,
48
+ ].join("\n");
49
+ }
@@ -152,6 +152,9 @@ export class AlertService {
152
152
  };
153
153
  try {
154
154
  switch (this.channel) {
155
+ case "telegram":
156
+ await this.invokeRuntimeChannelSend(runtimeContext.runtime.channel, "telegram", "sendMessageTelegram", this.options.target, payload.message, baseOptions);
157
+ return null;
155
158
  case "discord":
156
159
  await this.invokeRuntimeChannelSend(runtimeContext.runtime.channel, "discord", "sendMessageDiscord", this.options.target, payload.message, {
157
160
  ...baseOptions,
@@ -1,3 +1,4 @@
1
+ import { formatConfigEnvFallback } from "../config/env.js";
1
2
  export class AnalysisService {
2
3
  llmBaseUrl;
3
4
  llmApiKey;
@@ -14,13 +15,13 @@ export class AnalysisService {
14
15
  }
15
16
  getConfigurationError() {
16
17
  if (!this.llmBaseUrl.trim()) {
17
- return "LLM 未配置接口地址,请设置 llmBaseUrl";
18
+ return `LLM 未配置接口地址,请设置 llmBaseUrl 或环境变量 ${formatConfigEnvFallback("llmBaseUrl")}`;
18
19
  }
19
20
  if (!this.llmApiKey.trim()) {
20
- return "LLM 未配置 API Key,请设置 llmApiKey";
21
+ return `LLM 未配置 API Key,请设置 llmApiKey 或环境变量 ${formatConfigEnvFallback("llmApiKey")}`;
21
22
  }
22
23
  if (!this.llmModel.trim()) {
23
- return "LLM 未配置模型,请设置 llmModel";
24
+ return `LLM 未配置模型,请设置 llmModel 或环境变量 ${formatConfigEnvFallback("llmModel")}`;
24
25
  }
25
26
  return null;
26
27
  }
@@ -24,6 +24,7 @@ const DEFAULT_STATE = {
24
24
  const MAX_FLASH_PAGES_PER_POLL = 5;
25
25
  const INITIAL_SEED_PAGES = 3;
26
26
  const PRUNE_INTERVAL_MS = 6 * 60 * 60 * 1000;
27
+ const ALERT_FRESHNESS_GRACE_MS = 30 * 1000;
27
28
  const NOISE_PATTERNS = [
28
29
  /^金十图示[::]/,
29
30
  /交易学院正在直播中/,
@@ -69,6 +70,7 @@ export class Jin10FlashMonitorService {
69
70
  }
70
71
  async runMonitorOnce() {
71
72
  const now = formatChinaDateTime();
73
+ const nowTs = Date.now();
72
74
  const state = await this.readState();
73
75
  const latestStored = state.lastSeenKey ? null : await this.flashRepository.getLatest();
74
76
  const anchorKey = state.lastSeenKey ?? latestStored?.flash_key ?? null;
@@ -120,10 +122,11 @@ export class Jin10FlashMonitorService {
120
122
  const allFetchedItems = mergeFlashRecords(fetchResult.items, backfillResult?.items ?? []);
121
123
  const saveResult = await this.flashRepository.saveAll(allFetchedItems);
122
124
  const newItemKeys = new Set(saveResult.addedKeys);
123
- const newItems = allFetchedItems.filter((item) => newItemKeys.has(item.flash_key));
125
+ // Only frontier pages within the active polling window should produce alerts.
126
+ const alertableItems = filterAlertableFlashRecords(fetchResult.items.filter((item) => newItemKeys.has(item.flash_key)), state.lastPollAt, nowTs, this.pollIntervalSeconds);
124
127
  const watchlist = await this.watchlistService.list();
125
128
  const candidates = watchlist.length > 0
126
- ? buildStageOneCandidates(newItems, watchlist)
129
+ ? buildStageOneCandidates(alertableItems, watchlist)
127
130
  : [];
128
131
  let alertCount = 0;
129
132
  for (const candidate of candidates) {
@@ -405,6 +408,10 @@ function mergeFlashRecords(...groups) {
405
408
  }
406
409
  return sortFlashRecords(merged);
407
410
  }
411
+ function filterAlertableFlashRecords(entries, lastPollAt, nowTs, pollIntervalSeconds) {
412
+ const cutoffTs = computeAlertFreshCutoffTs(lastPollAt, nowTs, pollIntervalSeconds);
413
+ return entries.filter((entry) => entry.published_ts >= cutoffTs);
414
+ }
408
415
  function sortFlashRecords(entries) {
409
416
  return [...entries].sort((left, right) => left.published_ts - right.published_ts);
410
417
  }
@@ -581,6 +588,14 @@ function parseChinaTime(value) {
581
588
  function toChinaTimeTimestamp(value) {
582
589
  return parseChinaTime(value)?.getTime() ?? Date.now();
583
590
  }
591
+ function computeAlertFreshCutoffTs(lastPollAt, nowTs, pollIntervalSeconds) {
592
+ const intervalCutoffTs = nowTs - Math.max(1, pollIntervalSeconds) * 1000 - ALERT_FRESHNESS_GRACE_MS;
593
+ const lastPollTs = parseChinaTime(lastPollAt)?.getTime();
594
+ const lastPollCutoffTs = lastPollTs == null
595
+ ? Number.NEGATIVE_INFINITY
596
+ : lastPollTs - ALERT_FRESHNESS_GRACE_MS;
597
+ return Math.max(intervalCutoffTs, lastPollCutoffTs);
598
+ }
584
599
  function isNightQuietHour() {
585
600
  const hour = chinaHour();
586
601
  return hour >= 22 || hour < 6;
@@ -15,12 +15,16 @@ export declare class Jin10McpService {
15
15
  private requestId;
16
16
  private initialized;
17
17
  private sessionId;
18
+ private initializePromise;
18
19
  constructor(serverUrl: string, apiToken: string);
19
20
  isConfigured(): boolean;
20
21
  getConfigurationError(): string | null;
21
22
  listFlash(cursor?: string): Promise<Jin10FlashPage>;
22
23
  private initialize;
23
24
  private callTool;
25
+ private performInitialize;
26
+ private callToolAfterRecovery;
27
+ private resetSession;
24
28
  private request;
25
29
  private notify;
26
30
  private buildHeaders;
@@ -1,9 +1,11 @@
1
+ import { formatConfigEnvFallback } from "../config/env.js";
1
2
  export class Jin10McpService {
2
3
  serverUrl;
3
4
  apiToken;
4
5
  requestId = 1;
5
6
  initialized = false;
6
7
  sessionId = null;
8
+ initializePromise = null;
7
9
  constructor(serverUrl, apiToken) {
8
10
  this.serverUrl = serverUrl;
9
11
  this.apiToken = apiToken;
@@ -13,10 +15,10 @@ export class Jin10McpService {
13
15
  }
14
16
  getConfigurationError() {
15
17
  if (!this.serverUrl.trim()) {
16
- return "Jin10 MCP 未配置接口地址,请设置 jin10McpUrl";
18
+ return `Jin10 MCP 未配置接口地址,请设置 jin10McpUrl 或环境变量 ${formatConfigEnvFallback("jin10McpUrl")}`;
17
19
  }
18
20
  if (!this.apiToken.trim()) {
19
- return "Jin10 MCP 未配置 API Token,请设置 jin10ApiToken";
21
+ return `Jin10 MCP 未配置 API Token,请设置 jin10ApiToken 或环境变量 ${formatConfigEnvFallback("jin10ApiToken")}`;
20
22
  }
21
23
  return null;
22
24
  }
@@ -28,6 +30,52 @@ export class Jin10McpService {
28
30
  if (this.initialized) {
29
31
  return;
30
32
  }
33
+ this.initializePromise ??= this.performInitialize().finally(() => {
34
+ this.initializePromise = null;
35
+ });
36
+ await this.initializePromise;
37
+ }
38
+ async callTool(name, args) {
39
+ try {
40
+ await this.initialize();
41
+ const result = await this.request("tools/call", {
42
+ name,
43
+ arguments: args,
44
+ });
45
+ if (!result) {
46
+ throw new Error(`jin10 tool ${name} returned empty result`);
47
+ }
48
+ if (result.isError) {
49
+ throw new Error(`jin10 tool ${name} returned MCP error`);
50
+ }
51
+ if (result.structuredContent !== undefined) {
52
+ return result.structuredContent;
53
+ }
54
+ const structured = result.content?.find((item) => item.structuredContent !== undefined)?.structuredContent;
55
+ if (structured !== undefined) {
56
+ return structured;
57
+ }
58
+ const text = result.content?.find((item) => typeof item.text === "string")?.text;
59
+ if (typeof text === "string" && text.trim()) {
60
+ try {
61
+ return JSON.parse(text);
62
+ }
63
+ catch {
64
+ return text;
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+ catch (error) {
70
+ if (!isSessionExpiredError(error)) {
71
+ throw error;
72
+ }
73
+ this.resetSession();
74
+ await this.initialize();
75
+ return await this.callToolAfterRecovery(name, args);
76
+ }
77
+ }
78
+ async performInitialize() {
31
79
  await this.request("initialize", {
32
80
  protocolVersion: "2025-11-25",
33
81
  capabilities: {},
@@ -46,8 +94,7 @@ export class Jin10McpService {
46
94
  }
47
95
  this.initialized = true;
48
96
  }
49
- async callTool(name, args) {
50
- await this.initialize();
97
+ async callToolAfterRecovery(name, args) {
51
98
  const result = await this.request("tools/call", {
52
99
  name,
53
100
  arguments: args,
@@ -76,6 +123,11 @@ export class Jin10McpService {
76
123
  }
77
124
  return result;
78
125
  }
126
+ resetSession() {
127
+ this.initialized = false;
128
+ this.sessionId = null;
129
+ this.initializePromise = null;
130
+ }
79
131
  async request(method, params) {
80
132
  const configError = this.getConfigurationError();
81
133
  if (configError) {
@@ -204,6 +256,10 @@ function truncate(value, maxLength) {
204
256
  }
205
257
  return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
206
258
  }
259
+ function isSessionExpiredError(error) {
260
+ const message = error instanceof Error ? error.message : String(error);
261
+ return /session not found|unknown session|invalid session/i.test(message);
262
+ }
207
263
  function normalizeFlashPage(value) {
208
264
  const root = isRecord(value) ? value : {};
209
265
  const data = isRecord(root.data) ? root.data : {};
@@ -47,6 +47,8 @@ export declare class MonitorService {
47
47
  recordLoopError(error: unknown): Promise<void>;
48
48
  private tryAcquireRunLease;
49
49
  private removeStaleRunLock;
50
+ private tryAcquireAlertClaim;
51
+ private removeStaleAlertClaim;
50
52
  private trySendAlert;
51
53
  private trySendCandidate;
52
54
  private buildAlertDelivery;
@@ -56,4 +58,5 @@ export declare class MonitorService {
56
58
  private sendAlertAndCleanupMedia;
57
59
  private cleanupAlertMedia;
58
60
  private getRunLockFilePath;
61
+ private getAlertClaimFilePath;
59
62
  }
@@ -21,6 +21,7 @@ const DEFAULT_STATE = {
21
21
  };
22
22
  const INTRADAY_PERIOD = "1m";
23
23
  const MONITOR_RUN_LOCK_MIN_STALE_MS = 90_000;
24
+ const ALERT_CLAIM_MIN_STALE_MS = 90_000;
24
25
  export class MonitorService {
25
26
  baseDir;
26
27
  requestInterval;
@@ -413,25 +414,83 @@ export class MonitorService {
413
414
  throw error;
414
415
  }
415
416
  }
417
+ async tryAcquireAlertClaim(symbol, ruleName, sessionKey) {
418
+ const lockPath = this.getAlertClaimFilePath(symbol, ruleName, sessionKey);
419
+ await mkdir(path.dirname(lockPath), { recursive: true });
420
+ for (let attempt = 0; attempt < 2; attempt += 1) {
421
+ try {
422
+ await writeFile(lockPath, JSON.stringify({
423
+ pid: process.pid,
424
+ symbol,
425
+ ruleName,
426
+ sessionKey,
427
+ acquiredAt: formatChinaDateTime(),
428
+ }), { flag: "wx" });
429
+ return {
430
+ release: async () => {
431
+ await rm(lockPath, { force: true });
432
+ },
433
+ };
434
+ }
435
+ catch (error) {
436
+ const code = error.code;
437
+ if (code !== "EEXIST") {
438
+ throw error;
439
+ }
440
+ const cleared = await this.removeStaleAlertClaim(lockPath);
441
+ if (!cleared) {
442
+ return null;
443
+ }
444
+ }
445
+ }
446
+ return null;
447
+ }
448
+ async removeStaleAlertClaim(lockPath) {
449
+ try {
450
+ const lockStat = await stat(lockPath);
451
+ const staleMs = Math.max(this.requestInterval * 4 * 1000, ALERT_CLAIM_MIN_STALE_MS);
452
+ if (Date.now() - lockStat.mtimeMs <= staleMs) {
453
+ return false;
454
+ }
455
+ await rm(lockPath, { force: true });
456
+ return true;
457
+ }
458
+ catch (error) {
459
+ if (error.code === "ENOENT") {
460
+ return true;
461
+ }
462
+ throw error;
463
+ }
464
+ }
416
465
  async trySendAlert(symbol, ruleName, input) {
417
466
  const sessionKey = getSessionKey();
418
- if (await this.alertLogRepository.isSentThisSession(symbol, ruleName, sessionKey)) {
467
+ const claim = await this.tryAcquireAlertClaim(symbol, ruleName, sessionKey);
468
+ if (!claim) {
419
469
  await this.cleanupAlertMedia(input);
420
470
  return false;
421
471
  }
422
- const result = await this.sendAlertAndCleanupMedia(input);
423
- if (!result.ok) {
424
- return false;
472
+ try {
473
+ if (await this.alertLogRepository.isSentThisSession(symbol, ruleName, sessionKey)) {
474
+ await this.cleanupAlertMedia(input);
475
+ return false;
476
+ }
477
+ const result = await this.sendAlertAndCleanupMedia(input);
478
+ if (!result.ok) {
479
+ return false;
480
+ }
481
+ const message = typeof input === "string" ? input : input.message;
482
+ await this.alertLogRepository.append({
483
+ symbol,
484
+ alert_date: sessionKey,
485
+ rule_name: ruleName,
486
+ message,
487
+ triggered_at: formatChinaDateTime(),
488
+ });
489
+ return true;
490
+ }
491
+ finally {
492
+ await claim.release();
425
493
  }
426
- const message = typeof input === "string" ? input : input.message;
427
- await this.alertLogRepository.append({
428
- symbol,
429
- alert_date: sessionKey,
430
- rule_name: ruleName,
431
- message,
432
- triggered_at: formatChinaDateTime(),
433
- });
434
- return true;
435
494
  }
436
495
  async trySendCandidate(item, quote, candidate, levels, getIntradayRows) {
437
496
  if (await this.hasSentAlert(item.symbol, candidate.ruleName)) {
@@ -554,6 +613,9 @@ export class MonitorService {
554
613
  getRunLockFilePath() {
555
614
  return path.join(this.baseDir, "monitor-run.lock");
556
615
  }
616
+ getAlertClaimFilePath(symbol, ruleName, sessionKey) {
617
+ return path.join(this.baseDir, "alert-claims", `${sanitizeAlertClaimPart(sessionKey)}_${sanitizeAlertClaimPart(symbol)}_${sanitizeAlertClaimPart(ruleName)}.lock`);
618
+ }
557
619
  }
558
620
  function formatRunningState(state, requestInterval) {
559
621
  const heartbeat = getHeartbeatStatus(state, requestInterval);
@@ -604,6 +666,9 @@ function parseChinaDateTime(value) {
604
666
  const [, year, month, day, hour, minute, second] = match;
605
667
  return Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour) - 8, Number(minute), Number(second));
606
668
  }
669
+ function sanitizeAlertClaimPart(value) {
670
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
671
+ }
607
672
  function formatRuntimeHost(state) {
608
673
  return state.runtimeHost === "plugin_service"
609
674
  ? "plugin_service"
@@ -1,3 +1,4 @@
1
+ import { formatConfigEnvFallback } from "../config/env.js";
1
2
  export class MxSearchServiceError extends Error {
2
3
  constructor(message) {
3
4
  super(message);
@@ -16,10 +17,10 @@ export class MxApiService {
16
17
  }
17
18
  getConfigurationError() {
18
19
  if (!this.apiBaseUrl.trim()) {
19
- return "mx_search 未配置接口地址,请设置 mxSearchApiUrl 或环境变量 MX_SEARCH_API_URL";
20
+ return `mx_search 未配置接口地址,请设置 mxSearchApiUrl 或环境变量 ${formatConfigEnvFallback("mxSearchApiUrl")}`;
20
21
  }
21
22
  if (!this.apiKey.trim()) {
22
- return "mx_search 未配置 API Key,请设置插件配置 mxSearchApiKey 或环境变量 MX_APIKEY";
23
+ return `mx_search 未配置 API Key,请设置插件配置 mxSearchApiKey 或环境变量 ${formatConfigEnvFallback("mxSearchApiKey")}`;
23
24
  }
24
25
  return null;
25
26
  }
@@ -0,0 +1,21 @@
1
+ import { Jin10FlashRepository } from "../storage/repositories/jin10-flash-repo.js";
2
+ import type { DailyUpdateResultType } from "../types/daily-update.js";
3
+ import { AnalysisService } from "./analysis-service.js";
4
+ import { Jin10McpService } from "./jin10-mcp-service.js";
5
+ import { WatchlistService } from "./watchlist-service.js";
6
+ export interface PreMarketBriefRunResult {
7
+ resultType: DailyUpdateResultType;
8
+ message: string;
9
+ sourceCount: number;
10
+ matchedWatchlistCount: number;
11
+ }
12
+ export declare class PreMarketBriefService {
13
+ private readonly watchlistService;
14
+ private readonly jin10McpService;
15
+ private readonly flashRepository;
16
+ private readonly analysisService;
17
+ constructor(watchlistService: WatchlistService, jin10McpService: Jin10McpService, flashRepository: Jin10FlashRepository, analysisService: AnalysisService);
18
+ run(now?: Date): Promise<PreMarketBriefRunResult>;
19
+ private syncWindow;
20
+ private buildSummary;
21
+ }