koishi-plugin-ll-schedule 1.7.0 → 1.7.2
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 +45 -87
- package/package.json +3 -1
package/index.js
CHANGED
|
@@ -310,6 +310,24 @@ function findNextInstance(instances, current) {
|
|
|
310
310
|
return best
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// 从某一时刻(date+hm)往后找最近的一场(用于测试:按"现在"算下一场)
|
|
314
|
+
function findNextFromNow(instances, date, hm) {
|
|
315
|
+
const day = todayDay(date)
|
|
316
|
+
const nowMin = hmToMin(hm)
|
|
317
|
+
let best = null
|
|
318
|
+
let bestKey = Infinity
|
|
319
|
+
for (const inst of instances) {
|
|
320
|
+
let offset = (inst.day - day + 7) % 7
|
|
321
|
+
if (offset === 0 && inst.startMin < nowMin) offset = 7
|
|
322
|
+
const key = offset * 1440 + inst.startMin
|
|
323
|
+
if (key < bestKey) {
|
|
324
|
+
bestKey = key
|
|
325
|
+
best = inst
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return best
|
|
329
|
+
}
|
|
330
|
+
|
|
313
331
|
// 计算"现在这一分钟该发哪些提前提醒"
|
|
314
332
|
function computeReminders(activities, date, hm) {
|
|
315
333
|
const day = todayDay(date)
|
|
@@ -360,35 +378,15 @@ function renderRemindTemplate(tpl, vars) {
|
|
|
360
378
|
return kept.join('\n').replace(/\n{3,}/g, '\n\n').trim()
|
|
361
379
|
}
|
|
362
380
|
|
|
363
|
-
exports._test = { parseTimes, todayDay, parseDays, dayMatches, parseScheduleBlocks, parseScheduleRows, summarizeRows, computeFires, renderText, renderPushTemplate, hmToMin, parseActivities, expandInstances, findNextInstance, computeReminders, renderRemindTemplate, DEFAULT_REMIND_TPL, buildMd, buildTextFromMd }
|
|
381
|
+
exports._test = { parseTimes, todayDay, parseDays, dayMatches, parseScheduleBlocks, parseScheduleRows, summarizeRows, computeFires, renderText, renderPushTemplate, hmToMin, parseActivities, expandInstances, findNextInstance, findNextFromNow, computeReminders, renderRemindTemplate, DEFAULT_REMIND_TPL, buildMd, buildTextFromMd }
|
|
364
382
|
|
|
365
383
|
// ===== 配置 Schema =====
|
|
366
384
|
|
|
367
|
-
const TaskItem = Schema.object({
|
|
368
|
-
schedule: Schema.string()
|
|
369
|
-
.role('textarea', { rows: [6, 16] })
|
|
370
|
-
.default('')
|
|
371
|
-
.description('📅 日程:多行文本框,直接写整张日程。每个时间段分两段:第一行写 星期 时间,下面几行写内容(可多行 markdown)。示例见 usage。'),
|
|
372
|
-
groups: Schema.array(Schema.string())
|
|
373
|
-
.default([])
|
|
374
|
-
.description('手动指定的目标群 ID(可留空;推荐用群内"订阅"命令自动添加,无需手动填 openid)'),
|
|
375
|
-
markdown: Schema.boolean()
|
|
376
|
-
.default(false)
|
|
377
|
-
.description('用 QQ官方 Markdown 卡片发送(仅官方QQ机器人生效,失败自动回退纯文本;其他平台始终纯文本)'),
|
|
378
|
-
pushTemplate: Schema.string()
|
|
379
|
-
.role('textarea', { rows: [3, 8] })
|
|
380
|
-
.default('')
|
|
381
|
-
.description('接口推送模板(可选):HTTP 接口推送时套用的卡片模板。变量 {place1} {place2} 等 = 推送方传入的字段;留空用默认模板。多行 markdown,例见 usage'),
|
|
382
|
-
enabled: Schema.boolean()
|
|
383
|
-
.default(true)
|
|
384
|
-
.description('是否启用'),
|
|
385
|
-
})
|
|
386
|
-
|
|
387
385
|
const Config = Schema.object({
|
|
388
386
|
activities: Schema.string()
|
|
389
387
|
.role('textarea', { rows: [8, 20] })
|
|
390
388
|
.default('')
|
|
391
|
-
.description('📅 剑灵活动表(全部活动写这里,一份表管所有提醒)。格式:[活动名] 提前:N分钟,下面每行 = 星期 开始-结束 地点:xxx
|
|
389
|
+
.description('📅 剑灵活动表(全部活动写这里,一份表管所有提醒)。格式:[活动名] 提前:N分钟,下面每行 = 星期 开始-结束 地点:xxx'),
|
|
392
390
|
remindTemplate: Schema.string()
|
|
393
391
|
.role('textarea', { rows: [6, 14] })
|
|
394
392
|
.default('')
|
|
@@ -399,12 +397,6 @@ const Config = Schema.object({
|
|
|
399
397
|
globalGroups: Schema.array(Schema.string())
|
|
400
398
|
.default([])
|
|
401
399
|
.description('额外固定推送群(可选):这些群无需订阅也会收到活动提醒(留空即可,一般用群内"订阅"命令)'),
|
|
402
|
-
tasks: Schema.dict(TaskItem)
|
|
403
|
-
.default({})
|
|
404
|
-
.description('📋 旧版推送任务表格(活动表 activities 为空时生效;每个任务 = 一个日程大文本框 schedule:星期/时间/内容)'),
|
|
405
|
-
pushSecret: Schema.string()
|
|
406
|
-
.default('')
|
|
407
|
-
.description('🔑 HTTP 推送接口密钥:外部系统调用 POST /ll-schedule/push 时须携带相同 secret(留空则接口拒绝所有推送)'),
|
|
408
400
|
unsubAllNotice: Schema.string()
|
|
409
401
|
.default('本群已取消剑灵提醒推送')
|
|
410
402
|
.description('管理员用 schedule.unsuball 取消全部订阅时,发到每个被取消群的提示语(留空则不发送提示)'),
|
|
@@ -758,58 +750,7 @@ function apply(ctx, config) {
|
|
|
758
750
|
}
|
|
759
751
|
}
|
|
760
752
|
|
|
761
|
-
// =====
|
|
762
|
-
if (ctx.router) {
|
|
763
|
-
ctx.router.post('/ll-schedule/push', async (ctx) => {
|
|
764
|
-
const respond = (status, body) => { ctx.status = status; ctx.body = body }
|
|
765
|
-
let body = null
|
|
766
|
-
try {
|
|
767
|
-
body = (typeof ctx.request.body === 'string') ? JSON.parse(ctx.request.body) : (ctx.request.body || {})
|
|
768
|
-
}
|
|
769
|
-
catch (e) {
|
|
770
|
-
return respond(400, { ok: false, error: 'invalid json body' })
|
|
771
|
-
}
|
|
772
|
-
// 密钥校验(未配置密钥则一律拒绝)
|
|
773
|
-
if (!config.pushSecret) return respond(403, { ok: false, error: 'pushSecret not configured' })
|
|
774
|
-
if (body.secret !== config.pushSecret) return respond(403, { ok: false, error: 'invalid secret' })
|
|
775
|
-
|
|
776
|
-
const taskName = String(body.task || '')
|
|
777
|
-
const task = (config.tasks || {})[taskName]
|
|
778
|
-
if (!taskName || !task) return respond(404, { ok: false, error: `task "${taskName}" not found` })
|
|
779
|
-
|
|
780
|
-
// 除 secret/task/delay 外的字段都作为模板变量(place1/place2/time/...)
|
|
781
|
-
const data = { ...body }
|
|
782
|
-
delete data.secret
|
|
783
|
-
delete data.task
|
|
784
|
-
delete data.delay
|
|
785
|
-
const now = new Date()
|
|
786
|
-
const rendered = renderPushTemplate(task.pushTemplate, data, {
|
|
787
|
-
date: todayStr(now),
|
|
788
|
-
time: nowHM(now),
|
|
789
|
-
week: '星期' + WEEK_CN[todayDay(now)],
|
|
790
|
-
task: taskName,
|
|
791
|
-
})
|
|
792
|
-
if (!rendered.trim()) return respond(400, { ok: false, error: 'empty content' })
|
|
793
|
-
|
|
794
|
-
const delay = Math.max(0, parseInt(body.delay, 10) || 0)
|
|
795
|
-
const push = async () => {
|
|
796
|
-
// 只发给订阅群(含手动 groups),不发全量已知群
|
|
797
|
-
await sendTask(taskName, task, { text: rendered, markdown: task.markdown })
|
|
798
|
-
}
|
|
799
|
-
if (delay > 0) {
|
|
800
|
-
setTimeout(push, delay * 60 * 1000)
|
|
801
|
-
logger.info(`[schedule] 接口已接收任务 "${taskName}",${delay} 分钟后推送`)
|
|
802
|
-
}
|
|
803
|
-
else {
|
|
804
|
-
await push()
|
|
805
|
-
logger.info(`[schedule] 接口已推送任务 "${taskName}"`)
|
|
806
|
-
}
|
|
807
|
-
return respond(200, { ok: true, delay, task: taskName })
|
|
808
|
-
})
|
|
809
|
-
logger.info('[schedule] HTTP 推送接口已注册: POST /ll-schedule/push')
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// ===== 活动提醒(新:活动表模式)=====
|
|
753
|
+
// ===== 活动提醒(活动表模式)=====
|
|
813
754
|
|
|
814
755
|
// 发给所有"订阅了全部提醒"的群(+ 额外固定群)
|
|
815
756
|
async function sendToSubscribers(text, { markdown, image } = {}) {
|
|
@@ -984,21 +925,38 @@ function apply(ctx, config) {
|
|
|
984
925
|
})
|
|
985
926
|
|
|
986
927
|
// 活动表模式下:把某个活动的提醒测试发到当前群;返回 null 表示当前不是活动表模式
|
|
928
|
+
// 测试一律以"现在"为基准:
|
|
929
|
+
// · 指定活动名 → 取该活动从此刻起最近的一次
|
|
930
|
+
// · 不指定 → 直接取此刻之后最近的一次提醒(等于"下一条会发的提醒")
|
|
931
|
+
// · "下一时段活动"也按此刻往后算
|
|
987
932
|
async function testActivityToGroup(session, name) {
|
|
988
933
|
const acts = parseActivities(config.activities)
|
|
989
934
|
if (!acts.length) return null
|
|
990
|
-
const act = name
|
|
991
|
-
? (acts.find(a => a.name === name) || acts.find(a => a.name.includes(name)))
|
|
992
|
-
: acts[0]
|
|
993
|
-
if (!act) return `未找到活动 "${name}",可用 schedule.list 查看活动表`
|
|
994
935
|
const instances = expandInstances(acts)
|
|
995
|
-
const inst = instances.find(i => i.activity === act.name)
|
|
996
936
|
const now = new Date()
|
|
997
|
-
const
|
|
937
|
+
const hm = nowHM(now)
|
|
938
|
+
const act = name
|
|
939
|
+
? (acts.find(a => a.name === name) || acts.find(a => a.name.includes(name)))
|
|
940
|
+
: null
|
|
941
|
+
if (name && !act) return `未找到活动 "${name}",可用 schedule.list 查看活动表`
|
|
942
|
+
|
|
943
|
+
let inst
|
|
944
|
+
if (act) {
|
|
945
|
+
const own = instances.filter(i => i.activity === act.name)
|
|
946
|
+
inst = findNextFromNow(own, now, hm) || own[0]
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
inst = findNextFromNow(instances, now, hm) || instances[0]
|
|
950
|
+
}
|
|
951
|
+
if (!inst) return '活动表里还没有可用的时段'
|
|
952
|
+
// 下一场:按"现在"往后算,并排除自己
|
|
953
|
+
const next = findNextFromNow(instances.filter(i => i !== inst), now, hm)
|
|
954
|
+
|
|
955
|
+
const text = renderRemindTemplate(config.remindTemplate, makeRemindVars(inst, next, now, hm))
|
|
998
956
|
const bot = (ctx.bots || []).find(b => b.sid === session.bot?.sid)
|
|
999
957
|
if (!bot || !session.guildId) return '❌ 只能在群里测试'
|
|
1000
958
|
await sendOne(bot, String(session.guildId), text, { markdown: !!config.remindMarkdown })
|
|
1001
|
-
return `✅ 已向本群测试发送「${
|
|
959
|
+
return `✅ 已向本群测试发送「${inst.activity}」的提醒(星期${WEEK_CN[inst.day]} ${inst.start})`
|
|
1002
960
|
}
|
|
1003
961
|
|
|
1004
962
|
ctx.command('schedule.test [name:text]', '测试发送提醒(发到当前群)')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-ll-schedule",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
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": [
|
|
@@ -32,3 +32,5 @@
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
|
|
36
|
+
|