koishi-plugin-ll-schedule 1.5.1 → 1.6.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.
Files changed (2) hide show
  1. package/index.js +132 -23
  2. package/package.json +3 -1
package/index.js CHANGED
@@ -23,7 +23,13 @@ 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 任务名 / schedule.testcur 任务名(只发当前群) / 我的ID
26
+ 群命令:
27
+ 订阅 → 列出可以订阅的推送
28
+ 订阅 任务名 → 本群订阅
29
+ 取消订阅 → 列出本群已订阅的推送
30
+ 取消订阅 任务名 → 本群取消订阅
31
+ 订阅列表 / schedule.list / schedule.groups / schedule.test 任务名 / schedule.testcur 任务名(只发当前群) / 我的ID
32
+ 管理员:schedule.unsuball 任务名(取消该任务全部群订阅并通知各群;填 all 清空全部)
27
33
 
28
34
  推送接口:POST /ll-schedule/push {"secret":"密钥","task":"任务名","place1":"xxx","delay":0}
29
35
 
@@ -144,6 +150,25 @@ function parseScheduleRows(task) {
144
150
  return []
145
151
  }
146
152
 
153
+ // 汇总时间段的中文概览(用于订阅清单显示)
154
+ function summarizeRows(rows) {
155
+ if (!rows || !rows.length) return '不定时推送(有新内容时通知)'
156
+ const daily = rows.every(r => {
157
+ const w = String(r.wd || '').trim()
158
+ return !w || w === '每天' || w === '每日' || w === '天天'
159
+ })
160
+ if (daily) {
161
+ const times = []
162
+ for (const r of rows) {
163
+ for (const t of parseTimes(r.time)) if (!times.includes(t)) times.push(t)
164
+ }
165
+ return `每天 ${times.join(', ')}`
166
+ }
167
+ if (rows.length <= 3) return rows.map(r => `${r.wd || '每天'} ${r.time}`).join(' / ')
168
+ const head = rows.slice(0, 2).map(r => `${r.wd || '每天'} ${r.time}`).join(' / ')
169
+ return `${rows.length} 个时间段(${head} 等)`
170
+ }
171
+
147
172
  // 计算"现在这一刻该发什么":返回 [{ name, key, text, markdown, image }]
148
173
  function computeFires(tasks, date, hm) {
149
174
  const day = todayDay(date)
@@ -217,7 +242,7 @@ function buildTextFromMd(md) {
217
242
  .trim()
218
243
  }
219
244
 
220
- exports._test = { parseTimes, todayDay, parseDays, dayMatches, parseScheduleBlocks, parseScheduleRows, computeFires, renderText, renderPushTemplate, buildMd, buildTextFromMd }
245
+ exports._test = { parseTimes, todayDay, parseDays, dayMatches, parseScheduleBlocks, parseScheduleRows, summarizeRows, computeFires, renderText, renderPushTemplate, buildMd, buildTextFromMd }
221
246
 
222
247
  // ===== 配置 Schema =====
223
248
 
@@ -248,6 +273,9 @@ const Config = Schema.object({
248
273
  pushSecret: Schema.string()
249
274
  .default('')
250
275
  .description('🔑 HTTP 推送接口密钥:外部系统调用 POST /ll-schedule/push 时须携带相同 secret(留空则接口拒绝所有推送)'),
276
+ unsubAllNotice: Schema.string()
277
+ .default('本群已取消「{task}」的推送')
278
+ .description('管理员用 schedule.unsuball 取消全部订阅时,发到每个被取消群的提示语({task}=任务名;留空则不发送提示)'),
251
279
  subscribeAuthority: Schema.number()
252
280
  .min(1).max(4).default(1)
253
281
  .description('执行订阅命令所需的最低权限等级(Koishi 用户权限等级,与QQ群主/管理员身份无关;默认 1=所有人都能订阅。如需限制,先在控制台"用户管理"给指定账号提权,再调高此值)'),
@@ -737,24 +765,44 @@ function apply(ctx, config) {
737
765
  (config.subscribeAdminBypass ? ',或为本群群主/群管理' : '')
738
766
  }
739
767
 
740
- function resolveSubscribeTaskName(taskName) {
768
+ // 列出所有可订阅的任务(含时间概览 + 本群是否已订阅)
769
+ function listSubscribable(session) {
741
770
  const tasks = config.tasks || {}
742
- if (taskName) {
743
- if (!tasks[taskName]) return { name: null, hint: `未找到任务 "${taskName}",可用"订阅列表"或 schedule.list 查看任务名` }
744
- return { name: taskName, hint: null }
745
- }
746
771
  const names = Object.keys(tasks).filter(k => tasks[k] && tasks[k].enabled !== false)
747
- if (names.length === 1) return { name: names[0], hint: null }
748
- if (names.length === 0) return { name: null, hint: '暂无启用的任务,请先在插件设置页的 tasks 表格中添加' }
749
- return { name: null, hint: `有多个任务,请指定任务名:${names.join(' / ')}` }
772
+ if (!names.length) return '暂无任何可订阅的推送(请在插件设置页的 tasks 里添加任务)'
773
+ const key = (session.bot && session.guildId) ? `${session.bot.sid}:${session.guildId}` : null
774
+ const lines = ['📋 可以订阅的推送:', '']
775
+ names.forEach((n, i) => {
776
+ const rows = parseScheduleRows(tasks[n])
777
+ const subbed = (key && subs.get(n) && subs.get(n).has(key)) ? ' ✅本群已订阅' : ''
778
+ lines.push(`${i + 1}. ${n} —— ${summarizeRows(rows)}${subbed}`)
779
+ })
780
+ lines.push('', `发送「订阅 任务名」即可订阅,例如:订阅 ${names[0]}`)
781
+ return lines.join('\n')
782
+ }
783
+
784
+ // 列出本群已订阅的任务
785
+ function listMySubs(session) {
786
+ const key = `${session.bot.sid}:${session.guildId}`
787
+ const mine = []
788
+ for (const [tn, m] of subs) {
789
+ if (m.has(key)) mine.push(tn)
790
+ }
791
+ if (!mine.length) return '本群还没有订阅任何推送\n发送「订阅」可查看有哪些可以订阅'
792
+ return '📋 本群已订阅:\n' + mine.map((n, i) => `${i + 1}. ${n}`).join('\n') +
793
+ '\n\n发送「取消订阅 任务名」可取消'
750
794
  }
751
795
 
752
796
  async function subscribeCurrentGroup(session, taskName) {
753
- logger.info(`[schedule] 命令被调用: 订阅 taskName=${taskName || '(自动)'}`)
797
+ logger.info(`[schedule] 命令被调用: 订阅 taskName=${taskName || '(列表)'}`)
754
798
  if (!session.guildId || !session.bot) return '❌ 订阅只能在群聊里使用'
755
- const { name, hint } = resolveSubscribeTaskName(taskName)
756
- if (!name) return hint
757
- const task = config.tasks[name]
799
+ // 不带任务名 → 直接列出可以订阅什么
800
+ if (!taskName) return listSubscribable(session)
801
+ const tasks = config.tasks || {}
802
+ if (!tasks[taskName]) return `未找到任务 "${taskName}"\n发送「订阅」可查看可以订阅的列表`
803
+ if (tasks[taskName].enabled === false) return `任务 "${taskName}" 已停用,暂时不能订阅`
804
+ const name = taskName
805
+ const task = tasks[name]
758
806
  const key = `${session.bot.sid}:${session.guildId}`
759
807
  const taskSubs = getTaskSubs(name)
760
808
  if (taskSubs.has(key)) return `ℹ️ 本群已在任务 "${name}" 的推送列表里`
@@ -763,21 +811,21 @@ function apply(ctx, config) {
763
811
  persistSub(name, entry)
764
812
  logger.info(`[schedule] 群 ${session.guildId} 已订阅任务 "${name}"`)
765
813
  const rows = parseScheduleRows(task)
766
- return `✅ 本群已加入任务 "${name}" 的推送列表\n时间段:${rows.length} 个(schedule.list 查看详情)`
814
+ return `✅ 本群已订阅「${name}」\n时间:${summarizeRows(rows)}`
767
815
  }
768
816
 
769
817
  async function unsubscribeCurrentGroup(session, taskName) {
770
- logger.info(`[schedule] 命令被调用: 取消订阅 taskName=${taskName || '(自动)'}`)
818
+ logger.info(`[schedule] 命令被调用: 取消订阅 taskName=${taskName || '(列表)'}`)
771
819
  if (!session.guildId || !session.bot) return '❌ 取消订阅只能在群聊里使用'
772
- const { name, hint } = resolveSubscribeTaskName(taskName)
773
- if (!name) return hint
774
- const taskSubs = subs.get(name)
820
+ // 不带任务名 → 列出本群订阅了哪些
821
+ if (!taskName) return listMySubs(session)
822
+ const taskSubs = subs.get(taskName)
775
823
  const key = `${session.bot.sid}:${session.guildId}`
776
- if (!taskSubs || !taskSubs.has(key)) return `ℹ️ 本群不在任务 "${name}" 的推送列表里`
824
+ if (!taskSubs || !taskSubs.has(key)) return `ℹ️ 本群没有订阅「${taskName}」\n发送「取消订阅」可查看本群已订阅的推送`
777
825
  taskSubs.delete(key)
778
- removeSub(name, session.bot.sid, session.guildId)
779
- logger.info(`[schedule] 群 ${session.guildId} 已取消订阅任务 "${name}"`)
780
- return `✅ 本群已取消订阅任务 "${name}"`
826
+ removeSub(taskName, session.bot.sid, session.guildId)
827
+ logger.info(`[schedule] 群 ${session.guildId} 已取消订阅任务 "${taskName}"`)
828
+ return `✅ 本群已取消订阅「${taskName}」`
781
829
  }
782
830
 
783
831
  ctx.command('订阅 [taskName:text]', '让当前群订阅指定任务的推送')
@@ -809,6 +857,67 @@ function apply(ctx, config) {
809
857
  return '📋 订阅列表:\n' + lines.join('\n')
810
858
  })
811
859
 
860
+ // 管理员:取消指定任务的全部群订阅(并通知每个被取消的群)
861
+ // 任务名可填 all / * 表示清空所有任务的全部订阅
862
+ ctx.command('schedule.unsuball <name>', '取消某任务的全部群订阅(管理员;填 all 清空全部)', { authority: 3 })
863
+ .action(async ({ session }, name) => {
864
+ logger.info(`[schedule] 命令被调用: schedule.unsuball name=${name}`)
865
+ const tasks = config.tasks || {}
866
+ const all = name === 'all' || name === '*'
867
+ if (!all && (!name || !tasks[name])) {
868
+ return `未找到任务 "${name}",可用 schedule.list 查看任务名(也可以填 all 清空全部任务的订阅)`
869
+ }
870
+ // 收集要取消的订阅
871
+ const targets = []
872
+ if (all) {
873
+ for (const [tn, m] of subs) {
874
+ for (const e of m.values()) targets.push({ taskName: tn, botSid: e.botSid, guildId: e.guildId })
875
+ }
876
+ }
877
+ else {
878
+ const m = subs.get(name)
879
+ if (m) for (const e of m.values()) targets.push({ taskName: name, botSid: e.botSid, guildId: e.guildId })
880
+ }
881
+ if (!targets.length) return all ? 'ℹ️ 当前没有任何群订阅' : `ℹ️ 任务 "${name}" 当前没有任何群订阅`
882
+
883
+ // 逐群发送"已取消"提示
884
+ let notified = 0
885
+ if (config.unsubAllNotice) {
886
+ for (const t of targets) {
887
+ const bot = (ctx.bots || []).find(b => b.sid === t.botSid)
888
+ if (!bot || !bot.online) continue
889
+ const text = String(config.unsubAllNotice).split('{task}').join(t.taskName)
890
+ try {
891
+ await bot.sendMessage(t.guildId, [text])
892
+ notified++
893
+ }
894
+ catch (e) {
895
+ logger.warn(`[schedule] 取消提示发送失败 ${t.botSid}:${t.guildId}: ${e.message}`)
896
+ }
897
+ }
898
+ }
899
+
900
+ // 清空订阅(内存 + 数据库)
901
+ if (all) {
902
+ for (const m of subs.values()) m.clear()
903
+ if (ctx.database) {
904
+ try {
905
+ await ctx.database.remove('ll_schedule_subs', {})
906
+ }
907
+ catch (e) {
908
+ logger.warn('[schedule] 清空订阅表失败:', e)
909
+ }
910
+ }
911
+ }
912
+ else {
913
+ const m = subs.get(name)
914
+ if (m) m.clear()
915
+ for (const t of targets) await removeSub(t.taskName, t.botSid, t.guildId)
916
+ }
917
+ logger.info(`[schedule] 已取消 ${targets.length} 个群订阅(通知 ${notified} 个群)${all ? '(全部任务)' : `任务 "${name}"`}`)
918
+ return `✅ 已取消 ${targets.length} 个群订阅${all ? '(全部任务)' : `(任务「${name}」)`}\n已向 ${notified} 个群发送取消提示`
919
+ })
920
+
812
921
  ctx.command('我的ID', '查看自己的账号信息(openid/群ID/角色)')
813
922
  .action(({ session }) => {
814
923
  logger.info('[schedule] 命令被调用: 我的ID')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-ll-schedule",
3
- "version": "1.5.1",
3
+ "version": "1.6.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": [
@@ -27,3 +27,5 @@
27
27
 
28
28
 
29
29
 
30
+
31
+