runner.store.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. import { defineStore } from 'pinia'
  2. import { computed, reactive, readonly, ref } from 'vue'
  3. import { dayjs } from 'element-plus'
  4. export type RunnerStatus = 'idle' | 'connecting' | 'running' | 'finished' | 'error' | 'suspended'
  5. export type NodeStatus = 'idle' | 'running' | 'success' | 'failed' | 'suspended'
  6. export interface RunnerNodeInfo {
  7. nodeId: string
  8. nodeName: string
  9. nodeType: string
  10. }
  11. export interface RunnerNodeState extends RunnerNodeInfo {
  12. status: NodeStatus
  13. lastUpdateTime?: string
  14. track?: any
  15. }
  16. export interface RunnerExecution {
  17. runnerKey: string
  18. status: RunnerStatus
  19. startedAt: string | null
  20. finishedAt: string | null
  21. nodes: RunnerNodeState[]
  22. result?: any
  23. }
  24. interface AgentRunnerMessageBase {
  25. cmd: string
  26. time?: string
  27. }
  28. interface ConnectErrorMessage extends AgentRunnerMessageBase {
  29. cmd: 'CMD_CONNECT_ERROR_MSG'
  30. errorMsg: string
  31. }
  32. interface WelcomeMessage extends AgentRunnerMessageBase {
  33. cmd: 'CMD_WELCOME_MSG'
  34. }
  35. interface HeartbeatMessage extends AgentRunnerMessageBase {
  36. cmd: 'CMD_HEARTBEAT_MSG'
  37. }
  38. interface AgentRunningMessage extends AgentRunnerMessageBase {
  39. cmd: 'CMD_AGENT_RUNNING_MSG'
  40. }
  41. interface NodeRunningMessage extends AgentRunnerMessageBase {
  42. cmd: 'CMD_NODE_RUNNING_MSG'
  43. node: RunnerNodeInfo
  44. }
  45. interface NodeFinishMessage extends AgentRunnerMessageBase {
  46. cmd: 'CMD_NODE_FINISH_MSG'
  47. node: RunnerNodeInfo
  48. track: any
  49. }
  50. interface NodeIterationRunningMessage extends AgentRunnerMessageBase {
  51. cmd: 'CMD_NODE_ITERATION_RUNNING_MSG'
  52. count: number
  53. node: RunnerNodeInfo
  54. }
  55. interface NodeIterationStepMessage extends AgentRunnerMessageBase {
  56. cmd: 'CMD_NODE_ITERATION_STEP_MSG'
  57. count: number
  58. index: number
  59. node: RunnerNodeInfo
  60. }
  61. interface NodeIterationFinishMessage extends AgentRunnerMessageBase {
  62. cmd: 'CMD_NODE_ITERATION_FINISH_MSG'
  63. node: RunnerNodeInfo
  64. }
  65. interface AgentFinishMessage extends AgentRunnerMessageBase {
  66. cmd: 'CMD_AGENT_FINISH_MSG'
  67. result: any
  68. }
  69. interface AgentErrorMessage extends AgentRunnerMessageBase {
  70. cmd: 'CMD_AGENT_ERROR_MSG'
  71. errorMsg: string
  72. }
  73. interface AgentSuspendMessage extends AgentRunnerMessageBase {
  74. cmd: 'CMD_AGENT_SUSPEND_MSG'
  75. }
  76. export type AgentRunnerMessage =
  77. | ConnectErrorMessage
  78. | WelcomeMessage
  79. | HeartbeatMessage
  80. | AgentRunningMessage
  81. | NodeRunningMessage
  82. | NodeFinishMessage
  83. | NodeIterationRunningMessage
  84. | NodeIterationStepMessage
  85. | NodeIterationFinishMessage
  86. | AgentFinishMessage
  87. | AgentErrorMessage
  88. | AgentSuspendMessage
  89. const AGENT_RUNNER_WS_BASE = `wss://${import.meta.env.VITE_BASE_URL}/api/ws/agentRunner`
  90. export const useRunnerStore = defineStore('runner', () => {
  91. const currentRunnerKey = ref<string | null>(null)
  92. const currentStartNodeId = ref<string | null>(null)
  93. const status = ref<RunnerStatus>('idle')
  94. const errorMsg = ref<string | null>(null)
  95. const connected = ref(false)
  96. const lastHeartbeatAt = ref<number | null>(null)
  97. const agentResult = ref<any>(null)
  98. const nodesMap = reactive<Record<string, RunnerNodeState>>({})
  99. const executions = ref<RunnerExecution[]>([])
  100. const socket = ref<WebSocket | null>(null)
  101. const heartbeatTimer = ref<number | null>(null)
  102. const nodes = computed(() => Object.values(nodesMap))
  103. const getCurrentExecution = () => {
  104. if (!currentRunnerKey.value) return null
  105. return executions.value.find((item) => item.runnerKey === currentRunnerKey.value) || null
  106. }
  107. const clearHeartbeat = () => {
  108. if (heartbeatTimer.value) {
  109. window.clearInterval(heartbeatTimer.value)
  110. heartbeatTimer.value = null
  111. }
  112. }
  113. const resetState = () => {
  114. status.value = 'idle'
  115. errorMsg.value = null
  116. connected.value = false
  117. lastHeartbeatAt.value = null
  118. agentResult.value = null
  119. Object.keys(nodesMap).forEach((key) => {
  120. delete nodesMap[key]
  121. })
  122. }
  123. const closeSocket = () => {
  124. clearHeartbeat()
  125. if (socket.value) {
  126. try {
  127. socket.value.close()
  128. } catch {
  129. // ignore
  130. }
  131. socket.value = null
  132. }
  133. }
  134. const updateNodeState = (info: RunnerNodeInfo, partial: Partial<RunnerNodeState>) => {
  135. const existing = nodesMap[info.nodeId]
  136. const nextState: RunnerNodeState = {
  137. nodeId: info.nodeId,
  138. nodeName: info.nodeName,
  139. nodeType: info.nodeType,
  140. status: existing?.status ?? 'idle',
  141. ...existing,
  142. ...partial
  143. }
  144. nodesMap[info.nodeId] = nextState
  145. const execution = getCurrentExecution()
  146. if (execution) {
  147. const index = execution.nodes.findIndex((item) => item.nodeId === info.nodeId)
  148. if (index === -1) {
  149. execution.nodes.push({ ...nextState })
  150. } else {
  151. execution.nodes[index] = { ...nextState }
  152. }
  153. }
  154. }
  155. const handleMessage = (raw: MessageEvent<string>) => {
  156. let data: AgentRunnerMessage | null = null
  157. try {
  158. data = JSON.parse(raw.data)
  159. } catch {
  160. return
  161. }
  162. if (!data || typeof data !== 'object' || !('cmd' in data)) return
  163. switch (data.cmd) {
  164. /**
  165. * 连接运行器失败
  166. */
  167. case 'CMD_CONNECT_ERROR_MSG': {
  168. const msg = data as ConnectErrorMessage
  169. status.value = 'error'
  170. errorMsg.value = msg.errorMsg || '连接运行器失败'
  171. connected.value = false
  172. const execution = getCurrentExecution()
  173. if (execution) {
  174. execution.status = 'error'
  175. execution.finishedAt = msg.time || new Date().toISOString()
  176. }
  177. closeSocket()
  178. break
  179. }
  180. /**
  181. * 连接运行器成功
  182. */
  183. case 'CMD_WELCOME_MSG': {
  184. status.value = 'running'
  185. connected.value = true
  186. const execution = getCurrentExecution()
  187. if (execution) {
  188. execution.status = 'running'
  189. }
  190. break
  191. }
  192. /**
  193. * 心跳消息
  194. */
  195. case 'CMD_HEARTBEAT_MSG': {
  196. lastHeartbeatAt.value = Date.now()
  197. break
  198. }
  199. /**
  200. * 智能体运行消息
  201. */
  202. case 'CMD_AGENT_RUNNING_MSG': {
  203. status.value = 'running'
  204. const execution = getCurrentExecution()
  205. if (execution) {
  206. execution.status = 'running'
  207. }
  208. break
  209. }
  210. /**
  211. * 节点运行消息
  212. */
  213. case 'CMD_NODE_RUNNING_MSG': {
  214. const msg = data as NodeRunningMessage
  215. updateNodeState(msg.node, {
  216. status: 'running',
  217. lastUpdateTime: msg.time
  218. })
  219. break
  220. }
  221. /**
  222. * 节点运行完成消息
  223. */
  224. case 'CMD_NODE_FINISH_MSG': {
  225. const msg = data as NodeFinishMessage
  226. const isSuccess = !!msg.track?.is_success
  227. updateNodeState(msg.node, {
  228. status: isSuccess ? 'success' : 'failed',
  229. lastUpdateTime: msg.time,
  230. track: msg.track
  231. })
  232. break
  233. }
  234. /**
  235. * 节点迭代运行消息
  236. */
  237. case 'CMD_NODE_ITERATION_RUNNING_MSG': {
  238. const msg = data as NodeIterationRunningMessage
  239. updateNodeState(msg.node, {
  240. status: 'running',
  241. lastUpdateTime: msg.time
  242. })
  243. break
  244. }
  245. /**
  246. * 节点迭代运行步骤消息
  247. */
  248. case 'CMD_NODE_ITERATION_STEP_MSG': {
  249. const msg = data as NodeIterationStepMessage
  250. updateNodeState(msg.node, {
  251. status: 'running',
  252. lastUpdateTime: msg.time
  253. })
  254. break
  255. }
  256. /**
  257. * 节点迭代运行完成消息
  258. */
  259. case 'CMD_NODE_ITERATION_FINISH_MSG': {
  260. const msg = data as NodeIterationFinishMessage
  261. updateNodeState(msg.node, {
  262. status: 'success',
  263. lastUpdateTime: msg.time
  264. })
  265. break
  266. }
  267. /**
  268. * 智能体运行完成消息
  269. */
  270. case 'CMD_AGENT_FINISH_MSG': {
  271. const msg = data as AgentFinishMessage
  272. status.value = 'finished'
  273. agentResult.value = msg.result
  274. const execution = getCurrentExecution()
  275. if (execution) {
  276. execution.status = 'finished'
  277. execution.finishedAt = msg.time || new Date().toISOString()
  278. execution.result = msg.result
  279. }
  280. closeSocket()
  281. break
  282. }
  283. /**
  284. * 智能体运行异常失败
  285. */
  286. case 'CMD_AGENT_ERROR_MSG': {
  287. const msg = data as AgentErrorMessage
  288. status.value = 'error'
  289. agentResult.value = msg.errorMsg || '智能体运行异常失败'
  290. const execution = getCurrentExecution()
  291. if (execution) {
  292. execution.status = 'finished'
  293. execution.finishedAt = msg.time || new Date().toISOString()
  294. execution.result = msg.errorMsg || '智能体运行异常失败'
  295. }
  296. closeSocket()
  297. break
  298. }
  299. /**
  300. * 智能体运行被挂起
  301. */
  302. case 'CMD_AGENT_SUSPEND_MSG': {
  303. status.value = 'suspended'
  304. const execution = getCurrentExecution()
  305. if (execution) {
  306. execution.status = 'suspended'
  307. }
  308. Object.values(nodesMap).forEach((node) => {
  309. if (node.status === 'running') {
  310. updateNodeState(node, {
  311. status: 'suspended',
  312. lastUpdateTime: data?.time || node.lastUpdateTime
  313. })
  314. }
  315. })
  316. break
  317. }
  318. default:
  319. break
  320. }
  321. }
  322. const startHeartbeat = () => {
  323. clearHeartbeat()
  324. heartbeatTimer.value = window.setInterval(() => {
  325. if (!socket.value || socket.value.readyState !== WebSocket.OPEN) return
  326. socket.value.send(JSON.stringify({ cmd: 'CMD_HEARTBEAT_MSG' }))
  327. }, 10000)
  328. }
  329. const startRunner = (runnerKey: string, startNodeId?: string) => {
  330. if (!runnerKey) return
  331. closeSocket()
  332. resetState()
  333. currentRunnerKey.value = runnerKey
  334. currentStartNodeId.value = startNodeId || null
  335. status.value = 'connecting'
  336. executions.value.unshift({
  337. runnerKey,
  338. status: 'connecting',
  339. startedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
  340. finishedAt: null,
  341. nodes: [],
  342. result: undefined
  343. })
  344. const url = `${AGENT_RUNNER_WS_BASE}?agentRunnerKey=${encodeURIComponent(runnerKey)}`
  345. const ws = new WebSocket(url)
  346. socket.value = ws
  347. ws.onopen = () => {
  348. connected.value = true
  349. status.value = 'running'
  350. startHeartbeat()
  351. const execution = getCurrentExecution()
  352. if (execution) {
  353. execution.status = 'running'
  354. }
  355. }
  356. ws.onmessage = (event) => {
  357. handleMessage(event as MessageEvent<string>)
  358. }
  359. ws.onerror = () => {
  360. status.value = 'error'
  361. errorMsg.value = errorMsg.value || '运行器连接异常'
  362. const execution = getCurrentExecution()
  363. if (execution) {
  364. execution.status = 'error'
  365. if (!execution.finishedAt) {
  366. execution.finishedAt = dayjs().format('YYYY-MM-DD HH:mm:ss')
  367. }
  368. }
  369. }
  370. ws.onclose = () => {
  371. connected.value = false
  372. clearHeartbeat()
  373. if (status.value === 'running' || status.value === 'connecting') {
  374. status.value = 'finished'
  375. const execution = getCurrentExecution()
  376. if (execution && !execution.finishedAt) {
  377. execution.status = 'finished'
  378. execution.finishedAt = dayjs().format('YYYY-MM-DD HH:mm:ss')
  379. }
  380. }
  381. }
  382. }
  383. const stopRunner = () => {
  384. closeSocket()
  385. status.value = 'finished'
  386. const execution = getCurrentExecution()
  387. if (execution && !execution.finishedAt) {
  388. execution.status = 'finished'
  389. execution.finishedAt = dayjs().format('YYYY-MM-DD HH:mm:ss')
  390. }
  391. }
  392. const resetRunner = () => {
  393. closeSocket()
  394. currentRunnerKey.value = null
  395. currentStartNodeId.value = null
  396. resetState()
  397. }
  398. return {
  399. currentRunnerKey: readonly(currentRunnerKey),
  400. currentStartNodeId: readonly(currentStartNodeId),
  401. status,
  402. errorMsg,
  403. connected,
  404. lastHeartbeatAt,
  405. nodes,
  406. agentResult,
  407. executions,
  408. startRunner,
  409. stopRunner,
  410. resetRunner
  411. }
  412. })