workspaceRuntime.ts 31 KB

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