@hedgehog-finance/hedgehog-plugin 1.0.21 → 1.0.23
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/dist/index.d.ts +4 -3
- package/dist/index.js +49 -6
- package/dist/index.js.map +1 -1
- package/dist/setup-api.d.ts +2 -0
- package/dist/setup-api.js +8 -0
- package/dist/setup-api.js.map +1 -0
- package/dist/src/channel.js +25 -23
- package/dist/src/channel.js.map +1 -1
- package/dist/src/core/database.js +449 -39
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/dailyMorningBriefingCron.d.ts +46 -0
- package/dist/src/dailyMorningBriefingCron.js +82 -0
- package/dist/src/dailyMorningBriefingCron.js.map +1 -0
- package/dist/src/features/chartOutput.d.ts +2 -0
- package/dist/src/features/chartOutput.js +35 -0
- package/dist/src/features/chartOutput.js.map +1 -0
- package/dist/src/features/dailyMorningBriefing/schema.d.ts +56 -0
- package/dist/src/features/dailyMorningBriefing/schema.js +22 -0
- package/dist/src/features/dailyMorningBriefing/schema.js.map +1 -0
- package/dist/src/features/dailyMorningBriefing/tools.d.ts +12 -0
- package/dist/src/features/dailyMorningBriefing/tools.js +204 -0
- package/dist/src/features/dailyMorningBriefing/tools.js.map +1 -0
- package/dist/src/features/deepReasoning/schema.d.ts +43 -0
- package/dist/src/features/deepReasoning/schema.js +17 -0
- package/dist/src/features/deepReasoning/schema.js.map +1 -0
- package/dist/src/features/deepReasoning/tools.d.ts +12 -0
- package/dist/src/features/deepReasoning/tools.js +163 -0
- package/dist/src/features/deepReasoning/tools.js.map +1 -0
- package/dist/src/features/index.d.ts +2 -1
- package/dist/src/features/index.js +9 -1
- package/dist/src/features/index.js.map +1 -1
- package/dist/src/features/informationVerification/schema.d.ts +43 -0
- package/dist/src/features/informationVerification/schema.js +17 -0
- package/dist/src/features/informationVerification/schema.js.map +1 -0
- package/dist/src/features/informationVerification/tools.d.ts +12 -0
- package/dist/src/features/informationVerification/tools.js +162 -0
- package/dist/src/features/informationVerification/tools.js.map +1 -0
- package/dist/src/features/notes/schema.d.ts +136 -39
- package/dist/src/features/notes/schema.js +13 -10
- package/dist/src/features/notes/schema.js.map +1 -1
- package/dist/src/features/notes/tools.d.ts +1 -0
- package/dist/src/features/notes/tools.js +47 -14
- package/dist/src/features/notes/tools.js.map +1 -1
- package/dist/src/features/pluginInfo/schema.d.ts +79 -0
- package/dist/src/features/pluginInfo/schema.js +14 -0
- package/dist/src/features/pluginInfo/schema.js.map +1 -0
- package/dist/src/features/pluginInfo/tools.d.ts +1 -0
- package/dist/src/features/pluginInfo/tools.js +157 -2
- package/dist/src/features/pluginInfo/tools.js.map +1 -1
- package/dist/src/features/profileLibrary/schema.d.ts +34 -6
- package/dist/src/features/profileLibrary/schema.js +1 -1
- package/dist/src/features/profileLibrary/schema.js.map +1 -1
- package/dist/src/features/stockAnalysis/schema.d.ts +224 -31
- package/dist/src/features/stockAnalysis/schema.js +76 -12
- package/dist/src/features/stockAnalysis/schema.js.map +1 -1
- package/dist/src/features/stockAnalysis/tools.d.ts +6 -4
- package/dist/src/features/stockAnalysis/tools.js +389 -44
- package/dist/src/features/stockAnalysis/tools.js.map +1 -1
- package/dist/src/features/stockBasic/schema.d.ts +149 -0
- package/dist/src/features/stockBasic/schema.js +26 -0
- package/dist/src/features/stockBasic/schema.js.map +1 -0
- package/dist/src/features/stockBasic/tools.d.ts +12 -0
- package/dist/src/features/stockBasic/tools.js +124 -0
- package/dist/src/features/stockBasic/tools.js.map +1 -0
- package/dist/src/features/watchlist/logic.d.ts +3 -3
- package/dist/src/features/watchlist/logic.js +47 -46
- package/dist/src/features/watchlist/logic.js.map +1 -1
- package/dist/src/features/watchlist/schema.d.ts +89 -54
- package/dist/src/features/watchlist/schema.js +7 -4
- package/dist/src/features/watchlist/schema.js.map +1 -1
- package/dist/src/features/watchlist/tools.d.ts +106 -59
- package/dist/src/features/watchlist/tools.js +182 -104
- package/dist/src/features/watchlist/tools.js.map +1 -1
- package/dist/src/openclawConfig.d.ts +6 -0
- package/dist/src/openclawConfig.js +74 -0
- package/dist/src/openclawConfig.js.map +1 -0
- package/dist/src/openclawConstants.d.ts +4 -0
- package/dist/src/openclawConstants.js +10 -0
- package/dist/src/openclawConstants.js.map +1 -0
- package/dist/src/runtime.js +20 -0
- package/dist/src/runtime.js.map +1 -1
- package/index.ts +52 -5
- package/openclaw.plugin.json +24 -0
- package/package.json +20 -5
- package/setup-api.ts +10 -0
- package/src/channel.ts +26 -25
- package/src/core/database.ts +447 -40
- package/src/dailyMorningBriefingCron.ts +129 -0
- package/src/features/chartOutput.ts +35 -0
- package/src/features/dailyMorningBriefing/schema.ts +38 -0
- package/src/features/dailyMorningBriefing/tools.ts +246 -0
- package/src/features/deepReasoning/schema.ts +22 -0
- package/src/features/deepReasoning/tools.ts +182 -0
- package/src/features/index.ts +11 -2
- package/src/features/informationVerification/schema.ts +22 -0
- package/src/features/informationVerification/tools.ts +181 -0
- package/src/features/notes/schema.ts +17 -12
- package/src/features/notes/tools.ts +54 -17
- package/src/features/pluginInfo/schema.ts +19 -0
- package/src/features/pluginInfo/tools.ts +173 -2
- package/src/features/profileLibrary/schema.ts +1 -1
- package/src/features/stockAnalysis/schema.ts +99 -17
- package/src/features/stockAnalysis/tools.ts +447 -49
- package/src/features/stockBasic/schema.ts +33 -0
- package/src/features/stockBasic/tools.ts +157 -0
- package/src/features/watchlist/logic.ts +56 -53
- package/src/features/watchlist/schema.ts +11 -6
- package/src/features/watchlist/tools.ts +191 -106
- package/src/openclawConfig.ts +101 -0
- package/src/openclawConstants.ts +11 -0
- package/src/runtime.ts +19 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
// @ts-ignore
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
|
-
import { z } from "
|
|
4
|
+
import { z } from "zod";
|
|
5
5
|
import { PluginRuntime } from "openclaw/plugin-sdk";
|
|
6
6
|
import { getDB } from "../../core/database.js";
|
|
7
7
|
import { logger } from "../../core/logger.js";
|
|
@@ -18,11 +18,30 @@ import {
|
|
|
18
18
|
SyncCategoriesParams,
|
|
19
19
|
SyncCategoriesParamsSchema,
|
|
20
20
|
BatchUpdateSortOrdersParams,
|
|
21
|
-
BatchUpdateSortOrdersParamsSchema
|
|
21
|
+
BatchUpdateSortOrdersParamsSchema,
|
|
22
|
+
GetIndustryListParamsSchema,
|
|
23
|
+
GetIndustryListParams
|
|
22
24
|
} from "./schema.js";
|
|
23
25
|
|
|
24
26
|
let watchlistMutationQueue: Promise<void> = Promise.resolve();
|
|
25
27
|
|
|
28
|
+
const GetWatchlistAgentToolSchema = {
|
|
29
|
+
type: "object",
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
properties: {
|
|
32
|
+
categoryId: { type: "string", description: "分类 ID,不传返回所有" },
|
|
33
|
+
categoryType: { type: "string", enum: ["industry", "theme"], description: "分类类型" }
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const GetIndustryListAgentToolSchema = {
|
|
38
|
+
type: "object",
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
properties: {
|
|
41
|
+
type: { type: "string", enum: ["industry", "theme", ""], description: "分类类型:industry 行业,theme 主题,空字符串或不传表示全部" }
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
26
45
|
function enqueueWatchlistMutation<T>(task: () => Promise<T>): Promise<T> {
|
|
27
46
|
const previous = watchlistMutationQueue;
|
|
28
47
|
let release!: () => void;
|
|
@@ -47,12 +66,12 @@ function normalizeTags(input: { name: string; weight?: number } | { name: string
|
|
|
47
66
|
function normalizeWatchlistStock(stock: AddToWatchlistParams): AddToWatchlistParams {
|
|
48
67
|
return {
|
|
49
68
|
...stock,
|
|
50
|
-
|
|
69
|
+
stock_code: watchlistLogic._normalizeStockCodeForCache(stock.stock_code, stock.exchange)
|
|
51
70
|
};
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
function watchlistStockKey(stock: AddToWatchlistParams): string {
|
|
55
|
-
return `${watchlistLogic._normalizeStockCodeForCache(stock.
|
|
74
|
+
return `${watchlistLogic._normalizeStockCodeForCache(stock.stock_code, stock.exchange)}:${stock.exchange}`;
|
|
56
75
|
}
|
|
57
76
|
|
|
58
77
|
function updateWatchlistTags(
|
|
@@ -96,25 +115,91 @@ function upsertStockClassificationCache(
|
|
|
96
115
|
theme?: { name: string; weight?: number }[];
|
|
97
116
|
}
|
|
98
117
|
) {
|
|
99
|
-
const cacheCode = watchlistLogic._normalizeStockCodeForCache(stock.
|
|
100
|
-
const legacyCode = stock.
|
|
118
|
+
const cacheCode = watchlistLogic._normalizeStockCodeForCache(stock.stock_code, stock.exchange);
|
|
119
|
+
const legacyCode = stock.stock_code.trim().toUpperCase().replace(/\.(SH|SS|SZ|HK|US)$/i, "");
|
|
101
120
|
db.prepare(`
|
|
102
|
-
INSERT OR REPLACE INTO
|
|
121
|
+
INSERT OR REPLACE INTO stock_classification_cache (stock_code, exchange, stock_name, industry_classification, theme_classification)
|
|
103
122
|
VALUES (?, ?, ?, ?, ?)
|
|
104
123
|
`).run(
|
|
105
124
|
cacheCode,
|
|
106
125
|
stock.exchange,
|
|
107
|
-
stock.
|
|
126
|
+
stock.stock_name,
|
|
108
127
|
JSON.stringify(classification.industry),
|
|
109
128
|
JSON.stringify(classification.theme || [])
|
|
110
129
|
);
|
|
111
130
|
if (legacyCode && legacyCode !== cacheCode) {
|
|
112
131
|
db.prepare(`
|
|
113
|
-
DELETE FROM
|
|
132
|
+
DELETE FROM stock_classification_cache WHERE stock_code = ? AND exchange = ?
|
|
114
133
|
`).run(legacyCode, stock.exchange);
|
|
115
134
|
}
|
|
116
135
|
}
|
|
117
136
|
|
|
137
|
+
function readWatchlistRows(db: DatabaseSync, args: GetWatchlistParams, userId?: string): WatchlistRow[] {
|
|
138
|
+
let query: string;
|
|
139
|
+
const params: any[] = [];
|
|
140
|
+
|
|
141
|
+
if (userId) {
|
|
142
|
+
params.push(userId);
|
|
143
|
+
if (args.categoryId && args.categoryType) {
|
|
144
|
+
const table = args.categoryType === "industry" ? "watchlist_industry_items" : "watchlist_theme_items";
|
|
145
|
+
query = `
|
|
146
|
+
SELECT w.*, ci.weight as relWeight
|
|
147
|
+
FROM watchlist w
|
|
148
|
+
JOIN ${table} ci ON w.id = ci.watchlistId
|
|
149
|
+
WHERE w.userId = ? AND w.isDeleted = 0 AND ci.categoryId = ?
|
|
150
|
+
ORDER BY w.sortOrder ASC
|
|
151
|
+
`;
|
|
152
|
+
params.push(args.categoryId);
|
|
153
|
+
} else {
|
|
154
|
+
query = `
|
|
155
|
+
SELECT w.*
|
|
156
|
+
FROM watchlist w
|
|
157
|
+
WHERE w.userId = ? AND w.isDeleted = 0
|
|
158
|
+
ORDER BY w.sortOrder ASC
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
} else if (args.categoryId && args.categoryType) {
|
|
162
|
+
const table = args.categoryType === "industry" ? "watchlist_industry_items" : "watchlist_theme_items";
|
|
163
|
+
query = `
|
|
164
|
+
SELECT w.*, ci.weight as relWeight
|
|
165
|
+
FROM watchlist w
|
|
166
|
+
JOIN ${table} ci ON w.id = ci.watchlistId
|
|
167
|
+
WHERE w.isDeleted = 0 AND ci.categoryId = ?
|
|
168
|
+
ORDER BY w.userId ASC, w.sortOrder ASC
|
|
169
|
+
`;
|
|
170
|
+
params.push(args.categoryId);
|
|
171
|
+
} else {
|
|
172
|
+
query = `
|
|
173
|
+
SELECT w.*
|
|
174
|
+
FROM watchlist w
|
|
175
|
+
WHERE w.isDeleted = 0
|
|
176
|
+
ORDER BY w.userId ASC, w.sortOrder ASC
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return db.prepare(query).all(...params) as WatchlistRow[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function attachWatchlistTags(db: DatabaseSync, stocks: WatchlistRow[]) {
|
|
184
|
+
return stocks.map(stock => {
|
|
185
|
+
const industries = db.prepare(`
|
|
186
|
+
SELECT c.name FROM industry_theme_categories c
|
|
187
|
+
JOIN watchlist_industry_items i ON c.id = i.categoryId
|
|
188
|
+
WHERE i.watchlistId = ? ORDER BY i.weight DESC
|
|
189
|
+
`).all(stock.id) as { name: string }[];
|
|
190
|
+
const themes = db.prepare(`
|
|
191
|
+
SELECT c.name FROM industry_theme_categories c
|
|
192
|
+
JOIN watchlist_theme_items t ON c.id = t.categoryId
|
|
193
|
+
WHERE t.watchlistId = ? ORDER BY t.weight DESC
|
|
194
|
+
`).all(stock.id) as { name: string }[];
|
|
195
|
+
return {
|
|
196
|
+
...stock,
|
|
197
|
+
industries: industries.map(i => i.name),
|
|
198
|
+
themes: themes.map(t => t.name)
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
118
203
|
export const watchlistTools = {
|
|
119
204
|
add_to_watchlist: {
|
|
120
205
|
name: "add_to_watchlist",
|
|
@@ -129,8 +214,8 @@ export const watchlistTools = {
|
|
|
129
214
|
}
|
|
130
215
|
const db = getDB();
|
|
131
216
|
const stock = normalizeWatchlistStock(args);
|
|
132
|
-
const existingBeforeClassify = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND
|
|
133
|
-
.get(uId, stock.
|
|
217
|
+
const existingBeforeClassify = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stock_code = ? AND exchange = ?")
|
|
218
|
+
.get(uId, stock.stock_code, stock.exchange) as WatchlistRow | undefined;
|
|
134
219
|
if (existingBeforeClassify?.isDeleted === 0) {
|
|
135
220
|
return JSON.stringify({ success: true, skipped: true, reason: "duplicate", id: existingBeforeClassify.id });
|
|
136
221
|
}
|
|
@@ -142,7 +227,7 @@ export const watchlistTools = {
|
|
|
142
227
|
|
|
143
228
|
let classification: Awaited<ReturnType<typeof watchlistLogic.getStockClassification>>;
|
|
144
229
|
try {
|
|
145
|
-
classification = await watchlistLogic.getStockClassification(ctx.runtime, stock.
|
|
230
|
+
classification = await watchlistLogic.getStockClassification(ctx.runtime, stock.stock_name, stock.stock_code, stock.exchange, uId);
|
|
146
231
|
} catch (e: any) {
|
|
147
232
|
return JSON.stringify({ success: false, error: e.message });
|
|
148
233
|
}
|
|
@@ -157,14 +242,14 @@ export const watchlistTools = {
|
|
|
157
242
|
const nextOrder = (sortRow?.max ?? 0) + 1024;
|
|
158
243
|
|
|
159
244
|
let watchlistId: string;
|
|
160
|
-
const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND
|
|
245
|
+
const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stock_code = ? AND exchange = ?").get(uId, stock.stock_code, stock.exchange) as WatchlistRow | undefined;
|
|
161
246
|
|
|
162
247
|
if (existingItem) {
|
|
163
248
|
watchlistId = existingItem.id;
|
|
164
249
|
if (existingItem.isDeleted === 1) {
|
|
165
250
|
db.prepare(`
|
|
166
|
-
UPDATE watchlist SET isDeleted = 0,
|
|
167
|
-
`).run(stock.
|
|
251
|
+
UPDATE watchlist SET isDeleted = 0, stock_name = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
|
|
252
|
+
`).run(stock.stock_name, nextOrder, watchlistId);
|
|
168
253
|
} else {
|
|
169
254
|
db.exec("ROLLBACK");
|
|
170
255
|
return JSON.stringify({ success: true, skipped: true, reason: "duplicate", id: watchlistId });
|
|
@@ -172,9 +257,9 @@ export const watchlistTools = {
|
|
|
172
257
|
} else {
|
|
173
258
|
watchlistId = randomUUID();
|
|
174
259
|
db.prepare(`
|
|
175
|
-
INSERT INTO watchlist (id, userId,
|
|
260
|
+
INSERT INTO watchlist (id, userId, stock_code, stock_name, exchange, market, sortOrder)
|
|
176
261
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
177
|
-
`).run(watchlistId, uId, stock.
|
|
262
|
+
`).run(watchlistId, uId, stock.stock_code, stock.stock_name, stock.exchange, stock.market, nextOrder);
|
|
178
263
|
}
|
|
179
264
|
|
|
180
265
|
upsertStockClassificationCache(db, stock, classification);
|
|
@@ -207,12 +292,12 @@ export const watchlistTools = {
|
|
|
207
292
|
const uId = String(ctx.userId);
|
|
208
293
|
const uniqueStocks: AddToWatchlistParams[] = [];
|
|
209
294
|
const inputSeen = new Set<string>();
|
|
210
|
-
const skipped: {
|
|
295
|
+
const skipped: { stock_code: string; exchange: string; reason: "input_duplicate" | "duplicate"; id?: string }[] = [];
|
|
211
296
|
for (const rawStock of args.stocks) {
|
|
212
297
|
const stock = normalizeWatchlistStock(rawStock);
|
|
213
298
|
const key = watchlistStockKey(stock);
|
|
214
299
|
if (inputSeen.has(key)) {
|
|
215
|
-
skipped.push({
|
|
300
|
+
skipped.push({ stock_code: stock.stock_code, exchange: stock.exchange, reason: "input_duplicate" });
|
|
216
301
|
continue;
|
|
217
302
|
}
|
|
218
303
|
inputSeen.add(key);
|
|
@@ -220,10 +305,10 @@ export const watchlistTools = {
|
|
|
220
305
|
}
|
|
221
306
|
const stocksToAdd: AddToWatchlistParams[] = [];
|
|
222
307
|
for (const stock of uniqueStocks) {
|
|
223
|
-
const existing = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND
|
|
224
|
-
.get(uId, stock.
|
|
308
|
+
const existing = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stock_code = ? AND exchange = ?")
|
|
309
|
+
.get(uId, stock.stock_code, stock.exchange) as WatchlistRow | undefined;
|
|
225
310
|
if (existing?.isDeleted === 0) {
|
|
226
|
-
skipped.push({
|
|
311
|
+
skipped.push({ stock_code: stock.stock_code, exchange: stock.exchange, reason: "duplicate", id: existing.id });
|
|
227
312
|
} else {
|
|
228
313
|
stocksToAdd.push(stock);
|
|
229
314
|
}
|
|
@@ -246,7 +331,7 @@ export const watchlistTools = {
|
|
|
246
331
|
} catch (e: any) {
|
|
247
332
|
logger.warn({
|
|
248
333
|
count: stocksToAdd.length,
|
|
249
|
-
codes: stocksToAdd.map(stock => stock.
|
|
334
|
+
codes: stocksToAdd.map(stock => stock.stock_code),
|
|
250
335
|
error: e.message || String(e)
|
|
251
336
|
}, "[Watchlist] batch_add_to_watchlist classification failed");
|
|
252
337
|
return JSON.stringify({
|
|
@@ -269,28 +354,28 @@ export const watchlistTools = {
|
|
|
269
354
|
currentMaxOrder = nextOrder;
|
|
270
355
|
const classification = batchResults[i];
|
|
271
356
|
if (!classification) {
|
|
272
|
-
throw new Error(`行业/主题关系分析失败: ${stock.
|
|
357
|
+
throw new Error(`行业/主题关系分析失败: ${stock.stock_name}`);
|
|
273
358
|
}
|
|
274
359
|
|
|
275
360
|
let watchlistId: string;
|
|
276
|
-
const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND
|
|
361
|
+
const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stock_code = ? AND exchange = ?").get(uId, stock.stock_code, stock.exchange) as WatchlistRow | undefined;
|
|
277
362
|
|
|
278
363
|
if (existingItem) {
|
|
279
364
|
watchlistId = existingItem.id;
|
|
280
365
|
if (existingItem.isDeleted === 1) {
|
|
281
366
|
db.prepare(`
|
|
282
|
-
UPDATE watchlist SET isDeleted = 0,
|
|
283
|
-
`).run(stock.
|
|
367
|
+
UPDATE watchlist SET isDeleted = 0, stock_name = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
|
|
368
|
+
`).run(stock.stock_name, nextOrder, watchlistId);
|
|
284
369
|
} else {
|
|
285
|
-
skipped.push({
|
|
370
|
+
skipped.push({ stock_code: stock.stock_code, exchange: stock.exchange, reason: "duplicate", id: watchlistId });
|
|
286
371
|
return;
|
|
287
372
|
}
|
|
288
373
|
} else {
|
|
289
374
|
watchlistId = randomUUID();
|
|
290
375
|
db.prepare(`
|
|
291
|
-
INSERT INTO watchlist (id, userId,
|
|
376
|
+
INSERT INTO watchlist (id, userId, stock_code, stock_name, exchange, market, sortOrder)
|
|
292
377
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
293
|
-
`).run(watchlistId, uId, stock.
|
|
378
|
+
`).run(watchlistId, uId, stock.stock_code, stock.stock_name, stock.exchange, stock.market, nextOrder);
|
|
294
379
|
}
|
|
295
380
|
results.push(watchlistId);
|
|
296
381
|
writtenItems.push({ id: watchlistId, stock, classification });
|
|
@@ -327,12 +412,9 @@ export const watchlistTools = {
|
|
|
327
412
|
db.exec("BEGIN TRANSACTION");
|
|
328
413
|
try {
|
|
329
414
|
const info = db.prepare(`
|
|
330
|
-
|
|
331
|
-
SET isDeleted = 1,
|
|
332
|
-
updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
|
|
415
|
+
DELETE FROM watchlist
|
|
333
416
|
WHERE id = ?
|
|
334
417
|
AND userId = ?
|
|
335
|
-
AND isDeleted = 0
|
|
336
418
|
`).run(args.id, uId);
|
|
337
419
|
|
|
338
420
|
if (info.changes > 0) {
|
|
@@ -360,54 +442,49 @@ export const watchlistTools = {
|
|
|
360
442
|
|
|
361
443
|
get_watchlist: {
|
|
362
444
|
name: "get_watchlist",
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
445
|
+
label: "获取自选股",
|
|
446
|
+
description: "获取当前全部自选股列表及其行业、主题标签。",
|
|
447
|
+
parameters: GetWatchlistAgentToolSchema,
|
|
448
|
+
registerTool: true,
|
|
449
|
+
execute: async (rawArgs?: GetWatchlistParams, ctx?: { userId: string }) => {
|
|
367
450
|
try {
|
|
451
|
+
const args = GetWatchlistParamsSchema.parse(rawArgs ?? {});
|
|
368
452
|
const db = getDB();
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
453
|
+
const stocks = readWatchlistRows(db, args, ctx?.userId ? String(ctx.userId) : undefined);
|
|
454
|
+
const fullList = attachWatchlistTags(db, stocks);
|
|
455
|
+
return JSON.stringify({ success: true, data: fullList });
|
|
456
|
+
} catch (e: any) {
|
|
457
|
+
return JSON.stringify({ success: false, error: e.message });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
get_industry_list: {
|
|
463
|
+
name: "get_industry_list",
|
|
464
|
+
label: "拉取申万一级行业分类或主题板块列表",
|
|
465
|
+
description: "拉取申万一级行业分类或主题板块列表。",
|
|
466
|
+
parameters: GetIndustryListAgentToolSchema,
|
|
467
|
+
registerTool: true,
|
|
468
|
+
execute: async (rawArgs?: GetIndustryListParams) => {
|
|
469
|
+
try {
|
|
470
|
+
const args = GetIndustryListParamsSchema.parse(rawArgs ?? {});
|
|
471
|
+
const db = getDB();
|
|
472
|
+
let query = "SELECT DISTINCT name FROM industry_theme_categories";
|
|
473
|
+
const params: any[] = [];
|
|
474
|
+
const conditions: string[] = [];
|
|
475
|
+
|
|
476
|
+
if (args.type && args.type !== "") {
|
|
477
|
+
conditions.push("type = ?");
|
|
478
|
+
params.push(args.type);
|
|
390
479
|
}
|
|
391
480
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const themes = db.prepare(`
|
|
400
|
-
SELECT c.name FROM watchlist_categories c
|
|
401
|
-
JOIN watchlist_theme_items t ON c.id = t.categoryId
|
|
402
|
-
WHERE t.watchlistId = ? ORDER BY t.weight DESC
|
|
403
|
-
`).all(stock.id) as { name: string }[];
|
|
404
|
-
return {
|
|
405
|
-
...stock,
|
|
406
|
-
industries: industries.map(i => i.name),
|
|
407
|
-
themes: themes.map(t => t.name)
|
|
408
|
-
};
|
|
409
|
-
});
|
|
410
|
-
return JSON.stringify({ success: true, data: fullList });
|
|
481
|
+
if (conditions.length > 0) {
|
|
482
|
+
query += ` WHERE ${conditions.join(" AND ")}`;
|
|
483
|
+
}
|
|
484
|
+
query += " ORDER BY name ASC";
|
|
485
|
+
const rows = db.prepare(query).all(...params) as { name: string }[];
|
|
486
|
+
const data = rows.map(r => r.name);
|
|
487
|
+
return JSON.stringify({ success: true, data });
|
|
411
488
|
} catch (e: any) {
|
|
412
489
|
return JSON.stringify({ success: false, error: e.message });
|
|
413
490
|
}
|
|
@@ -424,15 +501,15 @@ export const watchlistTools = {
|
|
|
424
501
|
const db = getDB();
|
|
425
502
|
const uId = String(ctx.userId);
|
|
426
503
|
const industryData = db.prepare(`
|
|
427
|
-
SELECT c.name as category_name, i.weight, w.
|
|
428
|
-
FROM
|
|
504
|
+
SELECT c.name as category_name, i.weight, w.stock_code
|
|
505
|
+
FROM industry_theme_categories c
|
|
429
506
|
JOIN watchlist_industry_items i ON c.id = i.categoryId
|
|
430
507
|
JOIN watchlist w ON i.watchlistId = w.id
|
|
431
508
|
WHERE i.userId = ? AND w.isDeleted = 0
|
|
432
509
|
`).all(uId) as any[];
|
|
433
510
|
const themeData = db.prepare(`
|
|
434
|
-
SELECT c.name as category_name, t.weight, w.
|
|
435
|
-
FROM
|
|
511
|
+
SELECT c.name as category_name, t.weight, w.stock_code
|
|
512
|
+
FROM industry_theme_categories c
|
|
436
513
|
JOIN watchlist_theme_items t ON c.id = t.categoryId
|
|
437
514
|
JOIN watchlist w ON t.watchlistId = w.id
|
|
438
515
|
WHERE t.userId = ? AND w.isDeleted = 0
|
|
@@ -443,8 +520,8 @@ export const watchlistTools = {
|
|
|
443
520
|
const name = item.category_name;
|
|
444
521
|
const existing: { category_name: string, weight_total: number, stocks: string[] } = aggMap.get(name) || { category_name: name, weight_total: 0, stocks: [] };
|
|
445
522
|
existing.weight_total += (item.weight || 0);
|
|
446
|
-
if (!existing.stocks.includes(item.
|
|
447
|
-
existing.stocks.push(item.
|
|
523
|
+
if (!existing.stocks.includes(item.stock_code)) {
|
|
524
|
+
existing.stocks.push(item.stock_code);
|
|
448
525
|
}
|
|
449
526
|
aggMap.set(name, existing);
|
|
450
527
|
});
|
|
@@ -467,14 +544,14 @@ export const watchlistTools = {
|
|
|
467
544
|
const uId = String(ctx.userId);
|
|
468
545
|
const industries = db.prepare(`
|
|
469
546
|
SELECT DISTINCT c.id, c.name, c.type, c.sortOrder
|
|
470
|
-
FROM
|
|
547
|
+
FROM industry_theme_categories c
|
|
471
548
|
JOIN watchlist_industry_items i ON c.id = i.categoryId
|
|
472
549
|
WHERE i.userId = ?
|
|
473
550
|
ORDER BY c.sortOrder ASC, c.name ASC
|
|
474
551
|
`).all(uId) as any[];
|
|
475
552
|
const themes = db.prepare(`
|
|
476
553
|
SELECT DISTINCT c.id, c.name, c.type, c.sortOrder
|
|
477
|
-
FROM
|
|
554
|
+
FROM industry_theme_categories c
|
|
478
555
|
JOIN watchlist_theme_items t ON c.id = t.categoryId
|
|
479
556
|
WHERE t.userId = ?
|
|
480
557
|
ORDER BY c.sortOrder ASC, c.name ASC
|
|
@@ -501,14 +578,14 @@ export const watchlistTools = {
|
|
|
501
578
|
const db = getDB();
|
|
502
579
|
const uId = String(ctx.userId);
|
|
503
580
|
try {
|
|
504
|
-
const stocks = db.prepare("SELECT
|
|
581
|
+
const stocks = db.prepare("SELECT stock_code as code, stock_name as name FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
|
|
505
582
|
if (stocks.length === 0) return JSON.stringify({ success: true, message: "没有可排序的股票" });
|
|
506
583
|
const sortedResults = await watchlistLogic.applySmartSort(ctx.runtime, `sort-${uId}`, stocks);
|
|
507
584
|
if (sortedResults.length > 0) {
|
|
508
585
|
db.exec("BEGIN TRANSACTION");
|
|
509
586
|
const stmt = db.prepare(`
|
|
510
587
|
UPDATE watchlist SET sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
|
|
511
|
-
WHERE userId = ? AND
|
|
588
|
+
WHERE userId = ? AND stock_code = ? AND isDeleted = 0
|
|
512
589
|
`);
|
|
513
590
|
sortedResults.forEach((item: any, i: number) => {
|
|
514
591
|
const currentOrder = i * 10;
|
|
@@ -535,30 +612,38 @@ export const watchlistTools = {
|
|
|
535
612
|
const uId = String(ctx.userId);
|
|
536
613
|
db.exec("BEGIN TRANSACTION");
|
|
537
614
|
try {
|
|
615
|
+
const existing = db.prepare("SELECT name, type FROM industry_theme_categories WHERE userId = ?").all(uId) as { name: string, type: string }[];
|
|
616
|
+
const existingSet = new Set(existing.map(row => `${row.name}:${row.type}`));
|
|
617
|
+
|
|
618
|
+
const stmt = db.prepare(`
|
|
619
|
+
INSERT INTO industry_theme_categories (id, remoteId, userId, name, type, weight, sortOrder)
|
|
620
|
+
VALUES (?, ?, ?, ?, ?, 0, 0)
|
|
621
|
+
ON CONFLICT(userId, remoteId) DO UPDATE SET name = excluded.name, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
|
|
622
|
+
ON CONFLICT(userId, name, type) DO UPDATE SET remoteId = excluded.remoteId, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
|
|
623
|
+
`);
|
|
624
|
+
|
|
538
625
|
if (args.industries) {
|
|
539
626
|
for (const name of args.industries) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
`).run(randomUUID(), name, uId, name);
|
|
627
|
+
const key = `${name}:industry`;
|
|
628
|
+
if (!existingSet.has(key)) {
|
|
629
|
+
stmt.run(randomUUID(), name, uId, name, 'industry');
|
|
630
|
+
existingSet.add(key);
|
|
631
|
+
}
|
|
546
632
|
}
|
|
547
633
|
}
|
|
548
634
|
if (args.themes) {
|
|
549
635
|
for (const name of args.themes) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
`).run(randomUUID(), name, uId, name);
|
|
636
|
+
const key = `${name}:theme`;
|
|
637
|
+
if (!existingSet.has(key)) {
|
|
638
|
+
stmt.run(randomUUID(), name, uId, name, 'theme');
|
|
639
|
+
existingSet.add(key);
|
|
640
|
+
}
|
|
556
641
|
}
|
|
557
642
|
}
|
|
558
643
|
db.exec("COMMIT");
|
|
559
644
|
return JSON.stringify({ success: true });
|
|
560
645
|
} catch (e: any) {
|
|
561
|
-
db.exec("ROLLBACK");
|
|
646
|
+
if (db.inTransaction) db.exec("ROLLBACK");
|
|
562
647
|
return JSON.stringify({ success: false, error: e.message });
|
|
563
648
|
}
|
|
564
649
|
}
|
|
@@ -598,7 +683,7 @@ export const watchlistTools = {
|
|
|
598
683
|
const db = getDB();
|
|
599
684
|
const uId = String(ctx.userId);
|
|
600
685
|
try {
|
|
601
|
-
const stocks = db.prepare("SELECT
|
|
686
|
+
const stocks = db.prepare("SELECT stock_name, stock_code, exchange, market FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
|
|
602
687
|
if (stocks.length > 30) {
|
|
603
688
|
return JSON.stringify({ success: false, error: "自选股数量超过 30 只,暂不支持批量重置分类" });
|
|
604
689
|
}
|
|
@@ -615,15 +700,15 @@ export const watchlistTools = {
|
|
|
615
700
|
db.prepare("DELETE FROM watchlist_industry_items WHERE userId = ?").run(uId);
|
|
616
701
|
db.prepare("DELETE FROM watchlist_theme_items WHERE userId = ?").run(uId);
|
|
617
702
|
|
|
618
|
-
const userStocks = db.prepare("SELECT id,
|
|
703
|
+
const userStocks = db.prepare("SELECT id, stock_code, exchange FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
|
|
619
704
|
batchResults.forEach((res, i) => {
|
|
620
705
|
if (res) {
|
|
621
706
|
const s = stocks[i];
|
|
622
|
-
const match = userStocks.find(us => us.
|
|
707
|
+
const match = userStocks.find(us => us.stock_code === s.stock_code && us.exchange === s.exchange);
|
|
623
708
|
if (match) {
|
|
624
709
|
upsertStockClassificationCache(db, {
|
|
625
|
-
|
|
626
|
-
|
|
710
|
+
stock_name: s.stock_name,
|
|
711
|
+
stock_code: s.stock_code,
|
|
627
712
|
exchange: s.exchange,
|
|
628
713
|
market: s.market
|
|
629
714
|
}, res);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import {
|
|
3
|
+
HEDGEHOG_AGENT_ID,
|
|
4
|
+
MAIN_AGENT_EXTRA_TOOL_NAMES,
|
|
5
|
+
MAIN_AGENT_ID,
|
|
6
|
+
listRegisteredAgentToolNames
|
|
7
|
+
} from "./openclawConstants.js";
|
|
8
|
+
|
|
9
|
+
type AgentEntry = {
|
|
10
|
+
id: string;
|
|
11
|
+
tools?: {
|
|
12
|
+
allow?: string[];
|
|
13
|
+
alsoAllow?: string[];
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type HedgehogAgentToolAllowMigration = {
|
|
20
|
+
config: OpenClawConfig;
|
|
21
|
+
changes: string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function uniqueStrings(values: readonly string[]): string[] {
|
|
25
|
+
return Array.from(new Set(values));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function withAllowedTools(agent: AgentEntry, toolNames: string[]): { agent: AgentEntry; changed: boolean; added: string[] } {
|
|
29
|
+
const tools = agent.tools || {};
|
|
30
|
+
const existing = Array.isArray(tools.alsoAllow) ? tools.alsoAllow : [];
|
|
31
|
+
const existingSet = new Set(existing);
|
|
32
|
+
const added = toolNames.filter((name) => !existingSet.has(name));
|
|
33
|
+
const nextAlsoAllow = uniqueStrings([...existing, ...toolNames]);
|
|
34
|
+
return {
|
|
35
|
+
agent: {
|
|
36
|
+
...agent,
|
|
37
|
+
tools: {
|
|
38
|
+
...tools,
|
|
39
|
+
alsoAllow: nextAlsoAllow
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
changed: added.length > 0,
|
|
43
|
+
added
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function withRegisteredTools(agent: AgentEntry): { agent: AgentEntry; changed: boolean; added: string[] } {
|
|
48
|
+
return withAllowedTools(agent, listRegisteredAgentToolNames());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function upsertAgentTools(list: AgentEntry[], agentId: string, toolNames: string[]) {
|
|
52
|
+
const existingIndex = list.findIndex((agent) => agent?.id === agentId);
|
|
53
|
+
if (existingIndex >= 0) {
|
|
54
|
+
const result = withAllowedTools(list[existingIndex], toolNames);
|
|
55
|
+
if (!result.changed) return { changed: false, added: [] };
|
|
56
|
+
list[existingIndex] = result.agent;
|
|
57
|
+
return { changed: true, added: result.added };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = withAllowedTools({ id: agentId }, toolNames);
|
|
61
|
+
list.push(result.agent);
|
|
62
|
+
return { changed: result.changed, added: result.added };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function ensureRegisteredToolsAllowedInConfig(config: OpenClawConfig): HedgehogAgentToolAllowMigration | null {
|
|
66
|
+
const agents = config.agents || {};
|
|
67
|
+
const list = Array.isArray(agents.list) ? agents.list as AgentEntry[] : [];
|
|
68
|
+
const existingIndex = list.findIndex((agent) => agent?.id === HEDGEHOG_AGENT_ID);
|
|
69
|
+
const nextList = [...list];
|
|
70
|
+
const changes: string[] = [];
|
|
71
|
+
|
|
72
|
+
if (existingIndex >= 0) {
|
|
73
|
+
const result = withRegisteredTools(nextList[existingIndex]);
|
|
74
|
+
if (result.changed) {
|
|
75
|
+
nextList[existingIndex] = result.agent;
|
|
76
|
+
changes.push(`Added ${result.added.join(", ")} to agents.list[id=${HEDGEHOG_AGENT_ID}].tools.alsoAllow.`);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
const result = withRegisteredTools({ id: HEDGEHOG_AGENT_ID });
|
|
80
|
+
nextList.push(result.agent);
|
|
81
|
+
changes.push(`Added ${result.added.join(", ")} to agents.list[id=${HEDGEHOG_AGENT_ID}].tools.alsoAllow.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const mainResult = upsertAgentTools(nextList, MAIN_AGENT_ID, MAIN_AGENT_EXTRA_TOOL_NAMES);
|
|
85
|
+
if (mainResult.changed) {
|
|
86
|
+
changes.push(`Added ${mainResult.added.join(", ")} to agents.list[id=${MAIN_AGENT_ID}].tools.alsoAllow.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (changes.length === 0) return null;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
config: {
|
|
93
|
+
...config,
|
|
94
|
+
agents: {
|
|
95
|
+
...agents,
|
|
96
|
+
list: nextList
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
changes
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { allFeaturesTools } from "./features/index.js";
|
|
2
|
+
|
|
3
|
+
export const HEDGEHOG_AGENT_ID = "hedgehog-finance";
|
|
4
|
+
export const MAIN_AGENT_ID = "main";
|
|
5
|
+
export const MAIN_AGENT_EXTRA_TOOL_NAMES = ["update_hedgehog_skill_versions"];
|
|
6
|
+
|
|
7
|
+
export function listRegisteredAgentToolNames(): string[] {
|
|
8
|
+
return Object.entries(allFeaturesTools)
|
|
9
|
+
.filter(([, tool]) => tool.registerTool !== false && tool.agentToolTarget !== "main")
|
|
10
|
+
.map(([name]) => name);
|
|
11
|
+
}
|