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.
- package/index.js +57 -153
- 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
|
|
19
|
-
|
|
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 卡片发送(仅官机生效,失败自动回退纯文本;其他平台始终纯文本)。内容里可用标题/加粗/引用/链接/图片(图片直接写在内容里,如 \`\`),模板变量自动替换。
|
|
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
|
-
//
|
|
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
|
-
|
|
224
|
-
.
|
|
225
|
-
.
|
|
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 || '') : (
|
|
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}",可用"订阅列表"或
|
|
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
|
-
`"订阅" / "取消订阅" / 订阅列表 /
|
|
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
|
+
"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
|
+
|