UNPKG

@shangxueink/koishi-plugin-qq-markdown-button

Version:

[<ruby>**QQ机器人按钮菜单**<rp>(</rp><rt>点我查看使用说明</rt><rp>)</rp></ruby>](https://www.npmjs.com/package/@shangxueink/koishi-plugin-qq-markdown-button) 自用小插件咪~ 使用json文件设置你的机器人菜单这样就不需要一堆配置项还很烧脑了。自用插件哦~

140 lines (120 loc) 4.06 kB
import fs, { FSWatcher } from 'node:fs' import path from 'node:path' import { Context, Schema } from 'koishi' import { ensureTemplateDirs } from './files' import type { PluginLogger } from './logger' import type { MenuType, TemplateCandidate } from './types' export const TEMPLATE_SCHEMA_KEY = 'qq-markdown-button.templates' export const SEND_SEQUENCE_SCHEMA_KEY = 'qq-markdown-button.send-sequence' function listJsonTemplates(baseDir: string, type: 'json' | 'markdown'): TemplateCandidate[] { const dirPath = path.join(baseDir, type) if (!fs.existsSync(dirPath)) return [] return fs.readdirSync(dirPath, { withFileTypes: true }) .filter((entry) => entry.isFile() && path.extname(entry.name).toLowerCase() === '.json') .map((entry) => { const name = path.basename(entry.name, '.json') return { type, name, value: `${type}/${name}`, label: `${type}/${name}`, } }) } function listRawTemplates(baseDir: string): TemplateCandidate[] { const dirPath = path.join(baseDir, 'raw') if (!fs.existsSync(dirPath)) return [] const jsonNames = new Set<string>() const markdownNames = new Set<string>() for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) { if (!entry.isFile()) continue const extName = path.extname(entry.name).toLowerCase() const baseName = path.basename(entry.name, extName) if (extName === '.json') { jsonNames.add(baseName) } else if (extName === '.md') { markdownNames.add(baseName) } } const candidates: TemplateCandidate[] = [] for (const name of Array.from(jsonNames).sort((left, right) => left.localeCompare(right))) { if (!markdownNames.has(name)) continue candidates.push({ type: 'raw', name, value: `raw/${name}`, label: `raw/${name}`, }) } return candidates } function createTemplateSchema(candidates: TemplateCandidate[]): Schema<string> { if (!candidates.length) { return Schema.union([ Schema.const('').description('当前目录下没有可用模板'), ]) } return Schema.union( candidates.map((candidate) => Schema.const(candidate.value).description(candidate.label)), ) } function createSendSequenceSchema(candidates: TemplateCandidate[]): Schema<string[]> { return Schema.array(createTemplateSchema(candidates)) .role('table') .description('按顺序发送多个模板,支持排序和混合类型') .default([candidates[0]?.value ?? '']) } function scanTemplateCandidates(baseDir: string): TemplateCandidate[] { return [ ...listJsonTemplates(baseDir, 'json'), ...listJsonTemplates(baseDir, 'markdown'), ...listRawTemplates(baseDir), ] } export function setupTemplateCatalog( ctx: Context, baseDir: string, logger: PluginLogger, ): () => void { ensureTemplateDirs(baseDir) const watcherList: FSWatcher[] = [] let disposeDebounceTimer: (() => void) | undefined const refreshSchema = () => { const candidates = scanTemplateCandidates(baseDir) logger.debug('刷新模板候选项:', candidates.map((candidate) => candidate.value)) ctx.schema.set(TEMPLATE_SCHEMA_KEY, createTemplateSchema(candidates)) ctx.schema.set(SEND_SEQUENCE_SCHEMA_KEY, createSendSequenceSchema(candidates)) } const scheduleRefresh = () => { if (disposeDebounceTimer) { disposeDebounceTimer() } disposeDebounceTimer = ctx.setTimeout(() => { disposeDebounceTimer = undefined refreshSchema() }, 150) } const watchDir = (type: MenuType) => { const dirPath = path.join(baseDir, type) try { const watcher = fs.watch(dirPath, () => { scheduleRefresh() }) watcherList.push(watcher) } catch (error) { logger.error(`监听 ${dirPath} 目录时出错`, error) } } refreshSchema() watchDir('json') watchDir('markdown') watchDir('raw') return () => { for (const watcher of watcherList) { watcher.close() } if (disposeDebounceTimer) { disposeDebounceTimer() } } }