koishi-plugin-stock 2.0.4 → 2.0.6

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 (3) hide show
  1. package/README.md +12 -0
  2. package/lib/index.js +64 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -57,6 +57,18 @@ npm install koishi-plugin-stock
57
57
 
58
58
  ## 更新日志
59
59
 
60
+ ### v2.0.6
61
+ - 优化定时任务触发逻辑:改用 `Intl` API 获取确切的上海时区时间,解决部分环境下时区偏移导致的触发失败问题
62
+ - 增强时间格式容错:自动处理 `9:30` vs `09:30` 以及全角冒号等配置问题
63
+ - 完善节假日 API:增加对 Cloudflare 拦截页面的防御逻辑,请求失败时自动回退到基础周末检查,确保任务触发稳定性
64
+ - 改进日志输出:在 `info` 级别增加关键任务触发和跳过的日志,方便诊断问题
65
+
66
+ ### v2.0.5
67
+ - 使用标准 Koishi 配置注释、每个配置分类上壳有中文注释,更清晰地区分配置组
68
+
69
+ ### v2.0.4
70
+ - 实现 Koishi 业界标准的配置分组,每个配置项打上对应的 role 标签,在插件设置页中形成可折叠的分组
71
+
60
72
  ### v2.0.3
61
73
  - 优化插件配置页显示,使用 Koishi 行业标准方式实现配置分类,每个配置项描述前缀形成分类标签
62
74
 
package/lib/index.js CHANGED
@@ -17,22 +17,26 @@ const BroadcastTask = koishi_1.Schema.object({
17
17
  ]).default('活跃市值').description('广播内容'),
18
18
  });
19
19
  exports.Config = koishi_1.Schema.object({
20
+ // --- 系统设置 ---
20
21
  enableDebugLog: koishi_1.Schema.boolean().description('启用调试日志').default(false),
21
- allCommandsBlacklist: koishi_1.Schema.array(String).description('全部指令黑名单用户ID').role('collapse:userBlacklist'),
22
- activeMarketCapBlacklist: koishi_1.Schema.array(String).description('活跃市值指令黑名单用户ID').role('collapse:userBlacklist'),
23
- stockAlertBlacklist: koishi_1.Schema.array(String).description('异动指令黑名单用户ID').role('collapse:userBlacklist'),
24
- limitUpBoardBlacklist: koishi_1.Schema.array(String).description('涨停看板指令黑名单用户ID').role('collapse:userBlacklist'),
25
- limitDownBoardBlacklist: koishi_1.Schema.array(String).description('跌停看板指令黑名单用户ID').role('collapse:userBlacklist'),
26
- stockSelectionBlacklist: koishi_1.Schema.array(String).description('选股指令黑名单用户ID').role('collapse:userBlacklist'),
27
- rideBlacklist: koishi_1.Schema.array(String).description('骑指令黑名单用户ID').role('collapse:userBlacklist'),
28
- allCommandsChannelBlacklist: koishi_1.Schema.array(String).description('全部指令黑名单频道ID').role('collapse:channelBlacklist'),
29
- activeMarketCapChannelBlacklist: koishi_1.Schema.array(String).description('活跃市值指令黑名单频道ID').role('collapse:channelBlacklist'),
30
- stockAlertChannelBlacklist: koishi_1.Schema.array(String).description('异动指令黑名单频道ID').role('collapse:channelBlacklist'),
31
- limitUpBoardChannelBlacklist: koishi_1.Schema.array(String).description('涨停看板指令黑名单频道ID').role('collapse:channelBlacklist'),
32
- limitDownBoardChannelBlacklist: koishi_1.Schema.array(String).description('跌停看板指令黑名单频道ID').role('collapse:channelBlacklist'),
33
- stockSelectionChannelBlacklist: koishi_1.Schema.array(String).description('选股指令黑名单频道ID').role('collapse:channelBlacklist'),
34
- rideChannelBlacklist: koishi_1.Schema.array(String).description('骑指令黑名单频道ID').role('collapse:channelBlacklist'),
35
- broadcastTasks: koishi_1.Schema.array(BroadcastTask).description('定时广播任务列表').role('collapse:broadcast'),
22
+ // --- 用户黑名单 ---
23
+ allCommandsBlacklist: koishi_1.Schema.array(String).description('全部指令黑名单用户ID'),
24
+ activeMarketCapBlacklist: koishi_1.Schema.array(String).description('活跃市值指令黑名单用户ID'),
25
+ stockAlertBlacklist: koishi_1.Schema.array(String).description('异动指令黑名单用户ID'),
26
+ limitUpBoardBlacklist: koishi_1.Schema.array(String).description('涨停看板指令黑名单用户ID'),
27
+ limitDownBoardBlacklist: koishi_1.Schema.array(String).description('跌停看板指令黑名单用户ID'),
28
+ stockSelectionBlacklist: koishi_1.Schema.array(String).description('选股指令黑名单用户ID'),
29
+ rideBlacklist: koishi_1.Schema.array(String).description('骑指令黑名单用户ID'),
30
+ // --- 频道黑名单 ---
31
+ allCommandsChannelBlacklist: koishi_1.Schema.array(String).description('全部指令黑名单频道ID'),
32
+ activeMarketCapChannelBlacklist: koishi_1.Schema.array(String).description('活跃市值指令黑名单频道ID'),
33
+ stockAlertChannelBlacklist: koishi_1.Schema.array(String).description('异动指令黑名单频道ID'),
34
+ limitUpBoardChannelBlacklist: koishi_1.Schema.array(String).description('涨停看板指令黑名单频道ID'),
35
+ limitDownBoardChannelBlacklist: koishi_1.Schema.array(String).description('跌停看板指令黑名单频道ID'),
36
+ stockSelectionChannelBlacklist: koishi_1.Schema.array(String).description('选股指令黑名单频道ID'),
37
+ rideChannelBlacklist: koishi_1.Schema.array(String).description('骑指令黑名单频道ID'),
38
+ // --- 定时广播 ---
39
+ broadcastTasks: koishi_1.Schema.array(BroadcastTask).description('定时广播任务列表'),
36
40
  });
37
41
  function apply(ctx, config) {
38
42
  const logger = ctx.logger('stock');
@@ -48,14 +52,18 @@ function apply(ctx, config) {
48
52
  let lastCheckedMinute = '';
49
53
  ctx.setInterval(async () => {
50
54
  const now = new Date();
51
- // 手动调整时区:UTC+8(中国时间)
52
- const localTime = new Date(now.getTime() + 8 * 60 * 60 * 1000);
53
- const hours = localTime.getUTCHours().toString().padStart(2, '0');
54
- const minutes = localTime.getUTCMinutes().toString().padStart(2, '0');
55
- const currentTime = `${hours}:${minutes}`;
55
+ // 使用 Intl API 获取确切的中国时间,避免手动时区转换的误差
56
+ const chinaTimeStr = now.toLocaleTimeString('zh-CN', {
57
+ timeZone: 'Asia/Shanghai',
58
+ hour12: false,
59
+ hour: '2-digit',
60
+ minute: '2-digit'
61
+ });
62
+ // 确保格式为 HH:mm,并处理可能的中文冒号
63
+ const currentTime = chinaTimeStr.replace(':', ':');
56
64
  // 每分钟的第一次执行时打印当前时间
57
65
  if (config.enableDebugLog && currentTime !== lastCheckedMinute) {
58
- logger.info(`[定时任务检查] 当前时间: ${currentTime}`);
66
+ logger.info(`[定时任务检查] 当前时间: ${currentTime} (上海时区)`);
59
67
  }
60
68
  if (currentTime === lastCheckedMinute)
61
69
  return;
@@ -67,43 +75,64 @@ function apply(ctx, config) {
67
75
  logger.warn(`跳过配置不正确的任务: times 字段无效`);
68
76
  return false;
69
77
  }
70
- const times = t.times.split(',').map(s => s.trim()).filter(s => s);
78
+ // 支持中英文逗号,并自动补全 9:30 这种格式为 09:30
79
+ const times = t.times.split(/[,,]/).map(s => {
80
+ let time = s.trim();
81
+ if (/^\d:\d{2}$/.test(time))
82
+ time = '0' + time;
83
+ return time;
84
+ }).filter(s => s);
71
85
  const isMatch = times.includes(currentTime);
72
86
  if (config.enableDebugLog && isMatch) {
73
- logger.info(`[时间匹配] ${currentTime} 在列表 [${times.join(',')}] 中`);
87
+ logger.info(`[时间匹配成功] 当前时间 ${currentTime} 命中任务设定列表 [${times.join(',')}]`);
74
88
  }
75
89
  return isMatch;
76
90
  });
77
91
  if (activeTasks.length === 0)
78
92
  return;
79
93
  lastCheckedMinute = currentTime;
80
- if (config.enableDebugLog) {
81
- logger.info(`[任务触发] 检测到定时任务: ${currentTime}, 共有 ${activeTasks.length} 个待执行任务`);
82
- }
94
+ // 即使在 info 级别也打印匹配到的任务,方便用户确认
95
+ logger.info(`[任务触发] 检测到 ${activeTasks.length} 个待执行的广播任务 (时间: ${currentTime})`);
83
96
  try {
84
97
  // 检查是否为交易日(基本周末检查 + 节假日API)
85
- const day = localTime.getUTCDay();
86
- const isWeekend = (day === 0 || day === 6);
87
- const year = localTime.getUTCFullYear();
88
- const month = (localTime.getUTCMonth() + 1).toString().padStart(2, '0');
89
- const date = localTime.getUTCDate().toString().padStart(2, '0');
90
- const dateStr = `${year}-${month}-${date}`;
98
+ // 使用 Intl API 获取上海日期的组件,确保逻辑一致
99
+ const formatter = new Intl.DateTimeFormat('zh-CN', {
100
+ timeZone: 'Asia/Shanghai',
101
+ year: 'numeric',
102
+ month: '2-digit',
103
+ day: '2-digit',
104
+ weekday: 'narrow'
105
+ });
106
+ const parts = formatter.formatToParts(now);
107
+ const getValue = (type) => parts.find(p => p.type === type)?.value || '';
108
+ const year = getValue('year');
109
+ const month = getValue('month');
110
+ const day = getValue('day');
111
+ const shanghaiDate = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
112
+ const isWeekend = (shanghaiDate.getDay() === 0 || shanghaiDate.getDay() === 6);
113
+ const dateStr = `${year}-${month}-${day}`;
91
114
  let tradingDay = !isWeekend;
92
115
  try {
93
- logger.debug(`正在检查交易日状态: ${dateStr}`);
116
+ if (config.enableDebugLog)
117
+ logger.debug(`正在检查交易日状态: ${dateStr}`);
94
118
  const holidayData = await ctx.http.get(`https://timor.tech/api/holiday/info/${dateStr}`);
95
- if (holidayData && holidayData.type) {
119
+ // 增加对 HTML 响应的防御(Cloudflare 拦截时会返回 HTML 字符串)
120
+ if (holidayData && typeof holidayData === 'object' && holidayData.type) {
96
121
  // type: 0 工作日, 1 周末, 2 节日, 3 调休
97
122
  const typeCode = holidayData.type.type;
98
123
  tradingDay = (typeCode === 0 || typeCode === 3);
99
124
  logger.info(`节假日API返回类型: ${typeCode} (${holidayData.type.name}), 交易日状态: ${tradingDay}`);
100
125
  }
126
+ else {
127
+ if (config.enableDebugLog)
128
+ logger.warn(`节假日API返回了非预期的格式或被拦截,将回退到周末检查`);
129
+ }
101
130
  }
102
131
  catch (e) {
103
132
  logger.warn(`节假日API请求失败,将使用基础周末检查: ${e.message}`);
104
133
  }
105
134
  if (!tradingDay) {
106
- logger.info(`当前非交易日,跳过广播任务`);
135
+ logger.info(`[定时任务跳过] 当前日期 ${dateStr} 非交易日,跳过执行`);
107
136
  return;
108
137
  }
109
138
  for (const task of activeTasks) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-stock",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "A Koishi plugin that fetches stock data and provides market analysis, including active market cap, stock alerts, limit-up board, and stock selection features with configurable blacklists for each command.",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",