koishi-plugin-ll-schedule 1.4.1 → 1.5.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.
- package/index.js +101 -3
- package/package.json +3 -1
package/index.js
CHANGED
|
@@ -23,7 +23,10 @@ exports.usage = `定时提醒:每个任务一个大文本框(schedule)。每
|
|
|
23
23
|
星期:周一/周六/每天/工作日/周末/1,3,5 时间:12:20, 18:20, 21:20(可多个)
|
|
24
24
|
内容支持 Markdown 卡片、模板变量 {date} {time} {week} {task}
|
|
25
25
|
|
|
26
|
-
群命令:订阅 任务名 / 取消订阅 任务名 / 订阅列表 / schedule.list / schedule.groups / schedule.test 任务名 / 我的ID
|
|
26
|
+
群命令:订阅 任务名 / 取消订阅 任务名 / 订阅列表 / schedule.list / schedule.groups / schedule.test 任务名 / schedule.testcur 任务名(只发当前群) / 我的ID
|
|
27
|
+
|
|
28
|
+
推送接口:POST /ll-schedule/push {"secret":"密钥","task":"任务名","place1":"xxx","delay":0}
|
|
29
|
+
|
|
27
30
|
`
|
|
28
31
|
|
|
29
32
|
// ===== 纯函数(可单测) =====
|
|
@@ -173,6 +176,25 @@ function renderText(text, ctx) {
|
|
|
173
176
|
.replace(/\{task\}/g, ctx.task)
|
|
174
177
|
}
|
|
175
178
|
|
|
179
|
+
// 接口推送默认模板(任务没配 pushTemplate 时用)
|
|
180
|
+
const DEFAULT_PUSH_TEMPLATE = [
|
|
181
|
+
'# 🎉 {task}开始啦!',
|
|
182
|
+
'',
|
|
183
|
+
'> 地点1:{place1}',
|
|
184
|
+
'> 地点2:{place2}',
|
|
185
|
+
'{time}',
|
|
186
|
+
].join('\n')
|
|
187
|
+
|
|
188
|
+
// 渲染接口推送模板:body 里除 secret/task/delay 外的字段都可作为变量 {字段名} 填入
|
|
189
|
+
// 然后走 renderText 处理内置变量 {date} {time} {week} {task}
|
|
190
|
+
function renderPushTemplate(tpl, data, builtin) {
|
|
191
|
+
let out = String(tpl || DEFAULT_PUSH_TEMPLATE)
|
|
192
|
+
for (const [k, v] of Object.entries(data || {})) {
|
|
193
|
+
out = out.split('{' + k + '}').join(v === undefined || v === null ? '' : String(v))
|
|
194
|
+
}
|
|
195
|
+
return renderText(out, builtin)
|
|
196
|
+
}
|
|
197
|
+
|
|
176
198
|
// 组装 markdown:文本 + 图片
|
|
177
199
|
function buildMd(text, image) {
|
|
178
200
|
let md = String(text || '').trim()
|
|
@@ -195,7 +217,7 @@ function buildTextFromMd(md) {
|
|
|
195
217
|
.trim()
|
|
196
218
|
}
|
|
197
219
|
|
|
198
|
-
exports._test = { parseTimes, todayDay, parseDays, dayMatches, parseScheduleBlocks, parseScheduleRows, computeFires, renderText, buildMd, buildTextFromMd }
|
|
220
|
+
exports._test = { parseTimes, todayDay, parseDays, dayMatches, parseScheduleBlocks, parseScheduleRows, computeFires, renderText, renderPushTemplate, buildMd, buildTextFromMd }
|
|
199
221
|
|
|
200
222
|
// ===== 配置 Schema =====
|
|
201
223
|
|
|
@@ -210,6 +232,10 @@ const TaskItem = Schema.object({
|
|
|
210
232
|
markdown: Schema.boolean()
|
|
211
233
|
.default(false)
|
|
212
234
|
.description('用 QQ官方 Markdown 卡片发送(仅官方QQ机器人生效,失败自动回退纯文本;其他平台始终纯文本)'),
|
|
235
|
+
pushTemplate: Schema.string()
|
|
236
|
+
.role('textarea', { rows: [3, 8] })
|
|
237
|
+
.default('')
|
|
238
|
+
.description('接口推送模板(可选):HTTP 接口推送时套用的卡片模板。变量 {place1} {place2} 等 = 推送方传入的字段;留空用默认模板。多行 markdown,例见 usage'),
|
|
213
239
|
enabled: Schema.boolean()
|
|
214
240
|
.default(true)
|
|
215
241
|
.description('是否启用'),
|
|
@@ -219,6 +245,9 @@ const Config = Schema.object({
|
|
|
219
245
|
tasks: Schema.dict(TaskItem)
|
|
220
246
|
.default({})
|
|
221
247
|
.description('📋 推送任务表格(键=任务名;每个任务 = 一个日程大文本框 schedule:星期/时间/内容)'),
|
|
248
|
+
pushSecret: Schema.string()
|
|
249
|
+
.default('')
|
|
250
|
+
.description('🔑 HTTP 推送接口密钥:外部系统调用 POST /ll-schedule/push 时须携带相同 secret(留空则接口拒绝所有推送)'),
|
|
222
251
|
subscribeAuthority: Schema.number()
|
|
223
252
|
.min(1).max(4).default(1)
|
|
224
253
|
.description('执行订阅命令所需的最低权限等级(Koishi 用户权限等级,与QQ群主/管理员身份无关;默认 1=所有人都能订阅。如需限制,先在控制台"用户管理"给指定账号提权,再调高此值)'),
|
|
@@ -461,9 +490,11 @@ function apply(ctx, config) {
|
|
|
461
490
|
|
|
462
491
|
const groups = (task.groups && task.groups.length > 0) ? task.groups : null
|
|
463
492
|
const taskSubs = subs.get(name)
|
|
493
|
+
// 测试模式(skipSubs)时忽略所有订阅群,只发 groups(即当前测试群)
|
|
494
|
+
const useSubs = !opts.skipSubs
|
|
464
495
|
|
|
465
496
|
for (const bot of bots) {
|
|
466
|
-
const subTargets = taskSubs
|
|
497
|
+
const subTargets = (useSubs && taskSubs)
|
|
467
498
|
? [...taskSubs.values()].filter(s => s.botSid === bot.sid).map(s => s.guildId)
|
|
468
499
|
: []
|
|
469
500
|
|
|
@@ -515,6 +546,57 @@ function apply(ctx, config) {
|
|
|
515
546
|
}
|
|
516
547
|
}
|
|
517
548
|
|
|
549
|
+
// ===== HTTP 推送接口:外部系统 POST 数据 → 转为卡片发给该任务订阅群 =====
|
|
550
|
+
if (ctx.router) {
|
|
551
|
+
ctx.router.post('/ll-schedule/push', async (ctx) => {
|
|
552
|
+
const respond = (status, body) => { ctx.status = status; ctx.body = body }
|
|
553
|
+
let body = null
|
|
554
|
+
try {
|
|
555
|
+
body = (typeof ctx.request.body === 'string') ? JSON.parse(ctx.request.body) : (ctx.request.body || {})
|
|
556
|
+
}
|
|
557
|
+
catch (e) {
|
|
558
|
+
return respond(400, { ok: false, error: 'invalid json body' })
|
|
559
|
+
}
|
|
560
|
+
// 密钥校验(未配置密钥则一律拒绝)
|
|
561
|
+
if (!config.pushSecret) return respond(403, { ok: false, error: 'pushSecret not configured' })
|
|
562
|
+
if (body.secret !== config.pushSecret) return respond(403, { ok: false, error: 'invalid secret' })
|
|
563
|
+
|
|
564
|
+
const taskName = String(body.task || '')
|
|
565
|
+
const task = (config.tasks || {})[taskName]
|
|
566
|
+
if (!taskName || !task) return respond(404, { ok: false, error: `task "${taskName}" not found` })
|
|
567
|
+
|
|
568
|
+
// 除 secret/task/delay 外的字段都作为模板变量(place1/place2/time/...)
|
|
569
|
+
const data = { ...body }
|
|
570
|
+
delete data.secret
|
|
571
|
+
delete data.task
|
|
572
|
+
delete data.delay
|
|
573
|
+
const now = new Date()
|
|
574
|
+
const rendered = renderPushTemplate(task.pushTemplate, data, {
|
|
575
|
+
date: todayStr(now),
|
|
576
|
+
time: nowHM(now),
|
|
577
|
+
week: '星期' + WEEK_CN[todayDay(now)],
|
|
578
|
+
task: taskName,
|
|
579
|
+
})
|
|
580
|
+
if (!rendered.trim()) return respond(400, { ok: false, error: 'empty content' })
|
|
581
|
+
|
|
582
|
+
const delay = Math.max(0, parseInt(body.delay, 10) || 0)
|
|
583
|
+
const push = async () => {
|
|
584
|
+
// 只发给订阅群(含手动 groups),不发全量已知群
|
|
585
|
+
await sendTask(taskName, task, { text: rendered, markdown: task.markdown })
|
|
586
|
+
}
|
|
587
|
+
if (delay > 0) {
|
|
588
|
+
setTimeout(push, delay * 60 * 1000)
|
|
589
|
+
logger.info(`[schedule] 接口已接收任务 "${taskName}",${delay} 分钟后推送`)
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
await push()
|
|
593
|
+
logger.info(`[schedule] 接口已推送任务 "${taskName}"`)
|
|
594
|
+
}
|
|
595
|
+
return respond(200, { ok: true, delay, task: taskName })
|
|
596
|
+
})
|
|
597
|
+
logger.info('[schedule] HTTP 推送接口已注册: POST /ll-schedule/push')
|
|
598
|
+
}
|
|
599
|
+
|
|
518
600
|
async function checkAndSend() {
|
|
519
601
|
const now = new Date()
|
|
520
602
|
const today = todayStr(now)
|
|
@@ -612,6 +694,22 @@ function apply(ctx, config) {
|
|
|
612
694
|
return `✅ 已发送任务 "${name}"`
|
|
613
695
|
})
|
|
614
696
|
|
|
697
|
+
ctx.command('schedule.testcur <name>', '只向当前群测试发送(不发订阅群)')
|
|
698
|
+
.action(async ({ session }, name) => {
|
|
699
|
+
logger.info(`[schedule] 命令被调用: schedule.testcur name=${name}`)
|
|
700
|
+
const tasks = config.tasks || {}
|
|
701
|
+
const task = tasks[name]
|
|
702
|
+
if (!task) return `未找到任务 "${name}",可用 schedule.list 查看任务名`
|
|
703
|
+
if (!session.guildId) return '❌ 只能在群里使用'
|
|
704
|
+
const testTask = { ...task, groups: [String(session.guildId)] }
|
|
705
|
+
const rows = parseScheduleRows(task)
|
|
706
|
+
const day = todayDay()
|
|
707
|
+
const hit = rows.find(r => dayMatches(parseDays(r.wd), day))
|
|
708
|
+
const sendText = hit ? (hit.text || '') : (rows.length ? (rows[0].text || '') : '')
|
|
709
|
+
await sendTask(name, testTask, { text: sendText, markdown: task.markdown, onlyBotSid: session.bot?.sid, skipSubs: true })
|
|
710
|
+
return `✅ 已向当前群测试发送任务 "${name}"(没有发给订阅群)`
|
|
711
|
+
})
|
|
712
|
+
|
|
615
713
|
// ===== 群内订阅命令 =====
|
|
616
714
|
async function isGroupAdmin(session) {
|
|
617
715
|
if (!session.guildId || !session.userId) return false
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-ll-schedule",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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": [
|
|
@@ -24,3 +24,5 @@
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
|
|
28
|
+
|