Romio 开发:一、工单管理

Romio 开发:一、工单管理

Romio 开发,插件一,工单管理系统。

背景说明:Romio 是我在 Napcat+Koishi 框架的基础上以开发插件的形式实现的 QQ 机器人。主要目的是辅助我完成混音工作(还有我的毕设,实现一些渗透测试工具的调用),未来还计划开发一些能够提供情绪价值的插件,让她变得越来越“给力”。

需求

  • 提供了一个工单管理的功能。用户可以通过指令 创建工单 来创建一个工单,并填写相关参数。
  • 工单创建成功后,将转发消息给管理员。
  • 管理员完成工单处理后用户可以通过结单 指令来完成工单,并进行满意度评价。

功能

  1. 工单提交:用户通过指令提交工单,填写自定义参数。
  2. 排单查询:实时查看全局工单队列和预估完成时间。
  3. 满意度评分:工单完成后,用户可对服务进行评分。
  4. 管理员通知:新工单自动推送至管理员。
  5. 数据持久化:通过 Koishi ORM 存储工单信息。

使用

参数

image.png

如图,机器人会向用户询问三个参数 abc,例如我这里实现的是混音接单,所以要了解单主委托的这首歌有几位歌手、歌曲的时长、期望完成的日期。

adminUserIds 则是要通知的管理员的 QQ 号。

使用指令

  • 提交工单:提交工单
  • 查询排单情况:排单情况
  • 结单:结单 <工单ID>

设计思路

1. 数据库设计
插件使用 Koishi 的 ORM 扩展了一个 tickets 表,存储工单的核心信息:

  • id:工单唯一标识(主键)。
  • user_id:提交工单的用户 ID。
  • field_afield_bfield_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} 分。`
    })
}
Comment