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.
- package/README.md +13 -6
- package/dist/analysis/parsers/flash-alert-decision.parser.d.ts +8 -0
- package/dist/analysis/parsers/flash-alert-decision.parser.js +34 -0
- package/dist/analysis/types/composite-analysis.d.ts +11 -0
- package/dist/background/jin10-flash.worker.d.ts +8 -0
- package/dist/background/jin10-flash.worker.js +24 -0
- package/dist/bootstrap.d.ts +4 -0
- package/dist/bootstrap.js +18 -1
- package/dist/config/normalize.js +16 -6
- package/dist/config/schema.d.ts +6 -1
- package/dist/config/schema.js +4 -0
- package/dist/dev/run-monitor-loop.js +6 -2
- package/dist/dev/tickflow-assist-cli.js +70 -0
- package/dist/plugin-commands.js +7 -0
- package/dist/prompts/analysis/flash-monitor-alert-prompt.d.ts +11 -0
- package/dist/prompts/analysis/flash-monitor-alert-prompt.js +44 -0
- package/dist/prompts/analysis/index.d.ts +1 -0
- package/dist/prompts/analysis/index.js +1 -0
- package/dist/prompts/analysis/post-close-review-user-prompt.js +18 -0
- package/dist/services/alert-service.d.ts +1 -0
- package/dist/services/alert-service.js +21 -3
- package/dist/services/jin10-flash-monitor-service.d.ts +33 -0
- package/dist/services/jin10-flash-monitor-service.js +587 -0
- package/dist/services/jin10-mcp-service.d.ts +29 -0
- package/dist/services/jin10-mcp-service.js +242 -0
- package/dist/services/post-close-review-service.d.ts +6 -1
- package/dist/services/post-close-review-service.js +35 -1
- package/dist/storage/repositories/jin10-flash-delivery-repo.d.ts +11 -0
- package/dist/storage/repositories/jin10-flash-delivery-repo.js +93 -0
- package/dist/storage/repositories/jin10-flash-repo.d.ts +16 -0
- package/dist/storage/repositories/jin10-flash-repo.js +144 -0
- package/dist/storage/schemas.d.ts +2 -0
- package/dist/storage/schemas.js +19 -0
- package/dist/tools/flash-monitor-status.tool.d.ts +6 -0
- package/dist/tools/flash-monitor-status.tool.js +9 -0
- package/dist/types/flash-monitor.d.ts +17 -0
- package/dist/types/flash-monitor.js +1 -0
- package/dist/types/jin10.d.ts +30 -0
- package/dist/types/jin10.js +1 -0
- package/dist/utils/china-time.d.ts +1 -0
- package/dist/utils/china-time.js +5 -0
- package/openclaw.plugin.json +53 -1
- package/package.json +14 -6
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Jin10FlashPage } from "../types/jin10.js";
|
|
2
|
+
interface JsonRpcResponse<T> {
|
|
3
|
+
jsonrpc: "2.0";
|
|
4
|
+
id?: string | number;
|
|
5
|
+
result?: T;
|
|
6
|
+
error?: {
|
|
7
|
+
code?: number;
|
|
8
|
+
message?: string;
|
|
9
|
+
data?: unknown;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare class Jin10McpService {
|
|
13
|
+
private readonly serverUrl;
|
|
14
|
+
private readonly apiToken;
|
|
15
|
+
private requestId;
|
|
16
|
+
private initialized;
|
|
17
|
+
private sessionId;
|
|
18
|
+
constructor(serverUrl: string, apiToken: string);
|
|
19
|
+
isConfigured(): boolean;
|
|
20
|
+
getConfigurationError(): string | null;
|
|
21
|
+
listFlash(cursor?: string): Promise<Jin10FlashPage>;
|
|
22
|
+
private initialize;
|
|
23
|
+
private callTool;
|
|
24
|
+
private request;
|
|
25
|
+
private notify;
|
|
26
|
+
private buildHeaders;
|
|
27
|
+
}
|
|
28
|
+
export declare function parseJsonRpcResponse<T>(rawText: string, expectedId?: string | number): JsonRpcResponse<T>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
export class Jin10McpService {
|
|
2
|
+
serverUrl;
|
|
3
|
+
apiToken;
|
|
4
|
+
requestId = 1;
|
|
5
|
+
initialized = false;
|
|
6
|
+
sessionId = null;
|
|
7
|
+
constructor(serverUrl, apiToken) {
|
|
8
|
+
this.serverUrl = serverUrl;
|
|
9
|
+
this.apiToken = apiToken;
|
|
10
|
+
}
|
|
11
|
+
isConfigured() {
|
|
12
|
+
return this.getConfigurationError() == null;
|
|
13
|
+
}
|
|
14
|
+
getConfigurationError() {
|
|
15
|
+
if (!this.serverUrl.trim()) {
|
|
16
|
+
return "Jin10 MCP 未配置接口地址,请设置 jin10McpUrl";
|
|
17
|
+
}
|
|
18
|
+
if (!this.apiToken.trim()) {
|
|
19
|
+
return "Jin10 MCP 未配置 API Token,请设置 jin10ApiToken";
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
async listFlash(cursor) {
|
|
24
|
+
const payload = await this.callTool("list_flash", cursor ? { cursor } : {});
|
|
25
|
+
return normalizeFlashPage(payload);
|
|
26
|
+
}
|
|
27
|
+
async initialize() {
|
|
28
|
+
if (this.initialized) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await this.request("initialize", {
|
|
32
|
+
protocolVersion: "2025-11-25",
|
|
33
|
+
capabilities: {},
|
|
34
|
+
clientInfo: {
|
|
35
|
+
name: "mcp-client",
|
|
36
|
+
version: "1.0.0",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
await this.notify("notifications/initialized");
|
|
40
|
+
try {
|
|
41
|
+
await this.request("tools/list", {});
|
|
42
|
+
await this.request("resources/list", {});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Tool listing is a best-effort handshake step.
|
|
46
|
+
}
|
|
47
|
+
this.initialized = true;
|
|
48
|
+
}
|
|
49
|
+
async callTool(name, args) {
|
|
50
|
+
await this.initialize();
|
|
51
|
+
const result = await this.request("tools/call", {
|
|
52
|
+
name,
|
|
53
|
+
arguments: args,
|
|
54
|
+
});
|
|
55
|
+
if (!result) {
|
|
56
|
+
throw new Error(`jin10 tool ${name} returned empty result`);
|
|
57
|
+
}
|
|
58
|
+
if (result.isError) {
|
|
59
|
+
throw new Error(`jin10 tool ${name} returned MCP error`);
|
|
60
|
+
}
|
|
61
|
+
if (result.structuredContent !== undefined) {
|
|
62
|
+
return result.structuredContent;
|
|
63
|
+
}
|
|
64
|
+
const structured = result.content?.find((item) => item.structuredContent !== undefined)?.structuredContent;
|
|
65
|
+
if (structured !== undefined) {
|
|
66
|
+
return structured;
|
|
67
|
+
}
|
|
68
|
+
const text = result.content?.find((item) => typeof item.text === "string")?.text;
|
|
69
|
+
if (typeof text === "string" && text.trim()) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(text);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
async request(method, params) {
|
|
80
|
+
const configError = this.getConfigurationError();
|
|
81
|
+
if (configError) {
|
|
82
|
+
throw new Error(configError);
|
|
83
|
+
}
|
|
84
|
+
const payload = {
|
|
85
|
+
jsonrpc: "2.0",
|
|
86
|
+
id: this.requestId++,
|
|
87
|
+
method,
|
|
88
|
+
params,
|
|
89
|
+
};
|
|
90
|
+
const response = await fetch(this.serverUrl, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: this.buildHeaders(),
|
|
93
|
+
body: JSON.stringify(payload),
|
|
94
|
+
});
|
|
95
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
96
|
+
if (sessionId) {
|
|
97
|
+
this.sessionId = sessionId;
|
|
98
|
+
}
|
|
99
|
+
const rawText = await response.text();
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(`jin10 MCP request failed: ${response.status} ${response.statusText} ${rawText}`);
|
|
102
|
+
}
|
|
103
|
+
const parsed = parseJsonRpcResponse(rawText, payload.id);
|
|
104
|
+
if (parsed.error) {
|
|
105
|
+
throw new Error(`jin10 MCP error (${parsed.error.code ?? "unknown"}): ${parsed.error.message ?? "unknown"}`);
|
|
106
|
+
}
|
|
107
|
+
return parsed.result;
|
|
108
|
+
}
|
|
109
|
+
async notify(method) {
|
|
110
|
+
const configError = this.getConfigurationError();
|
|
111
|
+
if (configError) {
|
|
112
|
+
throw new Error(configError);
|
|
113
|
+
}
|
|
114
|
+
const payload = {
|
|
115
|
+
jsonrpc: "2.0",
|
|
116
|
+
method,
|
|
117
|
+
};
|
|
118
|
+
const response = await fetch(this.serverUrl, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: this.buildHeaders(),
|
|
121
|
+
body: JSON.stringify(payload),
|
|
122
|
+
});
|
|
123
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
124
|
+
if (sessionId) {
|
|
125
|
+
this.sessionId = sessionId;
|
|
126
|
+
}
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const text = await response.text();
|
|
129
|
+
throw new Error(`jin10 MCP notification failed: ${response.status} ${response.statusText} ${text}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
buildHeaders() {
|
|
133
|
+
const headers = {
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
136
|
+
};
|
|
137
|
+
if (this.sessionId) {
|
|
138
|
+
headers["Mcp-Session-Id"] = this.sessionId;
|
|
139
|
+
}
|
|
140
|
+
return headers;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function parseJsonRpcResponse(rawText, expectedId) {
|
|
144
|
+
const trimmed = rawText.trim();
|
|
145
|
+
if (!trimmed) {
|
|
146
|
+
throw new Error("jin10 MCP returned empty body");
|
|
147
|
+
}
|
|
148
|
+
if (!looksLikeSsePayload(trimmed)) {
|
|
149
|
+
return JSON.parse(trimmed);
|
|
150
|
+
}
|
|
151
|
+
const candidates = parseSseJsonRpcResponses(trimmed);
|
|
152
|
+
if (candidates.length === 0) {
|
|
153
|
+
throw new Error(`jin10 MCP SSE payload missing JSON-RPC data: ${truncate(trimmed, 160)}`);
|
|
154
|
+
}
|
|
155
|
+
if (expectedId !== undefined) {
|
|
156
|
+
const matched = candidates.find((entry) => entry.id === expectedId);
|
|
157
|
+
if (matched) {
|
|
158
|
+
return matched;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const withResult = candidates.find((entry) => entry.result !== undefined || entry.error !== undefined);
|
|
162
|
+
if (withResult) {
|
|
163
|
+
return withResult;
|
|
164
|
+
}
|
|
165
|
+
return candidates[candidates.length - 1];
|
|
166
|
+
}
|
|
167
|
+
function parseSseJsonRpcResponses(rawText) {
|
|
168
|
+
const events = rawText
|
|
169
|
+
.split(/\r?\n\r?\n/)
|
|
170
|
+
.map((chunk) => chunk.trim())
|
|
171
|
+
.filter(Boolean);
|
|
172
|
+
const responses = [];
|
|
173
|
+
for (const eventText of events) {
|
|
174
|
+
const payload = extractSseData(eventText);
|
|
175
|
+
if (!payload || payload === "[DONE]") {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
responses.push(JSON.parse(payload));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return responses;
|
|
186
|
+
}
|
|
187
|
+
function extractSseData(eventText) {
|
|
188
|
+
const dataLines = eventText
|
|
189
|
+
.split(/\r?\n/)
|
|
190
|
+
.filter((line) => line.startsWith("data:"))
|
|
191
|
+
.map((line) => line.replace(/^data:\s?/, ""));
|
|
192
|
+
return dataLines.join("\n").trim();
|
|
193
|
+
}
|
|
194
|
+
function looksLikeSsePayload(value) {
|
|
195
|
+
return value.startsWith("data:")
|
|
196
|
+
|| value.startsWith("event:")
|
|
197
|
+
|| value.startsWith(":")
|
|
198
|
+
|| /\r?\ndata:/.test(value)
|
|
199
|
+
|| /\r?\nevent:/.test(value);
|
|
200
|
+
}
|
|
201
|
+
function truncate(value, maxLength) {
|
|
202
|
+
if (value.length <= maxLength) {
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
206
|
+
}
|
|
207
|
+
function normalizeFlashPage(value) {
|
|
208
|
+
const root = isRecord(value) ? value : {};
|
|
209
|
+
const data = isRecord(root.data) ? root.data : {};
|
|
210
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
211
|
+
return {
|
|
212
|
+
hasMore: data.has_more === true,
|
|
213
|
+
items: items
|
|
214
|
+
.map((item) => normalizeFlashItem(item))
|
|
215
|
+
.filter((item) => item != null),
|
|
216
|
+
nextCursor: normalizeNullableString(data.next_cursor),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function normalizeFlashItem(value) {
|
|
220
|
+
if (!isRecord(value)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const content = String(value.content ?? "").trim();
|
|
224
|
+
const time = String(value.time ?? "").trim();
|
|
225
|
+
const url = String(value.url ?? "").trim();
|
|
226
|
+
if (!content || !time || !url) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
content,
|
|
231
|
+
time,
|
|
232
|
+
url,
|
|
233
|
+
raw: value,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function normalizeNullableString(value) {
|
|
237
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
238
|
+
return text || null;
|
|
239
|
+
}
|
|
240
|
+
function isRecord(value) {
|
|
241
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
242
|
+
}
|
|
@@ -8,6 +8,8 @@ import { KeyLevelsRepository } from "../storage/repositories/key-levels-repo.js"
|
|
|
8
8
|
import { KeyLevelsHistoryRepository } from "../storage/repositories/key-levels-history-repo.js";
|
|
9
9
|
import { KlinesRepository } from "../storage/repositories/klines-repo.js";
|
|
10
10
|
import { IntradayKlinesRepository } from "../storage/repositories/intraday-klines-repo.js";
|
|
11
|
+
import { Jin10FlashDeliveryRepository } from "../storage/repositories/jin10-flash-delivery-repo.js";
|
|
12
|
+
import { Jin10FlashRepository } from "../storage/repositories/jin10-flash-repo.js";
|
|
11
13
|
export interface PostCloseReviewRunResult {
|
|
12
14
|
overviewMessage: string;
|
|
13
15
|
detailMessages: string[];
|
|
@@ -22,11 +24,14 @@ export declare class PostCloseReviewService {
|
|
|
22
24
|
private readonly keyLevelsHistoryRepository;
|
|
23
25
|
private readonly klinesRepository;
|
|
24
26
|
private readonly intradayKlinesRepository;
|
|
25
|
-
|
|
27
|
+
private readonly flashDeliveryRepository;
|
|
28
|
+
private readonly flashRepository;
|
|
29
|
+
constructor(watchlistService: WatchlistService, compositeAnalysisOrchestrator: CompositeAnalysisOrchestrator, analysisService: AnalysisService, postCloseReviewTask: PostCloseReviewTask, keyLevelsRepository: KeyLevelsRepository, keyLevelsHistoryRepository: KeyLevelsHistoryRepository, klinesRepository: KlinesRepository, intradayKlinesRepository: IntradayKlinesRepository, flashDeliveryRepository: Jin10FlashDeliveryRepository, flashRepository: Jin10FlashRepository);
|
|
26
30
|
run(): Promise<PostCloseReviewRunResult>;
|
|
27
31
|
private persistReview;
|
|
28
32
|
private persistFallbackCompositeReview;
|
|
29
33
|
private buildValidationContext;
|
|
34
|
+
private buildFlashContext;
|
|
30
35
|
private formatOverviewMessage;
|
|
31
36
|
private formatDetailMessage;
|
|
32
37
|
private formatFailureMessage;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { formatChinaDateTime } from "../utils/china-time.js";
|
|
2
2
|
const LEVEL_BUFFER = 0.005;
|
|
3
3
|
const INTRADAY_PERIOD = "1m";
|
|
4
|
+
const MARKET_OVERVIEW_FLASH_KEYWORDS = [
|
|
5
|
+
"港股收评",
|
|
6
|
+
"金十数据整理:每日投行/机构观点梳理",
|
|
7
|
+
"金十数据整理:A股每日市场要闻回顾",
|
|
8
|
+
];
|
|
4
9
|
export class PostCloseReviewService {
|
|
5
10
|
watchlistService;
|
|
6
11
|
compositeAnalysisOrchestrator;
|
|
@@ -10,7 +15,9 @@ export class PostCloseReviewService {
|
|
|
10
15
|
keyLevelsHistoryRepository;
|
|
11
16
|
klinesRepository;
|
|
12
17
|
intradayKlinesRepository;
|
|
13
|
-
|
|
18
|
+
flashDeliveryRepository;
|
|
19
|
+
flashRepository;
|
|
20
|
+
constructor(watchlistService, compositeAnalysisOrchestrator, analysisService, postCloseReviewTask, keyLevelsRepository, keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository, flashDeliveryRepository, flashRepository) {
|
|
14
21
|
this.watchlistService = watchlistService;
|
|
15
22
|
this.compositeAnalysisOrchestrator = compositeAnalysisOrchestrator;
|
|
16
23
|
this.analysisService = analysisService;
|
|
@@ -19,6 +26,8 @@ export class PostCloseReviewService {
|
|
|
19
26
|
this.keyLevelsHistoryRepository = keyLevelsHistoryRepository;
|
|
20
27
|
this.klinesRepository = klinesRepository;
|
|
21
28
|
this.intradayKlinesRepository = intradayKlinesRepository;
|
|
29
|
+
this.flashDeliveryRepository = flashDeliveryRepository;
|
|
30
|
+
this.flashRepository = flashRepository;
|
|
22
31
|
}
|
|
23
32
|
async run() {
|
|
24
33
|
const watchlist = await this.watchlistService.list();
|
|
@@ -41,10 +50,12 @@ export class PostCloseReviewService {
|
|
|
41
50
|
const tradeDate = input.market.klines[input.market.klines.length - 1]?.trade_date ?? formatChinaDateTime().slice(0, 10);
|
|
42
51
|
const validation = await this.buildValidationContext(item.symbol, tradeDate);
|
|
43
52
|
compositeResult = await this.compositeAnalysisOrchestrator.analyzeInput(input);
|
|
53
|
+
const flashContext = await this.buildFlashContext(item.symbol, tradeDate);
|
|
44
54
|
const review = await this.analysisService.runTask(this.postCloseReviewTask, {
|
|
45
55
|
...input,
|
|
46
56
|
compositeResult,
|
|
47
57
|
validation,
|
|
58
|
+
flashContext,
|
|
48
59
|
});
|
|
49
60
|
const message = this.formatDetailMessage(item, validation, review);
|
|
50
61
|
await this.persistReview(item.symbol, message, review);
|
|
@@ -154,6 +165,25 @@ export class PostCloseReviewService {
|
|
|
154
165
|
lines,
|
|
155
166
|
};
|
|
156
167
|
}
|
|
168
|
+
async buildFlashContext(symbol, datePrefix) {
|
|
169
|
+
const [deliveries, overviewFlashes] = await Promise.all([
|
|
170
|
+
this.flashDeliveryRepository.listBySymbolsAndDate([symbol], datePrefix),
|
|
171
|
+
this.flashRepository.searchByContentKeywords(MARKET_OVERVIEW_FLASH_KEYWORDS, datePrefix),
|
|
172
|
+
]);
|
|
173
|
+
const stockAlerts = deliveries.map((entry) => ({
|
|
174
|
+
publishedAt: entry.published_at,
|
|
175
|
+
content: entry.reason,
|
|
176
|
+
headline: entry.headline,
|
|
177
|
+
source: "stock_alert",
|
|
178
|
+
}));
|
|
179
|
+
const marketOverviewFlashes = overviewFlashes.map((record) => ({
|
|
180
|
+
publishedAt: record.published_at,
|
|
181
|
+
content: record.content,
|
|
182
|
+
headline: extractHeadlineFromContent(record.content),
|
|
183
|
+
source: "market_overview",
|
|
184
|
+
}));
|
|
185
|
+
return { stockAlerts, marketOverviewFlashes };
|
|
186
|
+
}
|
|
157
187
|
formatOverviewMessage(marketOverview, entries) {
|
|
158
188
|
const successEntries = entries.filter((entry) => entry.ok);
|
|
159
189
|
const failureCount = entries.length - successEntries.length;
|
|
@@ -496,3 +526,7 @@ function formatPriceRail(markers) {
|
|
|
496
526
|
.map((entry) => `${entry.parts.join("/")} ${entry.value.toFixed(2)}`)
|
|
497
527
|
.join(" → ");
|
|
498
528
|
}
|
|
529
|
+
function extractHeadlineFromContent(content) {
|
|
530
|
+
const firstLine = content.split(/[\n。!!]/)[0]?.trim() ?? "";
|
|
531
|
+
return firstLine.length > 60 ? `${firstLine.slice(0, 60)}...` : firstLine;
|
|
532
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Jin10FlashDeliveryEntry } from "../../types/jin10.js";
|
|
2
|
+
import { Database } from "../db.js";
|
|
3
|
+
export declare class Jin10FlashDeliveryRepository {
|
|
4
|
+
private readonly db;
|
|
5
|
+
constructor(db: Database);
|
|
6
|
+
append(entry: Jin10FlashDeliveryEntry): Promise<void>;
|
|
7
|
+
hasDelivered(flashKey: string): Promise<boolean>;
|
|
8
|
+
countSinceDeliveredAt(deliveredAt: string): Promise<number>;
|
|
9
|
+
listBySymbolsAndDate(symbols: string[], datePrefix: string): Promise<Jin10FlashDeliveryEntry[]>;
|
|
10
|
+
pruneOlderThanDeliveredAt(deliveredAt: string): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jin10FlashDeliverySchema } from "../schemas.js";
|
|
2
|
+
const JIN10_FLASH_DELIVERY_TABLE = "jin10_flash_delivery";
|
|
3
|
+
export class Jin10FlashDeliveryRepository {
|
|
4
|
+
db;
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this.db = db;
|
|
7
|
+
}
|
|
8
|
+
async append(entry) {
|
|
9
|
+
const row = toDeliveryRow(entry);
|
|
10
|
+
if (!(await this.db.hasTable(JIN10_FLASH_DELIVERY_TABLE))) {
|
|
11
|
+
await this.db.createTable(JIN10_FLASH_DELIVERY_TABLE, [row], jin10FlashDeliverySchema);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const table = await this.db.openTable(JIN10_FLASH_DELIVERY_TABLE);
|
|
15
|
+
await table.add([row]);
|
|
16
|
+
}
|
|
17
|
+
async hasDelivered(flashKey) {
|
|
18
|
+
if (!flashKey || !(await this.db.hasTable(JIN10_FLASH_DELIVERY_TABLE))) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const table = await this.db.openTable(JIN10_FLASH_DELIVERY_TABLE);
|
|
22
|
+
const rows = (await table
|
|
23
|
+
.query()
|
|
24
|
+
.where(`flash_key = '${escapeSqlString(flashKey)}'`)
|
|
25
|
+
.toArray());
|
|
26
|
+
return rows.length > 0;
|
|
27
|
+
}
|
|
28
|
+
async countSinceDeliveredAt(deliveredAt) {
|
|
29
|
+
if (!(await this.db.hasTable(JIN10_FLASH_DELIVERY_TABLE))) {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
const table = await this.db.openTable(JIN10_FLASH_DELIVERY_TABLE);
|
|
33
|
+
return table.countRows(`delivered_at >= '${escapeSqlString(deliveredAt)}'`);
|
|
34
|
+
}
|
|
35
|
+
async listBySymbolsAndDate(symbols, datePrefix) {
|
|
36
|
+
if (symbols.length === 0 || !(await this.db.hasTable(JIN10_FLASH_DELIVERY_TABLE))) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const table = await this.db.openTable(JIN10_FLASH_DELIVERY_TABLE);
|
|
40
|
+
const rows = (await table
|
|
41
|
+
.query()
|
|
42
|
+
.where(`delivered_at >= '${escapeSqlString(datePrefix)} 00:00:00'`)
|
|
43
|
+
.toArray());
|
|
44
|
+
const symbolSet = new Set(symbols);
|
|
45
|
+
return rows
|
|
46
|
+
.map((row) => fromDeliveryRow(row))
|
|
47
|
+
.filter((entry) => entry.symbols.some((s) => symbolSet.has(s)));
|
|
48
|
+
}
|
|
49
|
+
async pruneOlderThanDeliveredAt(deliveredAt) {
|
|
50
|
+
if (!(await this.db.hasTable(JIN10_FLASH_DELIVERY_TABLE))) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const table = await this.db.openTable(JIN10_FLASH_DELIVERY_TABLE);
|
|
54
|
+
await table.delete(`delivered_at < '${escapeSqlString(deliveredAt)}'`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function toDeliveryRow(entry) {
|
|
58
|
+
return {
|
|
59
|
+
flash_key: entry.flash_key,
|
|
60
|
+
published_at: entry.published_at,
|
|
61
|
+
symbols_json: JSON.stringify(entry.symbols),
|
|
62
|
+
headline: entry.headline,
|
|
63
|
+
reason: entry.reason,
|
|
64
|
+
importance: entry.importance,
|
|
65
|
+
message: entry.message,
|
|
66
|
+
delivered_at: entry.delivered_at,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function fromDeliveryRow(row) {
|
|
70
|
+
let symbols = [];
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(String(row.symbols_json ?? "[]"));
|
|
73
|
+
if (Array.isArray(parsed)) {
|
|
74
|
+
symbols = parsed.map((v) => String(v));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// ignore
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
flash_key: String(row.flash_key ?? ""),
|
|
82
|
+
published_at: String(row.published_at ?? ""),
|
|
83
|
+
symbols,
|
|
84
|
+
headline: String(row.headline ?? ""),
|
|
85
|
+
reason: String(row.reason ?? ""),
|
|
86
|
+
importance: String(row.importance ?? "medium"),
|
|
87
|
+
message: String(row.message ?? ""),
|
|
88
|
+
delivered_at: String(row.delivered_at ?? ""),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function escapeSqlString(value) {
|
|
92
|
+
return value.replace(/'/g, "''");
|
|
93
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Jin10FlashRecord } from "../../types/jin10.js";
|
|
2
|
+
import { Database } from "../db.js";
|
|
3
|
+
export declare class Jin10FlashRepository {
|
|
4
|
+
private readonly db;
|
|
5
|
+
constructor(db: Database);
|
|
6
|
+
saveAll(entries: Jin10FlashRecord[]): Promise<{
|
|
7
|
+
added: number;
|
|
8
|
+
skipped: number;
|
|
9
|
+
addedKeys: string[];
|
|
10
|
+
}>;
|
|
11
|
+
getLatest(): Promise<Jin10FlashRecord | null>;
|
|
12
|
+
countSincePublishedTs(publishedTs: number): Promise<number>;
|
|
13
|
+
searchByContentKeywords(keywords: string[], datePrefix: string): Promise<Jin10FlashRecord[]>;
|
|
14
|
+
pruneOlderThanPublishedTs(publishedTs: number): Promise<void>;
|
|
15
|
+
private listExistingKeys;
|
|
16
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { jin10FlashSchema } from "../schemas.js";
|
|
2
|
+
const JIN10_FLASH_TABLE = "jin10_flash";
|
|
3
|
+
export class Jin10FlashRepository {
|
|
4
|
+
db;
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this.db = db;
|
|
7
|
+
}
|
|
8
|
+
async saveAll(entries) {
|
|
9
|
+
const uniqueEntries = dedupeEntries(entries);
|
|
10
|
+
if (uniqueEntries.length === 0) {
|
|
11
|
+
return { added: 0, skipped: 0, addedKeys: [] };
|
|
12
|
+
}
|
|
13
|
+
const rows = uniqueEntries.map((entry) => toFlashRow(entry));
|
|
14
|
+
if (!(await this.db.hasTable(JIN10_FLASH_TABLE))) {
|
|
15
|
+
await this.db.createTable(JIN10_FLASH_TABLE, rows, jin10FlashSchema);
|
|
16
|
+
return {
|
|
17
|
+
added: rows.length,
|
|
18
|
+
skipped: 0,
|
|
19
|
+
addedKeys: uniqueEntries.map((entry) => entry.flash_key),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const existingKeys = await this.listExistingKeys(uniqueEntries.map((entry) => entry.flash_key));
|
|
23
|
+
const newEntries = uniqueEntries.filter((entry) => !existingKeys.has(entry.flash_key));
|
|
24
|
+
const newRows = newEntries.map((entry) => toFlashRow(entry));
|
|
25
|
+
if (newRows.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
added: 0,
|
|
28
|
+
skipped: rows.length,
|
|
29
|
+
addedKeys: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const table = await this.db.openTable(JIN10_FLASH_TABLE);
|
|
33
|
+
await table.add(newRows);
|
|
34
|
+
return {
|
|
35
|
+
added: newRows.length,
|
|
36
|
+
skipped: rows.length - newRows.length,
|
|
37
|
+
addedKeys: newEntries.map((entry) => entry.flash_key),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async getLatest() {
|
|
41
|
+
if (!(await this.db.hasTable(JIN10_FLASH_TABLE))) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const rows = await this.db.tableToArray(JIN10_FLASH_TABLE);
|
|
45
|
+
if (rows.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return fromFlashRow(rows[rows.length - 1]);
|
|
49
|
+
}
|
|
50
|
+
async countSincePublishedTs(publishedTs) {
|
|
51
|
+
if (!(await this.db.hasTable(JIN10_FLASH_TABLE))) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
const table = await this.db.openTable(JIN10_FLASH_TABLE);
|
|
55
|
+
return table.countRows(`published_ts >= ${Math.trunc(publishedTs)}`);
|
|
56
|
+
}
|
|
57
|
+
async searchByContentKeywords(keywords, datePrefix) {
|
|
58
|
+
if (keywords.length === 0 || !(await this.db.hasTable(JIN10_FLASH_TABLE))) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const dayStart = `${datePrefix} 00:00:00`;
|
|
62
|
+
const dayStartTs = new Date(`${dayStart.replace(" ", "T")}+08:00`).getTime();
|
|
63
|
+
if (Number.isNaN(dayStartTs)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const table = await this.db.openTable(JIN10_FLASH_TABLE);
|
|
67
|
+
const rows = (await table
|
|
68
|
+
.query()
|
|
69
|
+
.where(`published_ts >= ${Math.trunc(dayStartTs)}`)
|
|
70
|
+
.toArray());
|
|
71
|
+
return rows
|
|
72
|
+
.map((row) => fromFlashRow(row))
|
|
73
|
+
.filter((record) => keywords.some((kw) => record.content.includes(kw)));
|
|
74
|
+
}
|
|
75
|
+
async pruneOlderThanPublishedTs(publishedTs) {
|
|
76
|
+
if (!(await this.db.hasTable(JIN10_FLASH_TABLE))) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const table = await this.db.openTable(JIN10_FLASH_TABLE);
|
|
80
|
+
await table.delete(`published_ts < ${Math.trunc(publishedTs)}`);
|
|
81
|
+
}
|
|
82
|
+
async listExistingKeys(keys) {
|
|
83
|
+
if (keys.length === 0 || !(await this.db.hasTable(JIN10_FLASH_TABLE))) {
|
|
84
|
+
return new Set();
|
|
85
|
+
}
|
|
86
|
+
const table = await this.db.openTable(JIN10_FLASH_TABLE);
|
|
87
|
+
const rows = (await table
|
|
88
|
+
.query()
|
|
89
|
+
.where(`flash_key IN (${keys.map((key) => `'${escapeSqlString(key)}'`).join(", ")})`)
|
|
90
|
+
.toArray());
|
|
91
|
+
return new Set(rows.map((row) => String(row.flash_key ?? "")));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function toFlashRow(entry) {
|
|
95
|
+
return {
|
|
96
|
+
flash_key: entry.flash_key,
|
|
97
|
+
published_at: entry.published_at,
|
|
98
|
+
published_ts: Math.trunc(entry.published_ts),
|
|
99
|
+
content: entry.content,
|
|
100
|
+
url: entry.url,
|
|
101
|
+
ingested_at: entry.ingested_at,
|
|
102
|
+
raw_json: JSON.stringify(entry.raw),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function fromFlashRow(row) {
|
|
106
|
+
return {
|
|
107
|
+
flash_key: String(row.flash_key ?? ""),
|
|
108
|
+
published_at: String(row.published_at ?? ""),
|
|
109
|
+
published_ts: Number(row.published_ts ?? 0),
|
|
110
|
+
content: String(row.content ?? ""),
|
|
111
|
+
url: String(row.url ?? ""),
|
|
112
|
+
ingested_at: String(row.ingested_at ?? ""),
|
|
113
|
+
raw: parseJsonObject(row.raw_json),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function dedupeEntries(entries) {
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
const result = [];
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (!entry.flash_key || seen.has(entry.flash_key)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
seen.add(entry.flash_key);
|
|
124
|
+
result.push(entry);
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function parseJsonObject(value) {
|
|
129
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(value);
|
|
134
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
135
|
+
? parsed
|
|
136
|
+
: {};
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function escapeSqlString(value) {
|
|
143
|
+
return value.replace(/'/g, "''");
|
|
144
|
+
}
|
|
@@ -11,3 +11,5 @@ export declare const financialAnalysisSchema: Schema<any>;
|
|
|
11
11
|
export declare const newsAnalysisSchema: Schema<any>;
|
|
12
12
|
export declare const compositeAnalysisSchema: Schema<any>;
|
|
13
13
|
export declare const alertLogSchema: Schema<any>;
|
|
14
|
+
export declare const jin10FlashSchema: Schema<any>;
|
|
15
|
+
export declare const jin10FlashDeliverySchema: Schema<any>;
|
package/dist/storage/schemas.js
CHANGED
|
@@ -175,3 +175,22 @@ export const alertLogSchema = new Schema([
|
|
|
175
175
|
new Field("message", new Utf8(), false),
|
|
176
176
|
new Field("triggered_at", new Utf8(), false),
|
|
177
177
|
]);
|
|
178
|
+
export const jin10FlashSchema = new Schema([
|
|
179
|
+
new Field("flash_key", new Utf8(), false),
|
|
180
|
+
new Field("published_at", new Utf8(), false),
|
|
181
|
+
new Field("published_ts", new Int64(), false),
|
|
182
|
+
new Field("content", new Utf8(), false),
|
|
183
|
+
new Field("url", new Utf8(), false),
|
|
184
|
+
new Field("ingested_at", new Utf8(), false),
|
|
185
|
+
new Field("raw_json", new Utf8(), false),
|
|
186
|
+
]);
|
|
187
|
+
export const jin10FlashDeliverySchema = new Schema([
|
|
188
|
+
new Field("flash_key", new Utf8(), false),
|
|
189
|
+
new Field("published_at", new Utf8(), false),
|
|
190
|
+
new Field("symbols_json", new Utf8(), false),
|
|
191
|
+
new Field("headline", new Utf8(), false),
|
|
192
|
+
new Field("reason", new Utf8(), false),
|
|
193
|
+
new Field("importance", new Utf8(), false),
|
|
194
|
+
new Field("message", new Utf8(), false),
|
|
195
|
+
new Field("delivered_at", new Utf8(), false),
|
|
196
|
+
]);
|