koishi-plugin-ll-schedule 1.0.9 → 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 (2) hide show
  1. package/index.js +339 -57
  2. package/package.json +5 -5
package/index.js CHANGED
@@ -3,12 +3,13 @@
3
3
  // koishi-plugin-ll-schedule
4
4
  // 定时发送插件:设置时间自动向指定群发送文字
5
5
  // v1.0.6:一个任务支持多个时间(逗号分隔),群内"订阅"关键词自动注册推送群
6
+ // v1.1.0:新增"排班表 slots"(按星期设置不同时间/内容)+ QQ官方Markdown卡片 + 模板变量
6
7
 
7
8
  const { Schema, h } = require('koishi')
8
9
 
9
10
  exports.name = 'll-schedule'
10
11
 
11
- exports.usage = `定时发送插件:设置时间自动向指定群发送文字。
12
+ exports.usage = `定时发送插件:设置时间自动向指定群发送文字/卡片。
12
13
 
13
14
  ## 快速上手(推荐用法)
14
15
 
@@ -16,10 +17,55 @@ exports.usage = `定时发送插件:设置时间自动向指定群发送文字
16
17
  2. 在任意群里 @机器人 发:\`订阅 任务名\` → 这个群就加入了该任务的推送列表(自动记录,重启不丢)
17
18
  3. 到点后插件自动把内容推送到所有订阅了该任务的群
18
19
 
20
+ ## 排班表 slots(按星期设置不同的时间/内容)
21
+
22
+ 每个任务除了简单的 \`time\` + \`text\`,还可以填一张**排班表 \`slots\`**。每行 = 星期 + 时间 + 内容,互不影响:
23
+
24
+ \`\`\`yaml
25
+ tasks:
26
+ 每日推送:
27
+ slots:
28
+ - time: "12:20, 18:20, 21:20" # 时间(可多个,逗号分隔)
29
+ days: "" # 星期:留空=每天;1,2,3=周一~周三;工作日;周末;一,三,五
30
+ text: "这段内容在设定的星期和时间发送"
31
+ - time: "20:55" # 同一任务可加任意多行
32
+ days: "工作日"
33
+ text: "工作日晚上说的话"
34
+ - time: "14:55, 20:55"
35
+ days: "周末"
36
+ text: "周末说的话(周六日各发两次)"
37
+ \`\`\`
38
+
39
+ - \`days\` 填写规则:留空 = **每天**;数字 \`1\`~\`7\`(1=周一,逗号分隔如 \`1,3,5\`);中文 \`一,三,五\`;快捷词 \`工作日\`(周一~周五)/ \`周末\`(周六日);也支持范围 \`1-5\`
40
+ - **任务一旦填了 slots,就完全以 slots 为准**,旧的 \`time\`/\`text\` 自动忽略(不会重复发);不填 slots 则行为和旧版完全一样
41
+ - 以后要加时段/改内容,直接增删改表格行即可,群订阅不用重新弄
42
+
43
+ ## Markdown 卡片(官方QQ机器人)
44
+
45
+ 任务或排班行勾选 \`markdown\` 后,推送会用 **QQ官方 Markdown 卡片**发送(仅官机生效;失败自动回退纯文本,其他平台始终纯文本)。支持标题、加粗、引用、链接、图片等:
46
+
47
+ \`\`\`markdown
48
+ # 🎉 每日公告
49
+
50
+ > 今天是 **{date}**({week}),现在是 {time}
51
+
52
+ [官网点此访问](https://example.com)
53
+ ![配图](https://example.com/pic.jpg)
54
+ \`\`\`
55
+
56
+ \`text\` 中可用模板变量(发送时自动替换):
57
+ - \`{date}\`:今天日期(如 2026-08-23)
58
+ - \`{time}\`:当前时间(如 12:20)
59
+ - \`{week}\`:今天星期(如 星期三)
60
+ - \`{task}\`:任务名
61
+
19
62
  ## 任务配置
20
63
 
21
64
  - \`time\`:发送时间(24小时制 HH:MM),**可填多个,用逗号分隔**,如 \`09:00, 21:30\`(一条内容会在每个时间点各发一次)
22
- - \`text\`:推送内容(文字,可留空)
65
+ - \`text\`:推送内容(文字,可留空;支持 Markdown 语法和模板变量)
66
+ - \`image\`:图片 URL(可选,随文本一起发送;markdown 模式自动嵌入为图片)
67
+ - \`markdown\`:是否用 QQ官方 Markdown 卡片发送(默认关闭)
68
+ - \`slots\`:排班表(可选,见上方说明;填了就以排班表为准)
23
69
  - \`groups\`:手动指定的目标群列表(**推荐留空**,用群内"订阅"命令添加更省事;留空且无订阅 = 发给所有已知群)
24
70
  - \`enabled\`:是否启用
25
71
 
@@ -28,7 +74,7 @@ exports.usage = `定时发送插件:设置时间自动向指定群发送文字
28
74
  - \`订阅 [任务名]\`:当前群订阅指定任务的推送(不填任务名时,只有一个启用任务则自动选它)
29
75
  - \`取消订阅 [任务名]\`:当前群取消订阅
30
76
  - \`订阅列表\`:查看所有任务的订阅群
31
- - \`schedule.list\`:查看任务列表
77
+ - \`schedule.list\`:查看任务列表(含排班表详情)
32
78
  - \`schedule.test <任务名>\`:立即测试发送
33
79
  - \`lls.*\`:以上命令的备用入口(lls.sub / lls.unsub / lls.sublist / lls.list / lls.test)
34
80
 
@@ -39,19 +85,173 @@ exports.usage = `定时发送插件:设置时间自动向指定群发送文字
39
85
  官方平台没有"群号",只有 openid——用群内"订阅"命令即可,插件会自动记录当前群的 openid,无需手动填写。手动填 \`groups\` 时可用 \`schedule.groups\` 查看已知群的 openid。官机主动发群消息有平台频率限制,且机器人需在群里。
40
86
  `
41
87
 
88
+ // ===== 纯函数(可单测) =====
89
+
90
+ // 解析时间:支持多个时间,逗号/顿号/空格分隔,如 "09:00, 21:30"
91
+ function parseTimes(timeStr) {
92
+ if (!timeStr) return []
93
+ return String(timeStr).split(/[,,、;;\s]+/)
94
+ .map(s => s.trim())
95
+ .filter(s => /^\d{1,2}:\d{2}$/.test(s))
96
+ .map(s => (s.length === 4 ? '0' + s : s))
97
+ }
98
+
99
+ // 今天星期几(1=周一 ... 7=周日)
100
+ function todayDay(date) {
101
+ const d = date || new Date()
102
+ const day = d.getDay()
103
+ return day === 0 ? 7 : day
104
+ }
105
+
106
+ const WEEK_CN = ['', '一', '二', '三', '四', '五', '六', '日']
107
+
108
+ // 解析星期配置:返回 Set(1..7) 或 null(null = 每天)
109
+ // 支持:留空/每天/每日、1-7 数字逗号分隔、范围 1-5、中文 一,三,五 / 星期X / 周X、工作日、周末
110
+ function parseDays(daysStr) {
111
+ if (daysStr === undefined || daysStr === null) return null
112
+ const s = String(daysStr).trim()
113
+ if (!s) return null
114
+ const alias = { '每天': null, '每日': null, '天天': null, '工作日': new Set([1, 2, 3, 4, 5]), '周末': new Set([6, 7]), '休息日': new Set([6, 7]) }
115
+ if (Object.prototype.hasOwnProperty.call(alias, s)) return alias[s]
116
+ const set = new Set()
117
+ const cnMap = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '日': 7, '天': 7 }
118
+ for (const part of s.split(/[,,、;;\s]+/)) {
119
+ const p = part.trim()
120
+ if (!p) continue
121
+ const range = p.match(/^(\d{1,2})\s*[-~]\s*(\d{1,2})$/)
122
+ if (range) {
123
+ for (let i = parseInt(range[1], 10); i <= parseInt(range[2], 10); i++) {
124
+ if (i >= 1 && i <= 7) set.add(i)
125
+ }
126
+ continue
127
+ }
128
+ const cn = p.match(/^(?:星期|周|礼拜)?([一二三四五六日天])$/)
129
+ if (cn) {
130
+ set.add(cnMap[cn[1]])
131
+ continue
132
+ }
133
+ const num = p.match(/^([1-7])$/)
134
+ if (num) {
135
+ set.add(parseInt(num[1], 10))
136
+ continue
137
+ }
138
+ }
139
+ return set.size ? set : null
140
+ }
141
+
142
+ // 某天是否命中排班(null=每天 恒命中)
143
+ function dayMatches(daysSet, day) {
144
+ return !daysSet || daysSet.has(day)
145
+ }
146
+
147
+ // 计算"现在这一刻该发什么":返回 [{ name, key, text, markdown, image }]
148
+ // date 为 Date 对象、hm 为 "HH:MM" 字符串;纯函数便于测试
149
+ function computeFires(tasks, date, hm) {
150
+ const day = todayDay(date)
151
+ const fires = []
152
+ for (const [name, task] of Object.entries(tasks || {})) {
153
+ if (!task || task.enabled === false) continue
154
+ const slots = Array.isArray(task.slots) && task.slots.length ? task.slots : null
155
+ if (slots) {
156
+ slots.forEach((slot, idx) => {
157
+ if (!slot || !dayMatches(parseDays(slot.days), day)) return
158
+ const timeList = parseTimes(slot.time)
159
+ if (!timeList.includes(hm)) return
160
+ fires.push({
161
+ name,
162
+ key: `${name}@${hm}@${idx}`,
163
+ text: slot.text || '',
164
+ markdown: slot.markdown !== undefined ? !!slot.markdown : !!task.markdown,
165
+ image: task.image,
166
+ })
167
+ })
168
+ }
169
+ else {
170
+ const timeList = parseTimes(task.time)
171
+ if (!timeList.includes(hm)) continue
172
+ fires.push({
173
+ name,
174
+ key: `${name}@${hm}`,
175
+ text: task.text || '',
176
+ markdown: !!task.markdown,
177
+ image: task.image,
178
+ })
179
+ }
180
+ }
181
+ return fires
182
+ }
183
+
184
+ // 模板变量渲染:{date} {time} {week} {task}
185
+ function renderText(text, ctx) {
186
+ return String(text || '')
187
+ .replace(/\{date\}/g, ctx.date)
188
+ .replace(/\{time\}/g, ctx.time)
189
+ .replace(/\{week\}/g, ctx.week)
190
+ .replace(/\{task\}/g, ctx.task)
191
+ }
192
+
193
+ // 组装 markdown:文本 + 图片
194
+ function buildMd(text, image) {
195
+ let md = String(text || '').trim()
196
+ if (image) md += (md ? '\n\n' : '') + `![图片](${image})`
197
+ return md
198
+ }
199
+
200
+ // markdown 转纯文本(发送时兜底)
201
+ function buildTextFromMd(md) {
202
+ return String(md || '')
203
+ .replace(/<qqbot-at-user[^>]*\/>/g, '')
204
+ .replace(/^#{1,6}\s*/gm, '')
205
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
206
+ .replace(/\*([^*]+)\*/g, '$1')
207
+ .replace(/`([^`]+)`/g, '$1')
208
+ .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[$1]($2)')
209
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1: $2')
210
+ .replace(/^>\s?/gm, '')
211
+ .replace(/\n{3,}/g, '\n\n')
212
+ .trim()
213
+ }
214
+
215
+ exports._test = { parseTimes, todayDay, parseDays, dayMatches, computeFires, renderText, buildMd, buildTextFromMd }
216
+
217
+ // ===== 配置 Schema =====
218
+
219
+ const TimeSlot = Schema.object({
220
+ time: Schema.string()
221
+ .required()
222
+ .description('发送时间(24小时制 HH:MM),可多个用逗号分隔,如 09:00, 21:30'),
223
+ days: Schema.string()
224
+ .default('')
225
+ .description('星期几生效:留空=每天;1-7(1=周一)逗号分隔如 1,3,5;快捷词"工作日"(周一~五)/"周末"(周六日);中文"一,三,五"'),
226
+ text: Schema.string()
227
+ .default('')
228
+ .description('该时段发送的内容(文字,可留空;支持 Markdown 语法和模板变量 {date} {time} {week} {task})'),
229
+ markdown: Schema.boolean()
230
+ .description('用 QQ官方 Markdown 卡片发送(留空则跟随任务级 markdown 设置)'),
231
+ })
232
+
42
233
  const TaskItem = Schema.object({
43
234
  time: Schema.string()
44
235
  .required()
45
- .description('发送时间(24小时制 HH:MM),可填多个用逗号分隔,如 09:00, 21:30'),
236
+ .description('发送时间(24小时制 HH:MM),可填多个用逗号分隔,如 09:00, 21:30;填了下方 slots 排班表时此项自动忽略'),
46
237
  groups: Schema.array(Schema.string())
47
238
  .default([])
48
239
  .description('手动指定的目标群 ID(可留空;推荐用群内"订阅"命令自动添加,无需手动填 openid)'),
49
240
  text: Schema.string()
50
241
  .default('')
51
- .description('推送内容(文字,可留空)'),
242
+ .description('推送内容(文字,可留空;支持 Markdown 语法和模板变量 {date} {time} {week} {task})'),
243
+ image: Schema.string()
244
+ .default('')
245
+ .description('图片 URL(可选):随文本一起发送(markdown 模式自动嵌入为图片)'),
246
+ markdown: Schema.boolean()
247
+ .default(false)
248
+ .description('用 QQ官方 Markdown 卡片发送(仅官方QQ机器人生效,失败自动回退纯文本;其他平台始终纯文本)'),
52
249
  enabled: Schema.boolean()
53
250
  .default(true)
54
251
  .description('是否启用'),
252
+ slots: Schema.array(TimeSlot)
253
+ .default([])
254
+ .description('📅 排班表(可选):每行=星期+时间+内容,按星期设置不同时段/内容;一旦填写就完全以排班表为准,上方 time/text 自动忽略'),
55
255
  })
56
256
 
57
257
  const Config = Schema.object({
@@ -241,64 +441,74 @@ function apply(ctx, config) {
241
441
  learnGroup(session)
242
442
  })
243
443
 
244
- function nowHM() {
245
- const d = new Date()
444
+ function nowHM(date) {
445
+ const d = date || new Date()
246
446
  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
247
447
  }
248
448
 
249
- // 解析任务时间:支持多个时间,逗号/顿号/空格分隔,如 "09:00, 21:30"
250
- function parseTimes(timeStr) {
251
- if (!timeStr) return []
252
- return String(timeStr).split(/[,,、;;\s]+/)
253
- .map(s => s.trim())
254
- .filter(s => /^\d{1,2}:\d{2}$/.test(s))
255
- .map(s => (s.length === 4 ? '0' + s : s))
256
- }
257
-
258
- function todayStr() {
259
- const d = new Date()
449
+ function todayStr(date) {
450
+ const d = date || new Date()
260
451
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
261
452
  }
262
453
 
263
- async function checkAndSend() {
264
- const today = todayStr()
265
- // 跨天重置
266
- if (firedToday.date !== today) {
267
- firedToday = { date: today }
268
- }
454
+ // ===== 发送 =====
455
+ // 官机 markdown 发送时忽略的已知错误码(主动消息受限等)
456
+ const KNOWN_IGNORE_CODES = new Set([11293, 40034101, 40034105, 304101, 304102])
269
457
 
270
- const hm = nowHM()
271
- const tasks = config.tasks || {}
458
+ function errCode(e) {
459
+ const d = e && e.response && e.response.data
460
+ if (!d) return undefined
461
+ return d.code !== undefined ? d.code : d.err_code
462
+ }
272
463
 
273
- for (const [name, task] of Object.entries(tasks)) {
274
- if (!task || task.enabled === false) continue
275
- const timeList = parseTimes(task.time)
276
- if (!timeList.includes(hm)) continue
277
- // 今天这个时间点已经发过这个任务 → 跳过
278
- const fireKey = `${name}@${hm}`
279
- if (firedToday[fireKey]) continue
280
- firedToday[fireKey] = true
464
+ function isKnownIgnoreError(e) {
465
+ const code = errCode(e)
466
+ return typeof code === 'number' && KNOWN_IGNORE_CODES.has(code)
467
+ }
281
468
 
282
- logger.info(`[schedule] 触发任务 "${name}" @ ${hm}`)
283
- await sendTask(name, task)
469
+ // 向一个目标发送一段内容;markdown 模式仅官机生效,失败自动回退纯文本
470
+ async function sendOne(bot, target, text, { markdown, image }) {
471
+ if (markdown && bot.platform === 'qq' && bot.internal && typeof bot.internal.sendMessage === 'function') {
472
+ const md = buildMd(text, image)
473
+ const plain = buildTextFromMd(md) || ' '
474
+ try {
475
+ await bot.internal.sendMessage(target, { msg_type: 2, msg_seq: 0, content: plain, markdown: { content: md } })
476
+ return true
477
+ }
478
+ catch (e) {
479
+ if (isKnownIgnoreError(e)) {
480
+ if (config.verbose) logger.debug(`[schedule] 主动消息受限 (code=${errCode(e)}),跳过 ${bot.sid}:${target}`)
481
+ return false
482
+ }
483
+ logger.warn(`[schedule] markdown 发送失败,回退纯文本 (${bot.sid}:${target}):`, e.message || e)
484
+ }
284
485
  }
486
+ const parts = []
487
+ if (text) parts.push(text)
488
+ if (image) parts.push(h.image(image))
489
+ if (!parts.length) return false
490
+ await bot.sendMessage(target, parts)
491
+ return true
285
492
  }
286
493
 
287
- async function sendTask(name, task, onlyBotSid) {
288
- // 组装消息
289
- const parts = []
290
- if (task.text) parts.push(task.text)
291
- if (task.image) {
292
- if (parts.length) parts.push('\n')
293
- parts.push(h.image(task.image))
294
- }
295
- if (!parts.length) {
494
+ async function sendTask(name, task, opts = {}) {
495
+ const now = new Date()
496
+ const text = opts.text !== undefined ? opts.text : (task.text || '')
497
+ const markdown = opts.markdown !== undefined ? !!opts.markdown : !!task.markdown
498
+ const image = task.image
499
+ const rendered = renderText(text, {
500
+ date: todayStr(now),
501
+ time: nowHM(now),
502
+ week: '星期' + WEEK_CN[todayDay(now)],
503
+ task: name,
504
+ })
505
+ if (!rendered && !image) {
296
506
  logger.warn(`[schedule] 任务 "${name}" 没有文字也没有图片,跳过`)
297
507
  return
298
508
  }
299
509
 
300
510
  let bots = (ctx.bots || []).filter(b => b.online)
301
- if (onlyBotSid) bots = bots.filter(b => b.sid === onlyBotSid)
511
+ if (opts.onlyBotSid) bots = bots.filter(b => b.sid === opts.onlyBotSid)
302
512
  if (!bots.length) return
303
513
 
304
514
  const groups = (task.groups && task.groups.length > 0) ? task.groups : null
@@ -320,7 +530,7 @@ function apply(ctx, config) {
320
530
  continue
321
531
  }
322
532
  try {
323
- await bot.sendMessage(target, parts)
533
+ await sendOne(bot, target, rendered, { markdown, image })
324
534
  if (config.verbose) logger.info(`[schedule] 已发送任务 "${name}" 到 ${bot.sid}:${target}`)
325
535
  }
326
536
  catch (e) {
@@ -329,7 +539,7 @@ function apply(ctx, config) {
329
539
  }
330
540
  for (const target of subTargets) {
331
541
  try {
332
- await bot.sendMessage(target, parts)
542
+ await sendOne(bot, target, rendered, { markdown, image })
333
543
  if (config.verbose) logger.info(`[schedule] 已发送任务 "${name}" 到订阅群 ${bot.sid}:${target}`)
334
544
  }
335
545
  catch (e) {
@@ -346,7 +556,7 @@ function apply(ctx, config) {
346
556
  }
347
557
  for (const g of targets) {
348
558
  try {
349
- await bot.sendMessage(g.guildId, parts)
559
+ await sendOne(bot, g.guildId, rendered, { markdown, image })
350
560
  if (config.verbose) logger.info(`[schedule] 已发送任务 "${name}" 到 ${bot.sid}:${g.guildId}`)
351
561
  }
352
562
  catch (e) {
@@ -360,6 +570,29 @@ function apply(ctx, config) {
360
570
  }
361
571
  }
362
572
 
573
+ async function checkAndSend() {
574
+ const now = new Date()
575
+ const today = todayStr(now)
576
+ // 跨天重置
577
+ if (firedToday.date !== today) {
578
+ firedToday = { date: today }
579
+ }
580
+
581
+ const hm = nowHM(now)
582
+ const fires = computeFires(config.tasks, now, hm)
583
+
584
+ for (const f of fires) {
585
+ // 今天这个时间点已经发过 → 跳过
586
+ if (firedToday[f.key]) continue
587
+ firedToday[f.key] = true
588
+
589
+ logger.info(`[schedule] 触发任务 "${f.name}" @ ${hm}`)
590
+ const task = (config.tasks || {})[f.name]
591
+ if (!task) continue
592
+ await sendTask(f.name, task, { text: f.text, markdown: f.markdown })
593
+ }
594
+ }
595
+
363
596
  // 每 20 秒检查一次(保证同一分钟内能触发)
364
597
  const timer = setInterval(checkAndSend, 20000)
365
598
 
@@ -385,13 +618,29 @@ function apply(ctx, config) {
385
618
  return keys.map(k => {
386
619
  const t = tasks[k]
387
620
  const s = t.enabled === false ? '⛔' : '✅'
388
- const content = [t.text && '文字'].filter(Boolean).join('+') || '(空)'
389
621
  const staticGroups = (t.groups || []).join(',')
390
622
  const subCount = (subs.get(k) || new Map()).size
391
623
  const target = staticGroups ? staticGroups : (subCount ? `订阅${subCount}群` : '所有已知群')
392
- const times = parseTimes(t.time)
393
- const timeStr = times.length ? times.join(', ') : (t.time || '(无时间)')
394
- return `${s} ${k} @ ${timeStr} → ${content} → 群:${target}${subCount && staticGroups ? ` +订阅${subCount}群` : ''}`
624
+ const slots = Array.isArray(t.slots) && t.slots.length ? t.slots : null
625
+
626
+ let timeStr, contentStr
627
+ if (slots) {
628
+ const lines = slots.map((sl, i) => {
629
+ const ds = sl.days ? `[${sl.days}]` : '[每天]'
630
+ const ts = parseTimes(sl.time).join(',')
631
+ return ` ${i + 1}. ${ds} ${ts} → ${sl.text || '(空)'}${sl.markdown || t.markdown ? ' (md卡片)' : ''}`
632
+ })
633
+ timeStr = `排班表 ${slots.length} 行`
634
+ contentStr = lines.join('\n')
635
+ }
636
+ else {
637
+ const times = parseTimes(t.time)
638
+ timeStr = times.length ? times.join(', ') : (t.time || '(无时间)')
639
+ contentStr = [t.text && '文字', t.image && '图片'].filter(Boolean).join('+') || '(空)'
640
+ if (t.markdown) contentStr += ' (md卡片)'
641
+ }
642
+
643
+ return `${s} ${k} @ ${timeStr} → ${contentStr} → 群:${target}${subCount && staticGroups ? ` +订阅${subCount}群` : ''}`
395
644
  }).join('\n')
396
645
  }
397
646
 
@@ -426,7 +675,20 @@ function apply(ctx, config) {
426
675
  if (!testTask.groups || !testTask.groups.length) {
427
676
  testTask.groups = session.guildId ? [String(session.guildId)] : []
428
677
  }
429
- await sendTask(name, testTask, session.bot?.sid)
678
+ // 有排班表时:发"今天会命中的第一行"的内容,更贴近真实
679
+ const slots = Array.isArray(task.slots) && task.slots.length ? task.slots : null
680
+ let sendText, sendMarkdown
681
+ if (slots) {
682
+ const day = todayDay()
683
+ const hit = slots.find(sl => dayMatches(parseDays(sl.days), day))
684
+ sendText = hit ? (hit.text || '') : (task.text || '')
685
+ sendMarkdown = hit ? (hit.markdown !== undefined ? !!hit.markdown : !!task.markdown) : !!task.markdown
686
+ }
687
+ else {
688
+ sendText = task.text || ''
689
+ sendMarkdown = !!task.markdown
690
+ }
691
+ await sendTask(name, testTask, { text: sendText, markdown: sendMarkdown, onlyBotSid: session.bot?.sid })
430
692
  return `✅ 已发送任务 "${name}"`
431
693
  })
432
694
 
@@ -486,8 +748,16 @@ function apply(ctx, config) {
486
748
  taskSubs.set(key, entry)
487
749
  persistSub(name, entry)
488
750
  logger.info(`[schedule] 群 ${session.guildId} 已订阅任务 "${name}"`)
489
- const times = parseTimes(task.time)
490
- return `✅ 本群已加入任务 "${name}" 的推送列表\n时间:${times.length ? times.join(', ') : '(未设置)'}\n内容:${task.text || '(空)'}`
751
+ const slots = Array.isArray(task.slots) && task.slots.length ? task.slots : null
752
+ let timeDesc
753
+ if (slots) {
754
+ timeDesc = `排班表 ${slots.length} 个时段(schedule.list 查看详情)`
755
+ }
756
+ else {
757
+ const times = parseTimes(task.time)
758
+ timeDesc = times.length ? times.join(', ') : '(未设置)'
759
+ }
760
+ return `✅ 本群已加入任务 "${name}" 的推送列表\n时间:${timeDesc}\n内容:${task.text || '(空)'}`
491
761
  }
492
762
 
493
763
  async function unsubscribeCurrentGroup(session, taskName) {
@@ -623,7 +893,19 @@ function apply(ctx, config) {
623
893
  if (!testTask.groups || !testTask.groups.length) {
624
894
  testTask.groups = session.guildId ? [String(session.guildId)] : []
625
895
  }
626
- await sendTask(name, testTask, session.bot?.sid)
896
+ const slots = Array.isArray(task.slots) && task.slots.length ? task.slots : null
897
+ let sendText, sendMarkdown
898
+ if (slots) {
899
+ const day = todayDay()
900
+ const hit = slots.find(sl => dayMatches(parseDays(sl.days), day))
901
+ sendText = hit ? (hit.text || '') : (task.text || '')
902
+ sendMarkdown = hit ? (hit.markdown !== undefined ? !!hit.markdown : !!task.markdown) : !!task.markdown
903
+ }
904
+ else {
905
+ sendText = task.text || ''
906
+ sendMarkdown = !!task.markdown
907
+ }
908
+ await sendTask(name, testTask, { text: sendText, markdown: sendMarkdown, onlyBotSid: session.bot?.sid })
627
909
  return `✅ 已发送任务 "${name}"`
628
910
  })
629
911
 
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "koishi-plugin-ll-schedule",
3
- "version": "1.0.9",
4
- "description": "Scheduled message sender with text/image support, works with official QQ bot",
3
+ "version": "1.1.0",
4
+ "description": "Scheduled message sender with weekly slots (per-day time & content), QQ official markdown card support",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "index.js"
8
8
  ],
9
9
  "license": "MIT",
10
- "keywords": ["koishi", "koishi-plugin", "schedule", "timer", "定时"],
10
+ "keywords": ["koishi", "koishi-plugin", "schedule", "timer", "定时", "markdown", "排班"],
11
11
  "peerDependencies": {
12
12
  "koishi": "^4.18.0"
13
13
  },
14
14
  "koishi": {
15
15
  "description": {
16
- "zh": "定时发送插件。设置时间后自动发送文字/图片到指定群,兼容官方QQ机器人(官机)。",
17
- "en": "Scheduled message sender. Auto-send text/image to groups at set times, works with official QQ bot."
16
+ "zh": "定时发送插件:设置时间自动发送文字/图片/QQ官方Markdown卡片到指定群,支持排班表(按星期设置不同时间与内容),兼容官方QQ机器人(官机)。",
17
+ "en": "Scheduled message sender. Auto-send text/image/markdown card to groups at set times, with weekly slots for per-day schedule, works with official QQ bot."
18
18
  }
19
19
  }
20
20
  }