@hedgehog-finance/hedgehog-plugin 1.0.12 → 1.0.13

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 (44) hide show
  1. package/dist/index.d.ts +10 -0
  2. package/dist/index.js +24 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/src/channel.d.ts +6 -0
  5. package/dist/src/channel.js +620 -0
  6. package/dist/src/channel.js.map +1 -0
  7. package/dist/src/core/database.d.ts +2 -0
  8. package/dist/src/core/database.js +220 -0
  9. package/dist/src/core/database.js.map +1 -0
  10. package/dist/src/core/logger.d.ts +3 -0
  11. package/dist/src/core/logger.js +20 -0
  12. package/dist/src/core/logger.js.map +1 -0
  13. package/dist/src/features/index.d.ts +22 -0
  14. package/dist/src/features/index.js +8 -0
  15. package/dist/src/features/index.js.map +1 -0
  16. package/dist/src/features/watchlist/logic.d.ts +48 -0
  17. package/dist/src/features/watchlist/logic.js +607 -0
  18. package/dist/src/features/watchlist/logic.js.map +1 -0
  19. package/dist/src/features/watchlist/schema.d.ts +85 -0
  20. package/dist/src/features/watchlist/schema.js +29 -0
  21. package/dist/src/features/watchlist/schema.js.map +1 -0
  22. package/dist/src/features/watchlist/store.d.ts +1 -0
  23. package/dist/src/features/watchlist/store.js +2 -0
  24. package/dist/src/features/watchlist/store.js.map +1 -0
  25. package/dist/src/features/watchlist/tools.d.ts +135 -0
  26. package/dist/src/features/watchlist/tools.js +572 -0
  27. package/dist/src/features/watchlist/tools.js.map +1 -0
  28. package/dist/src/runtime.d.ts +5 -0
  29. package/dist/src/runtime.js +40 -0
  30. package/dist/src/runtime.js.map +1 -0
  31. package/dist/src/types.d.ts +99 -0
  32. package/dist/src/types.js +16 -0
  33. package/dist/src/types.js.map +1 -0
  34. package/index.ts +4 -4
  35. package/package.json +23 -6
  36. package/src/channel.ts +26 -4
  37. package/src/core/database.ts +90 -3
  38. package/src/features/index.ts +2 -1
  39. package/src/features/watchlist/logic.ts +503 -128
  40. package/src/features/watchlist/schema.ts +1 -6
  41. package/src/features/watchlist/tools.ts +248 -103
  42. package/src/runtime.ts +3 -3
  43. package/src/types.ts +1 -1
  44. package/tsconfig.json +0 -16
@@ -24,10 +24,7 @@ export const AddToWatchlistParamsSchema = z.object({
24
24
  stockCode: z.string(),
25
25
  stockName: z.string(),
26
26
  exchange: ExchangeEnum,
27
- market: MarketEnum,
28
- // 支持:1. 字符串 "白酒" 2. 数组 ["白酒"] 3. 带权重的对象 { name: "白酒", weight: 99 }
29
- industry: z.union([z.string(), z.array(z.string()), z.any()]).optional(),
30
- theme: z.union([z.string(), z.array(z.string()), z.any()]).optional()
27
+ market: MarketEnum
31
28
  });
32
29
 
33
30
  export type AddToWatchlistParams = z.infer<typeof AddToWatchlistParamsSchema>;
@@ -63,5 +60,3 @@ export interface CategoryRow {
63
60
  type?: 'industry' | 'theme';
64
61
  weight?: number;
65
62
  }
66
-
67
- export type TagInput = string | string[] | { name: string; weight?: number } | { name: string; weight?: number }[];
@@ -3,8 +3,9 @@ import { randomUUID } from "node:crypto";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  import { z } from "openclaw/plugin-sdk/zod";
5
5
  import { PluginRuntime } from "openclaw/plugin-sdk";
6
- import { getDB } from "../../core/database";
7
- import { watchlistLogic } from "./logic";
6
+ import { getDB } from "../../core/database.js";
7
+ import { logger } from "../../core/logger.js";
8
+ import { watchlistLogic } from "./logic.js";
8
9
  import {
9
10
  AddToWatchlistParams,
10
11
  AddToWatchlistParamsSchema,
@@ -12,34 +13,64 @@ import {
12
13
  UpdateWatchlistItemSchema,
13
14
  WatchlistRow,
14
15
  CategoryRow,
15
- TagInput,
16
16
  GetWatchlistParams,
17
17
  GetWatchlistParamsSchema,
18
18
  SyncCategoriesParams,
19
19
  SyncCategoriesParamsSchema,
20
20
  BatchUpdateSortOrdersParams,
21
21
  BatchUpdateSortOrdersParamsSchema
22
- } from "./schema";
22
+ } from "./schema.js";
23
+
24
+ let watchlistMutationQueue: Promise<void> = Promise.resolve();
25
+
26
+ function enqueueWatchlistMutation<T>(task: () => Promise<T>): Promise<T> {
27
+ const previous = watchlistMutationQueue;
28
+ let release!: () => void;
29
+ watchlistMutationQueue = new Promise<void>((resolve) => {
30
+ release = resolve;
31
+ });
32
+
33
+ return previous
34
+ .catch(() => undefined)
35
+ .then(task)
36
+ .finally(release);
37
+ }
23
38
 
24
39
  /**
25
40
  * 辅助函数:统一处理归一化分类项
26
41
  */
27
- function normalizeTags(input: TagInput | undefined): { name: string, weight: number }[] {
42
+ function normalizeTags(input: { name: string; weight?: number } | { name: string; weight?: number }[] | null | undefined): { name: string, weight: number }[] {
28
43
  if (!input) return [];
29
44
  const items = Array.isArray(input) ? input : [input];
30
- return items.map(item => {
31
- if (typeof item === 'string') return { name: item, weight: 0 };
32
- if (typeof item === 'object' && item !== null && 'name' in item) {
33
- return { name: item.name, weight: item.weight ?? 0 };
34
- }
35
- return null;
36
- }).filter((i): i is { name: string, weight: number } => i !== null);
45
+ return items
46
+ .filter((item): item is { name: string; weight?: number } => typeof item?.name === 'string' && item.name.trim().length > 0)
47
+ .map(item => ({ name: item.name, weight: item.weight ?? 0 }));
48
+ }
49
+
50
+ function normalizeWatchlistStock(stock: AddToWatchlistParams): AddToWatchlistParams {
51
+ return {
52
+ ...stock,
53
+ stockCode: watchlistLogic._normalizeStockCodeForCache(stock.stockCode, stock.exchange)
54
+ };
55
+ }
56
+
57
+ function watchlistStockKey(stock: AddToWatchlistParams): string {
58
+ return `${watchlistLogic._normalizeStockCodeForCache(stock.stockCode, stock.exchange)}:${stock.exchange}`;
37
59
  }
38
60
 
39
61
  /**
40
62
  * 辅助函数:更新股票的分类标签
41
63
  */
42
- function updateWatchlistTags(db: DatabaseSync, watchlistId: string, userId: string, industry: TagInput | undefined, theme: TagInput | undefined) {
64
+ function updateWatchlistTags(
65
+ db: DatabaseSync,
66
+ watchlistId: string,
67
+ userId: string,
68
+ industry: { name: string; weight?: number } | null | undefined,
69
+ theme: { name: string; weight?: number }[] | undefined
70
+ ) {
71
+ db.prepare("DELETE FROM watchlist_industry_items WHERE watchlistId = ? AND userId = ?").run(watchlistId, userId);
72
+ db.prepare("DELETE FROM watchlist_theme_items WHERE watchlistId = ? AND userId = ?").run(watchlistId, userId);
73
+
43
74
  const normIndustries = normalizeTags(industry);
44
75
  for (const item of normIndustries) {
45
76
  const categoryId = watchlistLogic._ensureCategory(db, item.name, 'industry', userId);
@@ -63,77 +94,75 @@ function updateWatchlistTags(db: DatabaseSync, watchlistId: string, userId: stri
63
94
  }
64
95
  }
65
96
 
97
+ function upsertStockClassificationCache(
98
+ db: DatabaseSync,
99
+ stock: AddToWatchlistParams,
100
+ classification: {
101
+ industry: { name: string; weight?: number };
102
+ theme?: { name: string; weight?: number }[];
103
+ }
104
+ ) {
105
+ const cacheCode = watchlistLogic._normalizeStockCodeForCache(stock.stockCode, stock.exchange);
106
+ const legacyCode = stock.stockCode.trim().toUpperCase().replace(/\.(SH|SS|SZ|HK|US)$/i, "");
107
+ db.prepare(`
108
+ INSERT OR REPLACE INTO global_stock_metadata (stockCode, exchange, stockName, industryJson, themeJson)
109
+ VALUES (?, ?, ?, ?, ?)
110
+ `).run(
111
+ cacheCode,
112
+ stock.exchange,
113
+ stock.stockName,
114
+ JSON.stringify(classification.industry),
115
+ JSON.stringify(classification.theme || [])
116
+ );
117
+ if (legacyCode && legacyCode !== cacheCode) {
118
+ db.prepare(`
119
+ DELETE FROM global_stock_metadata WHERE stockCode = ? AND exchange = ?
120
+ `).run(legacyCode, stock.exchange);
121
+ }
122
+ logger.info({
123
+ stockCode: stock.stockCode,
124
+ cacheCode,
125
+ exchange: stock.exchange,
126
+ industry: classification.industry.name,
127
+ themeCount: classification.theme?.length || 0
128
+ }, "[Watchlist] classification cache upserted");
129
+ }
130
+
66
131
  export const watchlistTools = {
67
132
  add_to_watchlist: {
68
133
  name: "add_to_watchlist",
69
134
  description: "添加股票到自选列表",
70
135
  parameters: AddToWatchlistParamsSchema,
136
+ registerTool: false,
71
137
  execute: async (args: AddToWatchlistParams, ctx: { userId: string, runtime?: PluginRuntime }) => {
72
- const db = getDB();
73
- const uId = String(ctx.userId);
74
- db.exec("BEGIN TRANSACTION");
75
- try {
76
- const sortRow = db.prepare("SELECT MAX(sortOrder) as max FROM watchlist WHERE userId=? AND isDeleted=0").get(uId) as { max: number } | undefined;
77
- const nextOrder = (sortRow?.max ?? 0) + 1024;
78
-
79
- let watchlistId: string;
80
- const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?").get(uId, args.stockCode, args.exchange) as WatchlistRow | undefined;
81
-
82
- if (existingItem) {
83
- watchlistId = existingItem.id;
84
- if (existingItem.isDeleted === 1) {
85
- db.prepare(`
86
- UPDATE watchlist SET isDeleted = 0, stockName = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
87
- `).run(args.stockName, nextOrder, watchlistId);
88
- } else {
89
- db.prepare(`
90
- UPDATE watchlist SET stockName = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
91
- `).run(args.stockName, watchlistId);
92
- }
93
- } else {
94
- watchlistId = randomUUID();
95
- db.prepare(`
96
- INSERT INTO watchlist (id, userId, stockCode, stockName, exchange, market, sortOrder)
97
- VALUES (?, ?, ?, ?, ?, ?, ?)
98
- `).run(watchlistId, uId, args.stockCode, args.stockName, args.exchange, args.market, nextOrder);
138
+ return enqueueWatchlistMutation(async () => {
139
+ const uId = String(ctx.userId);
140
+ if (!ctx.runtime) {
141
+ return JSON.stringify({ success: false, error: "无法分析行业/主题关系:runtime 不可用" });
99
142
  }
100
-
101
- if (!args.industry && !args.theme && ctx.runtime) {
102
- watchlistLogic.getStockClassification(ctx.runtime, args.stockName, args.stockCode, args.exchange, uId)
103
- .then(classification => {
104
- if (classification) {
105
- const db2 = getDB();
106
- updateWatchlistTags(db2, watchlistId, uId, classification.industry, classification.theme);
107
- }
108
- }).catch(err => console.error("[Watchlist] 自动分类失败:", err));
109
- } else {
110
- updateWatchlistTags(db, watchlistId, uId, args.industry, args.theme);
143
+ const db = getDB();
144
+ const stock = normalizeWatchlistStock(args);
145
+ const existingBeforeClassify = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?")
146
+ .get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
147
+ if (existingBeforeClassify?.isDeleted === 0) {
148
+ return JSON.stringify({ success: true, skipped: true, reason: "duplicate", id: existingBeforeClassify.id });
111
149
  }
112
- db.exec("COMMIT");
113
- return JSON.stringify({ success: true, id: watchlistId });
114
- } catch (e: any) {
115
- db.exec("ROLLBACK");
116
- return JSON.stringify({ success: false, error: e.message });
117
- }
118
- }
119
- },
120
150
 
121
- batch_add_to_watchlist: {
122
- name: "batch_add_to_watchlist",
123
- description: "批量添加股票到自选列表",
124
- parameters: z.object({ stocks: z.array(AddToWatchlistParamsSchema) }),
125
- execute: async (args: { stocks: AddToWatchlistParams[] }, ctx: { userId: string, runtime?: PluginRuntime }) => {
126
- const db = getDB();
127
- const uId = String(ctx.userId);
128
- db.exec("BEGIN TRANSACTION");
129
- try {
130
- const results: string[] = [];
131
- const sortRow = db.prepare("SELECT MAX(sortOrder) as max FROM watchlist WHERE userId=? AND isDeleted=0").get(uId) as { max: number } | undefined;
132
- let currentMaxOrder = sortRow?.max ?? 0;
151
+ let classification: Awaited<ReturnType<typeof watchlistLogic.getStockClassification>>;
152
+ try {
153
+ classification = await watchlistLogic.getStockClassification(ctx.runtime, stock.stockName, stock.stockCode, stock.exchange, uId);
154
+ } catch (e: any) {
155
+ return JSON.stringify({ success: false, error: e.message });
156
+ }
157
+ if (!classification) {
158
+ return JSON.stringify({ success: false, error: "行业/主题关系分析失败" });
159
+ }
133
160
 
134
- for (const stock of args.stocks) {
135
- const nextOrder = currentMaxOrder + 1024;
136
- currentMaxOrder = nextOrder;
161
+ if (db.inTransaction) db.exec("ROLLBACK");
162
+ db.exec("BEGIN TRANSACTION");
163
+ try {
164
+ const sortRow = db.prepare("SELECT MAX(sortOrder) as max FROM watchlist WHERE userId=? AND isDeleted=0").get(uId) as { max: number } | undefined;
165
+ const nextOrder = (sortRow?.max ?? 0) + 1024;
137
166
 
138
167
  let watchlistId: string;
139
168
  const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?").get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
@@ -145,9 +174,8 @@ export const watchlistTools = {
145
174
  UPDATE watchlist SET isDeleted = 0, stockName = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
146
175
  `).run(stock.stockName, nextOrder, watchlistId);
147
176
  } else {
148
- db.prepare(`
149
- UPDATE watchlist SET stockName = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
150
- `).run(stock.stockName, watchlistId);
177
+ db.exec("ROLLBACK");
178
+ return JSON.stringify({ success: true, skipped: true, reason: "duplicate", id: watchlistId });
151
179
  }
152
180
  } else {
153
181
  watchlistId = randomUUID();
@@ -156,34 +184,145 @@ export const watchlistTools = {
156
184
  VALUES (?, ?, ?, ?, ?, ?, ?)
157
185
  `).run(watchlistId, uId, stock.stockCode, stock.stockName, stock.exchange, stock.market, nextOrder);
158
186
  }
159
- results.push(watchlistId);
187
+
188
+ upsertStockClassificationCache(db, stock, classification);
189
+ updateWatchlistTags(db, watchlistId, uId, classification.industry, classification.theme);
190
+ db.exec("COMMIT");
191
+ return JSON.stringify({ success: true, id: watchlistId });
192
+ } catch (e: any) {
193
+ if (db.inTransaction) db.exec("ROLLBACK");
194
+ return JSON.stringify({ success: false, error: e.message });
160
195
  }
196
+ });
197
+ }
198
+ },
161
199
 
162
- if (ctx.runtime && args.stocks.length > 0) {
163
- if (!args.stocks[0].industry && !args.stocks[0].theme) {
164
- watchlistLogic.getBatchStockClassification(ctx.runtime, args.stocks, uId)
165
- .then(batchResults => {
166
- const db2 = getDB();
167
- batchResults.forEach((res, i) => {
168
- if (res) {
169
- updateWatchlistTags(db2, results[i], uId, res.industry, res.theme);
170
- }
171
- });
172
- }).catch(err => console.error("[Watchlist] 批量自动分类失败:", err));
200
+ batch_add_to_watchlist: {
201
+ name: "batch_add_to_watchlist",
202
+ description: "批量添加股票到自选列表。添加多只股票时必须使用这个工具,不要循环调用 add_to_watchlist。",
203
+ parameters: z.object({ stocks: z.array(AddToWatchlistParamsSchema) }),
204
+ execute: async (args: { stocks: AddToWatchlistParams[] }, ctx: { userId: string, runtime?: PluginRuntime }) => {
205
+ return enqueueWatchlistMutation(async () => {
206
+ if (!Array.isArray(args.stocks) || args.stocks.length === 0) {
207
+ return JSON.stringify({ success: false, error: "批量添加失败:stocks 不能为空" });
208
+ }
209
+ if (!ctx.runtime) {
210
+ return JSON.stringify({ success: false, error: "无法分析行业/主题关系:runtime 不可用" });
211
+ }
212
+
213
+ const db = getDB();
214
+ const uId = String(ctx.userId);
215
+ const uniqueStocks: AddToWatchlistParams[] = [];
216
+ const inputSeen = new Set<string>();
217
+ const skipped: { stockCode: string; exchange: string; reason: "input_duplicate" | "duplicate"; id?: string }[] = [];
218
+ for (const rawStock of args.stocks) {
219
+ const stock = normalizeWatchlistStock(rawStock);
220
+ const key = watchlistStockKey(stock);
221
+ if (inputSeen.has(key)) {
222
+ skipped.push({ stockCode: stock.stockCode, exchange: stock.exchange, reason: "input_duplicate" });
223
+ continue;
224
+ }
225
+ inputSeen.add(key);
226
+ uniqueStocks.push(stock);
227
+ }
228
+ const stocksToAdd: AddToWatchlistParams[] = [];
229
+ for (const stock of uniqueStocks) {
230
+ const existing = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?")
231
+ .get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
232
+ if (existing?.isDeleted === 0) {
233
+ skipped.push({ stockCode: stock.stockCode, exchange: stock.exchange, reason: "duplicate", id: existing.id });
173
234
  } else {
174
- const db2 = getDB();
175
- args.stocks.forEach((s, i) => {
176
- updateWatchlistTags(db2, results[i], uId, s.industry, s.theme);
177
- });
235
+ stocksToAdd.push(stock);
178
236
  }
179
237
  }
238
+ logger.info({
239
+ count: args.stocks?.length ?? 0,
240
+ addCount: stocksToAdd.length,
241
+ skippedCount: skipped.length,
242
+ codes: args.stocks?.map(stock => stock.stockCode)
243
+ }, "[Watchlist] batch_add_to_watchlist received");
244
+ if (stocksToAdd.length === 0) {
245
+ return JSON.stringify({ success: true, ids: [], skipped });
246
+ }
180
247
 
181
- db.exec("COMMIT");
182
- return JSON.stringify({ success: true, ids: results });
183
- } catch (e: any) {
184
- db.exec("ROLLBACK");
185
- return JSON.stringify({ success: false, error: e.message });
186
- }
248
+ let batchResults: Awaited<ReturnType<typeof watchlistLogic.classifyStocksTogether>>;
249
+ try {
250
+ batchResults = await watchlistLogic.classifyStocksTogether(ctx.runtime, stocksToAdd, uId);
251
+ } catch (e: any) {
252
+ logger.warn({
253
+ count: stocksToAdd.length,
254
+ codes: stocksToAdd.map(stock => stock.stockCode),
255
+ error: e.message || String(e)
256
+ }, "[Watchlist] batch_add_to_watchlist classification failed");
257
+ return JSON.stringify({
258
+ success: false,
259
+ failedStage: "classification",
260
+ error: e.message || "批量添加失败:行业/主题关系分析失败"
261
+ });
262
+ }
263
+
264
+ if (db.inTransaction) db.exec("ROLLBACK");
265
+ db.exec("BEGIN TRANSACTION");
266
+ try {
267
+ const results: string[] = [];
268
+ const writtenItems: { id: string; stock: AddToWatchlistParams; classification: typeof batchResults[number] }[] = [];
269
+ const sortRow = db.prepare("SELECT MAX(sortOrder) as max FROM watchlist WHERE userId=? AND isDeleted=0").get(uId) as { max: number } | undefined;
270
+ let currentMaxOrder = sortRow?.max ?? 0;
271
+
272
+ stocksToAdd.forEach((stock, i) => {
273
+ const nextOrder = currentMaxOrder + 1024;
274
+ currentMaxOrder = nextOrder;
275
+ const classification = batchResults[i];
276
+ if (!classification) {
277
+ throw new Error(`行业/主题关系分析失败: ${stock.stockName}`);
278
+ }
279
+
280
+ let watchlistId: string;
281
+ const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?").get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
282
+
283
+ if (existingItem) {
284
+ watchlistId = existingItem.id;
285
+ if (existingItem.isDeleted === 1) {
286
+ db.prepare(`
287
+ UPDATE watchlist SET isDeleted = 0, stockName = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
288
+ `).run(stock.stockName, nextOrder, watchlistId);
289
+ } else {
290
+ skipped.push({ stockCode: stock.stockCode, exchange: stock.exchange, reason: "duplicate", id: watchlistId });
291
+ return;
292
+ }
293
+ } else {
294
+ watchlistId = randomUUID();
295
+ db.prepare(`
296
+ INSERT INTO watchlist (id, userId, stockCode, stockName, exchange, market, sortOrder)
297
+ VALUES (?, ?, ?, ?, ?, ?, ?)
298
+ `).run(watchlistId, uId, stock.stockCode, stock.stockName, stock.exchange, stock.market, nextOrder);
299
+ }
300
+ results.push(watchlistId);
301
+ writtenItems.push({ id: watchlistId, stock, classification });
302
+ });
303
+
304
+ if (writtenItems.length > 0) {
305
+ writtenItems.forEach(({ id, stock, classification }) => {
306
+ upsertStockClassificationCache(db, stock, classification);
307
+ updateWatchlistTags(db, id, uId, classification.industry, classification.theme);
308
+ });
309
+ }
310
+
311
+ if (results.length !== stocksToAdd.length) {
312
+ logger.info({
313
+ input: args.stocks.length,
314
+ addRequested: stocksToAdd.length,
315
+ written: results.length,
316
+ skipped: skipped.length
317
+ }, "[Watchlist] batch_add_to_watchlist skipped duplicates");
318
+ }
319
+ db.exec("COMMIT");
320
+ return JSON.stringify({ success: true, ids: results, skipped });
321
+ } catch (e: any) {
322
+ if (db.inTransaction) db.exec("ROLLBACK");
323
+ return JSON.stringify({ success: false, error: e.message });
324
+ }
325
+ });
187
326
  }
188
327
  },
189
328
 
@@ -431,9 +570,9 @@ export const watchlistTools = {
431
570
  try {
432
571
  db.prepare("DELETE FROM watchlist_industry_items WHERE userId = ?").run(uId);
433
572
  db.prepare("DELETE FROM watchlist_theme_items WHERE userId = ?").run(uId);
434
- const stocks = db.prepare("SELECT stockName, stockCode, exchange FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
573
+ const stocks = db.prepare("SELECT stockName, stockCode, exchange, market FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
435
574
  if (stocks.length > 0) {
436
- watchlistLogic.getBatchStockClassification(ctx.runtime, stocks, uId)
575
+ watchlistLogic.getBatchStockClassification(ctx.runtime, stocks, uId, { forceRefresh: true })
437
576
  .then(batchResults => {
438
577
  const db2 = getDB();
439
578
  const userStocks = db2.prepare("SELECT id, stockCode, exchange FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
@@ -442,8 +581,14 @@ export const watchlistTools = {
442
581
  const s = stocks[i];
443
582
  const match = userStocks.find(us => us.stockCode === s.stockCode && us.exchange === s.exchange);
444
583
  if (match) {
584
+ upsertStockClassificationCache(db2, {
585
+ stockName: s.stockName,
586
+ stockCode: s.stockCode,
587
+ exchange: s.exchange,
588
+ market: s.market
589
+ }, res);
445
590
  const cats = [
446
- { name: res.industry.name, type: 'industry' as const, weight: res.industry.weight },
591
+ ...(res.industry ? [{ name: res.industry.name, type: 'industry' as const, weight: res.industry.weight }] : []),
447
592
  ...res.theme.map((t: any) => ({ name: t.name, type: 'theme' as const, weight: t.weight }))
448
593
  ];
449
594
  cats.forEach(c => {
@@ -466,4 +611,4 @@ export const watchlistTools = {
466
611
  }
467
612
  }
468
613
  }
469
- };
614
+ };
package/src/runtime.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
2
2
  import path from "path";
3
3
  import * as os from "node:os";
4
- import { logger } from "./core/logger";
4
+ import { logger } from "./core/logger.js";
5
5
 
6
6
  let runtime: PluginRuntime | null = null;
7
7
  let dbPath: string = "";
@@ -11,10 +11,10 @@ export function setHedgehogRuntime(next: PluginRuntime): void {
11
11
  runtime = next;
12
12
 
13
13
  try {
14
- // 从配置里读取 hedgehog-workspace 的 workspace
14
+ // 从配置里读取 hedgehog-finance 的 workspace
15
15
  const cfg = next.config.loadConfig();
16
16
  const agentList = (cfg.agents?.list || []) as { id: string, workspace?: string }[];
17
- const hedgehogAgent = agentList.find((a) => a.id === "hedgehog-workspace");
17
+ const hedgehogAgent = agentList.find((a) => a.id === "hedgehog-finance");
18
18
  const workspaceDir = hedgehogAgent?.workspace ||
19
19
  cfg.agents?.defaults?.workspace ||
20
20
  path.join(os.homedir(), ".openclaw", "hedgehog-workspace");
package/src/types.ts CHANGED
@@ -62,7 +62,7 @@ export const StockClassificationSchema = z.object({
62
62
  industry: z.object({
63
63
  name: z.string(),
64
64
  weight: z.number().min(0).max(100).default(50)
65
- }).describe("Main industry category with weight"),
65
+ }).describe("Required main industry category with weight"),
66
66
  theme: z.array(z.object({
67
67
  name: z.string(),
68
68
  weight: z.number().min(0).max(100).default(50)
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "baseUrl": ".",
10
- "types": ["node"],
11
- "paths": {
12
- "openclaw/*": ["node_modules/openclaw/*"]
13
- }
14
- },
15
- "include": ["index.ts", "src/**/*.ts"]
16
- }