workspaceRoutineTools.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { randomUUID } from 'node:crypto'
  2. import { z } from 'zod'
  3. import { HttpError } from './errors.js'
  4. import { parse, type WorkspaceStore } from './workspaceStore.js'
  5. import type { WorkspaceNodes } from './workspaceGateway.js'
  6. import type { WorkspaceRuntime } from './workspaceRuntime.js'
  7. import type { Work } from './workspaceScheduler.js'
  8. import type { WorkspaceAgent as Agent, WorkspaceConversation as Conversation, WorkspaceRun as Run } from '../shared/workspace.js'
  9. import type { WorkspaceRoutine } from '../shared/workspacePanels.js'
  10. import { routineScheduleSchema, routineWriteSchema, type WorkspaceRoutines } from './workspaceRoutines.js'
  11. const uuid = z.string().uuid()
  12. const empty = z.object({}).strict()
  13. const createInput = routineWriteSchema.omit({ deviceHost: true }).extend({ requestId: uuid }).strict()
  14. const updateInput = z.object({
  15. requestId: uuid,
  16. routineId: uuid,
  17. name: z.string().trim().min(1).max(100).optional(),
  18. prompt: z.string().trim().min(1).max(24000).optional(),
  19. enabled: z.boolean().optional(),
  20. schedule: routineScheduleSchema.optional(),
  21. }).strict()
  22. const deleteInput = z.object({ requestId: uuid, routineId: uuid }).strict()
  23. const definitions = [
  24. ['workspace_list_routines', '列出当前 Bot 自己的定时任务。到点后在该 Bot 的单聊中执行,不会在群聊里自动发言。', empty],
  25. ['workspace_create_routine', '为当前 Bot 新建定时任务。用户要求定时、提醒或周期执行时使用。name 是任务名称,prompt 是到点后要执行的内容。schedule.kind 为 once、interval、daily 或 weekly。once 需要未来的 at(毫秒时间戳);interval 需要 everyMinutes;daily 需要 time(HH:mm)和 timezone;weekly 还需要 weekdays(0 为周日,6 为周六)。requestId 使用新的 UUID,重试同一操作必须复用。不要使用 Hermes cron。', createInput],
  26. ['workspace_update_routine', '修改当前 Bot 已有的定时任务,可改名称、内容、是否启用或日程。暂停时设 enabled 为 false。requestId 对同一次修改保持不变。', updateInput],
  27. ['workspace_delete_routine', '删除当前 Bot 的一个定时任务。requestId 对同一次删除保持不变。', deleteInput],
  28. ] as const
  29. export const ROUTINE_TOOL_RULES = '用户要求定时、提醒或周期执行时,使用 workspace_list_routines、workspace_create_routine、workspace_update_routine、workspace_delete_routine 管理当前 Bot 自己的定时任务。不要调用 Hermes 的 cronjob、cron 或其他 cron 工具。任务只属于当前 Bot,到点后在它的单聊中发送任务内容,不会在群聊里自动发言。新建和修改都要给出完整日程:once 用未来的 at,interval 用 everyMinutes,daily 用 timezone 和 time,weekly 再加 weekdays。requestId 使用新的 UUID,重试同一操作必须复用,不要因此创建重复任务。临时助手、已归档和远端机器人不能建立定时任务。'
  30. /** The speaking Bot is taken from the server turn. The model cannot name another agent. */
  31. export class WorkspaceRoutineTools {
  32. constructor(readonly routines: WorkspaceRoutines, readonly store: WorkspaceStore, readonly nodes: WorkspaceNodes, readonly runtime: WorkspaceRuntime) {}
  33. handles(toolId: string) {
  34. return definitions.some(([id]) => id === toolId)
  35. }
  36. private turn(owner: string, workId: string) {
  37. if (!this.runtime.userActive(owner)) throw new HttpError(403, '账号授权已失效', 'routine_tools_forbidden')
  38. const work = this.store.require<Work>(owner, 'turn', workId)
  39. const agent = this.store.require<Agent>(owner, 'agent', work.agentId)
  40. const conversation = this.store.require<Conversation>(owner, 'conversation', work.conversationId)
  41. const root = this.store.require<Run>(owner, 'run', work.runId)
  42. if (conversation.archived || !conversation.memberIds.includes(agent.id) || work.cancelRequested || root.stopRequested || !['running', 'waiting'].includes(work.status))
  43. throw new HttpError(403, '当前机器人的本轮授权已失效', 'routine_tools_forbidden')
  44. if (root.authorizationVersion !== undefined && root.authorizationVersion !== this.runtime.authorizationVersion(owner))
  45. throw new HttpError(403, '当前机器人的本轮授权已失效', 'routine_tools_forbidden')
  46. this.routines.requireSchedulable(owner, agent.id)
  47. return { agent, work }
  48. }
  49. catalog(owner: string, workId: string) {
  50. this.turn(owner, workId)
  51. return definitions.map(([id, description, schema]) => ({ id, name: id, description, inputSchema: z.toJSONSchema(schema) }))
  52. }
  53. private own(owner: string, agentId: string, routineId: string) {
  54. const routine = this.store.require<WorkspaceRoutine>(owner, 'routine', routineId)
  55. if (routine.agentId !== agentId) throw new HttpError(404, '定时任务不存在', 'not_found')
  56. return routine
  57. }
  58. private view(routine: WorkspaceRoutine) {
  59. return { id: routine.id, name: routine.name, prompt: routine.prompt, enabled: routine.enabled, schedule: routine.schedule, nextAt: routine.nextAt, lastAt: routine.lastAt }
  60. }
  61. call(owner: string, workId: string, toolId: string, input: unknown) {
  62. const { agent } = this.turn(owner, workId)
  63. if (toolId === 'workspace_list_routines') {
  64. parse(empty, input)
  65. return { delivery: '到点后在该 Bot 的单聊中执行', routines: this.store.list<WorkspaceRoutine>(owner, 'routine').filter(routine => routine.agentId === agent.id).map(routine => this.view(routine)) }
  66. }
  67. if (toolId === 'workspace_create_routine') {
  68. const { requestId, ...body } = parse(createInput, input)
  69. return this.store.command(owner, requestId, { agentId: agent.id, toolId, body }, () => {
  70. this.turn(owner, workId)
  71. return { routine: this.view(this.routines.save(owner, agent.id, body, randomUUID())) }
  72. })
  73. }
  74. if (toolId === 'workspace_update_routine') {
  75. const { requestId, routineId, ...patch } = parse(updateInput, input)
  76. if (patch.name === undefined && patch.prompt === undefined && patch.enabled === undefined && patch.schedule === undefined)
  77. throw new HttpError(400, '请提供要修改的内容', 'routine_patch_empty')
  78. this.own(owner, agent.id, routineId)
  79. return this.store.command(owner, requestId, { agentId: agent.id, toolId, routineId, patch }, () => {
  80. this.turn(owner, workId)
  81. const latest = this.own(owner, agent.id, routineId)
  82. return { routine: this.view(this.routines.save(owner, agent.id, {
  83. name: patch.name ?? latest.name,
  84. prompt: patch.prompt ?? latest.prompt,
  85. enabled: patch.enabled ?? latest.enabled,
  86. schedule: patch.schedule ?? latest.schedule,
  87. deviceHost: latest.deviceHost,
  88. }, latest.id)) }
  89. })
  90. }
  91. if (toolId === 'workspace_delete_routine') {
  92. const { requestId, routineId } = parse(deleteInput, input)
  93. this.own(owner, agent.id, routineId)
  94. return this.store.command(owner, requestId, { agentId: agent.id, toolId, routineId }, () => {
  95. this.turn(owner, workId)
  96. const latest = this.own(owner, agent.id, routineId)
  97. this.store.remove(owner, 'routine', latest.id)
  98. return { ok: true, id: latest.id }
  99. })
  100. }
  101. throw new HttpError(404, '本轮未授权此工具', 'routine_tool_not_found')
  102. }
  103. }