openclaw-github-trending 1.0.0

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +480 -0
  3. package/dist/channels/email.d.ts +61 -0
  4. package/dist/channels/email.d.ts.map +1 -0
  5. package/dist/channels/email.js +599 -0
  6. package/dist/channels/email.js.map +1 -0
  7. package/dist/channels/feishu.d.ts +50 -0
  8. package/dist/channels/feishu.d.ts.map +1 -0
  9. package/dist/channels/feishu.js +322 -0
  10. package/dist/channels/feishu.js.map +1 -0
  11. package/dist/channels/types.d.ts +66 -0
  12. package/dist/channels/types.d.ts.map +1 -0
  13. package/dist/channels/types.js +12 -0
  14. package/dist/channels/types.js.map +1 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/core/config.d.ts +83 -0
  19. package/dist/core/config.d.ts.map +1 -0
  20. package/dist/core/config.js +145 -0
  21. package/dist/core/config.js.map +1 -0
  22. package/dist/core/fetcher.d.ts +43 -0
  23. package/dist/core/fetcher.d.ts.map +1 -0
  24. package/dist/core/fetcher.js +306 -0
  25. package/dist/core/fetcher.js.map +1 -0
  26. package/dist/core/file-storage.d.ts +62 -0
  27. package/dist/core/file-storage.d.ts.map +1 -0
  28. package/dist/core/file-storage.js +253 -0
  29. package/dist/core/file-storage.js.map +1 -0
  30. package/dist/core/history.d.ts +71 -0
  31. package/dist/core/history.d.ts.map +1 -0
  32. package/dist/core/history.js +133 -0
  33. package/dist/core/history.js.map +1 -0
  34. package/dist/core/summarizer.d.ts +64 -0
  35. package/dist/core/summarizer.d.ts.map +1 -0
  36. package/dist/core/summarizer.js +324 -0
  37. package/dist/core/summarizer.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +668 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/models/config.d.ts +93 -0
  43. package/dist/models/config.d.ts.map +1 -0
  44. package/dist/models/config.js +3 -0
  45. package/dist/models/config.js.map +1 -0
  46. package/dist/models/history.d.ts +6 -0
  47. package/dist/models/history.d.ts.map +1 -0
  48. package/dist/models/history.js +7 -0
  49. package/dist/models/history.js.map +1 -0
  50. package/dist/models/repository.d.ts +28 -0
  51. package/dist/models/repository.d.ts.map +1 -0
  52. package/dist/models/repository.js +3 -0
  53. package/dist/models/repository.js.map +1 -0
  54. package/dist/models/service.types.d.ts +87 -0
  55. package/dist/models/service.types.d.ts.map +1 -0
  56. package/dist/models/service.types.js +3 -0
  57. package/dist/models/service.types.js.map +1 -0
  58. package/dist/services/trending.service.d.ts +29 -0
  59. package/dist/services/trending.service.d.ts.map +1 -0
  60. package/dist/services/trending.service.js +306 -0
  61. package/dist/services/trending.service.js.map +1 -0
  62. package/dist/tool.d.ts +47 -0
  63. package/dist/tool.d.ts.map +1 -0
  64. package/dist/tool.js +314 -0
  65. package/dist/tool.js.map +1 -0
  66. package/dist/utils/logger.d.ts +77 -0
  67. package/dist/utils/logger.d.ts.map +1 -0
  68. package/dist/utils/logger.js +214 -0
  69. package/dist/utils/logger.js.map +1 -0
  70. package/dist/utils/markdown.d.ts +9 -0
  71. package/dist/utils/markdown.d.ts.map +1 -0
  72. package/dist/utils/markdown.js +40 -0
  73. package/dist/utils/markdown.js.map +1 -0
  74. package/openclaw.plugin.json +152 -0
  75. package/package.json +78 -0
package/dist/index.js ADDED
@@ -0,0 +1,668 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.default = default_1;
37
+ const zod_1 = require("zod");
38
+ const fetcher_1 = require("./core/fetcher");
39
+ const summarizer_1 = require("./core/summarizer");
40
+ const history_1 = require("./core/history");
41
+ const feishu_1 = require("./channels/feishu");
42
+ const email_1 = require("./channels/email");
43
+ const config_1 = require("./core/config");
44
+ const logger_1 = require("./utils/logger");
45
+ const file_storage_1 = require("./core/file-storage");
46
+ const logger = logger_1.Logger.get('Plugin');
47
+ function default_1(api) {
48
+ let openclawConfigFromApi = null;
49
+ try {
50
+ openclawConfigFromApi = api.config;
51
+ }
52
+ catch (e) {
53
+ logger.warn('api.config not available', { error: e });
54
+ }
55
+ // Register CLI command for creating cron jobs or running immediately
56
+ api.registerCli(({ program }) => {
57
+ program
58
+ .command('gen-cron <mode> <since> <channels>')
59
+ .description('生成 GitHub 热榜定时任务或立即执行')
60
+ .addHelpText('after', `
61
+ 参数说明:
62
+ mode 执行模式
63
+ - "now" 表示立即执行
64
+ - Cron 表达式(格式:分 时 日 月 周)表示定时执行
65
+
66
+ since 热榜周期
67
+ - daily 今日热榜
68
+ - weekly 本周热榜
69
+ - monthly 本月热榜
70
+
71
+ channels 推送渠道(多个渠道用逗号分隔)
72
+ - email 推送到邮箱
73
+ - feishu 推送到飞书
74
+ - email,feishu 同时推送到邮箱和飞书
75
+
76
+ Cron 表达式格式:
77
+ 格式:分(0-59) 时(0-23) 日(1-31) 月(1-12) 周(0-7, 0和7都是周日)
78
+ 时区:使用服务器本地时间
79
+
80
+ 常用 Cron 示例:
81
+ "0 8 * * *" - 每天 8:00
82
+ "0 10 * * 3" - 每周三 10:00
83
+ "0 9 1 * *" - 每月 1 号 9:00
84
+
85
+ 示例:
86
+ # 立即执行:获取今日热榜并推送到飞书和邮箱
87
+ openclaw gen-cron now daily email,feishu
88
+
89
+ # 创建定时任务:每周三 10:00 获取本周热榜并推送到飞书
90
+ openclaw gen-cron "0 10 * * 3" weekly feishu
91
+
92
+ # 创建定时任务:每月 1 号 9:00 获取本月热榜并推送到邮箱和飞书
93
+ openclaw gen-cron "0 9 1 * *" monthly email,feishu
94
+
95
+ # 创建定时任务:每天早上 8:00 获取今日热榜并推送到邮箱
96
+ openclaw gen-cron "0 8 * * *" daily email
97
+
98
+ 提示:
99
+ - 推送渠道需要在 ~/.openclaw/openclaw.json 中配置
100
+ `)
101
+ .action(async (mode, since, channels) => {
102
+ const cliLogger = logger_1.Logger.get('CLI');
103
+ const pluginId = 'openclaw-github-trending';
104
+ const modeLower = mode.toLowerCase();
105
+ const sinceLower = since.toLowerCase();
106
+ let schedule;
107
+ let channelList = channels.split(',').map(c => c.trim());
108
+ // Validate since parameter
109
+ const validSince = ['daily', 'weekly', 'monthly'];
110
+ if (!validSince.includes(sinceLower)) {
111
+ console.error(``);
112
+ console.error(`❌ 错误:since 参数必须是 ${validSince.join('、')} 之一`);
113
+ console.error(``);
114
+ console.error(`📌 命令用法:`);
115
+ console.error(` openclaw gen-cron <mode> <since> <channels>`);
116
+ console.error(``);
117
+ console.error(`📘 参数说明:`);
118
+ console.error(` mode : 执行模式 - "now" 表示立即执行,或 Cron 表达式(格式:分 时 日 月 周)`);
119
+ console.error(` since : 热榜周期 - "daily"(今日)、"weekly"(本周)、"monthly"(本月)`);
120
+ console.error(` channels : 推送渠道 - "email"、"feishu" 或 "email,feishu"(多个渠道用逗号分隔)`);
121
+ console.error(``);
122
+ console.error(`📋 示例:`);
123
+ console.error(` # 立即执行:获取今日热榜并推送到飞书和邮箱`);
124
+ console.error(` openclaw gen-cron now daily email,feishu`);
125
+ console.error(``);
126
+ console.error(` # 创建定时任务:每周三 10:00 获取本周热榜并推送到飞书`);
127
+ console.error(` openclaw gen-cron "0 10 * * 3" weekly feishu`);
128
+ console.error(``);
129
+ console.error(` # 创建定时任务:每月 1 号 9:00 获取本月热榜并推送到邮箱和飞书`);
130
+ console.error(` openclaw gen-cron "0 9 1 * *" monthly email,feishu`);
131
+ console.error(``);
132
+ console.error(` # 创建定时任务:每天早上 8:00 获取今日热榜并推送到邮箱`);
133
+ console.error(` openclaw gen-cron "0 8 * * *" daily email`);
134
+ console.error(``);
135
+ console.error(`💡 提示:`);
136
+ console.error(` - Cron 表达式格式:分(0-59) 时(0-23) 日(1-31) 月(1-12) 周(0-7, 0和7都是周日)`);
137
+ console.error(` - 常用示例:`);
138
+ console.error(` "0 8 * * *" - 每天 8:00`);
139
+ console.error(` "0 10 * * 3" - 每周三 10:00`);
140
+ console.error(` "0 9 1 * *" - 每月 1 号 9:00`);
141
+ console.error(` - 推送渠道需要在 ~/.openclaw/openclaw.json 中配置`);
142
+ console.error(``);
143
+ process.exit(1);
144
+ }
145
+ // Validate channels
146
+ const validChannels = ['email', 'feishu'];
147
+ const invalidChannels = channelList.filter(c => !validChannels.includes(c));
148
+ if (invalidChannels.length > 0) {
149
+ console.error(``);
150
+ console.error(`❌ 错误:无效的渠道 "${invalidChannels.join(', ')}"`);
151
+ console.error(``);
152
+ console.error(`📌 可用渠道:${validChannels.join('、')}`);
153
+ console.error(``);
154
+ process.exit(1);
155
+ }
156
+ if (modeLower === 'now') {
157
+ // Immediate execution mode
158
+ cliLogger.info('Running immediately', { since: sinceLower, channels: channelList });
159
+ console.log(``);
160
+ console.log(`🚀 正在获取 GitHub ${sinceLower === 'daily' ? '今日' : sinceLower === 'weekly' ? '本周' : '本月'} 热榜项目...`);
161
+ console.log(`📬 推送渠道:${channelList.map(c => c === 'feishu' ? '🚀 飞书' : '📧 邮箱').join(' + ')}`);
162
+ console.log(``);
163
+ console.log(`⏳ 抓取热榜项目并让 AI 进行总结可能需要 1-3 分钟,请稍候...`);
164
+ console.log(``);
165
+ // Import and call the tool execute function directly
166
+ const githubTrendingTool = await Promise.resolve().then(() => __importStar(require('./tool')));
167
+ const toolParams = {
168
+ since: sinceLower,
169
+ channels: channelList
170
+ };
171
+ try {
172
+ // Get plugin config from api - use the same way as registerTool does
173
+ const pluginEntryConfig = api.config?.plugins?.entries?.[pluginId];
174
+ const pluginConfig = pluginEntryConfig?.config || {};
175
+ const openclawConfig = api.config || {};
176
+ cliLogger.info('CLI execution - Plugin config loaded', {
177
+ pluginId,
178
+ pluginConfigAvailable: Object.keys(pluginConfig).length > 0,
179
+ pluginConfigKeys: Object.keys(pluginConfig),
180
+ hasProxyConfig: !!pluginConfig.proxy
181
+ });
182
+ // Load history data from file storage (fallback to OpenClaw API if available)
183
+ let historyData = null;
184
+ try {
185
+ // Primary: Use file-based storage manager
186
+ const storageManager = (0, file_storage_1.getStorageManager)(pluginId);
187
+ historyData = await storageManager.get('github-trending-history');
188
+ if (historyData) {
189
+ cliLogger.info('CLI execution - History data loaded from file storage', {
190
+ hasHistory: true,
191
+ repoCount: Object.keys(historyData.repositories || {}).length,
192
+ storagePath: `~/.openclaw/plugins/${pluginId}/data/${storageManager['getCurrentMonthKey']()}.json`
193
+ });
194
+ }
195
+ else {
196
+ cliLogger.info('CLI execution - No existing history found in file storage');
197
+ }
198
+ }
199
+ catch (storageError) {
200
+ cliLogger.warn('Failed to load history data from file storage', { error: storageError });
201
+ }
202
+ const result = await githubTrendingTool.githubTrendingTool.handler(toolParams, pluginConfig, openclawConfig, historyData // ✅ 传递历史数据
203
+ );
204
+ // Save history data back to file storage using returned history
205
+ try {
206
+ const storageManager = (0, file_storage_1.getStorageManager)(pluginId);
207
+ // Use history_data returned from handler (contains updated data)
208
+ if (result.history_data) {
209
+ await storageManager.set('github-trending-history', result.history_data);
210
+ cliLogger.info('CLI execution - History data saved successfully to file storage', {
211
+ repoCount: Object.keys(result.history_data.repositories || {}).length,
212
+ pushedCount: result.pushed_count,
213
+ newCount: result.new_count
214
+ });
215
+ // Log storage statistics
216
+ const stats = await storageManager.getStats();
217
+ cliLogger.info('Storage statistics', {
218
+ currentMonth: stats.currentMonth,
219
+ currentMonthSize: stats.currentMonthSize,
220
+ totalMonths: stats.totalMonths,
221
+ totalSize: stats.totalSize
222
+ });
223
+ }
224
+ else {
225
+ cliLogger.warn('CLI execution - No history_data returned from handler');
226
+ }
227
+ }
228
+ catch (saveError) {
229
+ cliLogger.warn('Failed to save history data to file storage', { error: saveError });
230
+ }
231
+ if (result.success) {
232
+ console.log(`✅ 执行成功!`);
233
+ console.log(` 已推送 ${result.pushed_count} 个热榜项目`);
234
+ console.log(` 新项目:${result.new_count} 个`);
235
+ console.log(` 已见过:${result.seen_count} 个`);
236
+ console.log(``);
237
+ console.log(`📬 请查看您的 ${channelList.map(c => c === 'feishu' ? '飞书' : '邮箱').join(' 和 ')},查看详细推送内容。`);
238
+ console.log(``);
239
+ process.exit(0);
240
+ }
241
+ else {
242
+ console.error(`❌ 执行失败:${result.message}`);
243
+ console.error(``);
244
+ process.exit(1);
245
+ }
246
+ }
247
+ catch (error) {
248
+ cliLogger.error('Execution failed', { error: error.message, stack: error.stack });
249
+ console.error(`❌ 执行出错:${error.message}`);
250
+ console.error(``);
251
+ console.error(`📄 详细日志已记录到:~/.openclaw/logs/github-trending/`);
252
+ console.error(``);
253
+ process.exit(1);
254
+ }
255
+ }
256
+ else {
257
+ // Cron scheduling mode
258
+ schedule = mode;
259
+ console.log(`📅 正在创建定时任务...`);
260
+ console.log(` 热榜周期:${sinceLower === 'daily' ? '每日' : sinceLower === 'weekly' ? '每周' : '每月'}`);
261
+ console.log(` 执行时间:${schedule}`);
262
+ console.log(` 推送渠道:${channelList.map(c => c === 'feishu' ? '🚀 飞书' : '📧 邮箱').join(' + ')}`);
263
+ console.log(``);
264
+ // Build tool params for cron job
265
+ const toolParams = { since: sinceLower, channels: channelList };
266
+ // Create cron job using openclaw cron add command
267
+ const periodLabel = sinceLower === 'daily' ? '每日' : sinceLower === 'weekly' ? '每周' : '每月';
268
+ const channelLabel = channelList.map(c => c === 'feishu' ? '飞书' : '邮箱').join('+');
269
+ const jobName = `GitHub 热榜 ${periodLabel} ${channelLabel}`;
270
+ const cronCmd = `openclaw cron add --name "${jobName}" --cron "${schedule}" --system-event '${JSON.stringify({ tool: "openclaw-github-trending", params: toolParams })}'`;
271
+ const { exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
272
+ try {
273
+ await new Promise((resolve, reject) => {
274
+ exec(cronCmd, (error, stdout, stderr) => {
275
+ if (error)
276
+ reject(error);
277
+ else
278
+ resolve({ stdout, stderr });
279
+ });
280
+ });
281
+ console.log(`✅ 定时任务创建成功!`);
282
+ console.log(``);
283
+ console.log(`📌 任务信息:`);
284
+ console.log(` 执行时间:${schedule}`);
285
+ console.log(` 执行内容:抓取 GitHub ${sinceLower === 'daily' ? '今日' : sinceLower === 'weekly' ? '本周' : '本月'} 热榜`);
286
+ console.log(` 推送渠道:${channelList.map(c => c === 'feishu' ? '🚀 飞书' : '📧 邮箱').join(' + ')}`);
287
+ console.log(``);
288
+ console.log(`⚙️ 管理任务:`);
289
+ console.log(` openclaw cron list # 👀 查看所有定时任务`);
290
+ console.log(` openclaw cron run <id> # ▶️ 立即手动执行任务`);
291
+ console.log(` openclaw cron remove <id> # 🗑️ 删除任务`);
292
+ console.log(``);
293
+ process.exit(0);
294
+ }
295
+ catch (error) {
296
+ console.error(`❌ 创建任务失败:${error.message}`);
297
+ console.error(``);
298
+ process.exit(1);
299
+ }
300
+ }
301
+ });
302
+ }, { commands: ['gen-cron'] });
303
+ // Register the tool
304
+ api.registerTool({
305
+ name: 'openclaw-github-trending',
306
+ description: 'Fetch GitHub trending repositories and push to Feishu or Email with AI summaries',
307
+ parameters: {
308
+ since: zod_1.z.enum(['daily', 'weekly', 'monthly']).describe('Time period for trending'),
309
+ channels: zod_1.z.array(zod_1.z.enum(['feishu', 'email'])).optional().describe('Push channels (array: ["email"], ["feishu"], or ["email", "feishu"])'),
310
+ email_to: zod_1.z.string().email().optional().describe('Email recipient (overrides config)'),
311
+ feishu_webhook: zod_1.z.string().url().optional().describe('Feishu webhook URL (overrides config)')
312
+ },
313
+ async execute(params, context) {
314
+ const { since, channels, email_to, feishu_webhook } = params;
315
+ const { config: pluginConfig, logger, storage, openclawConfig: openclawConfigFromContext } = context;
316
+ // Use api.config as fallback if context.openclawConfig is not available
317
+ const openclawConfig = openclawConfigFromContext || openclawConfigFromApi;
318
+ // Internal logger instance for file logging
319
+ const internalLogger = logger_1.Logger.get('Tool');
320
+ // Check if plugin is enabled
321
+ const pluginId = 'openclaw-github-trending';
322
+ const entryConfig = openclawConfig?.plugins?.entries?.[pluginId];
323
+ const isEnabled = entryConfig?.enabled ?? true; // Default to enabled if not specified
324
+ internalLogger.info('Plugin enabled status check', { pluginId, isEnabled, entryConfigAvailable: !!entryConfig });
325
+ if (!isEnabled) {
326
+ internalLogger.warn('Plugin is disabled, rejecting execution');
327
+ throw new Error(`插件 ${pluginId} 已禁用,无法执行。请在 openclaw.json 中设置 plugins.entries.${pluginId}.enabled = true`);
328
+ }
329
+ internalLogger.info('Starting execution', {
330
+ params,
331
+ configAvailable: !!pluginConfig,
332
+ storageAvailable: !!storage,
333
+ openclawConfigAvailable: !!openclawConfig
334
+ });
335
+ // Create a logger wrapper that logs to both OpenClaw logger and file
336
+ const safeLogger = {
337
+ info: (msg, ...args) => {
338
+ logger?.info(msg, ...args);
339
+ internalLogger.info(msg, ...args);
340
+ },
341
+ warn: (msg, ...args) => {
342
+ logger?.warn(msg, ...args);
343
+ internalLogger.warn(msg, ...args);
344
+ },
345
+ error: (msg, ...args) => {
346
+ logger?.error(msg, ...args);
347
+ internalLogger.error(msg, ...args);
348
+ }
349
+ };
350
+ // Parse channels (use params.channels, not context)
351
+ const targetChannels = channels || [];
352
+ if (targetChannels.length === 0) {
353
+ // Fallback to configured channels if not specified in params
354
+ if (pluginConfig?.channels?.feishu?.webhook_url)
355
+ targetChannels.push('feishu');
356
+ if (pluginConfig?.channels?.email?.sender)
357
+ targetChannels.push('email');
358
+ }
359
+ if (targetChannels.length === 0) {
360
+ safeLogger.error('No channels configured or specified');
361
+ throw new Error('请指定至少一个推送通道:channels 参数,或在配置中配置 webhook_url 或 email');
362
+ }
363
+ // Override channels with provided values
364
+ if (email_to && targetChannels.includes('email')) {
365
+ pluginConfig.channels.email.recipient = email_to;
366
+ }
367
+ if (feishu_webhook && targetChannels.includes('feishu')) {
368
+ pluginConfig.channels.feishu.webhook_url = feishu_webhook;
369
+ }
370
+ try {
371
+ // Initialize history manager
372
+ const historyManager = new history_1.HistoryManager();
373
+ if (storage) {
374
+ const historyData = await storage.get('github-trending-history');
375
+ if (historyData) {
376
+ historyManager.importData(historyData);
377
+ }
378
+ }
379
+ // Resolve AI configuration
380
+ const aiConfig = config_1.ConfigManager.getAIConfig(pluginConfig, openclawConfig);
381
+ if (!aiConfig.apiKey) {
382
+ safeLogger.error('AI API key not found');
383
+ return {
384
+ content: [{
385
+ type: 'text',
386
+ text: JSON.stringify({
387
+ success: false,
388
+ error: 'AI API key is required. Please configure it in plugin settings, OpenClaw global config, or environment variables (OPENAI_API_KEY or ANTHROPIC_API_KEY).',
389
+ timestamp: new Date().toISOString()
390
+ }, null, 2)
391
+ }],
392
+ isError: true
393
+ };
394
+ }
395
+ safeLogger.info(`Using AI provider: ${aiConfig.provider}, model: ${aiConfig.model}`);
396
+ // Fetch trending repositories
397
+ safeLogger.info(`Fetching GitHub trending repositories (${since})`);
398
+ const fetcher = new fetcher_1.GitHubFetcher(pluginConfig);
399
+ const repositories = await fetcher.fetchTrending(since);
400
+ // Categorize repositories
401
+ const historyConfig = {
402
+ enabled: pluginConfig?.history?.enabled ?? true,
403
+ star_threshold: pluginConfig?.history?.star_threshold ?? 100
404
+ };
405
+ const { newlySeen, shouldPush, alreadySeen } = historyManager.categorizeRepositories(repositories, historyConfig);
406
+ safeLogger.info(`Found ${repositories.length} repos, ${shouldPush.length} to push`);
407
+ // Log detailed repository information
408
+ safeLogger.info('Detailed repository list:');
409
+ repositories.forEach((repo, index) => {
410
+ safeLogger.info(` ${index + 1}. ${repo.full_name}`);
411
+ safeLogger.info(` Stars: ${repo.stars.toLocaleString()} | Description: ${repo.description || 'N/A'}`);
412
+ });
413
+ safeLogger.info('Categorization results:');
414
+ if (newlySeen.length > 0) {
415
+ safeLogger.info(` ➕ Newly seen (${newlySeen.length}):`);
416
+ newlySeen.forEach((repo, idx) => {
417
+ safeLogger.info(` ${idx + 1}. ${repo.full_name} (${repo.stars} stars)`);
418
+ });
419
+ }
420
+ if (shouldPush.length > 0) {
421
+ safeLogger.info(` ✅ Should push (${shouldPush.length}):`);
422
+ shouldPush.forEach((repo, idx) => {
423
+ safeLogger.info(` ${idx + 1}. ${repo.full_name} (${repo.stars} stars)`);
424
+ });
425
+ }
426
+ if (alreadySeen.length > 0) {
427
+ safeLogger.info(` 🔁 Already seen (${alreadySeen.length}):`);
428
+ alreadySeen.forEach((repo, idx) => {
429
+ const history = historyManager.getProject(repo.full_name);
430
+ const starsDiff = repo.stars - (history?.last_stars || 0);
431
+ safeLogger.info(` ${idx + 1}. ${repo.full_name} (${repo.stars} stars, +${starsDiff} since last)`);
432
+ });
433
+ }
434
+ // Generate AI summaries (with concurrency control)
435
+ const summarizer = new summarizer_1.AISummarizer(aiConfig);
436
+ const maxWorkers = config_1.ConfigManager.getMaxWorkers(pluginConfig);
437
+ const reposWithSummary = [];
438
+ safeLogger.info(`Generating AI summaries with ${maxWorkers} workers...`);
439
+ // Process repositories in batches with concurrency control
440
+ for (let i = 0; i < shouldPush.length; i += maxWorkers) {
441
+ const batch = shouldPush.slice(i, i + maxWorkers);
442
+ safeLogger.info(`[Batch ${Math.floor(i / maxWorkers) + 1}/${Math.ceil(shouldPush.length / maxWorkers)}] Processing ${batch.length} repositories...`);
443
+ const batchResults = await Promise.allSettled(batch.map(async (repo) => {
444
+ try {
445
+ safeLogger.info(` 📖 [${repo.full_name}] Fetching README...`);
446
+ const readmeContent = await fetcher.fetchReadme(repo.full_name);
447
+ let summary = '';
448
+ if (readmeContent) {
449
+ const readmePreview = readmeContent.substring(0, 100).replace(/\n/g, ' ').trim();
450
+ safeLogger.info(` ✓ README found (${readmeContent.length} chars), preview: "${readmePreview}..."`);
451
+ safeLogger.info(` 🤖 [${repo.full_name}] Generating AI summary from README...`);
452
+ const startTime = Date.now();
453
+ summary = await summarizer.summarizeReadme(repo.full_name, readmeContent);
454
+ const duration = Date.now() - startTime;
455
+ safeLogger.info(` ✓ Summary generated (${summary.length} chars) in ${duration}ms`);
456
+ if (summary) {
457
+ safeLogger.info(` 📝 [${repo.full_name}] Summary: ${summary.substring(0, 100)}...`);
458
+ }
459
+ else {
460
+ safeLogger.warn(` ⚠ [${repo.full_name}] Summary is empty`);
461
+ }
462
+ }
463
+ else {
464
+ safeLogger.warn(` ✗ No README found for ${repo.full_name}`);
465
+ safeLogger.info(` 🤖 [${repo.full_name}] Generating AI summary from metadata...`);
466
+ const startTime = Date.now();
467
+ summary = await summarizer.generateSummary(repo);
468
+ const duration = Date.now() - startTime;
469
+ safeLogger.info(` ✓ Summary generated (${summary.length} chars) in ${duration}ms`);
470
+ if (summary) {
471
+ safeLogger.info(` 📝 [${repo.full_name}] Summary: ${summary.substring(0, 100)}...`);
472
+ }
473
+ else {
474
+ safeLogger.warn(` ⚠ [${repo.full_name}] Summary is empty`);
475
+ }
476
+ }
477
+ return { ...repo, ai_summary: summary };
478
+ }
479
+ catch (error) {
480
+ safeLogger.error(` ❌ [${repo.full_name}] Failed to generate summary: ${error}`);
481
+ return { ...repo, ai_summary: '' };
482
+ }
483
+ }));
484
+ // Collect results from this batch
485
+ for (const result of batchResults) {
486
+ if (result.status === 'fulfilled') {
487
+ reposWithSummary.push(result.value);
488
+ }
489
+ }
490
+ }
491
+ // Push to channels
492
+ const seenWithSummary = alreadySeen.map(r => ({
493
+ ...r,
494
+ ai_summary: historyManager.getProject(r.full_name)?.ai_summary || ''
495
+ }));
496
+ const pushResults = [];
497
+ for (const targetChannel of targetChannels) {
498
+ try {
499
+ if (targetChannel === 'feishu') {
500
+ const webhookUrl = pluginConfig?.channels?.feishu?.webhook_url;
501
+ if (!webhookUrl) {
502
+ safeLogger.warn('Feishu webhook URL not configured, skipping');
503
+ pushResults.push({ channel: 'feishu', success: false, error: 'Webhook URL not configured' });
504
+ continue;
505
+ }
506
+ safeLogger.info(`Pushing ${reposWithSummary.length} repos to Feishu...`);
507
+ const result = await feishu_1.FeishuChannel.push(webhookUrl, reposWithSummary, seenWithSummary, since);
508
+ if (!result) {
509
+ safeLogger.error('FeishuChannel.push returned undefined!');
510
+ pushResults.push({ channel: 'feishu', success: false, error: 'Push returned undefined' });
511
+ continue;
512
+ }
513
+ pushResults.push({
514
+ channel: 'feishu',
515
+ success: result.success,
516
+ messageId: result.messageId,
517
+ error: result.error || undefined
518
+ });
519
+ if (result.success) {
520
+ safeLogger.info(`✅ Feishu push successful!`);
521
+ }
522
+ else {
523
+ safeLogger.error(`❌ Feishu push failed: ${result.error || 'Unknown error'}`);
524
+ }
525
+ }
526
+ else if (targetChannel === 'email') {
527
+ const emailConfig = pluginConfig?.channels?.email;
528
+ const emailTo = emailConfig?.recipient || emailConfig?.sender;
529
+ if (!emailTo) {
530
+ safeLogger.warn('Email recipient not configured, skipping');
531
+ pushResults.push({ channel: 'email', success: false, error: 'Recipient not configured' });
532
+ continue;
533
+ }
534
+ if (!emailConfig) {
535
+ safeLogger.warn('Email SMTP configuration missing, skipping');
536
+ pushResults.push({ channel: 'email', success: false, error: 'SMTP configuration missing' });
537
+ continue;
538
+ }
539
+ if (!emailConfig.password) {
540
+ safeLogger.warn('Email SMTP password missing, skipping');
541
+ pushResults.push({ channel: 'email', success: false, error: 'SMTP password not configured' });
542
+ continue;
543
+ }
544
+ // Build email config for EmailChannel
545
+ const emailChannelConfig = {
546
+ from: emailConfig.sender || '',
547
+ to: emailTo,
548
+ subject: `GitHub Trending ${since === 'daily' ? 'Daily' : since === 'weekly' ? 'Weekly' : 'Monthly'}`,
549
+ smtp: {
550
+ host: emailConfig.smtp_host || 'smtp.gmail.com',
551
+ port: emailConfig.smtp_port || 587,
552
+ secure: emailConfig.use_tls !== false,
553
+ auth: {
554
+ user: emailConfig.sender || '',
555
+ pass: emailConfig.password
556
+ }
557
+ }
558
+ };
559
+ safeLogger.info(`Sending email...`);
560
+ safeLogger.info(` From: ${emailConfig.sender}`);
561
+ safeLogger.info(` To: ${emailTo}`);
562
+ safeLogger.info(` Subject: ${emailChannelConfig.subject}`);
563
+ safeLogger.info(` Repositories: ${reposWithSummary.length} new + ${alreadySeen.length} seen`);
564
+ const result = await email_1.EmailChannel.send(emailChannelConfig, reposWithSummary, seenWithSummary, since);
565
+ if (!result) {
566
+ safeLogger.error('EmailChannel.send returned undefined!');
567
+ pushResults.push({ channel: 'email', success: false, error: 'Send returned undefined' });
568
+ continue;
569
+ }
570
+ pushResults.push({
571
+ channel: 'email',
572
+ success: result.success,
573
+ messageId: result.messageId,
574
+ error: result.error || undefined
575
+ });
576
+ if (result.success) {
577
+ safeLogger.info(`✅ Email sent successfully! Message ID: ${result.messageId}`);
578
+ safeLogger.info(`Check inbox: ${emailTo}`);
579
+ }
580
+ else {
581
+ safeLogger.error(`❌ Email send failed: ${result.error || 'Unknown error'}`);
582
+ }
583
+ }
584
+ }
585
+ catch (error) {
586
+ safeLogger.error(`Failed to push to ${targetChannel}: ${error}`);
587
+ pushResults.push({
588
+ channel: targetChannel,
589
+ success: false,
590
+ error: error instanceof Error ? error.message : 'Unknown error'
591
+ });
592
+ }
593
+ }
594
+ // Update history
595
+ historyManager.markPushed(reposWithSummary);
596
+ // Save to both OpenClaw storage and file storage (for redundancy)
597
+ if (storage) {
598
+ await storage.set('github-trending-history', historyManager.exportData());
599
+ safeLogger.info('History saved to OpenClaw storage');
600
+ }
601
+ // Also save to file storage as backup
602
+ try {
603
+ const storageManager = (0, file_storage_1.getStorageManager)(pluginId);
604
+ await storageManager.set('github-trending-history', historyManager.exportData());
605
+ safeLogger.info('History saved to file storage', {
606
+ path: `~/.openclaw/plugins/${pluginId}/data/${storageManager['getCurrentMonthKey']()}.json`,
607
+ repoCount: Object.keys(historyManager['data'].repositories).length
608
+ });
609
+ }
610
+ catch (fileStorageError) {
611
+ safeLogger.warn('Failed to save history to file storage', { error: fileStorageError });
612
+ }
613
+ // Calculate result statistics
614
+ const successCount = pushResults.filter(r => r.success).length;
615
+ const failedCount = pushResults.filter(r => !r.success).length;
616
+ const pushedCount = reposWithSummary.length;
617
+ const newCount = newlySeen.length;
618
+ const seenCount = alreadySeen.length;
619
+ const totalCount = repositories.length;
620
+ // Build response
621
+ const response = {
622
+ content: [{
623
+ type: 'text',
624
+ text: JSON.stringify({
625
+ success: successCount > 0,
626
+ pushed_count: pushedCount,
627
+ new_count: newCount,
628
+ seen_count: seenCount,
629
+ total_count: totalCount,
630
+ channels: pushResults,
631
+ timestamp: new Date().toISOString(),
632
+ message: successCount > 0 ? `成功推送到所有 ${successCount} 个通道` : `推送失败`
633
+ }, null, 2)
634
+ }],
635
+ isError: successCount === 0
636
+ };
637
+ safeLogger.info('Tool execution completed', {
638
+ successCount,
639
+ failedCount,
640
+ totalChannels: targetChannels.length,
641
+ pushedCount,
642
+ newCount,
643
+ seenCount,
644
+ channels: pushResults
645
+ });
646
+ return response;
647
+ }
648
+ catch (error) {
649
+ safeLogger.error('Tool execution failed', {
650
+ error: error instanceof Error ? error.message : error,
651
+ stack: error instanceof Error ? error.stack : undefined
652
+ });
653
+ return {
654
+ content: [{
655
+ type: 'text',
656
+ text: JSON.stringify({
657
+ success: false,
658
+ error: error instanceof Error ? error.message : 'Unknown error',
659
+ timestamp: new Date().toISOString()
660
+ }, null, 2)
661
+ }],
662
+ isError: true
663
+ };
664
+ }
665
+ }
666
+ });
667
+ }
668
+ //# sourceMappingURL=index.js.map