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.
- package/LICENSE +21 -0
- package/README.md +480 -0
- package/dist/channels/email.d.ts +61 -0
- package/dist/channels/email.d.ts.map +1 -0
- package/dist/channels/email.js +599 -0
- package/dist/channels/email.js.map +1 -0
- package/dist/channels/feishu.d.ts +50 -0
- package/dist/channels/feishu.d.ts.map +1 -0
- package/dist/channels/feishu.js +322 -0
- package/dist/channels/feishu.js.map +1 -0
- package/dist/channels/types.d.ts +66 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +12 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/config.d.ts +83 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +145 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/fetcher.d.ts +43 -0
- package/dist/core/fetcher.d.ts.map +1 -0
- package/dist/core/fetcher.js +306 -0
- package/dist/core/fetcher.js.map +1 -0
- package/dist/core/file-storage.d.ts +62 -0
- package/dist/core/file-storage.d.ts.map +1 -0
- package/dist/core/file-storage.js +253 -0
- package/dist/core/file-storage.js.map +1 -0
- package/dist/core/history.d.ts +71 -0
- package/dist/core/history.d.ts.map +1 -0
- package/dist/core/history.js +133 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/summarizer.d.ts +64 -0
- package/dist/core/summarizer.d.ts.map +1 -0
- package/dist/core/summarizer.js +324 -0
- package/dist/core/summarizer.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +668 -0
- package/dist/index.js.map +1 -0
- package/dist/models/config.d.ts +93 -0
- package/dist/models/config.d.ts.map +1 -0
- package/dist/models/config.js +3 -0
- package/dist/models/config.js.map +1 -0
- package/dist/models/history.d.ts +6 -0
- package/dist/models/history.d.ts.map +1 -0
- package/dist/models/history.js +7 -0
- package/dist/models/history.js.map +1 -0
- package/dist/models/repository.d.ts +28 -0
- package/dist/models/repository.d.ts.map +1 -0
- package/dist/models/repository.js +3 -0
- package/dist/models/repository.js.map +1 -0
- package/dist/models/service.types.d.ts +87 -0
- package/dist/models/service.types.d.ts.map +1 -0
- package/dist/models/service.types.js +3 -0
- package/dist/models/service.types.js.map +1 -0
- package/dist/services/trending.service.d.ts +29 -0
- package/dist/services/trending.service.d.ts.map +1 -0
- package/dist/services/trending.service.js +306 -0
- package/dist/services/trending.service.js.map +1 -0
- package/dist/tool.d.ts +47 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +314 -0
- package/dist/tool.js.map +1 -0
- package/dist/utils/logger.d.ts +77 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +214 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/markdown.d.ts +9 -0
- package/dist/utils/markdown.d.ts.map +1 -0
- package/dist/utils/markdown.js +40 -0
- package/dist/utils/markdown.js.map +1 -0
- package/openclaw.plugin.json +152 -0
- 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
|