背景说明:Romio 是我在 Napcat+Koishi 框架的基础上以开发插件的形式实现的 QQ 机器人。主要目的是辅助我完成混音工作(还有我的毕设,实现一些渗透测试工具的调用),未来还计划开发一些能够提供情绪价值的插件,让她变得越来越“给力”。
需求
- 提供了一个工单管理的功能。用户可以通过指令
创建工单来创建一个工单,并填写相关参数。 - 工单创建成功后,将转发消息给管理员。
- 管理员完成工单处理后用户可以通过
结单指令来完成工单,并进行满意度评价。
功能
- 工单提交:用户通过指令提交工单,填写自定义参数。
- 排单查询:实时查看全局工单队列和预估完成时间。
- 满意度评分:工单完成后,用户可对服务进行评分。
- 管理员通知:新工单自动推送至管理员。
- 数据持久化:通过 Koishi ORM 存储工单信息。
使用
参数

如图,机器人会向用户询问三个参数 abc,例如我这里实现的是混音接单,所以要了解单主委托的这首歌有几位歌手、歌曲的时长、期望完成的日期。
adminUserIds 则是要通知的管理员的 QQ 号。
使用指令
- 提交工单:
提交工单 - 查询排单情况:
排单情况 - 结单:
结单 <工单ID>
设计思路
1. 数据库设计
插件使用 Koishi 的 ORM 扩展了一个 tickets 表,存储工单的核心信息:
id:工单唯一标识(主键)。user_id:提交工单的用户 ID。field_a、field_b、field_c:用户自定义的工单参数。status:工单状态(未完成/已完成)。satisfaction:用户满意度评分(可选字段)。
2. 用户交互
插件通过 Koishi 的命令系统实现用户交互:
- 提交工单:通过多轮对话收集用户输入,动态生成工单信息。
- 排单查询:计算未完成工单数量,提供预估完成时间。
- 结单评分:用户完成工单后,可对服务进行评分。
3. 管理员通知
通过 Koishi 的原生接口 session.bot.sendPrivateMessage(),将新工单信息发送给管理员,确保及时响应。
4. 错误处理
插件对关键操作进行了错误捕获和日志记录,例如消息发送失败时会返回友好的提示信息。
改进计划
尽管插件已经具备基本功能,但仍有改进空间:
1. AI 大模型 Function Calling 支持
2. 同一上下文中,比如同一个群聊中,出现并发任务时,如何区分两个用户的指令。
附:源码
import { Context, Schema } from 'koishi'
// 插件名称
export const name = 'ticket-system'
// 声明依赖
export const inject = ['database']
// TypeScript 用户需要进行类型合并
declare module 'koishi' {
interface Tables {
tickets: Ticket
}
}
// 定义 tickets 表的数据结构
export interface Ticket {
id: number
user_id: string
field_a: string
field_b: string
field_c: string
status: string
satisfaction?: number // 可选字段
}
// 插件配置接口
export interface Config {
name_para_a: string // 参数 A 的名字
name_para_b: string // 参数 B 的名字
name_para_c: string // 参数 C 的名字
adminUserIds: string[] // 管理员的用户 ID 列表
}
// 插件配置的 Schema
export const Config: Schema<Config> = Schema.object({
name_para_a: Schema.string()
.default('参数A')
.description('参数A的名字'),
name_para_b: Schema.string()
.default('参数B')
.description('参数B的名字'),
name_para_c: Schema.string()
.default('参数C')
.description('参数C的名字'),
adminUserIds: Schema.array(Schema.string())
.default([])
.description('管理员的用户 ID 列表,例如 ["adminUserId1", "adminUserId2"]'),
})
// 插件逻辑
export function apply(ctx: Context, config: Config) {
const { database } = ctx
// 扩展数据库表:tickets
ctx.model.extend('tickets', {
id: 'integer', // 工单 ID(主键)
user_id: 'string', // 用户 ID
field_a: 'string', // 参数 A
field_b: 'string', // 参数 B
field_c: 'string', // 参数 C
status: 'string', // 工单状态:已完成 / 未完成
satisfaction: { // 满意度评分(可为空)
type: 'integer',
nullable: true, // 设置字段为可空
},
}, {
primary: 'id', // 设置主键为字段 id
autoInc: true, // 设置主键自增
})
// 提交工单指令
ctx.command('提交工单', '提交一个新的工单')
.action(async ({ session }) => {
const questions = [
`请输入${config.name_para_a}:`,
`请输入${config.name_para_b}:`,
`请输入${config.name_para_c}:`,
]
const answers: string[] = []
for (const question of questions) {
await session.send(question) // 发送提示信息
const answer = await session.prompt() // 等待用户输入
// 如果用户输入“取消”,终止操作
if (!answer || answer.trim().toLowerCase() === '取消') {
return '操作已取消。'
}
answers.push(answer)
}
const [fieldA, fieldB, fieldC] = answers
// 创建新工单
const ticket = await database.create('tickets', {
user_id: session.userId,
field_a: fieldA,
field_b: fieldB,
field_c: fieldC,
status: '未完成',
})
// 构造通知消息
const notificationMessage = `
新工单提交:
工单 ID: ${ticket.id}
用户 ID: ${session.userId}
${config.name_para_a}: ${fieldA}
${config.name_para_b}: ${fieldB}
${config.name_para_c}: ${fieldC}
`.trim()
try {
// 遍历管理员列表,发送消息给每个管理员
for (const adminUserId of config.adminUserIds) {
await session.bot.sendPrivateMessage(adminUserId, notificationMessage)
}
} catch (error) {
console.error('消息发送失败:', error)
return `创建成功,您的工单 ID 为 ${ticket.id},但通知管理员失败,请联系管理员手动处理。`
}
return `创建成功,您的工单 ID 为 ${ticket.id}`
})
// 查询排单情况指令
ctx.command('排单情况', '查询前置工单数量和预估完成时间')
.action(async ({ session }) => {
// 查询所有未完成的工单
const tickets = await database.get('tickets', {
status: '未完成',
}, ['id'])
// 计算前置工单数量(包括当前用户的工单)
const pendingTicketsCount = tickets.length
if (pendingTicketsCount === 0) return '目前没有未完成的工单。'
// 每个工单需要两天完成
const daysLeft = pendingTicketsCount * 2
return `当前全局未完成工单数量:${pendingTicketsCount}\n预估上一单完成还需要的天数:${daysLeft} 天`
})
// 结单指令
ctx.command('结单 <ticketId>', '结束一个工单')
.action(async ({ session }, ticketId: string) => {
const parsedTicketId = parseInt(ticketId, 10)
if (isNaN(parsedTicketId)) return '无效的工单 ID。'
const ticket = await database.get('tickets', {
id: parsedTicketId,
user_id: session.userId,
status: '未完成',
})
if (!ticket) return '无效的工单 ID 或该工单已完成。'
// 更新工单状态为“已完成”
await database.set('tickets', { id: parsedTicketId }, {
status: '已完成',
})
await session.send('结单成功,请问满分 10 分,您对本单的满意度为几分?') // 发送提示信息
const satisfaction = await session.prompt() // 等待用户输入
// 如果用户输入“取消”,终止操作
if (!satisfaction || satisfaction.trim().toLowerCase() === '取消') {
return '操作已取消。'
}
if (isNaN(Number(satisfaction)) || Number(satisfaction) < 0 || Number(satisfaction) > 10) {
return '无效的评分,操作已取消。'
}
// 更新满意度评分
await database.set('tickets', { id: parsedTicketId }, {
satisfaction: parseInt(satisfaction, 10),
})
return `感谢您的反馈!您的满意度评分为 ${satisfaction} 分。`
})
}