koishi-plugin-ll-schedule 1.4.2 → 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 +81 -1
  2. package/package.json +2 -1
package/index.js CHANGED
@@ -24,6 +24,9 @@ exports.usage = `定时提醒:每个任务一个大文本框(schedule)。每
24
24
  内容支持 Markdown 卡片、模板变量 {date} {time} {week} {task}
25
25
 
26
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=所有人都能订阅。如需限制,先在控制台"用户管理"给指定账号提权,再调高此值)'),
@@ -517,6 +546,57 @@ function apply(ctx, config) {
517
546
  }
518
547
  }
519
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
+
520
600
  async function checkAndSend() {
521
601
  const now = new Date()
522
602
  const today = todayStr(now)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-ll-schedule",
3
- "version": "1.4.2",
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": [
@@ -25,3 +25,4 @@
25
25
 
26
26
 
27
27
 
28
+