koishi-plugin-ll-schedule 1.3.1 → 1.4.1

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 +57 -153
  2. package/package.json +4 -1
package/index.js CHANGED
@@ -11,62 +11,19 @@ const { Schema, h } = require('koishi')
11
11
 
12
12
  exports.name = 'll-schedule'
13
13
 
14
- exports.usage = `定时提醒插件:到时间自动向订阅了该任务的群发送提醒。每个任务 = 一张"时间段列表",点 + 添加一行,每行填 星期 / 时间 / 内容。
14
+ exports.usage = `定时提醒:每个任务一个大文本框(schedule)。每段 = 一行"星期 时间",下面几行是内容。
15
15
 
16
- ## 快速上手
16
+ 例:
17
+ 周一 21:00
18
+ # 世界BOSS马上刷新啦!
19
+ > 地点1:仙幻岛
20
+ > 地点2:白青山脉
21
+ 游戏下方有遁地按钮可以直达~
17
22
 
18
- 1. 在插件设置页的 \`tasks\` 里**添加任务**(键=任务名),在任务里 **添加时间段**:每行填 星期(留空=每天)/ 时间 / 内容
19
- 2. 在任意群 @机器人 发:\`订阅 任务名\` → 该群加入此任务的订阅列表
20
- 3. 到时间,插件自动把该时间段的内容发给所有订阅群
23
+ 星期:周一/周六/每天/工作日/周末/1,3,5 时间:12:20, 18:20, 21:20(可多个)
24
+ 内容支持 Markdown 卡片、模板变量 {date} {time} {week} {task}
21
25
 
22
- ## 每个时间段三格子
23
-
24
- - **星期**:留空 = 每天;写 \`周一\` \`周六\`...、\`工作日\`(周一到周五)、\`周末\`(周六日)、或 \`1,3,5\` 数字
25
- - **时间**:\`HH:MM\`,可填多个用逗号分隔,如 \`12:20, 18:20, 21:20\`(这些时间点都发同一段内容)
26
- - **内容**:到点发送的话;支持 Markdown 语法和模板变量 \`{date}\` \`{time}\` \`{week}\` \`{task}\`
27
-
28
- ## 示例:每天定时提醒
29
-
30
- 任务「轻功」,添加 1 行: 星期=空(每天) 时间=\`12:20, 18:20, 21:20\` 内容=\`⏰ 轻功时间到了!\`
31
- → 每天这三个点都发这句。
32
-
33
- ## 示例:按天/按时段发不同内容(如世界BOSS)
34
-
35
- 任务「世界BOSS」,添加多行:
36
- \`\`\`
37
- 周一 21:00 世界BOSS刷新!仙幻岛 → 白青山脉
38
- 周二 21:00 世界BOSS刷新!仙幻岛 → 大漠
39
- 周三 21:00 世界BOSS刷新!仙幻岛 → 御龙林
40
- 周四 21:00 世界BOSS刷新!仙幻岛 → 水月平原
41
- 周五 21:00 世界BOSS刷新!白青山脉 → 大漠
42
- 周六 15:00 世界BOSS刷新!仙幻岛 → 白青山脉
43
- 周六 21:00 世界BOSS刷新!仙幻岛 → 御龙林
44
- 周日 15:00 世界BOSS刷新!仙幻岛 → 大漠
45
- 周日 21:00 世界BOSS刷新!仙幻岛 → 水月平原
46
- \`\`\`
47
- 每行对应一个时间段,内容各不相同。
48
-
49
- ## Markdown 卡片(官方QQ机器人)
50
-
51
- 任务勾选 \`markdown\` 后,会用 QQ 官方 Markdown 卡片发送(仅官机生效,失败自动回退纯文本;其他平台始终纯文本)。内容里可用标题/加粗/引用/链接/图片(图片直接写在内容里,如 \`![配图](https://...)\`),模板变量自动替换。
52
-
53
- ## 其他配置
54
-
55
- - \`groups\`:手动指定目标群(推荐留空,用群内"订阅"命令自动添加)
56
- - \`enabled\`:是否启用该任务
57
-
58
- ## 群内命令
59
-
60
- - \`订阅 [任务名]\`:当前群订阅指定任务
61
- - \`取消订阅 [任务名]\`:当前群取消订阅
62
- - \`订阅列表\`:查看所有任务的订阅群
63
- - \`schedule.list\`:查看任务列表(含每段时间段)
64
- - \`schedule.test <任务名>\`:立即测试发送
65
- - \`lls.*\`:以上命令的备用入口
66
-
67
- ## 官机(官方QQ机器人)说明
68
-
69
- 官方平台没有群号只有 openid——用群内"订阅"命令即可,插件自动记录当前群 openid。手动填 \`groups\` 时用 \`schedule.groups\` 查看已知群 openid。官机主动发群消息有平台频率限制,且机器人需在群里。
26
+ 群命令:订阅 任务名 / 取消订阅 任务名 / 订阅列表 / schedule.list / schedule.groups / schedule.test 任务名 / 我的ID
70
27
  `
71
28
 
72
29
  // ===== 纯函数(可单测) =====
@@ -128,10 +85,46 @@ function dayMatches(daysSet, day) {
128
85
  return !daysSet || daysSet.has(day)
129
86
  }
130
87
 
88
+ // 判断某 token 是否是合法的星期写法
89
+ const WEEK_TOKEN_RE = /^(?:工作日|周末|休息日|每天|每日|天天|(?:每)?(?:星期|周|礼拜)[一二三四五六日天]|[一二三四五六日天]|[1-7](?:[,,、\s]+[1-7])*|[1-7]\s*[-~]\s*[1-7])$/
90
+
91
+ // 解析"日程大文本框":每个时间段 = 一行"星期 时间"作为开头,之后(到下一个"星期 时间"行之前)的行都是该时间段的内容(多行 markdown,保留换行)
92
+ // 例:
93
+ // 周一 21:00
94
+ // # 马上到世界BOSS
95
+ // > 地点:仙幻岛 → 白青山脉
96
+ // 游戏底部有道地按钮
97
+ //
98
+ // 周六 15:00
99
+ // # 世界BOSS
100
+ // > 地点:仙幻岛 → 白青山脉
101
+ function parseScheduleBlocks(text) {
102
+ const lines = String(text || '').split(/\r?\n/)
103
+ const blocks = []
104
+ let cur = null
105
+ for (const raw of lines) {
106
+ const line = raw.replace(/\s+$/, '')
107
+ // 尝试识别块头行:星期token 开头 + 后面是时间串
108
+ const m = line.match(/^(\S+)\s+(\d{1,2}:\d{2}(?:[,,、;;\s]*\d{1,2}:\d{2})*)\s*$/)
109
+ if (m && WEEK_TOKEN_RE.test(m[1])) {
110
+ cur = { idx: blocks.length, wd: m[1], time: m[2], textLines: [] }
111
+ blocks.push(cur)
112
+ }
113
+ else if (cur) {
114
+ cur.textLines.push(line)
115
+ }
116
+ // 未识别且还没遇到任何块头 → 忽略(前面是说明/空行)
117
+ }
118
+ return blocks.map(b => ({ idx: b.idx, wd: b.wd, time: b.time, text: b.textLines.join('\n').trim() }))
119
+ }
120
+
131
121
  // 把任务的时间段解析成统一行列表:[{ idx, wd(星期), time, text }]
132
- // 兼容三种写法:新 slots 数组(优先)、旧 time/text(每天)、旧 slots(days)
122
+ // 兼容四种写法:schedule 大文本框(优先)、旧 slots 数组、旧 time/text(每天)、旧 slots(days)
133
123
  function parseScheduleRows(task) {
134
124
  if (!task) return []
125
+ if (typeof task.schedule === 'string' && task.schedule.trim()) {
126
+ return parseScheduleBlocks(task.schedule)
127
+ }
135
128
  if (Array.isArray(task.slots) && task.slots.length) {
136
129
  return task.slots
137
130
  .map((s, i) => ({
@@ -202,27 +195,15 @@ function buildTextFromMd(md) {
202
195
  .trim()
203
196
  }
204
197
 
205
- exports._test = { parseTimes, todayDay, parseDays, dayMatches, parseScheduleRows, computeFires, renderText, buildMd, buildTextFromMd }
198
+ exports._test = { parseTimes, todayDay, parseDays, dayMatches, parseScheduleBlocks, parseScheduleRows, computeFires, renderText, buildMd, buildTextFromMd }
206
199
 
207
200
  // ===== 配置 Schema =====
208
201
 
209
- const SlotItem = Schema.object({
210
- wd: Schema.string()
211
- .default('')
212
- .description('星期:留空=每天;周一/周二...;工作日(周一到五)/周末(周六日);或 1,3,5 / 1-5'),
213
- time: Schema.string()
214
- .required()
215
- .description('时间(24小时制 HH:MM),可多个用逗号分隔,如 12:20, 18:20, 21:20'),
216
- text: Schema.string()
217
- .role('textarea', { rows: [3, 10] })
218
- .default('')
219
- .description('内容:到点发送的话,支持 Markdown 和模板变量 {date} {time} {week} {task}(大多行文本框,可多行写卡片)'),
220
- })
221
-
222
202
  const TaskItem = Schema.object({
223
- slots: Schema.array(SlotItem)
224
- .default([])
225
- .description('📅 时间段列表:每行 = 星期 + 时间 + 内容,点 + 可添加多行;每行内容各自独立'),
203
+ schedule: Schema.string()
204
+ .role('textarea', { rows: [6, 16] })
205
+ .default('')
206
+ .description('📅 日程:多行文本框,直接写整张日程。每个时间段分两段:第一行写 星期 时间,下面几行写内容(可多行 markdown)。示例见 usage。'),
226
207
  groups: Schema.array(Schema.string())
227
208
  .default([])
228
209
  .description('手动指定的目标群 ID(可留空;推荐用群内"订阅"命令自动添加,无需手动填 openid)'),
@@ -237,7 +218,7 @@ const TaskItem = Schema.object({
237
218
  const Config = Schema.object({
238
219
  tasks: Schema.dict(TaskItem)
239
220
  .default({})
240
- .description('📋 推送任务表格(键=任务名;每个任务 = 时间段列表:星期/时间/内容)'),
221
+ .description('📋 推送任务表格(键=任务名;每个任务 = 一个日程大文本框 schedule:星期/时间/内容)'),
241
222
  subscribeAuthority: Schema.number()
242
223
  .min(1).max(4).default(1)
243
224
  .description('执行订阅命令所需的最低权限等级(Koishi 用户权限等级,与QQ群主/管理员身份无关;默认 1=所有人都能订阅。如需限制,先在控制台"用户管理"给指定账号提权,再调高此值)'),
@@ -622,11 +603,11 @@ function apply(ctx, config) {
622
603
  if (!testTask.groups || !testTask.groups.length) {
623
604
  testTask.groups = session.guildId ? [String(session.guildId)] : []
624
605
  }
625
- // 发"今天会命中的第一行"的内容,更贴近真实
606
+ // 优先发"今天会命中的行",否则发第一条(保证测试总能发出内容)
626
607
  const rows = parseScheduleRows(task)
627
608
  const day = todayDay()
628
609
  const hit = rows.find(r => dayMatches(parseDays(r.wd), day))
629
- const sendText = hit ? (hit.text || '') : (task.text || '')
610
+ const sendText = hit ? (hit.text || '') : (rows.length ? (rows[0].text || '') : '')
630
611
  await sendTask(name, testTask, { text: sendText, markdown: task.markdown, onlyBotSid: session.bot?.sid })
631
612
  return `✅ 已发送任务 "${name}"`
632
613
  })
@@ -661,7 +642,7 @@ function apply(ctx, config) {
661
642
  function resolveSubscribeTaskName(taskName) {
662
643
  const tasks = config.tasks || {}
663
644
  if (taskName) {
664
- if (!tasks[taskName]) return { name: null, hint: `未找到任务 "${taskName}",可用"订阅列表"或 lls.list 查看任务名` }
645
+ if (!tasks[taskName]) return { name: null, hint: `未找到任务 "${taskName}",可用"订阅列表"或 schedule.list 查看任务名` }
665
646
  return { name: taskName, hint: null }
666
647
  }
667
648
  const names = Object.keys(tasks).filter(k => tasks[k] && tasks[k].enabled !== false)
@@ -707,24 +688,12 @@ function apply(ctx, config) {
707
688
  return await subscribeCurrentGroup(session, taskName)
708
689
  })
709
690
 
710
- ctx.command('lls.sub [taskName:text]', '订阅推送(备用入口)')
711
- .action(async ({ session }, taskName) => {
712
- if (!(await checkSubscribePerm(session))) return permDeniedHint()
713
- return await subscribeCurrentGroup(session, taskName)
714
- })
715
-
716
691
  ctx.command('取消订阅 [taskName:text]', '让当前群取消订阅指定任务的推送')
717
692
  .action(async ({ session }, taskName) => {
718
693
  if (!(await checkSubscribePerm(session))) return permDeniedHint()
719
694
  return await unsubscribeCurrentGroup(session, taskName)
720
695
  })
721
696
 
722
- ctx.command('lls.unsub [taskName:text]', '取消订阅(备用入口)')
723
- .action(async ({ session }, taskName) => {
724
- if (!(await checkSubscribePerm(session))) return permDeniedHint()
725
- return await unsubscribeCurrentGroup(session, taskName)
726
- })
727
-
728
697
  ctx.command('订阅列表', '查看所有任务的订阅群')
729
698
  .action(async () => {
730
699
  logger.info('[schedule] 命令被调用: 订阅列表')
@@ -742,23 +711,6 @@ function apply(ctx, config) {
742
711
  return '📋 订阅列表:\n' + lines.join('\n')
743
712
  })
744
713
 
745
- ctx.command('lls.sublist', '查看订阅列表(备用入口)')
746
- .action(async () => {
747
- logger.info('[schedule] 命令被调用: lls.sublist')
748
- if (!subs.size) return '暂无任何群订阅。群内发"订阅 任务名"即可订阅'
749
- const lines = []
750
- for (const [taskName, taskSubs] of subs) {
751
- const entries = [...taskSubs.values()]
752
- lines.push(`【${taskName}】${entries.length} 个群`)
753
- for (const s of entries) {
754
- const bot = ctx.bots.find(b => b.sid === s.botSid)
755
- const name = bot ? await resolveGroupName(bot, s.guildId) : ''
756
- lines.push(` · ${name || '未知群名'} (${s.guildId})`)
757
- }
758
- }
759
- return '📋 订阅列表:\n' + lines.join('\n')
760
- })
761
-
762
714
  ctx.command('我的ID', '查看自己的账号信息(openid/群ID/角色)')
763
715
  .action(({ session }) => {
764
716
  logger.info('[schedule] 命令被调用: 我的ID')
@@ -771,56 +723,8 @@ function apply(ctx, config) {
771
723
  return lines.join('\n')
772
724
  })
773
725
 
774
- ctx.command('lls.me', '查看自己的账号信息(备用入口)')
775
- .action(({ session }) => {
776
- logger.info('[schedule] 命令被调用: lls.me')
777
- const role = session.author?.roles?.[0]?.id || session.event?.member?.roles?.[0]?.id
778
- const roleName = role === 'owner' ? '群主' : role === 'admin' ? '群管理' : role === 'member' ? '普通成员' : (role || '未知')
779
- const lines = ['👤 我的账号信息:', `平台:${session.platform}`, `用户ID:${session.userId}`]
780
- if (session.guildId) lines.push(`当前群ID:${session.guildId}`)
781
- if (session.guildId) lines.push(`本群角色:${roleName}`)
782
- if (session.channelId && session.channelId !== session.guildId) lines.push(`频道ID:${session.channelId}`)
783
- return lines.join('\n')
784
- })
785
-
786
- ctx.command('lls', 'll-schedule 管理(备用入口)')
787
- .action(() => {
788
- logger.info('[schedule] 命令被调用: lls')
789
- return `⚙️ 备用入口:lls.list / lls.groups / lls.test <任务名> / lls.sub / lls.unsub / lls.sublist`
790
- })
791
-
792
- ctx.command('lls.list', '查看定时任务(备用入口)')
793
- .action(() => {
794
- logger.info('[schedule] 命令被调用: lls.list')
795
- return formatTaskList()
796
- })
797
-
798
- ctx.command('lls.groups', '查看机器人已知的群(备用入口,官机显示 openid)')
799
- .action(async () => {
800
- logger.info('[schedule] 命令被调用: lls.groups')
801
- return await listKnownGroups()
802
- })
803
-
804
- ctx.command('lls.test <name>', '立即测试发送指定任务(备用入口)')
805
- .action(async ({ session }, name) => {
806
- logger.info(`[schedule] 命令被调用: lls.test name=${name}`)
807
- const tasks = config.tasks || {}
808
- const task = tasks[name]
809
- if (!task) return `未找到任务 "${name}",可用 lls.list 查看任务名`
810
- const testTask = { ...task }
811
- if (!testTask.groups || !testTask.groups.length) {
812
- testTask.groups = session.guildId ? [String(session.guildId)] : []
813
- }
814
- const rows = parseScheduleRows(task)
815
- const day = todayDay()
816
- const hit = rows.find(r => dayMatches(parseDays(r.wd), day))
817
- const sendText = hit ? (hit.text || '') : (task.text || '')
818
- await sendTask(name, testTask, { text: sendText, markdown: task.markdown, onlyBotSid: session.bot?.sid })
819
- return `✅ 已发送任务 "${name}"`
820
- })
821
-
822
726
  logger.info('[schedule] 命令已注册: schedule / schedule.list / schedule.groups / schedule.test / ' +
823
- `"订阅" / "取消订阅" / 订阅列表 / lls.*`)
727
+ `"订阅" / "取消订阅" / 订阅列表 / 我的ID`)
824
728
 
825
729
  Promise.all([loadKnownGroups(), loadSubs()]).then(() => checkAndSend())
826
730
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-ll-schedule",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
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": [
@@ -21,3 +21,6 @@
21
21
 
22
22
 
23
23
 
24
+
25
+
26
+