@zhin.js/plugin-60s 1.0.0 → 1.1.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 (141) hide show
  1. package/README.md +1 -1
  2. package/agent/tools/60s_news.ts +13 -0
  3. package/agent/tools/bing_image.ts +13 -0
  4. package/agent/tools/douyin_hot.ts +13 -0
  5. package/agent/tools/duanzi.ts +13 -0
  6. package/agent/tools/exchange_rate.ts +13 -0
  7. package/agent/tools/fuel_price.ts +13 -0
  8. package/agent/tools/gold_price.ts +13 -0
  9. package/agent/tools/history_today.ts +13 -0
  10. package/agent/tools/hitokoto.ts +13 -0
  11. package/agent/tools/ip_query.ts +13 -0
  12. package/agent/tools/kfc.ts +13 -0
  13. package/agent/tools/moyu.ts +13 -0
  14. package/agent/tools/toutiao_hot.ts +13 -0
  15. package/agent/tools/translate_60s.ts +13 -0
  16. package/agent/tools/weather.ts +13 -0
  17. package/agent/tools/weibo_hot.ts +13 -0
  18. package/agent/tools/zhihu_hot.ts +13 -0
  19. package/commands/60s.js +7 -0
  20. package/commands/60s.ts +7 -0
  21. package/commands/bing.js +7 -0
  22. package/commands/bing.ts +7 -0
  23. package/commands/douyin/[[limit]].js +8 -0
  24. package/commands/douyin/[[limit]].ts +8 -0
  25. package/commands/duanzi.js +7 -0
  26. package/commands/duanzi.ts +7 -0
  27. package/commands/exchange.js +10 -0
  28. package/commands/exchange.ts +10 -0
  29. package/commands/fanyi/[text].js +8 -0
  30. package/commands/fanyi/[text].ts +8 -0
  31. package/commands/fuel/[province].js +8 -0
  32. package/commands/fuel/[province].ts +8 -0
  33. package/commands/gold.js +7 -0
  34. package/commands/gold.ts +7 -0
  35. package/commands/history.js +7 -0
  36. package/commands/history.ts +7 -0
  37. package/commands/hitokoto/[type].js +8 -0
  38. package/commands/hitokoto/[type].ts +8 -0
  39. package/commands/ip/[ip].js +8 -0
  40. package/commands/ip/[ip].ts +8 -0
  41. package/commands/kfc.js +7 -0
  42. package/commands/kfc.ts +7 -0
  43. package/commands/moyu.js +7 -0
  44. package/commands/moyu.ts +7 -0
  45. package/commands/toutiao/[[limit]].js +8 -0
  46. package/commands/toutiao/[[limit]].ts +8 -0
  47. package/commands/weather/[city].js +8 -0
  48. package/commands/weather/[city].ts +8 -0
  49. package/commands/weibo/[[limit]].js +8 -0
  50. package/commands/weibo/[[limit]].ts +8 -0
  51. package/commands/zhihu/[[limit]].js +8 -0
  52. package/commands/zhihu/[[limit]].ts +8 -0
  53. package/lib/api.js +59 -0
  54. package/lib/api.js.map +1 -0
  55. package/lib/handlers/60s-news.js +12 -0
  56. package/lib/handlers/60s-news.js.map +1 -0
  57. package/lib/handlers/bing-image.js +18 -0
  58. package/lib/handlers/bing-image.js.map +1 -0
  59. package/lib/handlers/douyin-hot.js +6 -0
  60. package/lib/handlers/douyin-hot.js.map +1 -0
  61. package/lib/handlers/duanzi.js +6 -0
  62. package/lib/handlers/duanzi.js.map +1 -0
  63. package/lib/handlers/exchange-rate.js +28 -0
  64. package/lib/handlers/exchange-rate.js.map +1 -0
  65. package/lib/handlers/fuel-price.js +21 -0
  66. package/lib/handlers/fuel-price.js.map +1 -0
  67. package/lib/handlers/gold-price.js +24 -0
  68. package/lib/handlers/gold-price.js.map +1 -0
  69. package/lib/handlers/history-today.js +16 -0
  70. package/lib/handlers/history-today.js.map +1 -0
  71. package/lib/handlers/hitokoto.js +16 -0
  72. package/lib/handlers/hitokoto.js.map +1 -0
  73. package/lib/handlers/ip-query.js +17 -0
  74. package/lib/handlers/ip-query.js.map +1 -0
  75. package/lib/handlers/kfc.js +6 -0
  76. package/lib/handlers/kfc.js.map +1 -0
  77. package/lib/handlers/moyu.js +39 -0
  78. package/lib/handlers/moyu.js.map +1 -0
  79. package/lib/handlers/toutiao-hot.js +6 -0
  80. package/lib/handlers/toutiao-hot.js.map +1 -0
  81. package/lib/handlers/translate.js +26 -0
  82. package/lib/handlers/translate.js.map +1 -0
  83. package/lib/handlers/weather.js +22 -0
  84. package/lib/handlers/weather.js.map +1 -0
  85. package/lib/handlers/weibo-hot.js +6 -0
  86. package/lib/handlers/weibo-hot.js.map +1 -0
  87. package/lib/handlers/zhihu-hot.js +6 -0
  88. package/lib/handlers/zhihu-hot.js.map +1 -0
  89. package/lib/index.js +1 -34
  90. package/lib/index.js.map +1 -1
  91. package/lib/runtime-deps.js +22 -0
  92. package/lib/runtime-deps.js.map +1 -0
  93. package/package.json +52 -12
  94. package/plugin.js +13 -0
  95. package/schema.json +12 -0
  96. package/src/api.ts +79 -0
  97. package/{tools/60s-news/handler.ts → src/handlers/60s-news.ts} +1 -1
  98. package/{tools/bing-image/handler.ts → src/handlers/bing-image.ts} +4 -4
  99. package/{tools/douyin-hot/handler.ts → src/handlers/douyin-hot.ts} +2 -2
  100. package/{tools/duanzi/handler.ts → src/handlers/duanzi.ts} +1 -1
  101. package/{tools/exchange-rate/handler.ts → src/handlers/exchange-rate.ts} +8 -6
  102. package/{tools/fuel-price/handler.ts → src/handlers/fuel-price.ts} +5 -4
  103. package/{tools/gold-price/handler.ts → src/handlers/gold-price.ts} +8 -7
  104. package/src/handlers/history-today.ts +16 -0
  105. package/{tools/hitokoto/handler.ts → src/handlers/hitokoto.ts} +1 -1
  106. package/{tools/ip-query/handler.ts → src/handlers/ip-query.ts} +1 -1
  107. package/{tools/kfc/handler.ts → src/handlers/kfc.ts} +1 -1
  108. package/src/handlers/moyu.ts +35 -0
  109. package/{tools/toutiao-hot/handler.ts → src/handlers/toutiao-hot.ts} +2 -2
  110. package/src/handlers/translate.ts +25 -0
  111. package/src/handlers/weather.ts +22 -0
  112. package/{tools/weibo-hot/handler.ts → src/handlers/weibo-hot.ts} +2 -2
  113. package/{tools/zhihu-hot/handler.ts → src/handlers/zhihu-hot.ts} +2 -2
  114. package/src/index.ts +8 -44
  115. package/src/runtime-deps.ts +26 -0
  116. package/lib/index.d.ts +0 -2
  117. package/lib/index.d.ts.map +0 -1
  118. package/plugin.yml +0 -3
  119. package/tools/60s-news/60s-news.tool.md +0 -8
  120. package/tools/api.ts +0 -34
  121. package/tools/bing-image/bing-image.tool.md +0 -8
  122. package/tools/douyin-hot/douyin-hot.tool.md +0 -12
  123. package/tools/duanzi/duanzi.tool.md +0 -8
  124. package/tools/exchange-rate/exchange-rate.tool.md +0 -14
  125. package/tools/fuel-price/fuel-price.tool.md +0 -11
  126. package/tools/gold-price/gold-price.tool.md +0 -8
  127. package/tools/history-today/handler.ts +0 -15
  128. package/tools/history-today/history-today.tool.md +0 -8
  129. package/tools/hitokoto/hitokoto.tool.md +0 -12
  130. package/tools/ip-query/ip-query.tool.md +0 -12
  131. package/tools/kfc/kfc.tool.md +0 -8
  132. package/tools/moyu/handler.ts +0 -28
  133. package/tools/moyu/moyu.tool.md +0 -8
  134. package/tools/toutiao-hot/toutiao-hot.tool.md +0 -12
  135. package/tools/translate/handler.ts +0 -19
  136. package/tools/translate/translate.tool.md +0 -15
  137. package/tools/weather/handler.ts +0 -22
  138. package/tools/weather/weather.tool.md +0 -12
  139. package/tools/weibo-hot/weibo-hot.tool.md +0 -12
  140. package/tools/zhihu-hot/zhihu-hot.tool.md +0 -12
  141. /package/{skills/60s/SKILL.md → agent/skills/60s.md} +0 -0
package/plugin.js ADDED
@@ -0,0 +1,13 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { definePlugin } from 'zhin.js';
3
+ import { DEFAULT_API_BASE, registerSixtySApiBase } from "./lib/runtime-deps.js";
4
+ export default definePlugin({
5
+ name: 'sixty-s',
6
+ metadata: {
7
+ displayName: '60s API',
8
+ },
9
+ setup(context) {
10
+ // 运行时读取配置:config patch 即时生效;卸载时 lifecycle 反注册,无 process.env 残留
11
+ context.lifecycle.add(registerSixtySApiBase(() => context.config.get().apiBase?.trim() || DEFAULT_API_BASE));
12
+ },
13
+ });
package/schema.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "apiBase": {
7
+ "type": "string",
8
+ "default": "https://60s.viki.moe",
9
+ "description": "60s API base URL"
10
+ }
11
+ }
12
+ }
package/src/api.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 60s API 共享模块 — 供 *.tool.md handler 使用
3
+ */
4
+
5
+ import { resolveApiBase } from './runtime-deps.js';
6
+
7
+ export type ListItem = string | Record<string, unknown>;
8
+
9
+ /** Untyped 60s API JSON payload (handlers narrow fields at runtime). */
10
+ export type ApiPayload = Record<string, unknown>;
11
+
12
+ export async function fetchApi<T = ApiPayload>(
13
+ endpoint: string,
14
+ params?: Record<string, string>,
15
+ ): Promise<T> {
16
+ const url = new URL(`${resolveApiBase()}/v2${endpoint}`);
17
+ if (params) {
18
+ Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
19
+ }
20
+ let res: Response;
21
+ try {
22
+ res = await fetch(url.toString(), { signal: AbortSignal.timeout(30_000) });
23
+ } catch (e) {
24
+ const err = e as Error;
25
+ throw new Error(
26
+ err.name === 'TimeoutError' ? '请求超时(30s)' : `请求失败: ${err.message}`,
27
+ { cause: e },
28
+ );
29
+ }
30
+ if (!res.ok) {
31
+ throw new Error(`API 请求失败: ${res.status} ${res.statusText}`);
32
+ }
33
+ const contentType = res.headers.get('content-type') ?? '';
34
+ if (!contentType.includes('application/json')) {
35
+ throw new Error(`API 返回非 JSON 响应 (content-type: ${contentType || 'unknown'})`);
36
+ }
37
+ const data = (await res.json()) as {
38
+ error?: string;
39
+ code?: number;
40
+ message?: string;
41
+ msg?: string;
42
+ data?: T;
43
+ };
44
+ if (data.error) throw new Error(data.error);
45
+ if (data.code !== undefined && data.code !== 200 && data.code !== 0) {
46
+ throw new Error(data.message || data.msg || `API 错误: ${data.code}`);
47
+ }
48
+ return (data.data ?? data) as T;
49
+ }
50
+
51
+ /** Coerce nested API JSON objects for property access in handlers. */
52
+ export function asRecord(value: unknown): ApiPayload {
53
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
54
+ ? (value as ApiPayload)
55
+ : {};
56
+ }
57
+
58
+ export function asString(value: unknown): string {
59
+ return value == null ? '' : String(value);
60
+ }
61
+
62
+ export function asArray(value: unknown): unknown[] {
63
+ return Array.isArray(value) ? value : [];
64
+ }
65
+
66
+ export function formatList(items: ListItem[], limit = 10): string {
67
+ return items
68
+ .slice(0, limit)
69
+ .map((item, i) => {
70
+ const obj = typeof item === 'string' ? null : item;
71
+ const title = obj
72
+ ? String(obj.title ?? obj.name ?? obj.word ?? item)
73
+ : item;
74
+ const hot = obj ? obj.hot_value ?? obj.hot : undefined;
75
+ const hotStr = hot ? ` 🔥${hot}` : '';
76
+ return `${i + 1}. ${title}${hotStr}`;
77
+ })
78
+ .join('\n');
79
+ }
@@ -1,7 +1,7 @@
1
1
  import { fetchApi } from '../api.js';
2
2
 
3
3
  export default async function () {
4
- const data = await fetchApi<any>('/60s');
4
+ const data = await fetchApi('/60s');
5
5
  const lines = [`📰 今日60秒新闻 (${data.date || ''})`, ''];
6
6
  if (data.news && Array.isArray(data.news)) {
7
7
  lines.push(...data.news.map((n: string, i: number) => `${i + 1}. ${n}`));
@@ -1,13 +1,13 @@
1
- import { fetchApi } from '../api.js';
1
+ import { asString, fetchApi } from '../api.js';
2
2
 
3
3
  export default async function () {
4
- const data = await fetchApi<any>('/bing');
4
+ const data = await fetchApi('/bing');
5
5
  const lines = ['🖼️ Bing 每日壁纸', ''];
6
6
  if (data.title) lines.push(`📌 ${data.title}`);
7
7
  if (data.headline) lines.push(`💡 ${data.headline}`);
8
8
  if (data.copyright) lines.push(`📝 ${data.copyright}`);
9
- if (data.description) lines.push('', data.description);
9
+ if (data.description) lines.push('', asString(data.description));
10
10
  const imgUrl = data.cover || data.cover_4k || data.url || data.image;
11
- if (imgUrl) lines.push('', imgUrl);
11
+ if (imgUrl) lines.push('', asString(imgUrl));
12
12
  return lines.join('\n');
13
13
  }
@@ -1,6 +1,6 @@
1
- import { fetchApi, formatList } from '../api.js';
1
+ import { fetchApi, formatList, type ListItem } from '../api.js';
2
2
 
3
3
  export default async function (args: { limit?: number }) {
4
- const data = await fetchApi<any[]>('/douyin');
4
+ const data = await fetchApi<ListItem[]>('/douyin');
5
5
  return ['🔥 抖音热搜', '', formatList(data, args.limit || 10)].join('\n');
6
6
  }
@@ -1,6 +1,6 @@
1
1
  import { fetchApi } from '../api.js';
2
2
 
3
3
  export default async function () {
4
- const data = await fetchApi<any>('/duanzi');
4
+ const data = await fetchApi('/duanzi');
5
5
  return `😂 段子\n\n${data.duanzi || data.content || data.text || (typeof data === 'string' ? data : '')}`;
6
6
  }
@@ -1,10 +1,10 @@
1
- import { fetchApi } from '../api.js';
1
+ import { asArray, asRecord, asString, fetchApi } from '../api.js';
2
2
 
3
3
  export default async function (args: { from?: string; to?: string }) {
4
4
  const params: Record<string, string> = {};
5
5
  if (args.from) params.from = args.from.toUpperCase();
6
6
  if (args.to) params.to = args.to.toUpperCase();
7
- const data = await fetchApi<any>(
7
+ const data = await fetchApi(
8
8
  '/exchange-rate',
9
9
  Object.keys(params).length ? params : undefined,
10
10
  );
@@ -13,13 +13,15 @@ export default async function (args: { from?: string; to?: string }) {
13
13
  lines.push(`基准货币: ${base}`);
14
14
  if (data.updated) lines.push(`更新时间: ${data.updated}`);
15
15
  lines.push('');
16
- const rates = data.rates || (Array.isArray(data) ? data : []);
16
+ const rates = asArray(data.rates);
17
17
  const targetCurrencies = args.to
18
18
  ? [args.to.toUpperCase()]
19
19
  : ['USD', 'EUR', 'JPY', 'GBP', 'HKD', 'KRW', 'AUD', 'CAD', 'SGD', 'CHF'];
20
- rates.forEach((item: any) => {
21
- if (targetCurrencies.includes(item.currency) && item.currency !== base) {
22
- lines.push(`${base} → ${item.currency}: ${item.rate}`);
20
+ rates.forEach((item) => {
21
+ const row = asRecord(item);
22
+ const currency = asString(row.currency);
23
+ if (targetCurrencies.includes(currency) && currency !== base) {
24
+ lines.push(`${base} → ${currency}: ${row.rate}`);
23
25
  }
24
26
  });
25
27
  return lines.join('\n');
@@ -1,19 +1,20 @@
1
- import { fetchApi } from '../api.js';
1
+ import { asRecord, asString, fetchApi } from '../api.js';
2
2
 
3
3
  export default async function (args: { province?: string }) {
4
4
  const params: Record<string, string> | undefined = args.province
5
5
  ? { province: args.province }
6
6
  : undefined;
7
- const data = await fetchApi<any>('/fuel-price', params);
7
+ const data = await fetchApi('/fuel-price', params);
8
8
  const lines = ['⛽ 今日油价', ''];
9
9
  if (data.region) lines.push(`📍 ${data.region}`);
10
10
  if (data.items && Array.isArray(data.items)) {
11
- data.items.forEach((item: any) => {
11
+ data.items.forEach((item: Record<string, unknown>) => {
12
12
  lines.push(`${item.name}: ${item.price_desc || `¥${item.price}/升`}`);
13
13
  });
14
14
  }
15
15
  if (data.trend) {
16
- lines.push('', `📊 ${data.trend.description || ''}`);
16
+ const trend = asRecord(data.trend);
17
+ lines.push('', `📊 ${asString(trend.description)}`);
17
18
  }
18
19
  return lines.join('\n');
19
20
  }
@@ -1,16 +1,17 @@
1
- import { fetchApi } from '../api.js';
1
+ import { asArray, asRecord, fetchApi } from '../api.js';
2
2
 
3
3
  export default async function () {
4
- const data = await fetchApi<any>('/gold-price');
4
+ const data = await fetchApi('/gold-price');
5
5
  const lines = ['💰 今日金价', ''];
6
6
  if (data.date) lines.push(`📅 ${data.date}`);
7
- const metals = data.metals || (Array.isArray(data) ? data : []);
7
+ const metals = asArray(data.metals);
8
8
  if (metals.length > 0) {
9
9
  lines.push('');
10
- metals.forEach((item: any) => {
11
- const name = item.name || '黄金';
12
- const price = item.today_price || item.sell_price || item.price || 'N/A';
13
- const unit = item.unit || '元/克';
10
+ metals.forEach((item) => {
11
+ const row = asRecord(item);
12
+ const name = row.name || '黄金';
13
+ const price = row.today_price || row.sell_price || row.price || 'N/A';
14
+ const unit = row.unit || '元/克';
14
15
  if (price !== 'N/A') lines.push(`${name}: ¥${price} ${unit}`);
15
16
  });
16
17
  } else if (data.price) {
@@ -0,0 +1,16 @@
1
+ import { asArray, asRecord, fetchApi } from '../api.js';
2
+
3
+ export default async function () {
4
+ const data = await fetchApi('/today-in-history');
5
+ const dateStr = data.date || `${data.month || ''}月${data.day || ''}日`;
6
+ const lines = [`📅 历史上的今天 (${dateStr})`, ''];
7
+ const items = asArray(data.items);
8
+ items.slice(0, 10).forEach((item, i: number) => {
9
+ const row = asRecord(item);
10
+ const year = row.year || '';
11
+ const title = row.title || row.event || row.content || item;
12
+ const typeIcon = row.event_type === 'birth' ? '👶' : row.event_type === 'death' ? '🕊️' : '📌';
13
+ lines.push(`${typeIcon} ${i + 1}. ${year ? `[${year}] ` : ''}${title}`);
14
+ });
15
+ return lines.join('\n');
16
+ }
@@ -4,7 +4,7 @@ export default async function (args: { type?: string }) {
4
4
  const params: Record<string, string> | undefined = args.type
5
5
  ? { c: args.type }
6
6
  : undefined;
7
- const data = await fetchApi<any>('/hitokoto', params);
7
+ const data = await fetchApi('/hitokoto', params);
8
8
  const text = data.hitokoto || data.content || (typeof data === 'string' ? data : '');
9
9
  const lines = ['💬 一言', '', `「${text}」`];
10
10
  if (data.from || data.source || data.from_who || data.author) {
@@ -4,7 +4,7 @@ export default async function (args: { ip?: string }) {
4
4
  const params: Record<string, string> | undefined = args.ip
5
5
  ? { ip: args.ip }
6
6
  : undefined;
7
- const data = await fetchApi<any>('/ip', params);
7
+ const data = await fetchApi('/ip', params);
8
8
  const lines = ['🌐 IP 查询', '', `IP: ${data.ip || args.ip || '当前 IP'}`];
9
9
  if (data.country || data.region || data.city) {
10
10
  lines.push(`位置: ${[data.country, data.region, data.city].filter(Boolean).join(' ')}`);
@@ -1,6 +1,6 @@
1
1
  import { fetchApi } from '../api.js';
2
2
 
3
3
  export default async function () {
4
- const data = await fetchApi<any>('/kfc');
4
+ const data = await fetchApi('/kfc');
5
5
  return `🍗 疯狂星期四\n\n${data.kfc || data.content || data.text || (typeof data === 'string' ? data : '')}`;
6
6
  }
@@ -0,0 +1,35 @@
1
+ import { asArray, asRecord, asString, fetchApi, type ApiPayload } from '../api.js';
2
+
3
+ export default async function () {
4
+ const data = await fetchApi<ApiPayload | string>('/moyu');
5
+ if (typeof data === 'string') return `🐟 摸鱼日历\n\n${data}`;
6
+ const lines = ['🐟 摸鱼日历', ''];
7
+ const date = asRecord(data.date);
8
+ if (date.gregorian) lines.push(`📅 ${date.gregorian} ${asString(date.weekday)}`);
9
+ const lunar = asRecord(date.lunar);
10
+ if (lunar.monthCN || lunar.dayCN) {
11
+ lines.push(`🌙 农历${asString(lunar.monthCN)}${asString(lunar.dayCN)} ${asString(lunar.yearGanZhi)}年`);
12
+ }
13
+ const today = asRecord(data.today);
14
+ if (today.holidayName) lines.push(`🎉 ${today.holidayName}`);
15
+ if (today.solarTerm) lines.push(`🌿 ${today.solarTerm}`);
16
+ if (today.isWorkday !== undefined) {
17
+ lines.push(today.isWorkday ? '💼 今天是工作日' : '🎮 今天不上班');
18
+ }
19
+ if (Array.isArray(data.festivals)) {
20
+ data.festivals.forEach((f: unknown) => {
21
+ lines.push(`🎊 ${typeof f === 'string' ? f : asString(asRecord(f).name ?? f)}`);
22
+ });
23
+ }
24
+ const countdown = asArray(data.countdown);
25
+ if (countdown.length > 0) {
26
+ lines.push('');
27
+ countdown.forEach((c: unknown) => {
28
+ const row = asRecord(c);
29
+ if (typeof row.name === 'string' && typeof row.days === 'number') {
30
+ lines.push(`⏳ 距 ${row.name} 还有 ${row.days} 天`);
31
+ }
32
+ });
33
+ }
34
+ return lines.join('\n');
35
+ }
@@ -1,6 +1,6 @@
1
- import { fetchApi, formatList } from '../api.js';
1
+ import { fetchApi, formatList, type ListItem } from '../api.js';
2
2
 
3
3
  export default async function (args: { limit?: number }) {
4
- const data = await fetchApi<any[]>('/toutiao');
4
+ const data = await fetchApi<ListItem[]>('/toutiao');
5
5
  return ['🔥 头条热搜', '', formatList(data, args.limit || 10)].join('\n');
6
6
  }
@@ -0,0 +1,25 @@
1
+ import { asRecord, asString, fetchApi } from '../api.js';
2
+
3
+ export default async function (args: { text: string; to?: string }) {
4
+ const params: Record<string, string> = { text: args.text };
5
+ if (args.to) params.to = args.to;
6
+ const data = await fetchApi('/fanyi', params);
7
+ const source = asRecord(data.source);
8
+ const target = asRecord(data.target);
9
+ const srcText = asString(source.text) || args.text;
10
+ const srcLang = asString(source.type_desc) || asString(source.type);
11
+ const tgtText =
12
+ asString(target.text) ||
13
+ asString(data.result) ||
14
+ asString(data.translation) ||
15
+ (typeof data === 'string' ? data : '');
16
+ const tgtLang = asString(target.type_desc) || asString(target.type);
17
+ const lines = [
18
+ '🌐 翻译结果',
19
+ '',
20
+ `原文${srcLang ? ` (${srcLang})` : ''}: ${srcText}`,
21
+ `译文${tgtLang ? ` (${tgtLang})` : ''}: ${tgtText}`,
22
+ ];
23
+ if (target.pronounce) lines.push(`发音: ${asString(target.pronounce)}`);
24
+ return lines.join('\n');
25
+ }
@@ -0,0 +1,22 @@
1
+ import { asRecord, asString, fetchApi } from '../api.js';
2
+
3
+ export default async function (args: { city: string }) {
4
+ const data = await fetchApi('/weather', { query: args.city });
5
+ const w = asRecord(data.weather);
6
+ const aq = asRecord(data.air_quality);
7
+ const loc = asRecord(data.location);
8
+ const cityName = asString(loc.city) || asString(loc.name) || args.city;
9
+ const lines = [
10
+ `🌤️ ${cityName} 天气`,
11
+ '',
12
+ `🌡️ 温度: ${asString(w.temperature)}°C`,
13
+ `☁️ 天气: ${asString(w.condition)}`,
14
+ `💧 湿度: ${asString(w.humidity)}%`,
15
+ `💨 风: ${asString(w.wind_direction)} ${asString(w.wind_power)}`,
16
+ ];
17
+ if (Object.keys(aq).length > 0) {
18
+ const quality = asString(aq.quality);
19
+ lines.push(`🌬️ 空气: ${quality}${aq.aqi ? ` (AQI ${asString(aq.aqi)})` : ''}`);
20
+ }
21
+ return lines.join('\n');
22
+ }
@@ -1,6 +1,6 @@
1
- import { fetchApi, formatList } from '../api.js';
1
+ import { fetchApi, formatList, type ListItem } from '../api.js';
2
2
 
3
3
  export default async function (args: { limit?: number }) {
4
- const data = await fetchApi<any[]>('/weibo');
4
+ const data = await fetchApi<ListItem[]>('/weibo');
5
5
  return ['🔥 微博热搜', '', formatList(data, args.limit || 10)].join('\n');
6
6
  }
@@ -1,6 +1,6 @@
1
- import { fetchApi, formatList } from '../api.js';
1
+ import { fetchApi, formatList, type ListItem } from '../api.js';
2
2
 
3
3
  export default async function (args: { limit?: number }) {
4
- const data = await fetchApi<any[]>('/zhihu');
4
+ const data = await fetchApi<ListItem[]>('/zhihu');
5
5
  return ['🔥 知乎热榜', '', formatList(data, args.limit || 10)].join('\n');
6
6
  }
package/src/index.ts CHANGED
@@ -1,44 +1,8 @@
1
- /**
2
- * @zhin.js/plugin-60s
3
- *
4
- * 60s API 聚合插件 —— 基于 https://github.com/vikiboss/60s
5
- *
6
- * 提供多种实用 API 功能,每个功能通过 tools/*.tool.md 文件定义:
7
- * - 聊天命令(/60s, /天气 北京, /微博 …)
8
- * - AI 工具(Agent 可通过关键词自动匹配并调用)
9
- *
10
- * 功能列表:
11
- * 60s新闻 · 天气 · 微博热搜 · 知乎热榜 · 抖音热搜 · 头条热搜
12
- * 一言 · 摸鱼日历 · IP查询 · Bing壁纸 · 金价 · 油价
13
- * 汇率 · 翻译 · 历史上的今天 · KFC文案 · 段子
14
- *
15
- * 所有工具定义在 tools/ 目录下,框架自动发现注册。
16
- *
17
- * 配置方式(zhin.config.yml):
18
- * ```yaml
19
- * plugins:
20
- * 60s:
21
- * apiBase: https://60s.viki.moe
22
- * ```
23
- */
24
- import { usePlugin } from 'zhin.js';
25
-
26
- const plugin = usePlugin();
27
- const { logger, root } = plugin;
28
-
29
- // ─── 配置 ────────────────────────────────────────────────────────────────────
30
-
31
- interface Config {
32
- apiBase?: string;
33
- }
34
-
35
- const configService = root.inject('config');
36
- const appConfig = configService?.getPrimary<{ '60s'?: Config }>() || {};
37
- const config: Config = {
38
- apiBase: 'https://60s.viki.moe',
39
- ...appConfig['60s'],
40
- };
41
-
42
- // 将 API 地址写入环境变量,供 tool handler 使用
43
- process.env.ZHIN_60S_API = config.apiBase || 'https://60s.viki.moe';
44
-
1
+ export {
2
+ asArray,
3
+ asRecord,
4
+ asString,
5
+ fetchApi,
6
+ formatList,
7
+ } from './api.js';
8
+ export type { ApiPayload, ListItem } from './api.js';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 60s apiBase 运行时注入 — plugin setup 注册 getter,fetchApi 每次调用时求值。
3
+ * 参考 lottery 的 registerLotteryAgentDeps:registration 模式,卸载时反注册,无 process.env 残留。
4
+ */
5
+
6
+ export const DEFAULT_API_BASE = 'https://60s.viki.moe';
7
+
8
+ export type SixtySApiBaseGetter = () => string;
9
+
10
+ const registrations: Array<{ readonly value: SixtySApiBaseGetter }> = [];
11
+
12
+ /** Generation-owned apiBase binding used by Plugin Runtime setup(). Returns unregister. */
13
+ export function registerSixtySApiBase(getter: SixtySApiBaseGetter): () => void {
14
+ const registration = Object.freeze({ value: getter });
15
+ registrations.push(registration);
16
+ return () => {
17
+ const index = registrations.lastIndexOf(registration);
18
+ if (index >= 0) registrations.splice(index, 1);
19
+ };
20
+ }
21
+
22
+ /** Resolve the apiBase at call time so config patches take effect without reload. */
23
+ export function resolveApiBase(): string {
24
+ const getter = registrations[registrations.length - 1]?.value;
25
+ return getter?.() || DEFAULT_API_BASE;
26
+ }
package/lib/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/plugin.yml DELETED
@@ -1,3 +0,0 @@
1
- name: 60s
2
- description: 60s API 聚合插件,提供新闻、天气、热搜等 17 种实用工具
3
- version: 1.0.0
@@ -1,8 +0,0 @@
1
- ---
2
- name: 60s_news
3
- description: 获取每日60秒新闻,快速了解今日要闻
4
- tags: [新闻, 资讯, 60s]
5
- keywords: [60s, 新闻, 今日新闻, 60秒, 每日新闻, 读懂世界]
6
- preExecutable: true
7
- handler: ./handler.ts
8
- ---
package/tools/api.ts DELETED
@@ -1,34 +0,0 @@
1
- /**
2
- * 60s API 共享模块 — 供 *.tool.md handler 使用
3
- */
4
-
5
- const API_BASE = process.env.ZHIN_60S_API || 'https://60s.viki.moe';
6
-
7
- export async function fetchApi<T = any>(
8
- endpoint: string,
9
- params?: Record<string, string>,
10
- ): Promise<T> {
11
- const url = new URL(`${API_BASE}/v2${endpoint}`);
12
- if (params) {
13
- Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
14
- }
15
- const res = await fetch(url.toString());
16
- const data = (await res.json()) as any;
17
- if (data.error) throw new Error(data.error);
18
- if (data.code !== undefined && data.code !== 200 && data.code !== 0) {
19
- throw new Error(data.message || data.msg || `API 错误: ${data.code}`);
20
- }
21
- return data.data ?? data;
22
- }
23
-
24
- export function formatList(items: any[], limit = 10): string {
25
- return items
26
- .slice(0, limit)
27
- .map((item, i) => {
28
- const title = item.title || item.name || item.word || item;
29
- const hot = item.hot_value || item.hot;
30
- const hotStr = hot ? ` 🔥${hot}` : '';
31
- return `${i + 1}. ${title}${hotStr}`;
32
- })
33
- .join('\n');
34
- }
@@ -1,8 +0,0 @@
1
- ---
2
- name: bing_image
3
- description: 获取 Bing 每日壁纸图片
4
- tags: [图片, 壁纸, Bing]
5
- keywords: [bing, 必应, 壁纸, 每日壁纸]
6
- preExecutable: true
7
- handler: ./handler.ts
8
- ---
@@ -1,12 +0,0 @@
1
- ---
2
- name: douyin_hot
3
- description: 获取抖音热搜榜
4
- tags: [热搜, 短视频, 抖音]
5
- keywords: [抖音, 热搜, douyin, dy]
6
- preExecutable: true
7
- parameters:
8
- limit:
9
- type: number
10
- description: 返回条数,默认10条
11
- handler: ./handler.ts
12
- ---
@@ -1,8 +0,0 @@
1
- ---
2
- name: duanzi
3
- description: 获取一个段子
4
- tags: [娱乐, 笑话, 段子]
5
- keywords: [段子, 笑话, 搞笑, joke, duanzi]
6
- preExecutable: true
7
- handler: ./handler.ts
8
- ---
@@ -1,14 +0,0 @@
1
- ---
2
- name: exchange_rate
3
- description: 查询货币汇率
4
- tags: [金融, 汇率, 查询]
5
- keywords: [汇率, 兑换, 外汇, exchange, rate]
6
- parameters:
7
- from:
8
- type: string
9
- description: 源货币,如 USD, EUR, JPY
10
- to:
11
- type: string
12
- description: 目标货币,如 CNY
13
- handler: ./handler.ts
14
- ---
@@ -1,11 +0,0 @@
1
- ---
2
- name: fuel_price
3
- description: 查询今日油价
4
- tags: [生活, 油价, 价格]
5
- keywords: [油价, 汽油, 柴油, fuel]
6
- parameters:
7
- province:
8
- type: string
9
- description: 省份名称,如"四川"、"北京"
10
- handler: ./handler.ts
11
- ---
@@ -1,8 +0,0 @@
1
- ---
2
- name: gold_price
3
- description: 查询今日黄金价格
4
- tags: [金融, 黄金, 价格]
5
- keywords: [金价, 黄金, 黄金价格, gold]
6
- preExecutable: true
7
- handler: ./handler.ts
8
- ---
@@ -1,15 +0,0 @@
1
- import { fetchApi } from '../api.js';
2
-
3
- export default async function () {
4
- const data = await fetchApi<any>('/today-in-history');
5
- const dateStr = data.date || `${data.month || ''}月${data.day || ''}日`;
6
- const lines = [`📅 历史上的今天 (${dateStr})`, ''];
7
- const items = data.items || (Array.isArray(data) ? data : []);
8
- items.slice(0, 10).forEach((item: any, i: number) => {
9
- const year = item.year || '';
10
- const title = item.title || item.event || item.content || item;
11
- const typeIcon = item.event_type === 'birth' ? '👶' : item.event_type === 'death' ? '🕊️' : '📌';
12
- lines.push(`${typeIcon} ${i + 1}. ${year ? `[${year}] ` : ''}${title}`);
13
- });
14
- return lines.join('\n');
15
- }
@@ -1,8 +0,0 @@
1
- ---
2
- name: history_today
3
- description: 查看历史上的今天发生了什么
4
- tags: [历史, 知识, 日历]
5
- keywords: [历史, 历史上的今天, 今天历史, history]
6
- preExecutable: true
7
- handler: ./handler.ts
8
- ---