@zhin.js/plugin-60s 0.0.4 → 0.0.7

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/src/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 60s API 聚合插件 —— 基于 https://github.com/vikiboss/60s
5
5
  *
6
- * 提供多种实用 API 功能,每个功能同时注册为:
6
+ * 提供多种实用 API 功能,每个功能通过 tools/*.tool.md 文件定义:
7
7
  * - 聊天命令(/60s, /天气 北京, /微博 …)
8
8
  * - AI 工具(Agent 可通过关键词自动匹配并调用)
9
9
  *
@@ -12,6 +12,8 @@
12
12
  * 一言 · 摸鱼日历 · IP查询 · Bing壁纸 · 金价 · 油价
13
13
  * 汇率 · 翻译 · 历史上的今天 · KFC文案 · 段子
14
14
  *
15
+ * 所有工具定义在 tools/ 目录下,框架自动发现注册。
16
+ *
15
17
  * 配置方式(zhin.config.yml):
16
18
  * ```yaml
17
19
  * plugins:
@@ -19,7 +21,7 @@
19
21
  * apiBase: https://60s.viki.moe
20
22
  * ```
21
23
  */
22
- import { usePlugin, ZhinTool } from 'zhin.js';
24
+ import { usePlugin } from 'zhin.js';
23
25
 
24
26
  const plugin = usePlugin();
25
27
  const { logger, root } = plugin;
@@ -37,451 +39,6 @@ const config: Config = {
37
39
  ...appConfig['60s'],
38
40
  };
39
41
 
40
- const API_BASE = config.apiBase || 'https://60s.viki.moe';
41
-
42
- logger.debug(`[60s] API 地址: ${API_BASE}`);
43
-
44
- // ─── 通用请求 ────────────────────────────────────────────────────────────────
45
-
46
- async function fetchApi<T = any>(
47
- endpoint: string,
48
- params?: Record<string, string>,
49
- ): Promise<T> {
50
- const url = new URL(`${API_BASE}/v2${endpoint}`);
51
- if (params) {
52
- Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
53
- }
54
-
55
- logger.debug(`[60s] 请求: ${url.toString()}`);
56
-
57
- const res = await fetch(url.toString());
58
- const data = (await res.json()) as any;
59
-
60
- if (data.error) throw new Error(data.error);
61
- if (data.code !== undefined && data.code !== 200 && data.code !== 0) {
62
- throw new Error(data.message || data.msg || `API 错误: ${data.code}`);
63
- }
64
-
65
- return data.data ?? data;
66
- }
67
-
68
- // ─── 工具函数 ────────────────────────────────────────────────────────────────
69
-
70
- function formatList(items: any[], limit = 10): string {
71
- return items
72
- .slice(0, limit)
73
- .map((item, i) => {
74
- const title = item.title || item.name || item.word || item;
75
- const hot = item.hot_value || item.hot;
76
- const hotStr = hot ? ` 🔥${hot}` : '';
77
- return `${i + 1}. ${title}${hotStr}`;
78
- })
79
- .join('\n');
80
- }
81
-
82
- // ═══════════════════════════════════════════════════════════════════════════════
83
- // 工具定义(ZhinTool 链式风格 — 同时注册命令 + AI 工具)
84
- // ═══════════════════════════════════════════════════════════════════════════════
85
-
86
- // ── 60 秒新闻 ────────────────────────────────────────────────────────────────
87
- const newsTool = new ZhinTool('60s_news')
88
- .desc('获取每日60秒新闻,快速了解今日要闻')
89
- .tag('新闻', '资讯', '60s')
90
- .keyword('60s', '新闻', '今日新闻', '60秒', '每日新闻', '读懂世界')
91
- .alias('新闻', '今日新闻', '60秒')
92
- .usage('获取今日60秒新闻')
93
- .examples('/60s')
94
- .execute(async () => {
95
- const data = await fetchApi<any>('/60s');
96
- const lines = [`📰 今日60秒新闻 (${data.date || ''})`, ''];
97
- if (data.news && Array.isArray(data.news)) {
98
- lines.push(...data.news.map((n: string, i: number) => `${i + 1}. ${n}`));
99
- }
100
- if (data.tip) lines.push('', `💡 ${data.tip}`);
101
- return lines.join('\n');
102
- });
103
-
104
- // ── 天气 ──────────────────────────────────────────────────────────────────────
105
- const weatherTool = new ZhinTool('weather')
106
- .desc('查询指定城市的当前天气信息')
107
- .tag('天气', '生活', '查询')
108
- .keyword('天气', '气温', '温度', '下雨', '晴天', '阴天', 'weather')
109
- .alias('天气', 'tq')
110
- .usage('查询城市天气')
111
- .examples('/weather 成都', '/天气 北京')
112
- .param('city', { type: 'string', description: '城市名称,如"成都"、"北京"' }, true)
113
- .execute(async (args) => {
114
- const data = await fetchApi<any>('/weather', { city: args.city });
115
- const w = data.weather;
116
- const aq = data.air_quality;
117
- const loc = data.location;
118
- const cityName = loc?.city || loc?.name || args.city;
119
- const lines = [
120
- `🌤️ ${cityName} 天气`,
121
- '',
122
- `🌡️ 温度: ${w.temperature}°C`,
123
- `☁️ 天气: ${w.condition}`,
124
- `💧 湿度: ${w.humidity}%`,
125
- `💨 风: ${w.wind_direction} ${w.wind_power}`,
126
- ];
127
- if (aq) {
128
- const quality = aq.quality || '';
129
- lines.push(`🌬️ 空气: ${quality}${aq.aqi ? ` (AQI ${aq.aqi})` : ''}`);
130
- }
131
- return lines.join('\n');
132
- });
133
-
134
- // ── 微博热搜 ──────────────────────────────────────────────────────────────────
135
- const weiboTool = new ZhinTool('weibo_hot')
136
- .desc('获取微博热搜榜')
137
- .tag('热搜', '社交', '微博')
138
- .keyword('微博', '热搜', 'weibo', 'wb')
139
- .alias('微博热搜', 'wb')
140
- .usage('获取微博热搜')
141
- .examples('/weibo', '/weibo 20')
142
- .param('limit', { type: 'number', description: '返回条数,默认10条' })
143
- .execute(async (args) => {
144
- const data = await fetchApi<any[]>('/weibo');
145
- return ['🔥 微博热搜', '', formatList(data, args.limit || 10)].join('\n');
146
- });
147
-
148
- // ── 知乎热榜 ──────────────────────────────────────────────────────────────────
149
- const zhihuTool = new ZhinTool('zhihu_hot')
150
- .desc('获取知乎热榜')
151
- .tag('热搜', '社交', '知乎')
152
- .keyword('知乎', '热榜', 'zhihu', 'zh')
153
- .alias('知乎热榜', 'zh')
154
- .usage('获取知乎热榜')
155
- .examples('/zhihu', '/zhihu 20')
156
- .param('limit', { type: 'number', description: '返回条数,默认10条' })
157
- .execute(async (args) => {
158
- const data = await fetchApi<any[]>('/zhihu');
159
- return ['🔥 知乎热榜', '', formatList(data, args.limit || 10)].join('\n');
160
- });
161
-
162
- // ── 抖音热搜 ──────────────────────────────────────────────────────────────────
163
- const douyinTool = new ZhinTool('douyin_hot')
164
- .desc('获取抖音热搜榜')
165
- .tag('热搜', '短视频', '抖音')
166
- .keyword('抖音', '热搜', 'douyin', 'dy')
167
- .alias('抖音热搜', 'dy')
168
- .usage('获取抖音热搜')
169
- .examples('/douyin', '/douyin 20')
170
- .param('limit', { type: 'number', description: '返回条数,默认10条' })
171
- .execute(async (args) => {
172
- const data = await fetchApi<any[]>('/douyin');
173
- return ['🔥 抖音热搜', '', formatList(data, args.limit || 10)].join('\n');
174
- });
175
-
176
- // ── 头条热搜 ──────────────────────────────────────────────────────────────────
177
- const toutiaoTool = new ZhinTool('toutiao_hot')
178
- .desc('获取今日头条热搜榜')
179
- .tag('热搜', '资讯', '头条')
180
- .keyword('头条', '今日头条', '热搜', 'toutiao', 'tt')
181
- .alias('头条热搜', 'tt')
182
- .usage('获取头条热搜')
183
- .examples('/toutiao', '/tt 20')
184
- .param('limit', { type: 'number', description: '返回条数,默认10条' })
185
- .execute(async (args) => {
186
- const data = await fetchApi<any[]>('/toutiao');
187
- return ['🔥 头条热搜', '', formatList(data, args.limit || 10)].join('\n');
188
- });
189
-
190
- // ── 一言 ──────────────────────────────────────────────────────────────────────
191
- const hitokotoTool = new ZhinTool('hitokoto')
192
- .desc('获取一言/每日一句,随机返回一条语句')
193
- .tag('语录', '文学', '随机')
194
- .keyword('一言', '每日一句', '语录', '名言', 'hitokoto')
195
- .alias('一言', '每日一句', 'yy')
196
- .usage('获取随机一言')
197
- .examples('/hitokoto', '/一言 i')
198
- .param('type', {
199
- type: 'string',
200
- description:
201
- '类型:a=动画, b=漫画, c=游戏, d=文学, e=原创, f=网络, g=其他, h=影视, i=诗词, j=网易云, k=哲学, l=抖机灵',
202
- })
203
- .execute(async (args) => {
204
- const params: Record<string, string> | undefined = args.type
205
- ? { c: args.type }
206
- : undefined;
207
- const data = await fetchApi<any>('/hitokoto', params);
208
- const text = data.hitokoto || data.content || (typeof data === 'string' ? data : '');
209
- const lines = ['💬 一言', '', `「${text}」`];
210
- if (data.from || data.source || data.from_who || data.author) {
211
- const author = data.from_who || data.author || '';
212
- const source = data.from || data.source || '';
213
- lines.push(`——${author}${source ? `《${source}》` : ''}`);
214
- }
215
- return lines.join('\n');
216
- });
217
-
218
- // ── 摸鱼日历 ──────────────────────────────────────────────────────────────────
219
- const moyuTool = new ZhinTool('moyu')
220
- .desc('获取摸鱼日历,查看今天适不适合摸鱼')
221
- .tag('生活', '摸鱼', '日历')
222
- .keyword('摸鱼', '摸鱼日历', 'moyu')
223
- .alias('摸鱼', '摸鱼日历')
224
- .usage('获取摸鱼日历')
225
- .examples('/moyu')
226
- .execute(async () => {
227
- const data = await fetchApi<any>('/moyu');
228
- if (typeof data === 'string') return `🐟 摸鱼日历\n\n${data}`;
229
- const lines = ['🐟 摸鱼日历', ''];
230
- if (data.date?.gregorian) lines.push(`📅 ${data.date.gregorian} ${data.date.weekday || ''}`);
231
- if (data.date?.lunar) {
232
- const l = data.date.lunar;
233
- lines.push(`🌙 农历${l.monthCN || ''}${l.dayCN || ''} ${l.yearGanZhi || ''}年`);
234
- }
235
- if (data.today) {
236
- const t = data.today;
237
- if (t.holidayName) lines.push(`🎉 ${t.holidayName}`);
238
- if (t.solarTerm) lines.push(`🌿 ${t.solarTerm}`);
239
- lines.push(t.isWorkday ? '💼 今天是工作日' : '🎮 今天不上班');
240
- }
241
- if (data.festivals && Array.isArray(data.festivals)) {
242
- data.festivals.forEach((f: any) => lines.push(`🎊 ${f.name || f}`));
243
- }
244
- if (data.countdown && Array.isArray(data.countdown)) {
245
- lines.push('');
246
- data.countdown.forEach((c: any) => {
247
- if (c.name && c.days !== undefined) lines.push(`⏳ 距 ${c.name} 还有 ${c.days} 天`);
248
- });
249
- }
250
- return lines.join('\n');
251
- });
252
-
253
- // ── IP 查询 ───────────────────────────────────────────────────────────────────
254
- const ipTool = new ZhinTool('ip_query')
255
- .desc('查询 IP 地址的地理位置信息')
256
- .tag('网络', '查询', 'IP')
257
- .keyword('ip', 'IP', 'IP查询', 'ip查询')
258
- .alias('IP查询')
259
- .usage('查询 IP 地址信息')
260
- .examples('/ip', '/ip 8.8.8.8')
261
- .param('ip', { type: 'string', description: 'IP 地址,不填则查询当前 IP' })
262
- .execute(async (args) => {
263
- const params: Record<string, string> | undefined = args.ip
264
- ? { ip: args.ip }
265
- : undefined;
266
- const data = await fetchApi<any>('/ip', params);
267
- const lines = ['🌐 IP 查询', '', `IP: ${data.ip || args.ip || '当前 IP'}`];
268
- if (data.country || data.region || data.city) {
269
- lines.push(`位置: ${[data.country, data.region, data.city].filter(Boolean).join(' ')}`);
270
- }
271
- if (data.location) lines.push(`位置: ${data.location}`);
272
- if (data.isp) lines.push(`运营商: ${data.isp}`);
273
- return lines.join('\n');
274
- });
275
-
276
- // ── Bing 每日图片 ─────────────────────────────────────────────────────────────
277
- const bingTool = new ZhinTool('bing_image')
278
- .desc('获取 Bing 每日壁纸图片')
279
- .tag('图片', '壁纸', 'Bing')
280
- .keyword('bing', '必应', '壁纸', '每日壁纸')
281
- .alias('必应', '每日壁纸')
282
- .usage('获取 Bing 每日壁纸')
283
- .examples('/bing')
284
- .execute(async () => {
285
- const data = await fetchApi<any>('/bing');
286
- const lines = ['🖼️ Bing 每日壁纸', ''];
287
- if (data.title) lines.push(`📌 ${data.title}`);
288
- if (data.headline) lines.push(`💡 ${data.headline}`);
289
- if (data.copyright) lines.push(`📝 ${data.copyright}`);
290
- if (data.description) lines.push('', data.description);
291
- const imgUrl = data.cover || data.cover_4k || data.url || data.image;
292
- if (imgUrl) lines.push('', imgUrl);
293
- return lines.join('\n');
294
- });
295
-
296
- // ── 金价 ──────────────────────────────────────────────────────────────────────
297
- const goldPriceTool = new ZhinTool('gold_price')
298
- .desc('查询今日黄金价格')
299
- .tag('金融', '黄金', '价格')
300
- .keyword('金价', '黄金', '黄金价格', 'gold')
301
- .alias('金价', '黄金价格', 'jj')
302
- .usage('查询今日金价')
303
- .examples('/gold', '/金价')
304
- .execute(async () => {
305
- const data = await fetchApi<any>('/gold-price');
306
- const lines = ['💰 今日金价', ''];
307
- if (data.date) lines.push(`📅 ${data.date}`);
308
- const metals = data.metals || (Array.isArray(data) ? data : []);
309
- if (metals.length > 0) {
310
- lines.push('');
311
- metals.forEach((item: any) => {
312
- const name = item.name || '黄金';
313
- const price = item.today_price || item.sell_price || item.price || 'N/A';
314
- const unit = item.unit || '元/克';
315
- if (price !== 'N/A') lines.push(`${name}: ¥${price} ${unit}`);
316
- });
317
- } else if (data.price) {
318
- lines.push(`当前金价: ¥${data.price}/克`);
319
- }
320
- return lines.join('\n');
321
- });
322
-
323
- // ── 油价 ──────────────────────────────────────────────────────────────────────
324
- const fuelPriceTool = new ZhinTool('fuel_price')
325
- .desc('查询今日油价')
326
- .tag('生活', '油价', '价格')
327
- .keyword('油价', '汽油', '柴油', 'fuel')
328
- .alias('油价', 'yj')
329
- .usage('查询今日油价')
330
- .examples('/fuel', '/油价 四川')
331
- .param('province', { type: 'string', description: '省份名称,如"四川"、"北京"' })
332
- .execute(async (args) => {
333
- const params: Record<string, string> | undefined = args.province
334
- ? { province: args.province }
335
- : undefined;
336
- const data = await fetchApi<any>('/fuel-price', params);
337
- const lines = ['⛽ 今日油价', ''];
338
- if (data.region) lines.push(`📍 ${data.region}`);
339
- if (data.items && Array.isArray(data.items)) {
340
- data.items.forEach((item: any) => {
341
- lines.push(`${item.name}: ${item.price_desc || `¥${item.price}/升`}`);
342
- });
343
- }
344
- if (data.trend) {
345
- lines.push('', `📊 ${data.trend.description || ''}`);
346
- }
347
- return lines.join('\n');
348
- });
349
-
350
- // ── 汇率 ──────────────────────────────────────────────────────────────────────
351
- const exchangeRateTool = new ZhinTool('exchange_rate')
352
- .desc('查询货币汇率')
353
- .tag('金融', '汇率', '查询')
354
- .keyword('汇率', '兑换', '外汇', 'exchange', 'rate')
355
- .alias('汇率', 'hl')
356
- .usage('查询汇率')
357
- .examples('/exchange USD CNY', '/汇率')
358
- .param('from', { type: 'string', description: '源货币,如 USD, EUR, JPY' })
359
- .param('to', { type: 'string', description: '目标货币,如 CNY' })
360
- .execute(async (args) => {
361
- const params: Record<string, string> = {};
362
- if (args.from) params.from = args.from.toUpperCase();
363
- if (args.to) params.to = args.to.toUpperCase();
364
- const data = await fetchApi<any>(
365
- '/exchange-rate',
366
- Object.keys(params).length ? params : undefined,
367
- );
368
- const lines = ['💱 汇率查询', ''];
369
- const base = data.base_code || args.from || 'CNY';
370
- lines.push(`基准货币: ${base}`);
371
- if (data.updated) lines.push(`更新时间: ${data.updated}`);
372
- lines.push('');
373
- const rates = data.rates || (Array.isArray(data) ? data : []);
374
- const targetCurrencies = args.to
375
- ? [args.to.toUpperCase()]
376
- : ['USD', 'EUR', 'JPY', 'GBP', 'HKD', 'KRW', 'AUD', 'CAD', 'SGD', 'CHF'];
377
- rates.forEach((item: any) => {
378
- if (targetCurrencies.includes(item.currency) && item.currency !== base) {
379
- lines.push(`${base} → ${item.currency}: ${item.rate}`);
380
- }
381
- });
382
- return lines.join('\n');
383
- });
384
-
385
- // ── 翻译 ──────────────────────────────────────────────────────────────────────
386
- const translateTool = new ZhinTool('translate_60s')
387
- .desc('翻译文本')
388
- .tag('工具', '翻译', '语言')
389
- .keyword('翻译', '英文', '中文', '日文', 'translate', '译')
390
- .alias('翻译', 'fy')
391
- .usage('翻译文本')
392
- .examples('/fanyi hello', '/翻译 你好 en')
393
- .param('text', { type: 'string', description: '要翻译的文本' }, true)
394
- .param('to', { type: 'string', description: '目标语言,如 en, zh, ja' })
395
- .execute(async (args) => {
396
- const params: Record<string, string> = { text: args.text };
397
- if (args.to) params.to = args.to;
398
- const data = await fetchApi<any>('/fanyi', params);
399
- const srcText = data.source?.text || args.text;
400
- const srcLang = data.source?.type_desc || data.source?.type || '';
401
- const tgtText = data.target?.text || data.result || data.translation || (typeof data === 'string' ? data : '');
402
- const tgtLang = data.target?.type_desc || data.target?.type || '';
403
- const lines = [
404
- '🌐 翻译结果',
405
- '',
406
- `原文${srcLang ? ` (${srcLang})` : ''}: ${srcText}`,
407
- `译文${tgtLang ? ` (${tgtLang})` : ''}: ${tgtText}`,
408
- ];
409
- if (data.target?.pronounce) lines.push(`发音: ${data.target.pronounce}`);
410
- return lines.join('\n');
411
- });
412
-
413
- // ── 历史上的今天 ──────────────────────────────────────────────────────────────
414
- const historyTodayTool = new ZhinTool('history_today')
415
- .desc('查看历史上的今天发生了什么')
416
- .tag('历史', '知识', '日历')
417
- .keyword('历史', '历史上的今天', '今天历史', 'history')
418
- .alias('历史上的今天', '历史')
419
- .usage('查看历史上的今天')
420
- .examples('/history')
421
- .execute(async () => {
422
- const data = await fetchApi<any>('/today-in-history');
423
- const dateStr = data.date || `${data.month || ''}月${data.day || ''}日`;
424
- const lines = [`📅 历史上的今天 (${dateStr})`, ''];
425
- const items = data.items || (Array.isArray(data) ? data : []);
426
- items.slice(0, 10).forEach((item: any, i: number) => {
427
- const year = item.year || '';
428
- const title = item.title || item.event || item.content || item;
429
- const typeIcon = item.event_type === 'birth' ? '👶' : item.event_type === 'death' ? '🕊️' : '📌';
430
- lines.push(`${typeIcon} ${i + 1}. ${year ? `[${year}] ` : ''}${title}`);
431
- });
432
- return lines.join('\n');
433
- });
434
-
435
- // ── KFC 疯狂星期四 ────────────────────────────────────────────────────────────
436
- const kfcTool = new ZhinTool('kfc')
437
- .desc('获取 KFC 疯狂星期四文案')
438
- .tag('娱乐', 'KFC', '文案')
439
- .keyword('kfc', 'KFC', '疯狂星期四', 'v50', '肯德基')
440
- .alias('疯狂星期四', 'v50')
441
- .usage('获取 KFC 文案')
442
- .examples('/kfc')
443
- .execute(async () => {
444
- const data = await fetchApi<any>('/kfc');
445
- return `🍗 疯狂星期四\n\n${data.kfc || data.content || data.text || (typeof data === 'string' ? data : '')}`;
446
- });
447
-
448
- // ── 段子 ──────────────────────────────────────────────────────────────────────
449
- const duanziTool = new ZhinTool('duanzi')
450
- .desc('获取一个段子')
451
- .tag('娱乐', '笑话', '段子')
452
- .keyword('段子', '笑话', '搞笑', 'joke', 'duanzi')
453
- .alias('段子', 'joke')
454
- .usage('获取段子')
455
- .examples('/duanzi')
456
- .execute(async () => {
457
- const data = await fetchApi<any>('/duanzi');
458
- return `😂 段子\n\n${data.duanzi || data.content || data.text || (typeof data === 'string' ? data : '')}`;
459
- });
460
-
461
- // ═══════════════════════════════════════════════════════════════════════════════
462
- // 注册所有工具
463
- // ═══════════════════════════════════════════════════════════════════════════════
464
-
465
- const allTools = [
466
- newsTool,
467
- weatherTool,
468
- weiboTool,
469
- zhihuTool,
470
- douyinTool,
471
- toutiaoTool,
472
- hitokotoTool,
473
- moyuTool,
474
- ipTool,
475
- bingTool,
476
- goldPriceTool,
477
- fuelPriceTool,
478
- exchangeRateTool,
479
- translateTool,
480
- historyTodayTool,
481
- kfcTool,
482
- duanziTool,
483
- ];
484
-
485
- allTools.forEach((tool) => plugin.addTool(tool.toTool()));
42
+ // 将 API 地址写入环境变量,供 tool handler 使用
43
+ process.env.ZHIN_60S_API = config.apiBase || 'https://60s.viki.moe';
486
44
 
487
- logger.debug(`[60s] 已注册 ${allTools.length} 个工具`);