tickflow-assist 0.2.19 → 0.3.1
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 +12 -5
- 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/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 +17 -0
- package/dist/config/normalize.js +15 -6
- package/dist/config/schema.d.ts +5 -1
- package/dist/config/schema.js +3 -0
- package/dist/dev/run-monitor-loop.js +6 -2
- package/dist/dev/tickflow-assist-cli.js +45 -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/services/alert-service.d.ts +1 -0
- package/dist/services/alert-service.js +21 -3
- package/dist/services/jin10-flash-monitor-service.d.ts +32 -0
- package/dist/services/jin10-flash-monitor-service.js +578 -0
- package/dist/services/jin10-mcp-service.d.ts +29 -0
- package/dist/services/jin10-mcp-service.js +242 -0
- package/dist/storage/repositories/jin10-flash-delivery-repo.d.ts +10 -0
- package/dist/storage/repositories/jin10-flash-delivery-repo.js +57 -0
- package/dist/storage/repositories/jin10-flash-repo.d.ts +15 -0
- package/dist/storage/repositories/jin10-flash-repo.js +126 -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/openclaw.plugin.json +43 -1
- package/package.json +11 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
pruneOlderThanDeliveredAt(deliveredAt: string): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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 pruneOlderThanDeliveredAt(deliveredAt) {
|
|
36
|
+
if (!(await this.db.hasTable(JIN10_FLASH_DELIVERY_TABLE))) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const table = await this.db.openTable(JIN10_FLASH_DELIVERY_TABLE);
|
|
40
|
+
await table.delete(`delivered_at < '${escapeSqlString(deliveredAt)}'`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function toDeliveryRow(entry) {
|
|
44
|
+
return {
|
|
45
|
+
flash_key: entry.flash_key,
|
|
46
|
+
published_at: entry.published_at,
|
|
47
|
+
symbols_json: JSON.stringify(entry.symbols),
|
|
48
|
+
headline: entry.headline,
|
|
49
|
+
reason: entry.reason,
|
|
50
|
+
importance: entry.importance,
|
|
51
|
+
message: entry.message,
|
|
52
|
+
delivered_at: entry.delivered_at,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function escapeSqlString(value) {
|
|
56
|
+
return value.replace(/'/g, "''");
|
|
57
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
pruneOlderThanPublishedTs(publishedTs: number): Promise<void>;
|
|
14
|
+
private listExistingKeys;
|
|
15
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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 pruneOlderThanPublishedTs(publishedTs) {
|
|
58
|
+
if (!(await this.db.hasTable(JIN10_FLASH_TABLE))) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const table = await this.db.openTable(JIN10_FLASH_TABLE);
|
|
62
|
+
await table.delete(`published_ts < ${Math.trunc(publishedTs)}`);
|
|
63
|
+
}
|
|
64
|
+
async listExistingKeys(keys) {
|
|
65
|
+
if (keys.length === 0 || !(await this.db.hasTable(JIN10_FLASH_TABLE))) {
|
|
66
|
+
return new Set();
|
|
67
|
+
}
|
|
68
|
+
const table = await this.db.openTable(JIN10_FLASH_TABLE);
|
|
69
|
+
const rows = (await table
|
|
70
|
+
.query()
|
|
71
|
+
.where(`flash_key IN (${keys.map((key) => `'${escapeSqlString(key)}'`).join(", ")})`)
|
|
72
|
+
.toArray());
|
|
73
|
+
return new Set(rows.map((row) => String(row.flash_key ?? "")));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function toFlashRow(entry) {
|
|
77
|
+
return {
|
|
78
|
+
flash_key: entry.flash_key,
|
|
79
|
+
published_at: entry.published_at,
|
|
80
|
+
published_ts: Math.trunc(entry.published_ts),
|
|
81
|
+
content: entry.content,
|
|
82
|
+
url: entry.url,
|
|
83
|
+
ingested_at: entry.ingested_at,
|
|
84
|
+
raw_json: JSON.stringify(entry.raw),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function fromFlashRow(row) {
|
|
88
|
+
return {
|
|
89
|
+
flash_key: String(row.flash_key ?? ""),
|
|
90
|
+
published_at: String(row.published_at ?? ""),
|
|
91
|
+
published_ts: Number(row.published_ts ?? 0),
|
|
92
|
+
content: String(row.content ?? ""),
|
|
93
|
+
url: String(row.url ?? ""),
|
|
94
|
+
ingested_at: String(row.ingested_at ?? ""),
|
|
95
|
+
raw: parseJsonObject(row.raw_json),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function dedupeEntries(entries) {
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const result = [];
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (!entry.flash_key || seen.has(entry.flash_key)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
seen.add(entry.flash_key);
|
|
106
|
+
result.push(entry);
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
function parseJsonObject(value) {
|
|
111
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(value);
|
|
116
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
117
|
+
? parsed
|
|
118
|
+
: {};
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function escapeSqlString(value) {
|
|
125
|
+
return value.replace(/'/g, "''");
|
|
126
|
+
}
|
|
@@ -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
|
+
]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function flashMonitorStatusTool(flashMonitorService) {
|
|
2
|
+
return {
|
|
3
|
+
name: "flash_monitor_status",
|
|
4
|
+
description: "Show Jin10 flash monitor state, recent poll summary, and storage counters.",
|
|
5
|
+
async run() {
|
|
6
|
+
return flashMonitorService.getStatusReport();
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface FlashMonitorState {
|
|
2
|
+
initialized: boolean;
|
|
3
|
+
lastSeenKey: string | null;
|
|
4
|
+
lastSeenPublishedAt: string | null;
|
|
5
|
+
lastSeenUrl: string | null;
|
|
6
|
+
backfillCursor: string | null;
|
|
7
|
+
runtimeHost: "plugin_service" | "fallback_process" | null;
|
|
8
|
+
runtimeObservedAt: string | null;
|
|
9
|
+
lastHeartbeatAt: string | null;
|
|
10
|
+
lastPollAt: string | null;
|
|
11
|
+
lastPollStored: number;
|
|
12
|
+
lastPollCandidates: number;
|
|
13
|
+
lastPollAlerts: number;
|
|
14
|
+
lastPrunedAt: string | null;
|
|
15
|
+
lastLoopError: string | null;
|
|
16
|
+
lastLoopErrorAt: string | null;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface Jin10FlashItem {
|
|
2
|
+
content: string;
|
|
3
|
+
time: string;
|
|
4
|
+
url: string;
|
|
5
|
+
raw: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface Jin10FlashPage {
|
|
8
|
+
hasMore: boolean;
|
|
9
|
+
items: Jin10FlashItem[];
|
|
10
|
+
nextCursor: string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface Jin10FlashRecord {
|
|
13
|
+
flash_key: string;
|
|
14
|
+
published_at: string;
|
|
15
|
+
published_ts: number;
|
|
16
|
+
content: string;
|
|
17
|
+
url: string;
|
|
18
|
+
ingested_at: string;
|
|
19
|
+
raw: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export interface Jin10FlashDeliveryEntry {
|
|
22
|
+
flash_key: string;
|
|
23
|
+
published_at: string;
|
|
24
|
+
symbols: string[];
|
|
25
|
+
headline: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
importance: "high" | "medium" | "low";
|
|
28
|
+
message: string;
|
|
29
|
+
delivered_at: string;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "tickflow-assist",
|
|
3
3
|
"name": "TickFlow Assist",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "A-share watchlist analysis, monitoring, and alert delivery powered by TickFlow and OpenClaw.",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills"
|
|
@@ -36,6 +36,28 @@
|
|
|
36
36
|
"default": "",
|
|
37
37
|
"description": "Optional. Enables mx_search, mx_select_stock, and non-Expert financial fallback."
|
|
38
38
|
},
|
|
39
|
+
"jin10McpUrl": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"default": "https://mcp.jin10.com/mcp",
|
|
42
|
+
"description": "Jin10 MCP server URL used for realtime flash monitoring."
|
|
43
|
+
},
|
|
44
|
+
"jin10ApiToken": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"default": "",
|
|
47
|
+
"description": "Optional. Enables 24/7 Jin10 realtime flash monitoring when configured."
|
|
48
|
+
},
|
|
49
|
+
"jin10FlashPollInterval": {
|
|
50
|
+
"type": "integer",
|
|
51
|
+
"minimum": 10,
|
|
52
|
+
"default": 300,
|
|
53
|
+
"description": "Jin10 flash polling interval in seconds."
|
|
54
|
+
},
|
|
55
|
+
"jin10FlashRetentionDays": {
|
|
56
|
+
"type": "integer",
|
|
57
|
+
"minimum": 1,
|
|
58
|
+
"default": 7,
|
|
59
|
+
"description": "How many days of raw Jin10 flash data to retain locally."
|
|
60
|
+
},
|
|
39
61
|
"llmBaseUrl": {
|
|
40
62
|
"type": "string",
|
|
41
63
|
"default": "https://api.openai.com/v1",
|
|
@@ -125,6 +147,26 @@
|
|
|
125
147
|
"help": "Optional. Enables mx_search, mx_select_stock, and lite financial fallback.",
|
|
126
148
|
"sensitive": true
|
|
127
149
|
},
|
|
150
|
+
"jin10McpUrl": {
|
|
151
|
+
"label": "Jin10 MCP URL",
|
|
152
|
+
"help": "Optional. MCP endpoint used by the 24/7 Jin10 flash monitor.",
|
|
153
|
+
"advanced": true
|
|
154
|
+
},
|
|
155
|
+
"jin10ApiToken": {
|
|
156
|
+
"label": "Jin10 API Token",
|
|
157
|
+
"help": "Optional. Enables Jin10 realtime flash monitoring.",
|
|
158
|
+
"sensitive": true
|
|
159
|
+
},
|
|
160
|
+
"jin10FlashPollInterval": {
|
|
161
|
+
"label": "Jin10 Poll Interval",
|
|
162
|
+
"help": "Polling interval in seconds for Jin10 flash monitoring.",
|
|
163
|
+
"advanced": true
|
|
164
|
+
},
|
|
165
|
+
"jin10FlashRetentionDays": {
|
|
166
|
+
"label": "Jin10 Retention Days",
|
|
167
|
+
"help": "How many days of Jin10 raw flash data to keep locally.",
|
|
168
|
+
"advanced": true
|
|
169
|
+
},
|
|
128
170
|
"llmBaseUrl": {
|
|
129
171
|
"label": "LLM Base URL",
|
|
130
172
|
"help": "OpenAI-compatible analysis endpoint."
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tickflow-assist",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
7
7
|
"main": "dist/plugin.js",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/robinspt/tickflow-assist.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/robinspt/tickflow-assist",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/robinspt/tickflow-assist/issues"
|
|
15
|
+
},
|
|
8
16
|
"bin": {
|
|
9
17
|
"tickflow-assist": "dist/dev/tickflow-assist-cli.js"
|
|
10
18
|
},
|
|
@@ -29,7 +37,7 @@
|
|
|
29
37
|
"dev": "tsc -p tsconfig.json --watch",
|
|
30
38
|
"prepack": "npm run build && node ./scripts/prepare-package-assets.mjs",
|
|
31
39
|
"postpack": "node ./scripts/restore-package-assets.mjs",
|
|
32
|
-
"test": "npm run build && node --test dist/plugin-registration.test.js dist/tools/test-alert.tool.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",
|
|
33
41
|
"community-setup": "node dist/dev/tickflow-assist-cli.js configure-openclaw",
|
|
34
42
|
"tool": "node dist/dev/run-tool.js",
|
|
35
43
|
"monitor-loop": "node dist/dev/run-monitor-loop.js",
|
|
@@ -64,6 +72,6 @@
|
|
|
64
72
|
"openclaw": "^2026.4.1",
|
|
65
73
|
"typescript": "^5.8.2"
|
|
66
74
|
},
|
|
67
|
-
"readme": "# TickFlow Assist\n\n基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE)
|
|
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.1` 修复 GitHub Actions 自动发布 npm 包时的 provenance 仓库元数据校验失败,并将金十数据 MCP 握手里的 `clientInfo` 调整为中性固定值。完整发布记录见 <https://github.com/robinspt/tickflow-assist/blob/main/CHANGELOG.md>。\n\n当前主线按 OpenClaw `v2026.3.31+` 对齐,并已验证社区安装在 `v2026.4.1` 上兼容。\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 小时金十数据快讯监控;`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",
|
|
68
76
|
"readmeFilename": "README.md"
|
|
69
77
|
}
|