@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.
Files changed (111) hide show
  1. package/dist/index.d.ts +4 -3
  2. package/dist/index.js +49 -6
  3. package/dist/index.js.map +1 -1
  4. package/dist/setup-api.d.ts +2 -0
  5. package/dist/setup-api.js +8 -0
  6. package/dist/setup-api.js.map +1 -0
  7. package/dist/src/channel.js +25 -23
  8. package/dist/src/channel.js.map +1 -1
  9. package/dist/src/core/database.js +449 -39
  10. package/dist/src/core/database.js.map +1 -1
  11. package/dist/src/dailyMorningBriefingCron.d.ts +46 -0
  12. package/dist/src/dailyMorningBriefingCron.js +82 -0
  13. package/dist/src/dailyMorningBriefingCron.js.map +1 -0
  14. package/dist/src/features/chartOutput.d.ts +2 -0
  15. package/dist/src/features/chartOutput.js +35 -0
  16. package/dist/src/features/chartOutput.js.map +1 -0
  17. package/dist/src/features/dailyMorningBriefing/schema.d.ts +56 -0
  18. package/dist/src/features/dailyMorningBriefing/schema.js +22 -0
  19. package/dist/src/features/dailyMorningBriefing/schema.js.map +1 -0
  20. package/dist/src/features/dailyMorningBriefing/tools.d.ts +12 -0
  21. package/dist/src/features/dailyMorningBriefing/tools.js +204 -0
  22. package/dist/src/features/dailyMorningBriefing/tools.js.map +1 -0
  23. package/dist/src/features/deepReasoning/schema.d.ts +43 -0
  24. package/dist/src/features/deepReasoning/schema.js +17 -0
  25. package/dist/src/features/deepReasoning/schema.js.map +1 -0
  26. package/dist/src/features/deepReasoning/tools.d.ts +12 -0
  27. package/dist/src/features/deepReasoning/tools.js +163 -0
  28. package/dist/src/features/deepReasoning/tools.js.map +1 -0
  29. package/dist/src/features/index.d.ts +2 -1
  30. package/dist/src/features/index.js +9 -1
  31. package/dist/src/features/index.js.map +1 -1
  32. package/dist/src/features/informationVerification/schema.d.ts +43 -0
  33. package/dist/src/features/informationVerification/schema.js +17 -0
  34. package/dist/src/features/informationVerification/schema.js.map +1 -0
  35. package/dist/src/features/informationVerification/tools.d.ts +12 -0
  36. package/dist/src/features/informationVerification/tools.js +162 -0
  37. package/dist/src/features/informationVerification/tools.js.map +1 -0
  38. package/dist/src/features/notes/schema.d.ts +136 -39
  39. package/dist/src/features/notes/schema.js +13 -10
  40. package/dist/src/features/notes/schema.js.map +1 -1
  41. package/dist/src/features/notes/tools.d.ts +1 -0
  42. package/dist/src/features/notes/tools.js +47 -14
  43. package/dist/src/features/notes/tools.js.map +1 -1
  44. package/dist/src/features/pluginInfo/schema.d.ts +79 -0
  45. package/dist/src/features/pluginInfo/schema.js +14 -0
  46. package/dist/src/features/pluginInfo/schema.js.map +1 -0
  47. package/dist/src/features/pluginInfo/tools.d.ts +1 -0
  48. package/dist/src/features/pluginInfo/tools.js +157 -2
  49. package/dist/src/features/pluginInfo/tools.js.map +1 -1
  50. package/dist/src/features/profileLibrary/schema.d.ts +34 -6
  51. package/dist/src/features/profileLibrary/schema.js +1 -1
  52. package/dist/src/features/profileLibrary/schema.js.map +1 -1
  53. package/dist/src/features/stockAnalysis/schema.d.ts +224 -31
  54. package/dist/src/features/stockAnalysis/schema.js +76 -12
  55. package/dist/src/features/stockAnalysis/schema.js.map +1 -1
  56. package/dist/src/features/stockAnalysis/tools.d.ts +6 -4
  57. package/dist/src/features/stockAnalysis/tools.js +389 -44
  58. package/dist/src/features/stockAnalysis/tools.js.map +1 -1
  59. package/dist/src/features/stockBasic/schema.d.ts +149 -0
  60. package/dist/src/features/stockBasic/schema.js +26 -0
  61. package/dist/src/features/stockBasic/schema.js.map +1 -0
  62. package/dist/src/features/stockBasic/tools.d.ts +12 -0
  63. package/dist/src/features/stockBasic/tools.js +124 -0
  64. package/dist/src/features/stockBasic/tools.js.map +1 -0
  65. package/dist/src/features/watchlist/logic.d.ts +3 -3
  66. package/dist/src/features/watchlist/logic.js +47 -46
  67. package/dist/src/features/watchlist/logic.js.map +1 -1
  68. package/dist/src/features/watchlist/schema.d.ts +89 -54
  69. package/dist/src/features/watchlist/schema.js +7 -4
  70. package/dist/src/features/watchlist/schema.js.map +1 -1
  71. package/dist/src/features/watchlist/tools.d.ts +106 -59
  72. package/dist/src/features/watchlist/tools.js +182 -104
  73. package/dist/src/features/watchlist/tools.js.map +1 -1
  74. package/dist/src/openclawConfig.d.ts +6 -0
  75. package/dist/src/openclawConfig.js +74 -0
  76. package/dist/src/openclawConfig.js.map +1 -0
  77. package/dist/src/openclawConstants.d.ts +4 -0
  78. package/dist/src/openclawConstants.js +10 -0
  79. package/dist/src/openclawConstants.js.map +1 -0
  80. package/dist/src/runtime.js +20 -0
  81. package/dist/src/runtime.js.map +1 -1
  82. package/index.ts +52 -5
  83. package/openclaw.plugin.json +24 -0
  84. package/package.json +20 -5
  85. package/setup-api.ts +10 -0
  86. package/src/channel.ts +26 -25
  87. package/src/core/database.ts +447 -40
  88. package/src/dailyMorningBriefingCron.ts +129 -0
  89. package/src/features/chartOutput.ts +35 -0
  90. package/src/features/dailyMorningBriefing/schema.ts +38 -0
  91. package/src/features/dailyMorningBriefing/tools.ts +246 -0
  92. package/src/features/deepReasoning/schema.ts +22 -0
  93. package/src/features/deepReasoning/tools.ts +182 -0
  94. package/src/features/index.ts +11 -2
  95. package/src/features/informationVerification/schema.ts +22 -0
  96. package/src/features/informationVerification/tools.ts +181 -0
  97. package/src/features/notes/schema.ts +17 -12
  98. package/src/features/notes/tools.ts +54 -17
  99. package/src/features/pluginInfo/schema.ts +19 -0
  100. package/src/features/pluginInfo/tools.ts +173 -2
  101. package/src/features/profileLibrary/schema.ts +1 -1
  102. package/src/features/stockAnalysis/schema.ts +99 -17
  103. package/src/features/stockAnalysis/tools.ts +447 -49
  104. package/src/features/stockBasic/schema.ts +33 -0
  105. package/src/features/stockBasic/tools.ts +157 -0
  106. package/src/features/watchlist/logic.ts +56 -53
  107. package/src/features/watchlist/schema.ts +11 -6
  108. package/src/features/watchlist/tools.ts +191 -106
  109. package/src/openclawConfig.ts +101 -0
  110. package/src/openclawConstants.ts +11 -0
  111. 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 "openclaw/plugin-sdk/zod";
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
- stockCode: watchlistLogic._normalizeStockCodeForCache(stock.stockCode, stock.exchange)
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.stockCode, stock.exchange)}:${stock.exchange}`;
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.stockCode, stock.exchange);
100
- const legacyCode = stock.stockCode.trim().toUpperCase().replace(/\.(SH|SS|SZ|HK|US)$/i, "");
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 global_stock_metadata (stockCode, exchange, stockName, industryJson, themeJson)
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.stockName,
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 global_stock_metadata WHERE stockCode = ? AND exchange = ?
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 stockCode = ? AND exchange = ?")
133
- .get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
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.stockName, stock.stockCode, stock.exchange, uId);
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 stockCode = ? AND exchange = ?").get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
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, stockName = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
167
- `).run(stock.stockName, nextOrder, watchlistId);
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, stockCode, stockName, exchange, market, sortOrder)
260
+ INSERT INTO watchlist (id, userId, stock_code, stock_name, exchange, market, sortOrder)
176
261
  VALUES (?, ?, ?, ?, ?, ?, ?)
177
- `).run(watchlistId, uId, stock.stockCode, stock.stockName, stock.exchange, stock.market, nextOrder);
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: { stockCode: string; exchange: string; reason: "input_duplicate" | "duplicate"; id?: string }[] = [];
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({ stockCode: stock.stockCode, exchange: stock.exchange, reason: "input_duplicate" });
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 stockCode = ? AND exchange = ?")
224
- .get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
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({ stockCode: stock.stockCode, exchange: stock.exchange, reason: "duplicate", id: existing.id });
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.stockCode),
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.stockName}`);
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 stockCode = ? AND exchange = ?").get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
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, stockName = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
283
- `).run(stock.stockName, nextOrder, watchlistId);
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({ stockCode: stock.stockCode, exchange: stock.exchange, reason: "duplicate", id: watchlistId });
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, stockCode, stockName, exchange, market, sortOrder)
376
+ INSERT INTO watchlist (id, userId, stock_code, stock_name, exchange, market, sortOrder)
292
377
  VALUES (?, ?, ?, ?, ?, ?, ?)
293
- `).run(watchlistId, uId, stock.stockCode, stock.stockName, stock.exchange, stock.market, nextOrder);
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
- UPDATE watchlist
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
- description: "获取自选股列表",
364
- parameters: GetWatchlistParamsSchema,
365
- registerTool: false,
366
- execute: async (args: GetWatchlistParams, ctx: { userId: string }) => {
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 uId = String(ctx.userId);
370
- let query: string;
371
- let params: any[] = [uId];
372
-
373
- if (args.categoryId && args.categoryType) {
374
- const table = args.categoryType === "industry" ? "watchlist_industry_items" : "watchlist_theme_items";
375
- query = `
376
- SELECT w.*, ci.weight as relWeight
377
- FROM watchlist w
378
- JOIN ${table} ci ON w.id = ci.watchlistId
379
- WHERE w.userId = ? AND w.isDeleted = 0 AND ci.categoryId = ?
380
- ORDER BY w.sortOrder ASC
381
- `;
382
- params.push(args.categoryId);
383
- } else {
384
- query = `
385
- SELECT w.*
386
- FROM watchlist w
387
- WHERE w.userId = ? AND w.isDeleted = 0
388
- ORDER BY w.sortOrder ASC
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
- const stocks = db.prepare(query).all(...params) as WatchlistRow[];
393
- const fullList = stocks.map(stock => {
394
- const industries = db.prepare(`
395
- SELECT c.name FROM watchlist_categories c
396
- JOIN watchlist_industry_items i ON c.id = i.categoryId
397
- WHERE i.watchlistId = ? ORDER BY i.weight DESC
398
- `).all(stock.id) as { name: string }[];
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.stockCode
428
- FROM watchlist_categories c
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.stockCode
435
- FROM watchlist_categories c
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.stockCode)) {
447
- existing.stocks.push(item.stockCode);
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 watchlist_categories c
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 watchlist_categories c
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 stockCode as code, stockName as name FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
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 stockCode = ? AND isDeleted = 0
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
- db.prepare(`
541
- INSERT INTO watchlist_categories (id, remoteId, userId, name, type, weight, sortOrder)
542
- VALUES (?, ?, ?, ?, 'industry', 0, 0)
543
- ON CONFLICT(userId, remoteId) DO UPDATE SET name = excluded.name, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
544
- ON CONFLICT(userId, name, type) DO UPDATE SET remoteId = excluded.remoteId, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
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
- db.prepare(`
551
- INSERT INTO watchlist_categories (id, remoteId, userId, name, type, weight, sortOrder)
552
- VALUES (?, ?, ?, ?, 'theme', 0, 0)
553
- ON CONFLICT(userId, remoteId) DO UPDATE SET name = excluded.name, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
554
- ON CONFLICT(userId, name, type) DO UPDATE SET remoteId = excluded.remoteId, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
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 stockName, stockCode, exchange, market FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
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, stockCode, exchange FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
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.stockCode === s.stockCode && us.exchange === s.exchange);
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
- stockName: s.stockName,
626
- stockCode: s.stockCode,
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
+ }