workspaceTeamTools.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { randomUUID } from 'node:crypto'
  2. import { z } from 'zod'
  3. import { HttpError } from './errors.js'
  4. import { agentInput, parse, type WorkspaceStore } from './workspaceStore.js'
  5. import type { WorkspaceNodes } from './workspaceGateway.js'
  6. import type { WorkspaceRuntime } from './workspaceRuntime.js'
  7. import { WORKSPACE_CONCURRENCY_LIMIT, type Work } from './workspaceScheduler.js'
  8. import type { WorkspaceAgent as Agent, WorkspaceConversation as Conversation, WorkspaceRun as Run, WorkspaceInteraction } from '../shared/workspace.js'
  9. import { requireTeamToolBridge } from './workspaceToolLease.js'
  10. import { assignmentInput, assignmentReview, assignmentUpdate, assignmentCancel, finishGoalInput, resumeGoalInput, goalCriteriaInput } from './taskCoordinator.js'
  11. const uuid = z.string().uuid()
  12. const empty = z.object({}).strict()
  13. const createAgent = agentInput.omit({ canManageTeam: true, allowHostEnvironment:true, vmExecution:true, approvalPolicy:true }).extend({ requestId: uuid,execution:z.enum(['profile','computer']).optional() }).strict()
  14. const createHelper=agentInput.pick({name:true,instructions:true}).extend({requestId:uuid,goalId:uuid,profile:z.string().min(1).max(256).optional(),title:z.string().trim().min(1).max(100),brief:z.string().trim().min(1).max(16000),acceptanceCriteria:z.array(z.string().trim().min(1).max(1000)).max(12).default([]),dependsOn:z.array(uuid).max(12).default([])}).strict()
  15. const createTeam = z.object({
  16. requestId: uuid,
  17. name: z.string().trim().min(1).max(100),
  18. memberIds: z.array(uuid).min(1).max(8),
  19. instructions: z.string().max(24_000).default(''),
  20. maxReplyRounds: z.number().int().min(1).max(12).default(6),
  21. collaborationMode: z.enum(['discussion', 'host', 'free']).default('discussion'),
  22. }).strict()
  23. const startTask = z.object({
  24. requestId: uuid,
  25. teamId: uuid,
  26. title: z.string().trim().min(1).max(100),
  27. content: z.string().trim().min(1).max(32_000),
  28. acceptanceCriteria: z.array(z.string().trim().min(1).max(1000)).max(12).default([]),
  29. }).strict()
  30. const getTask = z.object({ teamId: uuid, taskId: uuid }).strict()
  31. const editCreatedAgent = z.object({requestId:uuid,agentId:uuid,name:z.string().trim().min(1).max(100).optional(),instructions:z.string().max(24000).optional()}).strict()
  32. const archiveCreatedAgent = z.object({requestId:uuid,agentId:uuid,confirmName:z.string().min(1).max(100)}).strict()
  33. const editTeam = z.object({requestId:uuid,teamId:uuid,name:z.string().trim().min(1).max(100).optional(),instructions:z.string().max(24000).optional(),memberIds:z.array(uuid).min(2).max(8).optional(),archived:z.boolean().optional()}).strict()
  34. const definitions = [
  35. ['workspace_list_sources', '查看当前账号获准使用的基础机器人。创建成员前先查询。', empty],
  36. ['workspace_list_agents', '查看当前账号可复用的成员,优先复用已有机器人,避免重复创建。', empty],
  37. ['workspace_list_teams', '查看由你担任管理员的团队及其任务。', empty],
  38. ['workspace_create_agent', '创建持久保存的 Bot 机器人,复用已授权的基础 Profile。新成员不获得组队权限。requestId 使用新的 UUID;重试同一操作必须复用它。', createAgent],
  39. ['workspace_create_helper','为当前团队目标创建临时隔离助手并立即分派一个具体子任务。助手只参与此任务、不创建单聊、不获得组队权限;目标结束后退役。每个目标最多 8 个有效临时助手。优先用于一次性工作。',createHelper],
  40. ['workspace_create_team', '只建立持久团队,自动把你加入并设为管理员,不启动交付目标、不生成子任务或验收要求。其他成员使用真实机器人 ID,总人数最多 8 人。优先复用已有团队。requestId 使用新的 UUID,重试时复用。', createTeam],
  41. ['workspace_start_team_task', '仅当用户明确要求完成并交付具体结果时启动目标。简单问答、方案讨论、一次性咨询或仅创建群聊时不要调用。在当前群聊中可将当前话题升级为目标,在其他团队中启动独立话题;提炼少量可核对的验收要求。管理员可直接完成,不强制分工。立即返回不表示完成,requestId 重试时复用。', startTask],
  42. ['workspace_update_team_goal', '从用户的交付要求提炼验收条件,或按用户新指令调整。不要增加无关要求。expectedRevision 使用目标的 acceptanceRevision(旧记录默认为 1);冲突时先重新读取,不覆盖用户的新修改。', goalCriteriaInput.extend({ goalId: uuid })],
  43. ['workspace_get_team_task', '读取你管理的团队任务的运行状态、最近 20 条结果和待处理问题。运行中不代表目标已完成;避免频繁轮询,用户可在团队内查看进展。', getTask],
  44. ['workspace_assign_task', '仅确实需要分工时创建有负责人、依赖和验收要求的子任务,能直接完成的工作不要拆分。仅可分派给团队内其他成员,依赖必须先完成复核。requestId 为重试复用的 UUID。', assignmentInput],
  45. ['workspace_review_assignment', '管理员复核子任务:accept 接受实际结果,retry 附具体修改意见后返工,blocked 记录受阻原因。失败执行不能直接验收通过。', assignmentReview],
  46. ['workspace_update_assignment', '调整尚未运行或已失败子任务的说明、验收要求和依赖。禁止依赖循环;正在执行的任务需要先停止。', assignmentUpdate],
  47. ['workspace_cancel_assignment', '停止一个子任务,必须记录原因。执行停止尚未确认时显示正在停止,不能当作已经撤销外部副作用。其他子任务和团队不受影响。', assignmentCancel],
  48. ['workspace_finish_team_task', '根据实际结果记录目标完成、受阻或等待用户。无需为完成而创建子任务;已有有效子任务须验收通过。完成时传入当前 acceptanceRevision 并逐项提供依据;结果会自动送回原始会话。', finishGoalInput],
  49. ['workspace_resume_team_task', '仅在当前用户的新指令中恢复受阻、等待或停止的目标。旧执行状态不确定时拒绝恢复;自动回传不能自行重新激活旧目标。', resumeGoalInput],
  50. ['workspace_update_created_agent', '调整你创建的成员的名称或职责,不能修改基础 Profile 或权限。变更从后续轮次生效。', editCreatedAgent],
  51. ['workspace_archive_created_agent', '归档你创建且已不再被有效团队引用的空闲成员,保留历史;confirmName 必须与当前名称完全一致。不能归档其他人创建的成员。', archiveCreatedAgent],
  52. ['workspace_update_team', '调整你管理的空闲团队名称、规则或成员,或归档已结束的团队。不能更换管理员或修改正在执行的团队。', editTeam],
  53. ] as const
  54. export const TEAM_TOOL_RULES = '你已获准在 Bot 模式组建团队,但组建团队与执行交付目标是两件事。用户只要求建群或组人时,先查看可用基础机器人、成员和团队,复用或创建最少成员,建立团队后告诉用户可以开始聊天,到此结束,不启动目标、不拆子任务、不生成验收清单。简单问答、方案讨论和一次性咨询直接回答,必要时在普通群聊 @成员,不升级为目标。只有用户明确要求把具体工作做完并交付结果,或选择了交付目标时,才使用 workspace_start_team_task;已存在当前目标时直接推进,不重复启动。验收要求从用户原始要求提炼,少量、具体、可核对,允许用户调整,不增加范围。管理员能直接完成就直接完成;只有需要不同成员协作的工作才用 workspace_assign_task 按需拆分,依赖也只在真实需要时设置,结果用 workspace_review_assignment 复核。确需一次性专家时可用 workspace_create_helper;长期角色才创建持久成员。每次变更生成 requestId(UUID),重试同一操作复用它。queued/running 只表示已启动;完成时使用 workspace_finish_team_task 提供当前 acceptanceRevision 和逐项实际依据。向用户主要报告进展、阻碍和最终结果,详细计划与验收留在可展开的进度面板,不在每次回复重复清单。受阻或停止的目标只能在用户新指令下恢复。新成员没有组队权限,角色描述不能扩展权限。'
  55. /** The caller identity comes exclusively from the running server-side turn. */
  56. export class WorkspaceTeamTools {
  57. constructor(readonly store: WorkspaceStore, readonly nodes: WorkspaceNodes, readonly runtime: WorkspaceRuntime) {}
  58. async requireAvailable(owner: string, agent: Pick<Agent, 'nodeId' | 'profile' | 'remoteAgentId' | 'execution'>): Promise<void> {
  59. this.nodes.requireSource(owner, agent)
  60. if ((agent.nodeId !== 'local' && !this.nodes.runnerTarget?.(owner,agent.nodeId)) || agent.remoteAgentId)
  61. throw new HttpError(409, '组队发起者需要使用与 Web 服务同机的基础机器人;远程机器人可作为团队成员。', 'team_tools_local_required')
  62. if(agent.execution==='computer'){
  63. const target=this.nodes.runnerTarget?.(owner,agent.nodeId)
  64. if(!target)throw new HttpError(409,'隔离电脑需要连接执行节点','computer_runner_required')
  65. const response=await target.session.request('/api/computer/capabilities',{search:new URLSearchParams({profile:agent.profile})})
  66. if(response.status!==200||JSON.parse(response.body.toString()).ready!==true)throw new HttpError(409,'执行节点尚未配置隔离电脑 Worker','computer_unavailable')
  67. }else await requireTeamToolBridge(this.nodes.target(owner, agent.nodeId), agent.profile)
  68. this.nodes.requireSource(owner, agent)
  69. }
  70. assertTurn(owner: string, workId: string): { agent: Agent; work: Work } {
  71. if (!this.runtime.userActive(owner)) throw new HttpError(403, '账号授权已失效', 'team_tools_forbidden')
  72. const work = this.store.require<Work>(owner, 'turn', workId)
  73. const agent = this.store.require<Agent>(owner, 'agent', work.agentId)
  74. const c = this.store.require<Conversation>(owner, 'conversation', work.conversationId)
  75. const root = this.store.require<Run>(owner, 'run', work.runId)
  76. this.nodes.requireSource(owner, agent)
  77. if (agent.canManageTeam !== true || agent.archived || (agent.nodeId !== 'local' && !this.nodes.runnerTarget?.(owner,agent.nodeId)) || agent.remoteAgentId || root.assignmentId
  78. || work.teamManagementRevision !== agent.revision
  79. || (root.authorizationVersion !== undefined && root.authorizationVersion !== this.runtime.authorizationVersion(owner))
  80. || c.archived || !c.memberIds.includes(agent.id) || work.cancelRequested || root.stopRequested
  81. || !['running', 'waiting'].includes(work.status))
  82. throw new HttpError(403, '当前机器人的本轮组队授权已失效', 'team_tools_forbidden')
  83. return { agent, work }
  84. }
  85. catalog(owner: string, workId: string) {
  86. const {agent}=this.assertTurn(owner, workId)
  87. const helpers=this.nodes.runnerTarget?.(owner,agent.nodeId)?.runner?.helperRetirement===true
  88. return definitions.filter(([id])=>id!=='workspace_create_helper'||helpers).map(([id, description, schema]) => ({ id, name: id, description, inputSchema: z.toJSONSchema(schema) }))
  89. }
  90. private usable(owner: string, agent: Agent): boolean {
  91. if (agent.archived) return false
  92. try { this.nodes.requireSource(owner, agent); return true } catch { return false }
  93. }
  94. private team(owner: string, agentId: string, teamId: string): Conversation {
  95. const c = this.store.require<Conversation>(owner, 'conversation', teamId)
  96. if (c.kind !== 'group' || c.archived || c.administratorId !== agentId || !c.memberIds.includes(agentId))
  97. throw new HttpError(403, '只能操作由当前机器人担任管理员的有效团队', 'team_tools_forbidden')
  98. for (const id of c.memberIds) {
  99. const member = this.store.require<Agent>(owner, 'agent', id)
  100. this.nodes.requireSource(owner, member)
  101. if (member.archived) throw new HttpError(409, '团队成员已归档', 'agent_archived')
  102. }
  103. return c
  104. }
  105. async call(owner: string, workId: string, toolId: string, input: unknown): Promise<unknown> {
  106. const { agent, work } = this.assertTurn(owner, workId)
  107. if (toolId === 'workspace_list_sources') {
  108. parse(empty, input)
  109. const sources = await this.nodes.sources(owner)
  110. this.assertTurn(owner, workId)
  111. return { sources: sources.sources.filter(source => {
  112. try { this.nodes.requireSource(owner, source); return true } catch { return false }
  113. }), errors: sources.errors }
  114. }
  115. if (toolId === 'workspace_list_agents') {
  116. parse(empty, input)
  117. return { agents: this.store.list<Agent>(owner, 'agent').filter(a => this.usable(owner, a))
  118. .map(({ id, name, nodeId, profile, instructions }) => ({ id, name, nodeId, profile, instructions: instructions.slice(0, 2000) })) }
  119. }
  120. if (toolId === 'workspace_list_teams') {
  121. parse(empty, input)
  122. return { teams: this.store.list<Conversation>(owner, 'conversation').filter(c => {
  123. try { this.team(owner, agent.id, c.id); return true } catch { return false }
  124. }).map(c => ({ id: c.id, name: c.name, memberIds: c.memberIds, tasks: this.store.tasks(owner, c.id).slice(0, 20) })) }
  125. }
  126. if(toolId==='workspace_create_helper'){
  127. const body=parse(createHelper,input)
  128. const goal=this.runtime.tasks.requireCoordinator(owner,body.goalId,agent.id)
  129. if(!['running','review'].includes(goal.status))throw new HttpError(409,'目标已结束,不能创建临时助手','goal_terminal')
  130. const source={nodeId:agent.nodeId,profile:body.profile??agent.profile,execution:'computer' as const}
  131. await this.requireAvailable(owner,source);this.assertTurn(owner,workId)
  132. const runner=this.nodes.runnerTarget?.(owner,source.nodeId)?.runner,runnerId=runner?.id
  133. if(!runnerId||!runner.helperRetirement)throw new HttpError(409,'临时助手需要电脑执行节点','computer_runner_required')
  134. return this.store.command(owner,body.requestId,{agentId:agent.id,toolId,...body},()=>{
  135. const current=this.runtime.tasks.requireCoordinator(owner,goal.id,agent.id)
  136. if(current.activation!==goal.activation||!['running','review'].includes(current.status))throw new HttpError(409,'任务已变化,请重新读取状态','goal_changed')
  137. const all=this.store.list<Agent>(owner,'agent'),helpers=all.filter(item=>item.temporaryGoalId===goal.id)
  138. if(helpers.filter(item=>!item.archived).length>=8||helpers.length>=32)throw new HttpError(409,'临时助手已达任务上限,请复用现有助手','helper_limit')
  139. let name=body.name,index=2
  140. while(all.some(item=>item.name.toLocaleLowerCase()===name.toLocaleLowerCase()))name=`${body.name.slice(0,80)} · 临时${index++}`
  141. const helper=this.store.createAgent(owner,{...source,name,instructions:body.instructions,canManageTeam:false},{createdByAgentId:agent.id,createdFromRunId:work.runId,temporaryGoalId:goal.id,helperActivation:goal.activation??1,helperRunnerId:runnerId})
  142. const assignment=this.runtime.tasks.createAssignment(owner,agent.id,{requestId:randomUUID(),goalId:goal.id,agentId:helper.id,title:body.title,brief:body.brief,acceptanceCriteria:body.acceptanceCriteria,dependsOn:body.dependsOn})
  143. return {helper:this.store.agentSummary(helper),assignment}
  144. })
  145. }
  146. if (toolId === 'workspace_create_agent') {
  147. const { requestId, ...body } = parse(createAgent, input)
  148. body.execution??=agent.execution??'profile'
  149. if(agent.execution==='computer'&&body.execution!=='computer')throw new HttpError(403,'隔离机器人创建的成员必须使用隔离电脑','computer_scope_escalation')
  150. this.nodes.requireSource(owner, body)
  151. const sources = await this.nodes.sources(owner)
  152. this.assertTurn(owner, workId)
  153. this.nodes.requireSource(owner, body)
  154. if (!sources.sources.some(s => s.nodeId === body.nodeId && s.profile === body.profile))
  155. throw new HttpError(409, '基础机器人当前不可用', 'source_unavailable')
  156. if(body.execution==='computer'){await this.requireAvailable(owner,body);this.assertTurn(owner,workId)}
  157. const createdId = randomUUID()
  158. await this.runtime.bindNewAgent(owner, createdId)
  159. try {
  160. return await this.store.command(owner, requestId, { agentId: agent.id, toolId, body }, () => {
  161. if (this.store.list<Agent>(owner, 'agent').filter(a => !a.archived).length >= 100)
  162. throw new HttpError(409, '自动组队最多保留 100 个有效机器人,请先复用或归档已有成员。', 'team_agent_limit')
  163. const created = this.store.createAgent(owner, { ...body, canManageTeam: false }, { createdByAgentId: agent.id, createdFromRunId: work.runId }, createdId)
  164. return { agent: this.store.agentSummary(created) }
  165. })
  166. } catch (error) {
  167. await this.runtime.cleanupAgent(owner, createdId).catch(() => undefined)
  168. throw error
  169. }
  170. }
  171. if (toolId === 'workspace_create_team') {
  172. const { requestId, ...body } = parse(createTeam, input)
  173. const memberIds = [...new Set([agent.id, ...body.memberIds])]
  174. if (memberIds.length < 2 || memberIds.length > 8)
  175. throw new HttpError(400, '团队包含管理员在内需要 2 至 8 名成员', 'invalid_members')
  176. for (const id of memberIds) this.nodes.requireSource(owner, this.store.require<Agent>(owner, 'agent', id))
  177. return this.store.command(owner, requestId, { agentId: agent.id, toolId, body }, () => {
  178. const teams = this.store.list<Conversation>(owner, 'conversation').filter(c => c.kind === 'group' && !c.archived && c.administratorId === agent.id)
  179. if (teams.length >= 20) throw new HttpError(409, '自动组队最多管理 20 个有效团队,请先复用已有团队。', 'team_count_limit')
  180. if (teams.some(c => c.name.toLocaleLowerCase() === body.name.toLocaleLowerCase()))
  181. throw new HttpError(409, '已有同名团队,请先查询并复用', 'duplicate_team_name')
  182. const team = this.store.createGroup(owner, { ...body, memberIds, administratorId: agent.id, mode: body.collaborationMode === 'host' ? 'host' : 'free', autoReplyIds: [] })
  183. this.runtime.onTeamCreated(owner, team)
  184. return { team, task: this.store.tasks(owner, team.id)[0] }
  185. })
  186. }
  187. if (toolId === 'workspace_start_team_task') {
  188. const { requestId, ...body } = parse(startTask, input)
  189. const team = this.team(owner, agent.id, body.teamId)
  190. const sourceRun = this.store.require<Run>(owner, 'run', work.runId)
  191. const sourceMessage = this.store.require<{role: string}>(owner, 'message', sourceRun.messageId)
  192. if (sourceRun.triggerKind || sourceMessage.role !== 'user' || sourceRun.conversationId !== work.conversationId)
  193. throw new HttpError(409, '新目标需要用户的交付指令,自动回传不能重复启动目标', 'team_task_reentrant')
  194. return this.store.command(owner, requestId, { agentId: agent.id, toolId, body }, () => {
  195. if (team.id === work.conversationId) {
  196. const task = this.store.requireTask(owner, team.id, work.conversationTaskId!)
  197. const goal = this.runtime.tasks.begin(owner, task, agent, body.content,
  198. { conversationId: team.id, conversationTaskId: task.id, runId: work.runId, agentId: agent.id }, body.acceptanceCriteria)
  199. sourceRun.goalId = goal.id
  200. sourceRun.targetAgentId = agent.id
  201. this.store.saveRun(owner, sourceRun)
  202. return { teamId: team.id, teamName: team.name, task: { ...task, goal }, run: sourceRun, message: '当前话题已作为交付目标,请按需直接完成或分工,不要重复启动。' }
  203. }
  204. if (this.store.activeTaskCount(owner, team.id) >= WORKSPACE_CONCURRENCY_LIMIT)
  205. throw new HttpError(409, `最多同时运行 ${WORKSPACE_CONCURRENCY_LIMIT} 个任务`, 'workspace_task_concurrency_limit')
  206. const emptyTask = this.store.tasks(owner, team.id).find(t => t.messageCount === 0 && !t.activeRunId && t.titleSource === 'automatic')
  207. const task = emptyTask
  208. ? this.store.updateTask(owner, team.id, emptyTask.id, { title: body.title })
  209. : this.store.createTask(owner, team.id, { title: body.title })
  210. const goal = this.runtime.tasks.begin(owner, task, agent, body.content,
  211. { conversationId: work.conversationId, conversationTaskId: work.conversationTaskId, runId: work.runId, agentId: agent.id }, body.acceptanceCriteria)
  212. const run = this.runtime.dispatch(owner, team.id, { requestId: randomUUID(), taskId: task.id, content: `由机器人「${agent.name}」发起的团队任务:\n${body.content}` }, { agentId: agent.id, kind: 'task_review' })
  213. return { teamId: team.id, teamName: team.name, task: { ...task, goal }, run, message: '任务已启动,尚未完成。执行结果会回到原会话,由管理员复核。' }
  214. })
  215. }
  216. if (toolId === 'workspace_get_team_task') {
  217. const body = parse(getTask, input)
  218. const team = this.team(owner, agent.id, body.teamId)
  219. const task = this.store.requireTask(owner, team.id, body.taskId)
  220. const runs = this.store.list<Run>(owner, 'run').filter(r => r.conversationId === team.id && r.conversationTaskId === task.id)
  221. .sort((a, b) => b.createdAt - a.createdAt).slice(0, 4)
  222. const messages = this.store.messages(owner, team.id, Number.MAX_SAFE_INTEGER, 20, false, task.id)
  223. .map(m => ({ id: m.id, role: m.role, agentName: m.agentName, status: m.status, content: m.content.slice(0, 4000), attachments: m.attachments }))
  224. const interactions = this.store.list<WorkspaceInteraction>(owner, 'interaction')
  225. .filter(i => i.conversationId === team.id && i.conversationTaskId === task.id && !i.resolved)
  226. .map(({ agentId, kind, message, choices }) => ({ agentId, kind, message, choices }))
  227. return { teamId: team.id, teamName: team.name, task, runs, messages, interactions,
  228. assignments: this.runtime.tasks.assignments(owner, task.id) }
  229. }
  230. if (toolId === 'workspace_assign_task') return { assignment: this.runtime.tasks.createAssignment(owner, agent.id, input) }
  231. if (toolId === 'workspace_update_team_goal') {
  232. const { goalId, ...body } = parse(goalCriteriaInput.extend({ goalId: uuid }), input)
  233. return { goal: this.runtime.tasks.updateCriteria(owner, goalId, body, agent.id) }
  234. }
  235. if (toolId === 'workspace_review_assignment') return { assignment: this.runtime.tasks.reviewAssignment(owner, agent.id, input) }
  236. if (toolId === 'workspace_update_assignment') return { assignment: this.runtime.tasks.updateAssignment(owner, agent.id, input) }
  237. if (toolId === 'workspace_cancel_assignment') return { assignment: this.runtime.tasks.cancelAssignment(owner, agent.id, input) }
  238. if (toolId === 'workspace_finish_team_task') return { goal: this.runtime.tasks.finish(owner, agent.id, input, work.runId) }
  239. if (toolId === 'workspace_resume_team_task') return { goal: this.runtime.tasks.resume(owner,agent.id,input,{conversationId:work.conversationId,conversationTaskId:work.conversationTaskId,runId:work.runId,agentId:agent.id}) }
  240. if (toolId === 'workspace_update_created_agent' || toolId === 'workspace_archive_created_agent') {
  241. const body = toolId === 'workspace_update_created_agent' ? parse(editCreatedAgent,input) : parse(archiveCreatedAgent,input)
  242. const target = this.store.require<Agent>(owner,'agent',body.agentId)
  243. if (target.id === agent.id || target.createdByAgentId !== agent.id) throw new HttpError(403,'只能管理自己创建的成员','agent_creator_required')
  244. this.nodes.requireSource(owner,target)
  245. return this.store.command(owner,body.requestId,{agentId:agent.id,toolId,body},()=>{
  246. if ('confirmName' in body) {
  247. if (body.confirmName !== target.name) throw new HttpError(400,'成员名称不匹配','agent_name_mismatch')
  248. if (this.store.list<Conversation>(owner,'conversation').some(c=>c.kind==='group'&&!c.archived&&c.memberIds.includes(target.id)))
  249. throw new HttpError(409,'成员仍被有效团队使用,请先调整或归档团队','agent_in_use')
  250. if (this.store.list<Work>(owner,'turn').some(w=>w.agentId===target.id&&!['complete','failed','interrupted'].includes(w.status)))
  251. throw new HttpError(409,'成员仍在执行任务','agent_in_use')
  252. return {agent:this.store.agentSummary(this.store.updateAgent(owner,target.id,{archived:true}))}
  253. }
  254. const {requestId:_request,agentId:_target,...patch}=body
  255. return {agent:this.store.agentSummary(this.store.updateAgent(owner,target.id,patch))}
  256. })
  257. }
  258. if (toolId === 'workspace_update_team') {
  259. const {requestId,...body}=parse(editTeam,input), team=this.team(owner,agent.id,body.teamId)
  260. if (this.store.list<Run>(owner,'run').some(r=>r.conversationId===team.id&&!['complete','failed','interrupted'].includes(r.status)))
  261. throw new HttpError(409,'请在团队当前执行结束后调整','team_busy')
  262. if (body.archived && this.store.list<import('../shared/agentTasks.js').AgentGoal>(owner,'goal').some(g=>g.conversationId===team.id&&!['complete','blocked','cancelled'].includes(g.status)))
  263. throw new HttpError(409,'团队仍有未结束的目标','team_busy')
  264. if (body.memberIds && !body.memberIds.includes(agent.id)) throw new HttpError(400,'团队必须保留当前管理员','invalid_members')
  265. for (const id of body.memberIds??[]) this.nodes.requireSource(owner,this.store.require<Agent>(owner,'agent',id))
  266. return this.store.command(owner,requestId,{agentId:agent.id,toolId,body},()=>{
  267. const {teamId:_team,...patch}=body
  268. return {team:this.store.updateConversation(owner,team.id,patch)}
  269. })
  270. }
  271. throw new HttpError(404, '本轮未授权此工具', 'team_tool_not_found')
  272. }
  273. }