workspaceRuntime.ts 70 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  1. import {DESKTOP_ENVIRONMENT_TOOLS,DESKTOP_ENVIRONMENT_RULES,SERVER_COMPUTER_RULES,DESKTOP_FILE_TOOL_IDS,DESKTOP_FILE_TRANSFER_RULES,type DesktopEnvironments} from './desktopEnvironments.js'
  2. import {GROK_COMPUTER_TOOLS,grokComputerRules,type GrokCloud} from './grokCloud.js'
  3. import {VM_COMPUTER_TOOLS,VM_COMPUTER_RULES,VmToolSession} from './vmComputer.js'
  4. import { randomUUID } from 'node:crypto'
  5. import { readFileSync } from 'node:fs'
  6. import { z } from 'zod'
  7. import { WorkspaceStore, parse } from './workspaceStore.js'
  8. import { WorkspaceNodes, WorkspaceGateway, type GatewayFrame } from './workspaceGateway.js'
  9. import { HttpError } from './errors.js'
  10. import { applyWorkingDirectory, configuredWorkingDirectory } from './sessionWorkingDirectory.js'
  11. import {type StoredWorkspaceFile} from './workspaceAssets.js'
  12. import { UploadStore } from './uploads.js'
  13. import type {
  14. WorkspaceAgent as Agent,
  15. WorkspaceConversation as Conversation,
  16. WorkspaceMessage as Message,
  17. WorkspaceRun,
  18. WorkspaceInteraction,
  19. WorkspaceAgentUsageSummary,
  20. } from '../shared/workspace.js'
  21. import { personaSection } from '../shared/workspace.js'
  22. import { readHostTools } from './hostToolSettings.js'
  23. import { resolveBotModelSettings, applyBotModelSettings, botModelTarget, BotModelConfirmationError } from './botModelSettings.js'
  24. function globalComputers(home: string) {
  25. try { return readHostTools(home) }
  26. catch { return { denyServerTools: false, approvalPolicy: 'ask' as const, scriptMachine: false, serverComputer: false, vm: false, cloud: false } }
  27. }
  28. function estimateTokens(text: string): number {
  29. let units = 0
  30. for (const ch of text) units += (ch.codePointAt(0) ?? 0) > 0xff ? 1 : 0.25
  31. return Math.ceil(units)
  32. }
  33. function environmentMentions(text: string, hostNames: string[]) {
  34. const cloud = /云虚拟机|云电脑|云服务器|cloud_computer|Grok/i.test(text)
  35. const vm = /虚拟环境|虚拟机|\bVM\b/i.test(text) || /computer_(desktop|shell|action|read_file|write_file)/.test(text)
  36. const named = hostNames.some(name => name.length >= 2 && name !== '本机' && text.includes(name))
  37. const physicalComputerText = text.replace(/云虚拟机|云电脑|云服务器/g, '')
  38. const script = /电脑|客户端|本机/.test(physicalComputerText) || named
  39. const server = /服务器|本机/.test(text) || (/desktop_/.test(text) && !script)
  40. const desktop = server || script || /desktop_/.test(text)
  41. return { desktop, server, script, vm, cloud }
  42. }
  43. import { WorkspaceScheduler, type Work, NO_REPLY, HOST_FALLBACK, WORKSPACE_CONCURRENCY_LIMIT, RESOURCE_WAIT_CODES } from './workspaceScheduler.js'
  44. import type { WorkspacePlugins, OpenBotPlugins } from './botPlugins/workspacePlugins.js'
  45. import { mentionedAgents } from './workspaceMentions.js'
  46. import { WorkspaceTeamTools, TEAM_TOOL_RULES } from './workspaceTeamTools.js'
  47. import { createWorkspaceToolLease, type WorkspaceToolLease } from './workspaceToolLease.js'
  48. import { WorkspaceTaskCoordinator } from './taskCoordinator.js'
  49. import { isDiscussion, discussionRounds } from './workspaceDiscussion.js'
  50. import { WorkspaceKnowledge } from './workspaceKnowledge.js'
  51. import type { OpenVikingService } from './openVikingService.js'
  52. import { WorkspaceCollaboration } from './workspaceCollaboration.js'
  53. import { WorkspaceKnowledgeTools, BOT_KNOWLEDGE_RULES } from './workspaceKnowledgeTools.js'
  54. export { mentionedAgents } from './workspaceMentions.js'
  55. type Run = WorkspaceRun
  56. export interface WorkspaceBinding {
  57. memoryVersion?: string
  58. runnerId?: string
  59. execution?:string
  60. vmExecution?:Agent["vmExecution"]
  61. hermesComputer?:boolean
  62. computerEnvironmentId?:string
  63. remoteAgentId?: string
  64. id: string
  65. nodeId: string
  66. profile: string
  67. storedId: string
  68. runtimeId: string
  69. aliases: string[]
  70. runId: string
  71. messageId: string
  72. conversationTaskId?: string
  73. taskId?: string
  74. contextSeq?: number
  75. }
  76. interface LiveTurn {
  77. gateway: WorkspaceGateway
  78. runtimeId: string
  79. runId: string
  80. agentId: string
  81. conversationTaskId?: string
  82. taskId: string
  83. done(error?: Error): void
  84. }
  85. const deviceHostId = z.union([
  86. z.literal('local'),
  87. z.string().uuid(),
  88. ])
  89. export const sendInput = z
  90. .object({
  91. requestId: z.string().uuid(),
  92. content: z.string().max(65_536).default(''),
  93. taskId: z.string().uuid().optional(),
  94. mode: z.enum(['chat', 'goal']).optional(),
  95. rounds: z.number().int().min(1).max(12).optional(),
  96. coordinatorId: z.string().uuid().optional(),
  97. projectId: z.string().min(1).max(100).nullable().optional(),
  98. mentionIds: z.array(z.string().uuid()).max(8).default([]),
  99. fileIds: z.array(z.string().uuid()).max(8).default([]),
  100. /** Computer the client is messaging from: 'local' or paired computer uuid. */
  101. deviceHost: deviceHostId.nullish(),
  102. })
  103. .strict()
  104. .refine((b) => b.content.trim() || b.fileIds.length, '请输入消息或添加附件')
  105. export class WorkspaceRuntime extends WorkspaceScheduler {
  106. readonly knowledge: WorkspaceKnowledge
  107. readonly openViking?: OpenVikingService
  108. readonly collaboration: WorkspaceCollaboration
  109. readonly knowledgeTools: WorkspaceKnowledgeTools
  110. onMemoryCandidate: (owner: string, run: Run) => void = () => {}
  111. plugins?: WorkspacePlugins
  112. assertCanSubmit:()=>void=()=>{}
  113. get idleForUpdate(): boolean {
  114. return this.live.size === 0 && this.executing.size === 0 && !this.store.db.prepare("SELECT 1 FROM workspace_entities WHERE kind IN ('run','turn') AND COALESCE(json_extract(data,'$.status'),'unknown') NOT IN ('complete','failed','interrupted') LIMIT 1").get()
  115. }
  116. desktopEnvironments?:DesktopEnvironments
  117. cloud?:GrokCloud
  118. /** Wired to the runner hub: false degrades envs VM turns to profile sessions. */
  119. computerAvailable?:(owner:string,agent:Agent)=>boolean
  120. inspector?:import('./workspaceInspector.js').WorkspaceInspector
  121. private live = new Map<string, LiveTurn>()
  122. readonly teamTools: WorkspaceTeamTools
  123. readonly tasks: WorkspaceTaskCoordinator
  124. retireHelper:(owner:string,helper:Agent)=>Promise<void>=async()=>{throw new Error('电脑清理服务未连接')}
  125. publishArtifact:(owner:string,message:Message,agent:Agent,name:string,bytes:Buffer)=>import('../shared/workspace.js').WorkspaceFile=()=>{throw new Error('产物服务未初始化')}
  126. onTeamCreated: (owner: string, team: Conversation) => void = () => {}
  127. constructor(store: WorkspaceStore, readonly nodes: WorkspaceNodes, readonly uploads: UploadStore, userActive: (owner: string) => boolean = () => true,
  128. readonly authorizationVersion: (owner: string) => number = () => 0, openViking?: OpenVikingService) {
  129. super(store, userActive)
  130. this.openViking = openViking
  131. this.teamTools = new WorkspaceTeamTools(store, nodes, this)
  132. this.tasks = new WorkspaceTaskCoordinator(store, this)
  133. this.knowledge = new WorkspaceKnowledge(store.home, store, openViking)
  134. this.collaboration = new WorkspaceCollaboration(store, this)
  135. this.knowledgeTools = new WorkspaceKnowledgeTools(this)
  136. this.onRunSettled = (owner, run) => {
  137. this.collaboration.settled(owner, run)
  138. if (run.status === 'complete' && (!run.triggerKind || run.triggerKind === 'peer')) queueMicrotask(() => { if (!this.closing) this.onMemoryCandidate(owner, run) })
  139. }
  140. }
  141. async bindNewAgent(owner: string, id: string): Promise<void> {
  142. if (!this.openViking?.enabled) return
  143. await this.openViking.ensureUser(owner, { id, temporaryGoalId: undefined })
  144. }
  145. async cleanupAgent(owner: string, id: string): Promise<void> {
  146. if (!this.openViking) return
  147. await this.openViking.removeUser(owner, { id, temporaryGoalId: undefined })
  148. }
  149. send(owner: string, conversationId: string, input: unknown): Run {
  150. return this.submit(owner, conversationId, input)
  151. }
  152. async botCapabilities(owner: string, agent: Agent, target = this.nodes.target(owner, agent.nodeId)): Promise<{ tools: boolean; memory: boolean; extraction: boolean }> {
  153. try {
  154. const response = await target.session.request('/api/plugins/yaoyao-bot-bridge/capabilities', { search: new URLSearchParams({ profile: agent.profile }) })
  155. const value = JSON.parse(response.body.toString())
  156. const tools = response.status === 200 && value.ready === true && value.native_tools === true && value.in_process === true
  157. return { tools, memory: tools && value.memory_isolation === true, extraction: value.memory_extraction === true }
  158. } catch (error) {
  159. if (error instanceof HttpError && RESOURCE_WAIT_CODES.has(error.code ?? '')) throw error
  160. return { tools: false, memory: false, extraction: false }
  161. }
  162. }
  163. async stopPeerRun(owner: string, id: string): Promise<void> { await super.stop(owner, id) }
  164. async preemptPeerWork(owner: string, agentId: string, exceptRunId: string): Promise<void> {
  165. const ids = [...new Set(this.store.list<Work>(owner, 'turn').filter(w => w.agentId === agentId && w.runId !== exceptRunId && ['running', 'waiting'].includes(w.status)).map(w => w.runId))]
  166. for (const id of ids) if (this.store.get<Run>(owner, 'run', id)?.triggerKind) await super.stop(owner, id).catch(() => {})
  167. }
  168. override async stop(owner: string, id: string): Promise<void> {
  169. const root = this.store.require<Run>(owner, 'run', id)
  170. await this.collaboration.stopFor(owner, chain => chain.rootRunId === id || chain.id === root.collaborationChainId)
  171. await super.stop(owner, id)
  172. }
  173. /** Server-owned provenance. This is never exposed as a model-supplied identity. */
  174. dispatch(owner: string, conversationId: string, input: unknown, source: {
  175. agentId: string; assignmentId?: string; targetAgentId?: string; kind: NonNullable<Run['triggerKind']>; instruction?: string; taskReference?: {conversationId:string;taskId:string}
  176. peerMessageId?: string; collaborationChainId?: string; priority?: boolean; projectId?: string; deviceHost?: string
  177. }): Run {
  178. this.nodes.requireSource(owner, this.store.require<Agent>(owner, 'agent', source.agentId))
  179. return this.submit(owner, conversationId, input, source)
  180. }
  181. private files(owner:string,ids:string[]){return ids.map(id=>{const file=this.store.get<StoredWorkspaceFile>(owner,'file',id);if(file){if(file.sourceNodeId&&file.profile)this.nodes.requireSource(owner,{nodeId:file.sourceNodeId,profile:file.profile});return file}return {...this.uploads.records([id],owner)[0]!,sender:'user' as const}})}
  182. private submit(owner: string, conversationId: string, input: unknown, source?: {
  183. agentId: string; assignmentId?: string; targetAgentId?: string; kind: NonNullable<Run['triggerKind']>; instruction?: string; taskReference?: {conversationId:string;taskId:string}
  184. peerMessageId?: string; collaborationChainId?: string; priority?: boolean; projectId?: string; deviceHost?: string
  185. }): Run {
  186. if (!this.userActive(owner)) throw new HttpError(403,'账号授权已失效','account_inactive')
  187. this.assertCanSubmit()
  188. const body = parse(sendInput, input)
  189. for (const id of this.store.require<Conversation>(owner, 'conversation', conversationId).memberIds)
  190. this.nodes.requireSource(owner, this.store.require<Agent>(owner, 'agent', id))
  191. const result = this.store.command(owner, body.requestId, { conversationId, ...body, ...(source ? { source } : {}) }, () => {
  192. const c = this.store.require<Conversation>(owner, 'conversation', conversationId)
  193. if (c.archived) throw new HttpError(409, '聊天已归档', 'conversation_archived')
  194. const projectId = source?.kind === 'peer' ? source.projectId : c.kind === 'group' ? c.projectId : body.projectId ?? undefined
  195. if (projectId) for (const id of source?.targetAgentId ? [source.targetAgentId] : c.memberIds) this.knowledge.requireProjectMember(owner, projectId, id)
  196. const conversationTask = this.store.resolveTask(owner, conversationId, body.taskId),
  197. conversationTaskId = conversationTask?.id
  198. if (conversationTaskId) {
  199. const alreadyActive = this.store.list<Run>(owner, 'run').some(run => run.conversationTaskId === conversationTaskId && !['complete', 'failed', 'interrupted'].includes(run.status))
  200. if (!alreadyActive && this.store.activeTaskCount(owner, conversationId) >= WORKSPACE_CONCURRENCY_LIMIT)
  201. throw new HttpError(409, `最多同时运行 ${WORKSPACE_CONCURRENCY_LIMIT} 个任务`, 'workspace_task_concurrency_limit')
  202. }
  203. if (body.mentionIds.some((a) => !this.store.taskMemberIds(owner,c,conversationTaskId).includes(a)))
  204. throw new HttpError(400, '只能 @ 群内成员', 'invalid_mentions')
  205. const records = this.files(owner,body.fileIds)
  206. const now = Date.now(),
  207. runId = randomUUID(),
  208. agents = this.store.taskMemberIds(owner,c,conversationTaskId).map((id) => this.store.require<Agent>(owner, 'agent', id))
  209. let goal = conversationTaskId ? this.store.get<import('../shared/agentTasks.js').AgentGoal>(owner, 'goal', conversationTaskId) : undefined
  210. if (body.mode === 'goal') {
  211. if (source || c.kind !== 'group' || !conversationTask || !body.content.trim())
  212. throw new HttpError(400, '请在群聊中说明需要交付的结果', 'goal_request_invalid')
  213. if (goal) throw new HttpError(409, '当前话题已有目标,请继续原目标或新建话题', 'goal_exists')
  214. if (body.coordinatorId) {
  215. if (!c.memberIds.includes(body.coordinatorId)) throw new HttpError(400, '负责人需要是群成员', 'invalid_members')
  216. c.administratorId = body.coordinatorId
  217. }
  218. const coordinator = this.store.require<Agent>(owner, 'agent', c.administratorId)
  219. if (coordinator.temporaryGoalId || coordinator.archived || coordinator.remoteAgentId)
  220. throw new HttpError(403, '负责人需要是本机上未归档的 Bot', 'goal_forbidden')
  221. if (body.coordinatorId) this.store.put(owner, 'conversation', c.id, c)
  222. if (this.store.list<Run>(owner, 'run').some(r => r.conversationTaskId === conversationTaskId && !['complete', 'failed', 'interrupted'].includes(r.status)))
  223. throw new HttpError(409, '请等待当前回复结束后再启动目标', 'goal_busy')
  224. goal = this.tasks.begin(owner, conversationTask, coordinator, body.content.trim(),
  225. { conversationId, conversationTaskId, runId, agentId: coordinator.id })
  226. }
  227. const mentions = [
  228. ...new Set(
  229. [...body.mentionIds, ...mentionedAgents(body.content, agents)],
  230. ),
  231. ]
  232. const message: Message = {
  233. id: randomUUID(),
  234. conversationId,
  235. conversationTaskId,
  236. seq: 0,
  237. role: source ? 'system' : 'user',
  238. peerMessageId: source?.peerMessageId,
  239. ...(source?.taskReference ? {taskReference:source.taskReference} : {}),
  240. ...(source ? { agentId: source.agentId, agentName: this.store.require<Agent>(owner, 'agent', source.agentId).name } : {}),
  241. ...((source ? source.deviceHost : body.deviceHost) ? { deviceHost: (source ? source.deviceHost : body.deviceHost)! } : {}),
  242. content: body.content.trim(),
  243. reasoning: '',
  244. runId,
  245. status: 'complete',
  246. attachments: records.map((f) => ({
  247. id: f.id,
  248. name: f.name,
  249. mimeType: f.mimeType,
  250. size: f.size,
  251. sender: f.sender,
  252. createdAt: now,
  253. })),
  254. tools: [],
  255. createdAt: now,
  256. }
  257. this.store.saveMessage(owner, message)
  258. for (const file of records) {
  259. const existing = this.store.get<Record<string, unknown>>(owner, 'file', file.id)
  260. if (existing && !existing.messageId)
  261. this.store.put(owner, 'file', file.id, {
  262. ...existing,
  263. conversationId,
  264. messageId: message.id,
  265. })
  266. }
  267. const run: Run = {
  268. id: runId,
  269. conversationId,
  270. conversationTaskId,
  271. messageId: message.id,
  272. ...((source ? source.deviceHost : body.deviceHost) ? { deviceHost: (source ? source.deviceHost : body.deviceHost)! } : {}),
  273. mentionIds: mentions,
  274. activeAgentId: c.administratorId,
  275. status: 'queued',
  276. round: 0,
  277. createdAt: now,
  278. updatedAt: now,
  279. authorizationVersion: this.authorizationVersion(owner),
  280. projectId,
  281. peerMessageId: source?.peerMessageId,
  282. collaborationChainId: source?.collaborationChainId,
  283. priority: source?.priority,
  284. ...(source ? { assignmentId: source.assignmentId, targetAgentId: source.targetAgentId, triggerKind: source.kind, internalInstruction: source.instruction } : {}),
  285. ...(source?.kind !== 'peer' && goal && ['running', 'review', 'waiting'].includes(goal.status) ? { goalId: goal.id, targetAgentId: source?.targetAgentId ?? goal.coordinatorId } : {}),
  286. }
  287. if (isDiscussion(c) && !run.goalId && !run.assignmentId && !source?.targetAgentId) run.discussion = { memberIds: mentions.length ? mentions : [...c.memberIds], rounds: discussionRounds(body.content, body.rounds) }
  288. this.admit(owner, run, c, message)
  289. this.uploads.markReferenced(body.fileIds, owner)
  290. return run
  291. })
  292. this.wake()
  293. return this.store.require<Run>(owner, 'run', result.id)
  294. }
  295. protected async performTurn(
  296. owner: string,
  297. c: Conversation,
  298. agent: Agent,
  299. run: Work,
  300. recovering = false,
  301. ): Promise<Message> {
  302. this.requireAuthorization(owner, run.runId)
  303. this.store.requireAgentTask(owner,agent,c.id,run.conversationTaskId)
  304. this.nodes.requireSource(owner, agent)
  305. if (agent.remoteAgentId) throw new HttpError(410, '远程机器人已停用', 'remote_agent_removed')
  306. const key = this.bindingKey(c.id, run.conversationTaskId, agent.id),
  307. chatExecution = 'profile',
  308. target = this.nodes.target(owner, agent.nodeId)
  309. let vmTools: VmToolSession | undefined
  310. const gateway = new WorkspaceGateway(target,{workId:run.id,publishArtifact:async(name,bytes)=>{
  311. this.requireAuthorization(owner,run.runId);this.nodes.requireSource(owner,agent)
  312. const file=this.publishArtifact(owner,resultMessage,agent,name,bytes)
  313. if(!resultMessage.attachments.some(item=>item.id===file.id))resultMessage.attachments.push(file)
  314. this.store.saveMessage(owner,resultMessage)
  315. return {...file,url:`/api/app/files/${file.id}/download`}
  316. },authorize:()=>{
  317. this.requireAuthorization(owner,run.runId);this.nodes.requireSource(owner,agent)
  318. const current=this.getWork(owner,run.id),latest=this.store.require<Agent>(owner,'agent',agent.id),conversation=this.store.require<Conversation>(owner,'conversation',c.id)
  319. if(this.closing||conversation.archived||!this.store.taskMemberIds(owner,conversation,run.conversationTaskId).includes(agent.id)||this.store.require<Run>(owner,'run',run.runId).stopRequested||current.cancelRequested||['interrupted','complete','failed'].includes(current.status)||latest.archived||latest.computerEnvironmentId!==agent.computerEnvironmentId||(latest.vmExecution??'worker')!==(agent.vmExecution??'worker')||(latest.allowHostEnvironment===true)!==(agent.allowHostEnvironment===true)||(latest.execution??'profile')!==(agent.execution??'profile')||latest.teamAuthorizationVersion!==agent.teamAuthorizationVersion)throw new HttpError(403,'本轮机器人或任务授权已结束','run_authorization_revoked')
  320. }})
  321. gateway.onTrace=entry=>this.inspector?.record(owner,c.id,{...entry,agentId:agent.id,runId:run.runId,taskId:run.conversationTaskId})
  322. const botCapabilities = await this.botCapabilities(owner, agent, target)
  323. const dispatchedVm = globalComputers(this.store.home).vm && botCapabilities.tools && !agent.remoteAgentId
  324. const root = this.store.require<Run>(owner, 'run', run.runId)
  325. if (root.peerMessageId && !recovering) {
  326. const peer = this.store.require<import('../shared/workspaceKnowledge.js').WorkspacePeerMessage>(owner, 'peer-message', root.peerMessageId)
  327. const sender = this.store.require<Agent>(owner, 'agent', peer.fromAgentId)
  328. if (sender.archived || peer.status === 'stopped') throw new HttpError(403, 'Bot 协作权限已撤销', 'collaboration_forbidden')
  329. this.nodes.requireSource(owner, sender)
  330. }
  331. const memory = botCapabilities.memory && !recovering ? await this.knowledge.context(owner, agent.id, root.projectId) : { text: '', version: 'unsupported' }
  332. const memoryStatus = botCapabilities.memory ? 'ready' : 'upgrade_required'
  333. if (agent.memoryStatus !== memoryStatus) {
  334. agent = { ...agent, memoryStatus }
  335. this.store.put(owner, 'agent', agent.id, agent)
  336. this.store.event(owner, 'agent.changed', this.store.agentSummary(agent))
  337. }
  338. let binding = this.store.get<WorkspaceBinding>(owner, 'binding', key)
  339. const memoryChanged = !recovering && !!binding?.memoryVersion && binding.memoryVersion !== memory.version
  340. const movedRunner=memoryChanged||!!this.store.get(owner,'binding-reset',key)||!!binding&&(binding.runnerId!==target.runner?.id||(binding.execution??'profile')!==chatExecution||!!binding.hermesComputer!==!!target.runner?.hermesComputer)
  341. if(movedRunner) {
  342. if(recovering)throw new HttpError(409,'执行节点已变化,不能在另一节点重放原执行','runner_target_changed')
  343. binding=undefined
  344. }
  345. let message =
  346. run.currentMessageId
  347. ? this.store.require<Message>(owner, 'message', run.currentMessageId)
  348. : undefined
  349. if (!message) {
  350. message = {
  351. id: randomUUID(),
  352. conversationId: c.id,
  353. conversationTaskId: run.conversationTaskId,
  354. seq: 0,
  355. role: 'assistant',
  356. execution:chatExecution,
  357. agentId: agent.id,
  358. agentName: agent.name,
  359. content: '',
  360. reasoning: '',
  361. status: 'streaming',
  362. runId: run.runId,
  363. taskId: run.id,
  364. visible: run.replyMode !== 'automatic' || run.requiredReply,
  365. tools: [],
  366. attachments: [],
  367. createdAt: Date.now(),
  368. }
  369. run.currentMessageId = message.id
  370. this.store.atomic(() => {
  371. this.store.saveMessage(owner, message!)
  372. this.saveWork(owner, run)
  373. })
  374. }
  375. if(!recovering&&message.status==='queued'){message.status='streaming';message.error=undefined;message.visible=run.replyMode!=='automatic'||run.requiredReply}
  376. const resultMessage = message
  377. let submitted = recovering,
  378. settled = false,
  379. completedEvidence = false,
  380. completing = false,
  381. runtimeId = '',
  382. lastFlush = 0
  383. let flushTimer: ReturnType<typeof setTimeout> | undefined
  384. let usageCompletion: Promise<void> | undefined
  385. let submittedText = ''
  386. const toolController = new AbortController()
  387. let toolLease: WorkspaceToolLease | undefined
  388. let pluginLease: OpenBotPlugins | undefined
  389. let resolveTurn!: (m: Message) => void, rejectTurn!: (e: Error) => void
  390. const completion = new Promise<Message>((resolve, reject) => {
  391. resolveTurn = resolve
  392. rejectTurn = reject
  393. })
  394. // Register rejection immediately; setup RPCs can still be awaiting a reply.
  395. void completion.catch(() => {})
  396. const finish = (error?: Error) => {
  397. if (settled) return
  398. settled = true
  399. toolController.abort()
  400. void toolLease?.dispose()
  401. void pluginLease?.dispose()
  402. clearTimeout(flushTimer)
  403. flushTimer = undefined
  404. this.live.delete(key)
  405. try {
  406. const current = this.getWork(owner, run.id)
  407. if (current.cancelRequested && current.status === 'interrupted') {
  408. resultMessage.status = 'interrupted'
  409. resultMessage.error = '已停止'
  410. this.store.atomic(() => {
  411. this.store.saveMessage(owner, resultMessage)
  412. this.saveWork(owner, current)
  413. this.resolveInteractions(owner, current.id)
  414. })
  415. if (error) rejectTurn(error)
  416. else resolveTurn(resultMessage)
  417. return
  418. }
  419. if(error&&!submitted&&current.status!=='interrupted'&&error instanceof HttpError&&RESOURCE_WAIT_CODES.has(error.code??'')){
  420. current.status='queued';current.resourceWait=true;current.error='等待执行节点或电脑资源';resultMessage.status='queued';resultMessage.visible=false;resultMessage.error=undefined
  421. }else if (error) {
  422. current.status = current.status === 'interrupted' ? 'interrupted' : submitted ? 'uncertain' : 'failed'
  423. current.error = error.message.slice(0, 1000)
  424. resultMessage.status = current.status
  425. resultMessage.error = current.error
  426. const optionalUnpublished = current.replyMode === 'automatic' && !current.requiredReply && resultMessage.visible === false
  427. resultMessage.visible = !optionalUnpublished && (submitted || current.status !== 'interrupted')
  428. if (!resultMessage.content.trim() && current.status === 'failed') resultMessage.content = `执行失败:${current.error}`
  429. } else {
  430. if (root.discussion && /^(?:\(?pass\)?\.?|\[\[YAOYAO_NO_REPLY_V1\]\])$/i.test(resultMessage.content.trim())) resultMessage.content = ''
  431. const silent = (resultMessage.content.trim() === NO_REPLY || !resultMessage.content.trim()) && current.replyMode === 'automatic' && (!current.requiredReply || current.hadInteraction) && !resultMessage.tools.length && !resultMessage.attachments.length
  432. current.silent = silent
  433. if (current.replyMode === 'automatic') {
  434. resultMessage.content = resultMessage.content.replace(/\[\[YAOYAO_[A-Z0-9_]*(?:\]\])?/g, '').trim()
  435. resultMessage.reasoning = resultMessage.reasoning.replace(/\[\[YAOYAO_[A-Z0-9_]*(?:\]\])?/g, '')
  436. }
  437. if (current.requiredReply && !silent && !resultMessage.content) resultMessage.content = HOST_FALLBACK
  438. if (!silent && !resultMessage.content && !resultMessage.attachments.length && !resultMessage.tools.length) {
  439. current.status = 'failed'; current.error = '未返回有效回复'; resultMessage.content = '执行失败:未返回有效回复'
  440. } else { current.status = 'complete'; current.error = undefined }
  441. resultMessage.visible = !silent
  442. if (silent) resultMessage.reasoning = ''
  443. resultMessage.status = current.status
  444. resultMessage.error = current.error
  445. }
  446. if (current.status !== 'uncertain') {
  447. for (const tool of resultMessage.tools) {
  448. if (!/complete|error|failed/.test(String(tool.status))) tool.status = current.status === 'complete' ? 'tool.complete' : 'tool.error'
  449. }
  450. }
  451. this.store.atomic(() => {
  452. this.store.saveMessage(owner, resultMessage)
  453. this.saveWork(owner, current)
  454. if (current.status !== 'uncertain') this.resolveInteractions(owner, current.id)
  455. })
  456. if (error) rejectTurn(error)
  457. else resolveTurn(resultMessage)
  458. } catch (failure) { rejectTurn(failure instanceof Error ? failure : new Error('无法保存执行状态')) }
  459. finally { vmTools?.close(); if (usageCompletion) void usageCompletion.finally(() => gateway.close()); else gateway.close() }
  460. }
  461. gateway.onDisconnect = () =>
  462. finish(completedEvidence ? undefined : new Error('Hermes 连接断开'))
  463. gateway.onEvent = (frame: GatewayFrame) => {
  464. if (settled || !runtimeId || frame.session_id !== runtimeId) return
  465. const currentTurn = this.getWork(owner, run.id)
  466. if (currentTurn.cancelRequested || currentTurn.status === 'interrupted') return
  467. this.inspector?.record(owner,c.id,{direction:'event',method:frame.type,data:frame.payload,agentId:agent.id,runId:run.runId,taskId:run.conversationTaskId})
  468. const p = frame.payload ?? {},
  469. type = frame.type
  470. const firstContent = type === 'message.delta' && !resultMessage.content.trim()
  471. if(type==='computer.paused'||type==='computer.resumed'){const current=this.getWork(owner,run.id);current.status=type==='computer.paused'?'waiting':'running';this.saveWork(owner,current);return}
  472. if (type === 'message.delta') {
  473. resultMessage.content += String(p.text ?? p.delta ?? '')
  474. if (resultMessage.content.trim() && !NO_REPLY.startsWith(resultMessage.content.trim()) && !resultMessage.content.trim().startsWith('[[YAOYAO_')) resultMessage.visible = true
  475. }
  476. else if (type === 'reasoning.delta')
  477. resultMessage.reasoning += String(p.text ?? p.delta ?? '')
  478. else if (type === 'message.interim') {
  479. resultMessage.content += '\n\n'
  480. } else if (type.startsWith('tool.')) {
  481. resultMessage.visible = true
  482. const id = String(p.tool_id ?? p.id ?? ''),
  483. index = resultMessage.tools.findIndex((t) => t.id === id)
  484. const value = { ...p, id, status: type }
  485. if (index >= 0) resultMessage.tools[index] = { ...resultMessage.tools[index], ...value }
  486. else resultMessage.tools.push(value)
  487. } else if (
  488. ['approval.request', 'approval.requested', 'clarify.request', 'clarify.requested'].includes(
  489. type,
  490. )
  491. ) {
  492. const upstreamId = String(p.request_id ?? p.requestId ?? p.id ?? '')
  493. const previous = this.store.list<WorkspaceInteraction>(owner, 'interaction').find(i => {
  494. const b = this.store.get<{ taskId: string; upstreamId: string }>(owner, 'interaction-binding', i.id)
  495. return b?.taskId === run.id && b.upstreamId === upstreamId
  496. })
  497. if (previous?.resolved) return
  498. const interaction: WorkspaceInteraction = previous ?? {
  499. id: randomUUID(),
  500. conversationId: c.id,
  501. conversationTaskId: run.conversationTaskId,
  502. runId: run.runId,
  503. agentId: agent.id,
  504. kind: type.startsWith('approval') ? 'approval' : 'clarification',
  505. message: String(p.question ?? p.message ?? p.prompt ?? '需要确认'),
  506. choices: Array.isArray(p.choices) ? p.choices.map(String) : [],
  507. resolved: false,
  508. }
  509. this.store.put(owner, 'interaction', interaction.id, interaction)
  510. this.store.put(owner, 'interaction-binding', interaction.id, { key, upstreamId, taskId: run.id, conversationTaskId: run.conversationTaskId })
  511. this.store.event(owner, 'interaction.changed', interaction, c.id)
  512. const current = this.getWork(owner, run.id)
  513. current.hadInteraction = true
  514. current.status = 'waiting'
  515. this.saveWork(owner, current)
  516. const policy = globalComputers(this.store.home).approvalPolicy
  517. if (interaction.kind === 'approval' && policy !== 'ask' && !previous) {
  518. // Use the same ownership, authorization and response deduplication as
  519. // a manual answer. Never grant session-wide or permanent approval.
  520. void this.respond(owner, interaction.id, policy === 'allow' ? 'once' : 'deny').catch(() => {
  521. const latest = this.store.get<WorkspaceInteraction>(owner, 'interaction', interaction.id)
  522. if (latest && !latest.resolved) this.notify(owner, c, this.store.require<Run>(owner, 'run', run.runId), undefined, latest)
  523. })
  524. } else if (!previous) this.notify(owner, c, this.store.require<Run>(owner, 'run', run.runId), undefined, interaction)
  525. } else if(type==='interaction.cancelled'){
  526. const upstreamId=String(p.request_id??'')
  527. for(const interaction of this.store.list<WorkspaceInteraction>(owner,'interaction')){
  528. const binding=this.store.get<{taskId:string;upstreamId:string}>(owner,'interaction-binding',interaction.id)
  529. if(!interaction.resolved&&binding?.taskId===run.id&&binding.upstreamId===upstreamId){
  530. interaction.resolved=true;this.store.put(owner,'interaction',interaction.id,interaction)
  531. this.store.event(owner,'interaction.changed',interaction,c.id)
  532. }
  533. }
  534. const current=this.getWork(owner,run.id)
  535. if(current.status==='waiting'&&!this.store.list<WorkspaceInteraction>(owner,'interaction').some(i=>i.runId===run.runId&&i.agentId===agent.id&&!i.resolved)){
  536. current.status='running';this.saveWork(owner,current)
  537. }
  538. } else if (['message.complete', 'run.completed'].includes(type)) {
  539. if (typeof p.text === 'string') resultMessage.content = p.text
  540. if (typeof p.reasoning === 'string') resultMessage.reasoning = p.reasoning
  541. if (p.status === 'interrupted') {
  542. const current = this.getWork(owner, run.id)
  543. current.status = 'interrupted'
  544. this.saveWork(owner, current)
  545. finish(new Error('已停止'))
  546. } else if (p.error || p.status === 'failed') {
  547. submitted = false
  548. finish(new HttpError(502, String(p.error ?? '运行失败'), 'run_failed'))
  549. } else if (!completing) {
  550. completedEvidence = true
  551. completing = true
  552. const outputText = [resultMessage.content, resultMessage.reasoning, resultMessage.tools.length ? JSON.stringify(resultMessage.tools) : ''].filter(Boolean).join('\n')
  553. this.recordTurnTokens(owner, agent.id, estimateTokens(submittedText), estimateTokens(outputText))
  554. finish()
  555. }
  556. return
  557. } else if (['error', 'message.error', 'run.failed'].includes(type)) {
  558. submitted = false
  559. finish(new HttpError(502, String(p.message ?? p.error ?? '运行失败'), 'run_failed'))
  560. return
  561. } else if (['session.usage', 'usage.update', 'context.update'].includes(type)) {
  562. if (type !== 'context.update') this.recordAgentUsage(owner, key, agent.id, p)
  563. const context = { ...p, conversationTaskId: run.conversationTaskId }
  564. this.store.put(owner, 'context', run.conversationTaskId ?? c.id, context)
  565. this.store.event(owner, 'context.changed', context, c.id)
  566. } else if (type === 'session.info' && binding) {
  567. const storedId = p.stored_session_id ?? p.session_key
  568. if (typeof storedId === 'string' && storedId && storedId !== binding.storedId) {
  569. binding.aliases = [...new Set([...binding.aliases, binding.storedId, storedId])]
  570. binding.storedId = storedId
  571. this.store.put(owner, 'binding', key, binding)
  572. }
  573. }
  574. const flush = () => {
  575. clearTimeout(flushTimer)
  576. flushTimer = undefined
  577. if (settled || this.closing) return
  578. try {
  579. this.store.saveMessage(owner, resultMessage)
  580. lastFlush = Date.now()
  581. } catch (error) {
  582. finish(error instanceof Error ? error : new Error('无法保存流式消息'))
  583. }
  584. }
  585. const remaining = (this.store.messagePatchesEnabled ? 33 : 100) - (Date.now() - lastFlush)
  586. if (firstContent || !['message.delta', 'reasoning.delta', 'message.interim'].includes(type) || remaining <= 0) flush()
  587. // A stream may pause after any chunk. Publish the latest text even when
  588. // no subsequent event arrives to trigger the next leading-edge flush.
  589. else if (flushTimer === undefined) flushTimer = setTimeout(flush, remaining)
  590. }
  591. try {
  592. await gateway.connect()
  593. this.requireAuthorization(owner, run.runId)
  594. this.nodes.requireSource(owner, agent)
  595. // Paired Web nodes enforce their own profile config at the destination.
  596. const cwd = recovering || target.pairedToken ? undefined
  597. : await configuredWorkingDirectory((path, options) => target.session.request(path, options), agent.profile)
  598. this.nodes.requireSource(owner, agent)
  599. // Hermes rejects disabled_toolsets on session.create. Host-tool denial
  600. // stays a YaoYao policy; it is not a session-create parameter.
  601. const createSession = () => gateway.rpc('session.create', {
  602. profile: agent.profile,
  603. title: `Yaoyao ${c.id}${run.conversationTaskId ? ` / ${run.conversationTaskId}` : ''}`,
  604. source: 'yaoyao_workspace',
  605. ...(cwd ? { cwd } : {}),
  606. hidden: true,
  607. room_plumbing: true,
  608. close_on_disconnect: false,
  609. })
  610. let opened
  611. if (binding?.storedId) {
  612. try {
  613. opened = await gateway.rpc('session.resume', {
  614. profile: agent.profile,
  615. session_id: binding.storedId,
  616. omit_messages: true,
  617. ...(recovering&&target.runner?.computer?{recoverOnly:true}:{}),
  618. close_on_disconnect: false,
  619. })
  620. } catch (error) {
  621. const code = error instanceof HttpError ? error.code : ''
  622. const message = error instanceof Error ? error.message : ''
  623. if (recovering || (code !== 'computer_session_mode_changed' && !message.includes('执行方式已改变'))) throw error
  624. binding = undefined
  625. opened = await createSession()
  626. }
  627. } else opened = await createSession()
  628. runtimeId = String(opened.session_id ?? '')
  629. const storedId = String(
  630. opened.stored_session_id ?? opened.session_key ?? opened.resumed ?? binding?.storedId ?? '',
  631. )
  632. if (
  633. !runtimeId ||
  634. !storedId ||
  635. (opened.info?.profile_name && opened.info.profile_name !== agent.profile)
  636. )
  637. throw new Error('Hermes 会话身份不匹配')
  638. binding = {
  639. memoryVersion: recovering ? binding?.memoryVersion : memory.version,
  640. runnerId: target.runner?.id,
  641. execution:chatExecution,
  642. vmExecution:undefined,
  643. hermesComputer:target.runner?.hermesComputer,
  644. computerEnvironmentId:agent.computerEnvironmentId,
  645. id: key,
  646. nodeId: agent.nodeId,
  647. remoteAgentId: agent.remoteAgentId,
  648. profile: agent.profile,
  649. storedId,
  650. runtimeId,
  651. aliases: [
  652. ...new Set([
  653. ...(binding?.aliases ?? []),
  654. ...(binding ? [binding.storedId] : []),
  655. storedId,
  656. ]),
  657. ],
  658. runId: run.runId,
  659. conversationTaskId: run.conversationTaskId,
  660. taskId: run.id,
  661. contextSeq: binding?.contextSeq ?? 0,
  662. messageId: resultMessage.id,
  663. }
  664. this.store.put(owner, 'binding', key, binding)
  665. this.live.set(key, { gateway, runtimeId, runId: run.runId, conversationTaskId: run.conversationTaskId, taskId: run.id, agentId: agent.id, done: finish })
  666. if (this.closing || this.getWork(owner, run.id).cancelRequested || this.getWork(owner, run.id).status === 'interrupted')
  667. throw new Error('运行已停止')
  668. if (recovering) {
  669. if (opened.running) {
  670. const current = this.getWork(owner, run.id)
  671. current.status = 'running'
  672. this.saveWork(owner, current)
  673. } else {
  674. const marker = `[yaoyao-run:${run.runId}:${resultMessage.id}]`
  675. let answer: { content?: string; text?: string } | undefined
  676. let found = false, seenLastEvent = false
  677. for (let page = 0; page < 20 && !found; page++) {
  678. const response = await target.session.request(`/api/sessions/${encodeURIComponent(storedId)}/messages`, {
  679. search: new URLSearchParams({ profile: agent.profile, limit: '500', offset: String(page * 500), order: 'latest', include_compacted: 'true' }),
  680. maxResponseBytes: 8 * 1024 * 1024,
  681. })
  682. if (response.status !== 200) throw new Error('无法核对历史')
  683. const history = JSON.parse(response.body.toString()).messages
  684. if (!Array.isArray(history)) throw new Error('历史响应无效')
  685. for (const item of [...history].reverse()) {
  686. if (item.role === 'user') {
  687. if (String(item.content ?? item.text).includes(marker)) { found = true; break }
  688. answer = undefined; seenLastEvent = false
  689. } else if (!seenLastEvent && ['assistant', 'tool'].includes(item.role)) {
  690. seenLastEvent = true
  691. const calls = item.tool_calls
  692. const hasCalls = Array.isArray(calls) ? calls.length > 0 : !!calls && calls !== '[]'
  693. if (item.role === 'assistant' && !hasCalls && !item.function_call && item.finish_reason !== 'tool_calls' && (item.content || item.text)) answer = item
  694. }
  695. }
  696. if (history.length < 500) break
  697. }
  698. if (!found || !answer) throw new Error('无法确认本轮的最终回复,正在核对原执行;不会重复提交')
  699. resultMessage.content = String(answer.content ?? answer.text)
  700. binding.contextSeq = Math.max(binding.contextSeq ?? 0, run.contextThroughSeq ?? 0)
  701. this.store.put(owner, 'binding', key, binding)
  702. finish()
  703. }
  704. } else {
  705. if (opened.running) throw new HttpError(409, '上游会话仍在运行', 'session_busy')
  706. this.nodes.requireSource(owner, agent)
  707. const team=botCapabilities.tools&&!agent.temporaryGoalId&&!agent.remoteAgentId&&!this.store.require<Run>(owner,'run',run.runId).assignmentId
  708. const cloud=!!this.cloud?.selected(owner,agent)
  709. const plugins=!!this.plugins?.selected(owner,agent)
  710. const envTools=this.desktopEnvironments?.envTools(owner,agent)??{view:false,browser:false,file:false}
  711. const desktop=envTools.view||envTools.file
  712. const desktopEpochs=this.desktopEnvironments?.hostEpochs(owner,agent)??{}
  713. const knowledge = botCapabilities.tools && !agent.remoteAgentId
  714. const vmHolder:{session?:VmToolSession}={}
  715. const requireVm=()=>{if(!globalComputers(this.store.home).vm)throw new HttpError(403,'全局设置未开放虚拟机。','computer_disabled')}
  716. const callVm=async(name:string,args:unknown)=>{
  717. requireVm()
  718. if(!vmHolder.session){
  719. const vmTarget=this.nodes.targetForAgent(owner,{...agent,execution:'computer'})
  720. vmHolder.session=new VmToolSession(vmTarget,agent.profile,run.id,()=>{
  721. requireVm();this.requireAuthorization(owner,run.runId);this.nodes.requireSource(owner,agent)
  722. const current=this.getWork(owner,run.id),latest=this.store.require<Agent>(owner,'agent',agent.id),conversation=this.store.require<Conversation>(owner,'conversation',c.id)
  723. if(this.closing||conversation.archived||!this.store.taskMemberIds(owner,conversation,run.conversationTaskId).includes(agent.id)||this.store.require<Run>(owner,'run',run.runId).stopRequested||current.cancelRequested||['interrupted','complete','failed'].includes(current.status)||latest.archived||latest.computerEnvironmentId!==agent.computerEnvironmentId)throw new HttpError(403,'本轮机器人或任务授权已结束','run_authorization_revoked')
  724. },async(name,bytes)=>{
  725. this.requireAuthorization(owner,run.runId)
  726. const file=this.publishArtifact(owner,resultMessage,agent,name,bytes)
  727. if(!resultMessage.attachments.some(item=>item.id===file.id))resultMessage.attachments.push(file)
  728. this.store.saveMessage(owner,resultMessage)
  729. return {...file,url:`/api/app/files/${file.id}/download`}
  730. })
  731. vmTools=vmHolder.session
  732. }
  733. return vmHolder.session.call(name,args)
  734. }
  735. if(team||cloud||desktop||plugins||knowledge||dispatchedVm){
  736. if(team){const granted=this.getWork(owner,run.id);granted.teamManagementRevision=agent.revision;this.saveWork(owner,granted)}
  737. if(desktop)await this.desktopEnvironments!.requireAvailable(owner,agent,target)
  738. if(cloud)await this.cloud!.requireAvailable(owner,agent,target)
  739. const assertActive=()=>{
  740. if(settled||this.closing||toolController.signal.aborted)throw new HttpError(410,'本轮运行已结束,请发起新一轮对话后重试。','run_finished')
  741. this.requireAuthorization(owner,run.runId);this.nodes.requireSource(owner,this.store.require<Agent>(owner,'agent',agent.id))
  742. if(team)this.teamTools.assertTurn(owner,run.id)
  743. if(plugins){const current=this.getWork(owner,run.id),latest=this.store.require<Agent>(owner,'agent',agent.id);if(current.cancelRequested||this.store.require<Run>(owner,'run',run.runId).stopRequested||latest.archived||!['running','waiting'].includes(current.status))throw new HttpError(403,'本轮插件授权已结束','plugin_grant_revoked')}
  744. }
  745. if(plugins)pluginLease=await this.plugins!.open(owner,agent,target,toolController.signal,assertActive)
  746. toolLease=await createWorkspaceToolLease({
  747. target,profile:agent.profile,workId:run.id,signal:toolController.signal,
  748. workspaceMemory:botCapabilities.memory,
  749. session:()=>({runtimeId,storedId:binding!.storedId}),assertActive,
  750. catalog:()=>[...(team?this.teamTools.catalog(owner,run.id):[]),...(knowledge?this.knowledgeTools.catalog(owner,run.id,botCapabilities.memory):[]),...(cloud?GROK_COMPUTER_TOOLS:[]),...(dispatchedVm?VM_COMPUTER_TOOLS:[]),...(desktop?DESKTOP_ENVIRONMENT_TOOLS.filter(t=>(envTools.view&&['desktop_environment_view','desktop_environment_action'].includes(t.id))||(envTools.file&&DESKTOP_FILE_TOOL_IDS.has(t.id))):[]),...(pluginLease?.catalog()??[])],
  751. call:async(toolId,args)=>{assertActive();return knowledge&&this.knowledgeTools.handles(toolId)?this.knowledgeTools.call(owner,run.id,toolId,args,botCapabilities.memory):toolId.startsWith('plugin_')&&pluginLease?pluginLease.call(toolId,args):toolId.startsWith('desktop_')?this.desktopEnvironments!.call(owner,agent.id,toolId,args,toolController.signal,assertActive,'local',desktopEpochs,root.deviceHost):toolId.startsWith('cloud_computer_')?this.cloud!.call(owner,agent.id,toolId,args,toolController.signal):toolId.startsWith('computer_')&&dispatchedVm?callVm(toolId,args):this.teamTools.call(owner,run.id,toolId,args)},
  752. onFailure:error=>{if(!settled)void gateway.rpc('session.interrupt',{session_id:runtimeId}).catch(()=>{}).finally(()=>finish(error))},
  753. })
  754. await toolLease.bind();if(settled)return await completion
  755. }
  756. await applyWorkingDirectory((method, params) => gateway.rpc(method, params), runtimeId, cwd, undefined, opened.info?.cwd)
  757. // Binding initializes cold Hermes sessions; its persisted runtime can overwrite
  758. // pre-build config acknowledgements. Apply Bot settings only after that boundary.
  759. if (agent.modelSettings !== undefined) {
  760. const entry = { method: 'bot.model-settings', agentId: agent.id, runId: run.runId, taskId: run.conversationTaskId }
  761. const snapshot = { revision: agent.revision, settings: agent.modelSettings }
  762. this.inspector?.record(owner, c.id, { ...entry, direction: 'request', data: snapshot })
  763. try {
  764. const resolution = await resolveBotModelSettings(target, agent.profile, agent.modelSettings)
  765. const confirmed = agent.modelSettingsConfirmation === botModelTarget(resolution.effective)
  766. if (resolution.confirmationMessage && !confirmed) throw new HttpError(409, '此模型需要确认,请打开 Bot 个人资料保存模型设置。', 'model_confirmation_required')
  767. await applyBotModelSettings((method, params) => gateway.rpc(method, params), runtimeId, resolution.effective, confirmed)
  768. this.inspector?.record(owner, c.id, { ...entry, direction: 'response', data: { ...snapshot, applied: true, effective: resolution.effective } })
  769. } catch (error) {
  770. // Context-dependent native warnings cannot be known from the model
  771. // catalogue. Bring them back to the profile's existing save flow.
  772. if (error instanceof BotModelConfirmationError) {
  773. const latest = this.store.require<Agent>(owner, 'agent', agent.id)
  774. if (latest.revision === agent.revision) this.store.put(owner, 'agent', latest.id, { ...latest,
  775. modelSettingsPendingConfirmation: { target: error.target, message: error.confirmationMessage } })
  776. }
  777. this.inspector?.record(owner, c.id, { ...entry, direction: 'response', data: { ...snapshot, applied: false, error: error instanceof Error ? error.message : '模型设置应用失败' } })
  778. throw error
  779. }
  780. }
  781. const trigger = this.store.require<Message>(owner, 'message', run.messageId)
  782. const attachmentRefs: string[] = []
  783. for (const file of trigger.attachments) {
  784. this.requireAuthorization(owner, run.runId)
  785. const record = this.files(owner,[file.id])[0]!
  786. const bytes = readFileSync(record.path).toString('base64')
  787. const attached = record.mimeType.startsWith('image/')
  788. ? await gateway.rpc('image.attach_bytes', {
  789. session_id: runtimeId,
  790. filename: record.name,
  791. content_base64: bytes,
  792. })
  793. : await gateway.rpc('file.attach', {
  794. session_id: runtimeId,
  795. name: record.name,
  796. data_url: `data:${record.mimeType};base64,${bytes}`,
  797. })
  798. if (attached.ref_text) attachmentRefs.push(String(attached.ref_text))
  799. }
  800. const members = run.turnConfiguration!.members
  801. const goal = run.conversationTaskId ? this.store.get<import('../shared/agentTasks.js').AgentGoal>(owner, 'goal', run.conversationTaskId) : undefined
  802. const assignmentId = this.store.require<Run>(owner, 'run', run.runId).assignmentId
  803. const globals = globalComputers(this.store.home)
  804. const eligible = !agent.remoteAgentId && !agent.archived
  805. const open = [eligible && globals.scriptMachine ? '电脑' : '', eligible && globals.serverComputer ? '服务器' : '', eligible && globals.vm ? '虚拟环境' : '', eligible && globals.cloud ? '云虚拟机' : ''].filter(Boolean)
  806. const hostNames = this.desktopEnvironments?.hostStates(owner).map(host => host.name).filter(Boolean) ?? []
  807. const prior = this.store.messages(owner, c.id, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, true, run.conversationTaskId)
  808. .filter(message => message.id !== resultMessage.id && message.seq <= run.triggerSeq)
  809. .map(message => `${message.content}\n${message.tools.map(tool => String(tool.name ?? tool.tool_name ?? '')).join(' ')}`)
  810. .join('\n')
  811. const hit = environmentMentions(`${trigger.content}\n${prior}`, hostNames)
  812. const envDirective = `Hermes 只运行在服务器,其原生终端和文件工具始终操作服务器,不代表用户所说的「本机」。操作用户消息来源电脑必须使用带目标的 desktop_* 工具。Hermes 的网页、浏览器、终端和文件工具均可使用。不要使用 Hermes 的 memory 工具,长期记忆只用 Bot 自己的记忆。另外可以使用全局开放的电脑环境:${open.join('、') || '无'}。所有 Bot 使用同一套全局电脑权限,环境可同时使用。没开放的电脑环境不要用。这些环境是额外的桌面,不代替 Hermes 工具。`
  813. const rules = [
  814. envDirective,
  815. `你是 ${agent.name}。${agent.description ? `描述:${agent.description}\n\n` : ''}${personaSection(agent) ? `用户为它设定的工作规范:\n${personaSection(agent)}\n\n` : ''}以下是用户为这个独立机器人配置的角色与规则(版本 ${agent.revision}):\n${agent.remoteAgentId ? '配置由远端机器人管理。' : agent.instructions}`,
  816. c.kind === 'group' && Object.keys(c.memberRoles ?? {}).length
  817. ? `本群角色分工(仅在本群生效):\n${members.flatMap(member => {
  818. const role = c.memberRoles?.[member.id]
  819. return role ? [`@${member.name}:${role.name};${role.description}`] : []
  820. }).join('\n')}\n协作时使用上面的真实成员名称进行 @,不要使用职责名称代替成员名称。`
  821. : '',
  822. this.store.require<Run>(owner, 'run', run.runId).discussion
  823. ? `你正在群聊「${c.name}」平等讨论,第 ${run.depth + 1} 轮。你只代表自己,必须给出一条公开、有实质内容的回答,保留用户原始要求。不要代替其他成员发言,不要用私信或 @ 再次派发本轮工作。系统会安排其他成员。`
  824. : c.kind === 'group'
  825. ? `你正在群聊「${c.name}」发言。群成员:${members.map((a) => `@${a.name} (id=${a.id})`).join('、')}。\n群规则:${c.instructions}\n${c.mode === 'host' ? (agent.id === c.administratorId ? '你是管理员。必要时用精确 @成员名称 委派工作;收到结果后复核并给用户结论。任务完成时不要继续 @。' : '执行当前委派任务。公开给出结果,由管理员复核;不要安排其他成员。') : '按自己的职责回复,只在需要协作时 @成员。不要重复已完成的工作。'}`
  826. : '',
  827. c.kind === 'group' ? '只有安排具体的新工作时才用 @成员派工,并写明需要执行的动作。收到、感谢、审核通过、等待用户指令等确认不需要再次 @。任务收尾直接向用户报告结果;不要重复确认或把同一结果反复交回其他成员。' : '',
  828. run.requiredReply ? '你必须公开处理本次消息,直接回答、委派或澄清;禁止静默。管理员可按依赖一次 @一人,也可同时 @多人并行执行,整批结束后系统统一交回复核。' : run.replyMode === 'automatic' ? `你按自动参与配置收到消息。若与职责无关或仅是已完成工作的重复确认,禁止调用工具、禁止 @,完整答复只能是 ${NO_REPLY}。有新工作或新结果时正常回答。` : '',
  829. `本轮用户指定成员:${this.store.require<Run>(owner, 'run', run.runId).mentionIds.map(id => members.find(a => a.id === id)).filter(Boolean).map(a => '@' + a!.name).join('、') || '未指定'}`,
  830. '角色规则不赋予额外工具权限;仍遵守基础 Hermes 的工具和安全约束。',
  831. this.store.require<Run>(owner, 'run', run.runId).internalInstruction || '',
  832. goal && ['running', 'review', 'waiting'].includes(goal.status) ? `当前团队目标 ID:${goal.id}。目标:${goal.objective.slice(0,8000)}\n验收要求(版本 ${goal.acceptanceRevision ?? 1}):${goal.acceptanceCriteria.join(';')}\n${assignmentId ? `你在执行子任务 ${assignmentId},请完成分派并提交结果,不要扩大团队或再次委派。` : '先从用户要求提炼少量具体、可核对的交付条件;若仍是默认验收要求,使用 workspace_update_team_goal 保存。尊重用户调整后的要求。能直接完成就直接完成,不必创建子任务;仅确实需要分工时使用 workspace_assign_task,成员结果通过 workspace_review_assignment 复核,不要再用 @ 重复派发同一工作。最终使用 workspace_finish_team_task 记录完成、受阻或等待用户,完成时提供实际依据。'}` : '',
  833. agent.temporaryGoalId ? `你是当前任务的临时助手,任务 ID:${agent.temporaryGoalId}。仅处理分派工作,使用 computer_export 回传产物。任务结束后会退役;不要创建团队或改变自身权限。` : '',
  834. team ? TEAM_TOOL_RULES : '',
  835. knowledge ? BOT_KNOWLEDGE_RULES : '',
  836. memory.text,
  837. root.projectId ? `当前项目 ID:${root.projectId}。只能使用当前项目记忆。` : '',
  838. `本次来源消息 ID:${run.messageId};当前会话 ID:${c.id}。`,
  839. plugins ? '本轮已挂载用户为当前 Bot 授权的插件工具,工具名以 plugin_ 开头,说明中包含实际服务和操作。仅按用户当前任务使用;连接或重新授权应用请让用户打开 Bot 模式的工具 → 已连接应用。不要索取 API Key 或在回复中展示凭据。' : '',
  840. desktop && hit.server && globals.serverComputer ? SERVER_COMPUTER_RULES+(this.desktopEnvironments?.onlineHostsLine(owner,root.deviceHost)??'') : '',
  841. desktop && hit.script && globals.scriptMachine ? DESKTOP_ENVIRONMENT_RULES+(this.desktopEnvironments?.onlineHostsLine(owner,root.deviceHost)??'') : '',
  842. this.desktopEnvironments?.deviceContextLine(owner,root.deviceHost) ?? '本轮未提供来源电脑;操作电脑时必须明确目标。',
  843. envTools.file ? DESKTOP_FILE_TRANSFER_RULES : '',
  844. cloud && hit.cloud ? grokComputerRules() : '',
  845. dispatchedVm && hit.vm ? VM_COMPUTER_RULES : '',
  846. `[yaoyao-run:${run.runId}:${resultMessage.id}]`,
  847. ]
  848. .filter(Boolean)
  849. .join('\n\n')
  850. const text = c.kind === 'group' ? this.contextText(owner, c, agent, run, binding.contextSeq ?? 0, movedRunner)
  851. : movedRunner ? `执行节点已切换。以下是原会话的近期记录,旧路径和执行状态需要在当前节点重新核实;不要重新执行已完成的操作。\n\n${this.contextText(owner,c,agent,run,0,true)}` : trigger.content
  852. const admission = this.getWork(owner, run.id)
  853. if (!this.store.taskMemberIds(owner,this.store.require<Conversation>(owner,'conversation',c.id),run.conversationTaskId).includes(agent.id)) {
  854. admission.status = 'interrupted'; admission.error = '执行前成员已移除'; this.saveWork(owner, admission)
  855. throw new Error('执行前成员已移除')
  856. }
  857. this.nodes.requireSource(owner, agent)
  858. this.requireAuthorization(owner, run.runId)
  859. if (admission.cancelRequested || admission.status === 'interrupted') throw new Error('运行已停止')
  860. admission.contextThroughSeq = run.contextThroughSeq ?? run.triggerSeq
  861. admission.submitted = true
  862. this.saveWork(owner, admission)
  863. submitted = true
  864. try {
  865. submittedText = `${rules}\n\n${text}\n${attachmentRefs.join('\n')}`
  866. await gateway.rpc('prompt.submit', {
  867. session_id: runtimeId,
  868. ...(target.runner?.computer?{workMarker:`[yaoyao-run:${run.runId}:${resultMessage.id}]`}:{}),
  869. text: submittedText,
  870. })
  871. this.store.remove(owner,'binding-reset',key)
  872. binding.contextSeq = Math.max(binding.contextSeq ?? 0, admission.contextThroughSeq)
  873. this.store.put(owner, 'binding', key, binding)
  874. } catch (error) {
  875. // A JSON-RPC error is a definitive rejection, not a lost receipt.
  876. if (error instanceof HttpError && ['gateway_rejected','runner_command_not_admitted'].includes(error.code??'')) { submitted = false; const current = this.getWork(owner, run.id); current.submitted = false; this.saveWork(owner, current) }
  877. throw error
  878. }
  879. }
  880. return await completion
  881. } catch (error) {
  882. finish(error instanceof Error ? error : new Error('执行失败'))
  883. return await completion
  884. }
  885. }
  886. private requireAuthorization(owner: string, runId: string): void {
  887. const run=this.store.require<Run>(owner,'run',runId)
  888. if(!this.userActive(owner)||(run.authorizationVersion!==undefined&&run.authorizationVersion!==this.authorizationVersion(owner)))
  889. throw new HttpError(403,'本轮账号授权已失效','run_authorization_revoked')
  890. }
  891. private contextText(owner: string, c: Conversation, agent: Agent, work: Work, after: number, includeOwn = false): string {
  892. const rootTrigger = this.store.require<Message>(owner, 'message', work.messageId)
  893. const roots = new Map(this.store.list<Run>(owner, 'run').filter(r => r.conversationId === c.id && r.conversationTaskId === work.conversationTaskId).map(r => [r.id, this.store.get<Message>(owner, 'message', r.messageId)?.seq ?? 0]))
  894. const history = this.store.messages(owner, c.id, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, true, work.conversationTaskId)
  895. const unfinished = history.find(m => m.seq > after && m.seq <= work.triggerSeq && (['queued', 'streaming', 'uncertain'].includes(m.status) || (m.runId && (roots.get(m.runId) ?? 0) > rootTrigger.seq)))
  896. work.contextThroughSeq = unfinished ? unfinished.seq - 1 : work.triggerSeq
  897. const eligible = history
  898. .filter(m => m.seq > after && m.seq <= work.triggerSeq && m.visible !== false && ['complete', 'failed', 'interrupted'].includes(m.status)
  899. && (includeOwn || !(m.role === 'assistant' && m.agentId === agent.id)) && (!m.runId || (roots.get(m.runId) ?? 0) <= rootTrigger.seq))
  900. const selected: string[] = []
  901. let size = 0, omitted = 0
  902. for (const message of [...eligible].reverse()) {
  903. const files = message.attachments.map(f => `[附件 ${f.name}](${f.sourcePath || `/api/app/files/${f.id}/download`})`).join('\n')
  904. const content = message.content.length > 24_000 ? message.content.slice(0, 12_000) + '\n[内容过长,保留首尾片段]\n' + message.content.slice(-12_000) : message.content
  905. const reasoning = message.reasoning ? `\n思考摘要:${message.reasoning.slice(0, 2000)}` : ''
  906. const tools = [...new Set(message.tools.map(t => String(t.name ?? t.tool_name ?? '')).filter(name => /^[A-Za-z0-9][A-Za-z0-9_.-]{0,119}$/.test(name)))].slice(0, 16)
  907. const row = `${message.role === 'user' ? '用户' : message.agentName ?? '系统'}(${message.status}):${content}${reasoning}${tools.length ? `\n使用工具:${tools.join('、')}` : ''}${message.error ? `\n错误:${message.error}` : ''}${files ? '\n' + files : ''}`
  908. if (selected.length >= 50 || size + row.length > 30_000) { omitted++; continue }
  909. selected.unshift(row); size += row.length
  910. }
  911. // Preserve a large trigger as a bounded excerpt rather than silently losing it.
  912. if (!selected.length && eligible.length) selected.push(eligible.at(-1)!.content.slice(-30_000))
  913. const batch = work.reviewOf ? this.works(owner, work.runId).filter(t => t.batchId === work.reviewOf).map(t => `${this.store.get<Agent>(owner, 'agent', t.agentId)?.name ?? t.agentId}:${t.status}${t.error ? `;${t.error}` : ''}`).join('\n') : ''
  914. const request = this.store.require<Run>(owner, 'run', work.runId).discussion ? `本次用户原始要求:\n${rootTrigger.content.slice(0, 16000)}\n\n` : ''
  915. return `${request}${batch ? `本批次执行结果:\n${batch}\n\n` : ''}${omitted ? `较早上下文有 ${omitted} 条因长度限制省略,请勿假定已完整读取。\n\n` : ''}${selected.join('\n\n')}`
  916. }
  917. async respond(owner: string, id: string, answer: string): Promise<void> {
  918. type Reply = WorkspaceInteraction & { answer?: string; responseState?: 'sending' | 'uncertain' | 'sent' }
  919. const interaction = this.store.require<Reply>(owner, 'interaction', id)
  920. this.requireAuthorization(owner,interaction.runId)
  921. this.nodes.requireSource(owner, this.store.require<Agent>(owner, 'agent', interaction.agentId))
  922. if (interaction.resolved) {
  923. if (interaction.answer && interaction.answer !== answer) throw new HttpError(409, '该请求已使用其他答复完成', 'interaction_answer_conflict')
  924. return
  925. }
  926. if (interaction.responseState === 'sending' || interaction.responseState === 'uncertain') throw new HttpError(409, '答复状态待确认,请核对原请求', 'interaction_uncertain')
  927. const binding = this.store.require<{ key: string; upstreamId: string; taskId: string }>(owner, 'interaction-binding', id)
  928. const live = this.live.get(binding.key)
  929. if (!live || live.taskId !== binding.taskId || live.runId !== interaction.runId) throw new HttpError(409, '请先恢复此轮连接', 'interaction_offline')
  930. if (interaction.kind === 'approval' && !['once', 'session', 'always', 'deny', 'allow', 'approve', 'auto'].includes(answer)) throw new HttpError(400, '审批选项无效', 'invalid_approval')
  931. const automatic = interaction.kind === 'approval' && answer === 'auto'
  932. interaction.answer = answer; interaction.responseState = 'sending'
  933. this.store.atomic(() => {
  934. // Approval replies never change the global policy.
  935. // Legacy auto replies approve this operation only. Global policy is admin-owned.
  936. this.store.put(owner, 'interaction', id, interaction)
  937. })
  938. try {
  939. await live.gateway.rpc(interaction.kind === 'approval' ? 'approval.respond' : 'clarify.respond', {
  940. session_id: live.runtimeId, request_id: binding.upstreamId,
  941. ...(interaction.kind === 'approval' ? { choice: automatic ? 'once' : answer } : { answer }),
  942. })
  943. } catch (error) {
  944. const latest = this.store.require<Reply>(owner, 'interaction', id)
  945. if (!latest.resolved) {
  946. latest.responseState = error instanceof HttpError && error.code === 'gateway_rejected' ? undefined : 'uncertain'
  947. this.store.put(owner, 'interaction', id, latest)
  948. }
  949. if (automatic) throw new HttpError(409, '本次答复未确认,请检查当前任务状态;全局审批策略未改变。', 'approval_auto_response_failed')
  950. throw error
  951. }
  952. interaction.resolved = true; interaction.responseState = 'sent'
  953. this.store.put(owner, 'interaction', id, interaction)
  954. this.store.event(owner, 'interaction.changed', interaction, interaction.conversationId)
  955. const current = this.getWork(owner, binding.taskId)
  956. if (current.status === 'waiting') { current.status = 'running'; this.saveWork(owner, current) }
  957. }
  958. protected async interruptTurn(owner: string, work: Work): Promise<void> {
  959. const key = this.bindingKey(work.conversationId, work.conversationTaskId, work.agentId)
  960. const live = this.live.get(key)
  961. if (live?.taskId === work.id) {
  962. await live.gateway.rpc('session.interrupt', { session_id: live.runtimeId })
  963. const current = this.getWork(owner, work.id)
  964. if (['complete', 'failed', 'interrupted'].includes(current.status)) {
  965. live.done(new Error('已停止'))
  966. return
  967. }
  968. current.status = 'interrupted'; current.error = '已停止'
  969. this.saveWork(owner, current)
  970. if(current.currentMessageId){const message=this.store.require<Message>(owner,'message',current.currentMessageId);message.status='interrupted';message.error='已停止';this.store.saveMessage(owner,message)}
  971. live.done(new Error('已停止'))
  972. } else {
  973. const binding = this.store.get<WorkspaceBinding>(owner, 'binding', key)
  974. if (work.submitted) {
  975. if (!binding || binding.taskId !== work.id) throw new Error('无法确认待停止成员的会话身份')
  976. const target=binding.remoteAgentId||binding.execution==='computer'?this.nodes.targetForAgent(owner,{...binding,id:work.agentId}):this.nodes.target(owner,binding.nodeId)
  977. if(binding.runnerId!==target.runner?.id)throw new Error('执行节点已变化,无法确认原任务已停止')
  978. const gateway = new WorkspaceGateway(target,{workId:work.id,cleanupOnly:true,sessionId:binding.storedId,authorize:()=>{const current=this.store.get<WorkspaceBinding>(owner,'binding',key);if(current?.taskId!==work.id||current.storedId!==binding.storedId)throw new Error('原会话绑定已改变')}})
  979. try {
  980. await gateway.connect()
  981. const opened = await gateway.rpc('session.resume', { profile: binding.profile, session_id: binding.storedId, omit_messages: true, close_on_disconnect: false,...(binding.execution==='computer'?{recoverOnly:true}:{}) })
  982. if (opened.running) await gateway.rpc('session.interrupt', { session_id: opened.session_id })
  983. } finally { gateway.close() }
  984. }
  985. const current = this.getWork(owner, work.id)
  986. current.status = 'interrupted'; current.error = '已停止'
  987. this.saveWork(owner, current)
  988. if (current.currentMessageId) {
  989. const message = this.store.require<Message>(owner, 'message', current.currentMessageId)
  990. message.status = 'interrupted'; this.store.saveMessage(owner, message)
  991. }
  992. }
  993. this.resolveInteractions(owner, work.id)
  994. }
  995. override async stopTask(owner: string, conversationId: string, taskId: string, visited = new Set<string>()): Promise<void> {
  996. const key = `${owner}:${taskId}`
  997. if (visited.has(key)) return
  998. visited.add(key)
  999. await this.collaboration.stopFor(owner, chain => chain.originConversationId === conversationId && chain.originTaskId === taskId)
  1000. this.tasks.cancel(owner, taskId)
  1001. await Promise.all([super.stopTask(owner, conversationId, taskId), this.tasks.cancelOrigin(owner, conversationId, taskId, visited)])
  1002. }
  1003. override async stopConversation(owner: string, conversationId: string): Promise<void> {
  1004. await this.collaboration.stopFor(owner, chain => chain.originConversationId === conversationId)
  1005. for (const task of this.store.list<import('../shared/workspace.js').WorkspaceTask>(owner, 'conversation-task'))
  1006. if (task.conversationId === conversationId) this.tasks.cancel(owner, task.id)
  1007. await Promise.all([super.stopConversation(owner, conversationId), this.tasks.cancelOrigin(owner, conversationId)])
  1008. }
  1009. private recordTurnTokens(owner: string, agentId: string, input: number, output: number): void {
  1010. if (!input && !output) return
  1011. this.store.atomic(() => {
  1012. const now = new Date(),
  1013. date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`,
  1014. id = `${agentId}:${date}`
  1015. const day = this.store.get<WorkspaceAgentUsageSummary & { agentId: string; date: string; updatedAt: number }>(owner, 'agent-token-day', id)
  1016. ?? { agentId, date, input: 0, output: 0, total: 0, updatedAt: 0 }
  1017. day.input += input
  1018. day.output += output
  1019. day.total += input + output
  1020. day.updatedAt = now.getTime()
  1021. this.store.put(owner, 'agent-token-day', id, day)
  1022. })
  1023. }
  1024. /**
  1025. * Hermes reports cumulative per-session token counters (input/output/total) alongside
  1026. * context occupancy. Attribute the growth since the last snapshot to the agent's daily
  1027. * usage bucket; a session reset (counters dropping) only re-baselines.
  1028. */
  1029. private recordAgentUsage(owner: string, key: string, agentId: string, usage: unknown): void {
  1030. if (!usage || typeof usage !== 'object') return
  1031. const value = usage as Record<string, unknown>
  1032. const counter = (...names: string[]) => {
  1033. for (const name of names) {
  1034. const raw = value[name]
  1035. if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return Math.round(raw)
  1036. }
  1037. return undefined
  1038. }
  1039. const input = counter('input', 'prompt', 'input_tokens'),
  1040. output = counter('output', 'completion', 'output_tokens'),
  1041. total = counter('total', 'total_tokens') ?? (input === undefined && output === undefined ? undefined : (input ?? 0) + (output ?? 0))
  1042. if (total === undefined) return
  1043. this.store.atomic(() => {
  1044. const baseline = this.store.get<{ input?: number; output?: number; total?: number }>(owner, 'usage-baseline', key) ?? {}
  1045. const grew = (current: number | undefined, previous: number | undefined) => (current === undefined ? 0 : Math.max(0, current - (previous ?? 0)))
  1046. const inputDelta = grew(input, baseline.input), outputDelta = grew(output, baseline.output), totalDelta = grew(total, baseline.total)
  1047. this.store.put(owner, 'usage-baseline', key, { input, output, total })
  1048. if (!inputDelta && !outputDelta && !totalDelta) return
  1049. const now = new Date(),
  1050. date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`,
  1051. id = `${agentId}:${date}`
  1052. const day = this.store.get<WorkspaceAgentUsageSummary & { agentId: string; date: string; updatedAt: number }>(owner, 'agent-token-day', id)
  1053. ?? { agentId, date, input: 0, output: 0, total: 0, updatedAt: 0 }
  1054. day.input += inputDelta; day.output += outputDelta; day.total += Math.max(totalDelta, inputDelta + outputDelta)
  1055. day.updatedAt = now.getTime()
  1056. this.store.put(owner, 'agent-token-day', id, day)
  1057. })
  1058. }
  1059. close(): void {
  1060. this.collaboration.close()
  1061. this.tasks.close()
  1062. this.closeScheduler()
  1063. for (const live of [...this.live.values()]) live.done(new Error('服务关闭'))
  1064. this.live.clear()
  1065. }
  1066. }