workspaceRuntime.ts 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. import { randomUUID } from 'node:crypto'
  2. import { readFileSync } from 'node:fs'
  3. import { z } from 'zod'
  4. import { WorkspaceStore, parse } from './workspaceStore.js'
  5. import { WorkspaceNodes, WorkspaceGateway, type GatewayFrame } from './workspaceGateway.js'
  6. import { HttpError } from './errors.js'
  7. import { UploadStore } from './uploads.js'
  8. import type {
  9. WorkspaceAgent as Agent,
  10. WorkspaceConversation as Conversation,
  11. WorkspaceMessage as Message,
  12. WorkspaceRun,
  13. WorkspaceInteraction,
  14. } from '../shared/workspace.js'
  15. import { WorkspaceScheduler, type Work, NO_REPLY, HOST_FALLBACK } from './workspaceScheduler.js'
  16. import { mentionedAgents } from './workspaceMentions.js'
  17. export { mentionedAgents } from './workspaceMentions.js'
  18. type Run = WorkspaceRun
  19. export interface WorkspaceBinding {
  20. remoteAgentId?: string
  21. id: string
  22. nodeId: string
  23. profile: string
  24. storedId: string
  25. runtimeId: string
  26. aliases: string[]
  27. runId: string
  28. messageId: string
  29. conversationTaskId?: string
  30. taskId?: string
  31. contextSeq?: number
  32. }
  33. interface LiveTurn {
  34. gateway: WorkspaceGateway
  35. runtimeId: string
  36. runId: string
  37. agentId: string
  38. conversationTaskId?: string
  39. taskId: string
  40. done(error?: Error): void
  41. }
  42. export const sendInput = z
  43. .object({
  44. requestId: z.string().uuid(),
  45. content: z.string().max(65_536).default(''),
  46. taskId: z.string().uuid().optional(),
  47. mentionIds: z.array(z.string().uuid()).max(8).default([]),
  48. fileIds: z.array(z.string().uuid()).max(8).default([]),
  49. })
  50. .strict()
  51. .refine((b) => b.content.trim() || b.fileIds.length, '请输入消息或添加附件')
  52. export class WorkspaceRuntime extends WorkspaceScheduler {
  53. private live = new Map<string, LiveTurn>()
  54. constructor(store: WorkspaceStore, readonly nodes: WorkspaceNodes, readonly uploads: UploadStore, userActive: (owner: string) => boolean = () => true) { super(store, userActive) }
  55. send(owner: string, conversationId: string, input: unknown): Run {
  56. const body = parse(sendInput, input)
  57. for (const id of this.store.require<Conversation>(owner, 'conversation', conversationId).memberIds)
  58. this.nodes.requireSource(owner, this.store.require<Agent>(owner, 'agent', id))
  59. const result = this.store.command(owner, body.requestId, { conversationId, ...body }, () => {
  60. const c = this.store.require<Conversation>(owner, 'conversation', conversationId)
  61. if (c.archived) throw new HttpError(409, '聊天已归档', 'conversation_archived')
  62. const conversationTask = this.store.resolveTask(owner, conversationId, body.taskId),
  63. conversationTaskId = conversationTask?.id
  64. if (conversationTaskId) {
  65. const alreadyActive = this.store.list<Run>(owner, 'run').some(run => run.conversationTaskId === conversationTaskId && !['complete', 'failed', 'interrupted'].includes(run.status))
  66. if (!alreadyActive && this.store.activeTaskCount(owner, conversationId) >= 4)
  67. throw new HttpError(409, '最多同时运行 4 个任务', 'workspace_task_concurrency_limit')
  68. }
  69. if (body.mentionIds.some((a) => !c.memberIds.includes(a)))
  70. throw new HttpError(400, '只能 @ 群内成员', 'invalid_mentions')
  71. const records = this.uploads.records(body.fileIds, owner)
  72. const now = Date.now(),
  73. runId = randomUUID(),
  74. agents = c.memberIds.map((id) => this.store.require<Agent>(owner, 'agent', id))
  75. const mentions = [
  76. ...new Set(
  77. [...body.mentionIds, ...mentionedAgents(body.content, agents)],
  78. ),
  79. ]
  80. const message: Message = {
  81. id: randomUUID(),
  82. conversationId,
  83. conversationTaskId,
  84. seq: 0,
  85. role: 'user',
  86. content: body.content.trim(),
  87. reasoning: '',
  88. runId,
  89. status: 'complete',
  90. attachments: records.map((f) => ({
  91. id: f.id,
  92. name: f.name,
  93. mimeType: f.mimeType,
  94. size: f.size,
  95. sender: 'user',
  96. createdAt: now,
  97. })),
  98. tools: [],
  99. createdAt: now,
  100. }
  101. this.store.saveMessage(owner, message)
  102. for (const file of records) {
  103. const existing = this.store.get<Record<string, unknown>>(owner, 'file', file.id)
  104. if (existing)
  105. this.store.put(owner, 'file', file.id, {
  106. ...existing,
  107. conversationId,
  108. messageId: message.id,
  109. })
  110. }
  111. const run: Run = {
  112. id: runId,
  113. conversationId,
  114. conversationTaskId,
  115. messageId: message.id,
  116. mentionIds: mentions,
  117. activeAgentId: c.administratorId,
  118. status: 'queued',
  119. round: 0,
  120. createdAt: now,
  121. updatedAt: now,
  122. }
  123. this.admit(owner, run, c, message)
  124. this.uploads.markReferenced(body.fileIds, owner)
  125. return run
  126. })
  127. this.wake()
  128. return this.store.require<Run>(owner, 'run', result.id)
  129. }
  130. protected async performTurn(
  131. owner: string,
  132. c: Conversation,
  133. agent: Agent,
  134. run: Work,
  135. recovering = false,
  136. ): Promise<Message> {
  137. this.nodes.requireSource(owner, agent)
  138. if (agent.remoteAgentId) agent = await this.nodes.refreshRemoteAgent(owner,agent)
  139. const key = this.bindingKey(c.id, run.conversationTaskId, agent.id),
  140. target = agent.remoteAgentId ? this.nodes.targetForAgent(owner,agent) : this.nodes.target(owner, agent.nodeId)
  141. const gateway = new WorkspaceGateway(target)
  142. let binding = this.store.get<WorkspaceBinding>(owner, 'binding', key)
  143. let message =
  144. run.currentMessageId
  145. ? this.store.require<Message>(owner, 'message', run.currentMessageId)
  146. : undefined
  147. if (!message) {
  148. message = {
  149. id: randomUUID(),
  150. conversationId: c.id,
  151. conversationTaskId: run.conversationTaskId,
  152. seq: 0,
  153. role: 'assistant',
  154. agentId: agent.id,
  155. agentName: agent.name,
  156. content: '',
  157. reasoning: '',
  158. status: 'streaming',
  159. runId: run.runId,
  160. taskId: run.id,
  161. visible: run.replyMode !== 'automatic' || run.requiredReply,
  162. tools: [],
  163. attachments: [],
  164. createdAt: Date.now(),
  165. }
  166. run.currentMessageId = message.id
  167. this.store.atomic(() => {
  168. this.store.saveMessage(owner, message!)
  169. this.saveWork(owner, run)
  170. })
  171. }
  172. const resultMessage = message
  173. let submitted = recovering,
  174. settled = false,
  175. completedEvidence = false,
  176. completing = false,
  177. runtimeId = '',
  178. lastFlush = 0
  179. let resolveTurn!: (m: Message) => void, rejectTurn!: (e: Error) => void
  180. const completion = new Promise<Message>((resolve, reject) => {
  181. resolveTurn = resolve
  182. rejectTurn = reject
  183. })
  184. // Register rejection immediately; setup RPCs can still be awaiting a reply.
  185. void completion.catch(() => {})
  186. const finish = (error?: Error) => {
  187. if (settled) return
  188. settled = true
  189. this.live.delete(key)
  190. try {
  191. const current = this.getWork(owner, run.id)
  192. if (error) {
  193. current.status = current.status === 'interrupted' ? 'interrupted' : submitted ? 'uncertain' : 'failed'
  194. current.error = error.message.slice(0, 1000)
  195. resultMessage.status = current.status
  196. resultMessage.error = current.error
  197. const optionalUnpublished = current.replyMode === 'automatic' && !current.requiredReply && resultMessage.visible === false
  198. resultMessage.visible = !optionalUnpublished && (submitted || current.status !== 'interrupted')
  199. if (!resultMessage.content.trim() && current.status === 'failed') resultMessage.content = `执行失败:${current.error}`
  200. } else {
  201. const silent = (resultMessage.content.trim() === NO_REPLY || !resultMessage.content.trim()) && current.replyMode === 'automatic' && (!current.requiredReply || current.hadInteraction) && !resultMessage.tools.length && !resultMessage.attachments.length
  202. current.silent = silent
  203. if (current.replyMode === 'automatic') {
  204. resultMessage.content = resultMessage.content.replace(/\[\[YAOYAO_[A-Z0-9_]*(?:\]\])?/g, '').trim()
  205. resultMessage.reasoning = resultMessage.reasoning.replace(/\[\[YAOYAO_[A-Z0-9_]*(?:\]\])?/g, '')
  206. }
  207. if (current.requiredReply && !silent && !resultMessage.content) resultMessage.content = HOST_FALLBACK
  208. if (!silent && !resultMessage.content && !resultMessage.attachments.length && !resultMessage.tools.length) {
  209. current.status = 'failed'; current.error = '未返回有效回复'; resultMessage.content = '执行失败:未返回有效回复'
  210. } else { current.status = 'complete'; current.error = undefined }
  211. resultMessage.visible = !silent
  212. if (silent) resultMessage.reasoning = ''
  213. resultMessage.status = current.status
  214. resultMessage.error = current.error
  215. }
  216. if (current.status !== 'uncertain') {
  217. for (const tool of resultMessage.tools) {
  218. if (!/complete|error|failed/.test(String(tool.status))) tool.status = current.status === 'complete' ? 'tool.complete' : 'tool.error'
  219. }
  220. }
  221. this.store.atomic(() => {
  222. this.store.saveMessage(owner, resultMessage)
  223. this.saveWork(owner, current)
  224. if (current.status !== 'uncertain') this.resolveInteractions(owner, current.id)
  225. })
  226. if (error) rejectTurn(error)
  227. else resolveTurn(resultMessage)
  228. } catch (failure) { rejectTurn(failure instanceof Error ? failure : new Error('无法保存执行状态')) }
  229. finally { gateway.close() }
  230. }
  231. gateway.onDisconnect = () =>
  232. finish(completedEvidence ? undefined : new Error('Hermes 连接断开'))
  233. gateway.onEvent = (frame: GatewayFrame) => {
  234. if (settled || !runtimeId || frame.session_id !== runtimeId) return
  235. const p = frame.payload ?? {},
  236. type = frame.type
  237. if (type === 'message.delta') {
  238. resultMessage.content += String(p.text ?? p.delta ?? '')
  239. if (resultMessage.content.trim() && !NO_REPLY.startsWith(resultMessage.content.trim()) && !resultMessage.content.trim().startsWith('[[YAOYAO_')) resultMessage.visible = true
  240. }
  241. else if (type === 'reasoning.delta')
  242. resultMessage.reasoning += String(p.text ?? p.delta ?? '')
  243. else if (type === 'message.interim') {
  244. resultMessage.content += '\n\n'
  245. } else if (type.startsWith('tool.')) {
  246. resultMessage.visible = true
  247. const id = String(p.tool_id ?? p.id ?? ''),
  248. index = resultMessage.tools.findIndex((t) => t.id === id)
  249. const value = { ...p, id, status: type }
  250. if (index >= 0) resultMessage.tools[index] = { ...resultMessage.tools[index], ...value }
  251. else resultMessage.tools.push(value)
  252. } else if (
  253. ['approval.request', 'approval.requested', 'clarify.request', 'clarify.requested'].includes(
  254. type,
  255. )
  256. ) {
  257. const upstreamId = String(p.request_id ?? p.requestId ?? p.id ?? '')
  258. const previous = this.store.list<WorkspaceInteraction>(owner, 'interaction').find(i => {
  259. const b = this.store.get<{ taskId: string; upstreamId: string }>(owner, 'interaction-binding', i.id)
  260. return b?.taskId === run.id && b.upstreamId === upstreamId
  261. })
  262. if (previous?.resolved) return
  263. const interaction: WorkspaceInteraction = previous ?? {
  264. id: randomUUID(),
  265. conversationId: c.id,
  266. conversationTaskId: run.conversationTaskId,
  267. runId: run.runId,
  268. agentId: agent.id,
  269. kind: type.startsWith('approval') ? 'approval' : 'clarification',
  270. message: String(p.question ?? p.message ?? p.prompt ?? '需要确认'),
  271. choices: Array.isArray(p.choices) ? p.choices.map(String) : [],
  272. resolved: false,
  273. }
  274. this.store.put(owner, 'interaction', interaction.id, interaction)
  275. this.store.put(owner, 'interaction-binding', interaction.id, { key, upstreamId, taskId: run.id, conversationTaskId: run.conversationTaskId })
  276. this.store.event(owner, 'interaction.changed', interaction, c.id)
  277. const current = this.getWork(owner, run.id)
  278. current.hadInteraction = true
  279. current.status = 'waiting'
  280. this.saveWork(owner, current)
  281. this.notify(owner, c, this.store.require<Run>(owner, 'run', run.runId), undefined, interaction)
  282. } else if (['message.complete', 'run.completed'].includes(type)) {
  283. if (typeof p.text === 'string') resultMessage.content = p.text
  284. if (typeof p.reasoning === 'string') resultMessage.reasoning = p.reasoning
  285. if (p.status === 'interrupted') {
  286. const current = this.getWork(owner, run.id)
  287. current.status = 'interrupted'
  288. this.saveWork(owner, current)
  289. finish(new Error('已停止'))
  290. } else if (p.error || p.status === 'failed') {
  291. submitted = false
  292. finish(new HttpError(502, String(p.error ?? '运行失败'), 'run_failed'))
  293. } else if (!completing) {
  294. completedEvidence = true
  295. completing = true
  296. let timeout: ReturnType<typeof setTimeout>
  297. void Promise.race([
  298. gateway.rpc('session.usage', { session_id: runtimeId }),
  299. new Promise<undefined>((resolve) => {
  300. timeout = setTimeout(() => resolve(undefined), 1000)
  301. }),
  302. ])
  303. .then((usage) => {
  304. if (usage && !this.closing) {
  305. const used = usage.context_used ?? usage.used_tokens,
  306. limit = usage.context_max ?? usage.context_limit
  307. const contextKey = run.conversationTaskId ?? c.id,
  308. context = {
  309. ...usage,
  310. conversationTaskId: run.conversationTaskId,
  311. usedTokens: used,
  312. limitTokens: limit,
  313. percent:
  314. typeof used === 'number' && typeof limit === 'number' && limit > 0
  315. ? Math.round((used / limit) * 1000) / 10
  316. : undefined,
  317. }
  318. this.store.put(owner, 'context', contextKey, context)
  319. this.store.event(
  320. owner,
  321. 'context.changed',
  322. context,
  323. c.id,
  324. )
  325. }
  326. })
  327. .catch(() => {})
  328. .finally(() => {
  329. clearTimeout(timeout)
  330. finish()
  331. })
  332. }
  333. return
  334. } else if (['error', 'message.error', 'run.failed'].includes(type)) {
  335. submitted = false
  336. finish(new HttpError(502, String(p.message ?? p.error ?? '运行失败'), 'run_failed'))
  337. return
  338. } else if (['session.usage', 'usage.update', 'context.update'].includes(type)) {
  339. const context = { ...p, conversationTaskId: run.conversationTaskId }
  340. this.store.put(owner, 'context', run.conversationTaskId ?? c.id, context)
  341. this.store.event(owner, 'context.changed', context, c.id)
  342. } else if (type === 'session.info' && binding) {
  343. const storedId = p.stored_session_id ?? p.session_key
  344. if (typeof storedId === 'string' && storedId && storedId !== binding.storedId) {
  345. binding.aliases = [...new Set([...binding.aliases, binding.storedId, storedId])]
  346. binding.storedId = storedId
  347. this.store.put(owner, 'binding', key, binding)
  348. }
  349. }
  350. if (Date.now() - lastFlush >= 100) {
  351. this.store.saveMessage(owner, resultMessage)
  352. lastFlush = Date.now()
  353. }
  354. }
  355. try {
  356. await gateway.connect()
  357. this.nodes.requireSource(owner, agent)
  358. const opened = binding?.storedId
  359. ? await gateway.rpc('session.resume', {
  360. profile: agent.profile,
  361. session_id: binding.storedId,
  362. omit_messages: true,
  363. close_on_disconnect: false,
  364. })
  365. : await gateway.rpc('session.create', {
  366. profile: agent.profile,
  367. title: `Yaoyao ${c.id}${run.conversationTaskId ? ` / ${run.conversationTaskId}` : ''}`,
  368. source: 'yaoyao_workspace',
  369. hidden: true,
  370. room_plumbing: true,
  371. close_on_disconnect: false,
  372. })
  373. runtimeId = String(opened.session_id ?? '')
  374. const storedId = String(
  375. opened.stored_session_id ?? opened.session_key ?? opened.resumed ?? binding?.storedId ?? '',
  376. )
  377. if (
  378. !runtimeId ||
  379. !storedId ||
  380. (opened.info?.profile_name && opened.info.profile_name !== agent.profile)
  381. )
  382. throw new Error('Hermes 会话身份不匹配')
  383. binding = {
  384. id: key,
  385. nodeId: agent.nodeId,
  386. remoteAgentId: agent.remoteAgentId,
  387. profile: agent.profile,
  388. storedId,
  389. runtimeId,
  390. aliases: [
  391. ...new Set([
  392. ...(binding?.aliases ?? []),
  393. ...(binding ? [binding.storedId] : []),
  394. storedId,
  395. ]),
  396. ],
  397. runId: run.runId,
  398. conversationTaskId: run.conversationTaskId,
  399. taskId: run.id,
  400. contextSeq: binding?.contextSeq ?? 0,
  401. messageId: resultMessage.id,
  402. }
  403. this.store.put(owner, 'binding', key, binding)
  404. this.live.set(key, { gateway, runtimeId, runId: run.runId, conversationTaskId: run.conversationTaskId, taskId: run.id, agentId: agent.id, done: finish })
  405. if (this.closing || this.getWork(owner, run.id).cancelRequested || this.getWork(owner, run.id).status === 'interrupted')
  406. throw new Error('运行已停止')
  407. if (recovering) {
  408. if (opened.running) {
  409. const current = this.getWork(owner, run.id)
  410. current.status = 'running'
  411. this.saveWork(owner, current)
  412. } else {
  413. const marker = `[yaoyao-run:${run.runId}:${resultMessage.id}]`
  414. let answer: { content?: string; text?: string } | undefined
  415. let found = false, seenLastEvent = false
  416. for (let page = 0; page < 20 && !found; page++) {
  417. const response = await target.session.request(`/api/sessions/${encodeURIComponent(storedId)}/messages`, {
  418. search: new URLSearchParams({ profile: agent.profile, limit: '500', offset: String(page * 500), order: 'latest', include_compacted: 'true' }),
  419. maxResponseBytes: 8 * 1024 * 1024,
  420. })
  421. if (response.status !== 200) throw new Error('无法核对历史')
  422. const history = JSON.parse(response.body.toString()).messages
  423. if (!Array.isArray(history)) throw new Error('历史响应无效')
  424. for (const item of [...history].reverse()) {
  425. if (item.role === 'user') {
  426. if (String(item.content ?? item.text).includes(marker)) { found = true; break }
  427. answer = undefined; seenLastEvent = false
  428. } else if (!seenLastEvent && ['assistant', 'tool'].includes(item.role)) {
  429. seenLastEvent = true
  430. const calls = item.tool_calls
  431. const hasCalls = Array.isArray(calls) ? calls.length > 0 : !!calls && calls !== '[]'
  432. if (item.role === 'assistant' && !hasCalls && !item.function_call && item.finish_reason !== 'tool_calls' && (item.content || item.text)) answer = item
  433. }
  434. }
  435. if (history.length < 500) break
  436. }
  437. if (!found || !answer) throw new Error('无法确认本轮的最终回复,正在核对原执行;不会重复提交')
  438. resultMessage.content = String(answer.content ?? answer.text)
  439. binding.contextSeq = Math.max(binding.contextSeq ?? 0, run.contextThroughSeq ?? 0)
  440. this.store.put(owner, 'binding', key, binding)
  441. finish()
  442. }
  443. } else {
  444. if (opened.running) throw new HttpError(409, '上游会话仍在运行', 'session_busy')
  445. const trigger = this.store.require<Message>(owner, 'message', run.messageId)
  446. const attachmentRefs: string[] = []
  447. for (const file of trigger.attachments) {
  448. const record = this.uploads.records([file.id], owner)[0]!
  449. const bytes = readFileSync(record.path).toString('base64')
  450. const attached = record.mimeType.startsWith('image/')
  451. ? await gateway.rpc('image.attach_bytes', {
  452. session_id: runtimeId,
  453. filename: record.name,
  454. content_base64: bytes,
  455. })
  456. : await gateway.rpc('file.attach', {
  457. session_id: runtimeId,
  458. name: record.name,
  459. data_url: `data:${record.mimeType};base64,${bytes}`,
  460. })
  461. if (attached.ref_text) attachmentRefs.push(String(attached.ref_text))
  462. }
  463. const members = run.turnConfiguration!.members
  464. const rules = [
  465. `你是 ${agent.name}。以下是用户为这个独立 Agent 配置的角色与规则(版本 ${agent.revision}):\n${agent.remoteAgentId ? '配置由远端 Agent 管理。' : agent.instructions}`,
  466. c.kind === 'group' && Object.keys(c.memberRoles ?? {}).length
  467. ? `本群角色分工(仅在本群生效):\n${members.flatMap(member => {
  468. const role = c.memberRoles?.[member.id]
  469. return role ? [`@${member.name}:${role.name};${role.description}`] : []
  470. }).join('\n')}\n协作时使用上面的真实成员名称进行 @,不要使用职责名称代替成员名称。`
  471. : '',
  472. c.kind === 'group'
  473. ? `你正在群聊「${c.name}」发言。群成员:${members.map((a) => `@${a.name} (id=${a.id})`).join('、')}。\n群规则:${c.instructions}\n${c.mode === 'host' ? (agent.id === c.administratorId ? '你是管理员。必要时用精确 @成员名称 委派工作;收到结果后复核并给用户结论。任务完成时不要继续 @。' : '执行当前委派任务。公开给出结果,由管理员复核;不要安排其他成员。') : '按自己的职责回复,只在需要协作时 @成员。不要重复已完成的工作。'}`
  474. : '',
  475. run.requiredReply ? '你必须公开处理本次消息,直接回答、委派或澄清;禁止静默。管理员可按依赖一次 @一人,也可同时 @多人并行执行,整批结束后系统统一交回复核。' : run.replyMode === 'automatic' ? `你按自动参与配置收到消息。若与职责无关,禁止调用工具、禁止 @,完整答复只能是 ${NO_REPLY}。有关时正常回答。` : '',
  476. `本轮用户指定成员:${this.store.require<Run>(owner, 'run', run.runId).mentionIds.map(id => members.find(a => a.id === id)).filter(Boolean).map(a => '@' + a!.name).join('、') || '未指定'}`,
  477. '角色规则不赋予额外工具权限;仍遵守基础 Hermes 的工具和安全约束。',
  478. `[yaoyao-run:${run.runId}:${resultMessage.id}]`,
  479. ]
  480. .filter(Boolean)
  481. .join('\n\n')
  482. const text = c.kind === 'group' ? this.contextText(owner, c, agent, run, binding.contextSeq ?? 0) : trigger.content
  483. const admission = this.getWork(owner, run.id)
  484. if (!this.store.require<Conversation>(owner, 'conversation', c.id).memberIds.includes(agent.id)) {
  485. admission.status = 'interrupted'; admission.error = '执行前成员已移除'; this.saveWork(owner, admission)
  486. throw new Error('执行前成员已移除')
  487. }
  488. this.nodes.requireSource(owner, agent)
  489. if (admission.cancelRequested || admission.status === 'interrupted') throw new Error('运行已停止')
  490. admission.contextThroughSeq = run.contextThroughSeq ?? run.triggerSeq
  491. admission.submitted = true
  492. this.saveWork(owner, admission)
  493. submitted = true
  494. try {
  495. await gateway.rpc('prompt.submit', {
  496. session_id: runtimeId,
  497. text: `${rules}\n\n${text}\n${attachmentRefs.join('\n')}`,
  498. })
  499. binding.contextSeq = Math.max(binding.contextSeq ?? 0, admission.contextThroughSeq)
  500. this.store.put(owner, 'binding', key, binding)
  501. } catch (error) {
  502. // A JSON-RPC error is a definitive rejection, not a lost receipt.
  503. if (error instanceof HttpError && error.code === 'gateway_rejected') { submitted = false; const current = this.getWork(owner, run.id); current.submitted = false; this.saveWork(owner, current) }
  504. throw error
  505. }
  506. }
  507. return await completion
  508. } catch (error) {
  509. finish(error instanceof Error ? error : new Error('执行失败'))
  510. return await completion
  511. }
  512. }
  513. private contextText(owner: string, c: Conversation, agent: Agent, work: Work, after: number): string {
  514. const rootTrigger = this.store.require<Message>(owner, 'message', work.messageId)
  515. 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]))
  516. const history = this.store.messages(owner, c.id, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, true, work.conversationTaskId)
  517. 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)))
  518. work.contextThroughSeq = unfinished ? unfinished.seq - 1 : work.triggerSeq
  519. const eligible = history
  520. .filter(m => m.seq > after && m.seq <= work.triggerSeq && m.visible !== false && ['complete', 'failed', 'interrupted'].includes(m.status)
  521. && !(m.role === 'assistant' && m.agentId === agent.id) && (!m.runId || (roots.get(m.runId) ?? 0) <= rootTrigger.seq))
  522. const selected: string[] = []
  523. let size = 0, omitted = 0
  524. for (const message of [...eligible].reverse()) {
  525. const files = message.attachments.map(f => `[附件 ${f.name}](${f.sourcePath || `/api/app/files/${f.id}/download`})`).join('\n')
  526. const content = message.content.length > 24_000 ? message.content.slice(0, 12_000) + '\n[内容过长,保留首尾片段]\n' + message.content.slice(-12_000) : message.content
  527. const reasoning = message.reasoning ? `\n思考摘要:${message.reasoning.slice(0, 2000)}` : ''
  528. 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)
  529. const row = `${message.role === 'user' ? '用户' : message.agentName ?? '系统'}(${message.status}):${content}${reasoning}${tools.length ? `\n使用工具:${tools.join('、')}` : ''}${message.error ? `\n错误:${message.error}` : ''}${files ? '\n' + files : ''}`
  530. if (selected.length >= 50 || size + row.length > 30_000) { omitted++; continue }
  531. selected.unshift(row); size += row.length
  532. }
  533. // Preserve a large trigger as a bounded excerpt rather than silently losing it.
  534. if (!selected.length && eligible.length) selected.push(eligible.at(-1)!.content.slice(-30_000))
  535. 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') : ''
  536. return `${batch ? `本批次执行结果:\n${batch}\n\n` : ''}${omitted ? `较早上下文有 ${omitted} 条因长度限制省略,请勿假定已完整读取。\n\n` : ''}${selected.join('\n\n')}`
  537. }
  538. private resolveInteractions(owner: string, taskId: string): void {
  539. for (const interaction of this.store.list<WorkspaceInteraction>(owner, 'interaction')) {
  540. const binding = this.store.get<{ taskId?: string }>(owner, 'interaction-binding', interaction.id)
  541. if (binding?.taskId === taskId && !interaction.resolved) {
  542. interaction.resolved = true
  543. this.store.put(owner, 'interaction', interaction.id, interaction)
  544. this.store.event(owner, 'interaction.changed', interaction, interaction.conversationId)
  545. }
  546. }
  547. }
  548. async respond(owner: string, id: string, answer: string): Promise<void> {
  549. type Reply = WorkspaceInteraction & { answer?: string; responseState?: 'sending' | 'uncertain' | 'sent' }
  550. const interaction = this.store.require<Reply>(owner, 'interaction', id)
  551. this.nodes.requireSource(owner, this.store.require<Agent>(owner, 'agent', interaction.agentId))
  552. if (interaction.resolved) {
  553. if (interaction.answer && interaction.answer !== answer) throw new HttpError(409, '该请求已使用其他答复完成', 'interaction_answer_conflict')
  554. return
  555. }
  556. if (interaction.responseState === 'sending' || interaction.responseState === 'uncertain') throw new HttpError(409, '答复状态待确认,请核对原请求', 'interaction_uncertain')
  557. const binding = this.store.require<{ key: string; upstreamId: string; taskId: string }>(owner, 'interaction-binding', id)
  558. const live = this.live.get(binding.key)
  559. if (!live || live.taskId !== binding.taskId || live.runId !== interaction.runId) throw new HttpError(409, '请先恢复此轮连接', 'interaction_offline')
  560. if (interaction.kind === 'approval' && !['once', 'session', 'always', 'deny', 'allow', 'approve'].includes(answer)) throw new HttpError(400, '审批选项无效', 'invalid_approval')
  561. interaction.answer = answer; interaction.responseState = 'sending'
  562. this.store.put(owner, 'interaction', id, interaction)
  563. try {
  564. await live.gateway.rpc(interaction.kind === 'approval' ? 'approval.respond' : 'clarify.respond', {
  565. session_id: live.runtimeId, request_id: binding.upstreamId,
  566. ...(interaction.kind === 'approval' ? { choice: answer } : { answer }),
  567. })
  568. } catch (error) {
  569. const latest = this.store.require<Reply>(owner, 'interaction', id)
  570. if (!latest.resolved) {
  571. latest.responseState = error instanceof HttpError && error.code === 'gateway_rejected' ? undefined : 'uncertain'
  572. this.store.put(owner, 'interaction', id, latest)
  573. }
  574. throw error
  575. }
  576. interaction.resolved = true; interaction.responseState = 'sent'
  577. this.store.put(owner, 'interaction', id, interaction)
  578. this.store.event(owner, 'interaction.changed', interaction, interaction.conversationId)
  579. const current = this.getWork(owner, binding.taskId)
  580. if (current.status === 'waiting') { current.status = 'running'; this.saveWork(owner, current) }
  581. }
  582. protected async interruptTurn(owner: string, work: Work): Promise<void> {
  583. const key = this.bindingKey(work.conversationId, work.conversationTaskId, work.agentId)
  584. const live = this.live.get(key)
  585. if (live?.taskId === work.id) {
  586. await live.gateway.rpc('session.interrupt', { session_id: live.runtimeId })
  587. const current = this.getWork(owner, work.id)
  588. if (['complete', 'failed', 'interrupted'].includes(current.status)) return
  589. current.status = 'interrupted'; current.error = '已停止'
  590. this.saveWork(owner, current)
  591. live.done(new Error('已停止'))
  592. } else {
  593. const binding = this.store.get<WorkspaceBinding>(owner, 'binding', key)
  594. if (work.submitted) {
  595. if (!binding || binding.taskId !== work.id) throw new Error('无法确认待停止成员的会话身份')
  596. const gateway = new WorkspaceGateway(binding.remoteAgentId ? this.nodes.targetForAgent(owner,binding) : this.nodes.target(owner, binding.nodeId))
  597. try {
  598. await gateway.connect()
  599. const opened = await gateway.rpc('session.resume', { profile: binding.profile, session_id: binding.storedId, omit_messages: true, close_on_disconnect: false })
  600. if (opened.running) await gateway.rpc('session.interrupt', { session_id: opened.session_id })
  601. } finally { gateway.close() }
  602. }
  603. const current = this.getWork(owner, work.id)
  604. current.status = 'interrupted'; current.error = '已停止'
  605. this.saveWork(owner, current)
  606. if (current.currentMessageId) {
  607. const message = this.store.require<Message>(owner, 'message', current.currentMessageId)
  608. message.status = 'interrupted'; this.store.saveMessage(owner, message)
  609. }
  610. }
  611. this.resolveInteractions(owner, work.id)
  612. }
  613. close(): void {
  614. this.closeScheduler()
  615. for (const live of [...this.live.values()]) live.done(new Error('服务关闭'))
  616. this.live.clear()
  617. }
  618. }