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.
Files changed (2) hide show
  1. package/index.js +101 -3
  2. 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.4.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
+