nolo-cli 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +0 -32
  2. package/agentRuntimeCommands.ts +1 -1
  3. package/client/compactDialog.ts +2 -5
  4. package/commandRegistry.ts +2 -2
  5. package/machineCommands.ts +28 -3
  6. package/package.json +5 -25
  7. package/ai/agent/_executeModel.ts +0 -118
  8. package/ai/agent/agentSlice.ts +0 -525
  9. package/ai/agent/appWorkingMemory.ts +0 -126
  10. package/ai/agent/avatarUtils.ts +0 -24
  11. package/ai/agent/buildEditingContext.ts +0 -373
  12. package/ai/agent/buildSystemPrompt.ts +0 -532
  13. package/ai/agent/cleanAgentMessages.ts +0 -140
  14. package/ai/agent/cliChatClient.ts +0 -119
  15. package/ai/agent/contextCompiler.ts +0 -107
  16. package/ai/agent/contextLayerContract.ts +0 -44
  17. package/ai/agent/createAgentSchema.ts +0 -234
  18. package/ai/agent/executeToolCall.ts +0 -58
  19. package/ai/agent/fetchAgentContexts.ts +0 -42
  20. package/ai/agent/generatePrompt.ts +0 -3
  21. package/ai/agent/getFullChatContextKeys.ts +0 -168
  22. package/ai/agent/hooks/fetchPublicAgents.ts +0 -133
  23. package/ai/agent/hooks/useAgentConfig.ts +0 -61
  24. package/ai/agent/hooks/useAgentDialog.ts +0 -35
  25. package/ai/agent/hooks/useAgentFormValidation.ts +0 -202
  26. package/ai/agent/hooks/usePublicAgents.ts +0 -473
  27. package/ai/agent/persistMessageWithFixedId.ts +0 -37
  28. package/ai/agent/planSlice.ts +0 -259
  29. package/ai/agent/referenceUtils.ts +0 -229
  30. package/ai/agent/runAgentBackground.ts +0 -238
  31. package/ai/agent/runAgentClientLoop.ts +0 -138
  32. package/ai/agent/runtimeGuidance.ts +0 -97
  33. package/ai/agent/runtimeServerBase.ts +0 -37
  34. package/ai/agent/server/fetchPublicAgents.ts +0 -128
  35. package/ai/agent/startParallelAgentStreams.ts +0 -424
  36. package/ai/agent/startupProtocol.ts +0 -53
  37. package/ai/agent/streamAgentChatTurn.ts +0 -1278
  38. package/ai/agent/streamAgentChatTurnUtils.ts +0 -738
  39. package/ai/agent/types.ts +0 -71
  40. package/ai/agent/utils/imageOutput.ts +0 -33
  41. package/ai/agent/utils/sortUtils.ts +0 -250
  42. package/ai/agent/web/referencePickerUtils.ts +0 -146
  43. package/ai/ai.locale.ts +0 -1079
  44. package/ai/chat/accumulateToolCallChunks.ts +0 -95
  45. package/ai/chat/fetchUtils.native.ts +0 -276
  46. package/ai/chat/fetchUtils.ts +0 -153
  47. package/ai/chat/parseApiError.ts +0 -64
  48. package/ai/chat/parseMultilineSSE.ts +0 -95
  49. package/ai/chat/sendOpenAICompletionsRequest.native.ts +0 -682
  50. package/ai/chat/sendOpenAICompletionsRequest.ts +0 -703
  51. package/ai/chat/sendOpenAIResponseRequest.ts +0 -491
  52. package/ai/chat/shouldUseServerProxy.ts +0 -18
  53. package/ai/chat/sseClient.native.ts +0 -91
  54. package/ai/chat/sseClient.ts +0 -67
  55. package/ai/chat/streamReader.native.ts +0 -31
  56. package/ai/chat/streamReader.ts +0 -62
  57. package/ai/chat/updateTotalUsage.ts +0 -72
  58. package/ai/context/buildReferenceContext.ts +0 -437
  59. package/ai/context/calculateContextUsage.ts +0 -133
  60. package/ai/context/retention.ts +0 -165
  61. package/ai/context/tokenUtils.ts +0 -78
  62. package/ai/index.ts +0 -1
  63. package/ai/llm/calculateGeminiImageTokens.ts +0 -57
  64. package/ai/llm/deepinfra.ts +0 -28
  65. package/ai/llm/fireworks.ts +0 -50
  66. package/ai/llm/generateRequestBody.ts +0 -165
  67. package/ai/llm/getModelContextWindow.ts +0 -84
  68. package/ai/llm/getNoloKey.ts +0 -31
  69. package/ai/llm/getPricing.ts +0 -199
  70. package/ai/llm/hooks/useModelPricing.ts +0 -75
  71. package/ai/llm/imagePricing.ts +0 -40
  72. package/ai/llm/isResponseAPIModel.ts +0 -13
  73. package/ai/llm/mimo.ts +0 -71
  74. package/ai/llm/mistral.ts +0 -22
  75. package/ai/llm/modelAvatar.ts +0 -427
  76. package/ai/llm/models.ts +0 -45
  77. package/ai/llm/openrouterModels.ts +0 -269
  78. package/ai/llm/providers.ts +0 -306
  79. package/ai/llm/reasoningModels.ts +0 -28
  80. package/ai/llm/types.ts +0 -59
  81. package/ai/llm/usageRequestOptions.ts +0 -59
  82. package/ai/memory/capture.ts +0 -148
  83. package/ai/memory/consolidate.ts +0 -104
  84. package/ai/memory/delete.ts +0 -147
  85. package/ai/memory/overlay.ts +0 -84
  86. package/ai/memory/query.ts +0 -38
  87. package/ai/memory/queryShared.ts +0 -160
  88. package/ai/memory/rank.ts +0 -105
  89. package/ai/memory/recentRelationshipRecap.ts +0 -249
  90. package/ai/memory/remember.ts +0 -167
  91. package/ai/memory/runtime.ts +0 -76
  92. package/ai/memory/store.ts +0 -20
  93. package/ai/memory/storeShared.ts +0 -76
  94. package/ai/memory/types.ts +0 -46
  95. package/ai/memory/understanding.ts +0 -349
  96. package/ai/memory/understandingGreeting.ts +0 -264
  97. package/ai/messages/type.ts +0 -20
  98. package/ai/policy/personalizationDialog.ts +0 -333
  99. package/ai/policy/runtimePolicy.ts +0 -440
  100. package/ai/policy/selfUpdateFields.ts +0 -48
  101. package/ai/policy/types.ts +0 -64
  102. package/ai/skills/referenceRuntime.ts +0 -274
  103. package/ai/skills/skillDiagnostics.ts +0 -251
  104. package/ai/skills/skillDocBuilder.ts +0 -139
  105. package/ai/skills/skillDocProtocol.ts +0 -434
  106. package/ai/skills/skillReferenceSummary.ts +0 -63
  107. package/ai/skills/skillSummaryMarker.ts +0 -26
  108. package/ai/token/calculatePrice.ts +0 -544
  109. package/ai/token/db.ts +0 -98
  110. package/ai/token/externalToolCost.ts +0 -330
  111. package/ai/token/hooks/useRecords.ts +0 -65
  112. package/ai/token/missingUsageEstimate.ts +0 -42
  113. package/ai/token/modelUsageQuery.ts +0 -252
  114. package/ai/token/normalizeUsage.ts +0 -84
  115. package/ai/token/openaiImageGenerationUsage.ts +0 -56
  116. package/ai/token/prepareTokenUsageData.ts +0 -88
  117. package/ai/token/query.ts +0 -88
  118. package/ai/token/queryUserTokens.ts +0 -59
  119. package/ai/token/resolveBillingTarget.ts +0 -52
  120. package/ai/token/saveTokenRecord.ts +0 -53
  121. package/ai/token/serverDialogProjection.ts +0 -78
  122. package/ai/token/serverTokenWriter.ts +0 -143
  123. package/ai/token/stats.ts +0 -21
  124. package/ai/token/tokenThunks.ts +0 -24
  125. package/ai/token/types.ts +0 -93
  126. package/ai/tools/agent/agentTools.ts +0 -176
  127. package/ai/tools/agent/agentUpdateShared.ts +0 -311
  128. package/ai/tools/agent/callAgentTool.ts +0 -139
  129. package/ai/tools/agent/createAgentTool.ts +0 -512
  130. package/ai/tools/agent/createDialogTool.ts +0 -69
  131. package/ai/tools/agent/createSkillAgentTool.ts +0 -62
  132. package/ai/tools/agent/parallelBudget.ts +0 -221
  133. package/ai/tools/agent/presets/appBuilderPreset.ts +0 -145
  134. package/ai/tools/agent/runLlmTool.ts +0 -96
  135. package/ai/tools/agent/runStreamingAgentTool.ts +0 -73
  136. package/ai/tools/agent/skillAgentArgs.ts +0 -106
  137. package/ai/tools/agent/skillAgentPreset.ts +0 -89
  138. package/ai/tools/agent/streamParallelAgentsTool.ts +0 -122
  139. package/ai/tools/agent/updateAgentTool.ts +0 -96
  140. package/ai/tools/agent/updateSelfTool.ts +0 -113
  141. package/ai/tools/amazonProductScraperTool.ts +0 -86
  142. package/ai/tools/apifyActorClient.ts +0 -45
  143. package/ai/tools/appEditGuard.ts +0 -372
  144. package/ai/tools/appReadSnapshot.ts +0 -153
  145. package/ai/tools/appTools.ts +0 -1549
  146. package/ai/tools/applyEditTool.ts +0 -256
  147. package/ai/tools/applyLineEditsTool.ts +0 -312
  148. package/ai/tools/browserTools/click.ts +0 -33
  149. package/ai/tools/browserTools/closeSession.ts +0 -29
  150. package/ai/tools/browserTools/common.ts +0 -27
  151. package/ai/tools/browserTools/openSession.ts +0 -48
  152. package/ai/tools/browserTools/readContent.ts +0 -38
  153. package/ai/tools/browserTools/selectOption.ts +0 -46
  154. package/ai/tools/browserTools/typeText.ts +0 -42
  155. package/ai/tools/category/createCategoryTool.ts +0 -66
  156. package/ai/tools/category/queryContentsByCategoryTool.ts +0 -69
  157. package/ai/tools/category/updateContentCategoryTool.ts +0 -75
  158. package/ai/tools/cfBrowserTools.ts +0 -319
  159. package/ai/tools/cfSpeechToTextTool.ts +0 -49
  160. package/ai/tools/checkEnvTool.ts +0 -65
  161. package/ai/tools/cloudflareCrawlTool.ts +0 -289
  162. package/ai/tools/codeSearchTool.ts +0 -111
  163. package/ai/tools/codeTools.ts +0 -101
  164. package/ai/tools/createDocTool.ts +0 -132
  165. package/ai/tools/createPlanTool.ts +0 -999
  166. package/ai/tools/createSkillDocTool.ts +0 -155
  167. package/ai/tools/createWorkflowTool.ts +0 -154
  168. package/ai/tools/deepseekOcrTool.ts +0 -34
  169. package/ai/tools/delayTool.ts +0 -31
  170. package/ai/tools/deleteSpacesTool.ts +0 -325
  171. package/ai/tools/deleteSpacesToolModel.ts +0 -159
  172. package/ai/tools/devReloadUtils.ts +0 -29
  173. package/ai/tools/dialogMessageSearch.ts +0 -137
  174. package/ai/tools/doctorSkillTool.ts +0 -72
  175. package/ai/tools/ecommerceScraperTool.ts +0 -86
  176. package/ai/tools/emailTools.ts +0 -549
  177. package/ai/tools/evalSkillTool.ts +0 -92
  178. package/ai/tools/exaSearchTool.ts +0 -64
  179. package/ai/tools/execBashTool.ts +0 -379
  180. package/ai/tools/executeSqlTool.ts +0 -192
  181. package/ai/tools/fetchWebpageSupport.ts +0 -309
  182. package/ai/tools/fetchWebpageTool.ts +0 -84
  183. package/ai/tools/geminiImagePreviewTool.ts +0 -361
  184. package/ai/tools/generateDocxTool.ts +0 -215
  185. package/ai/tools/googleSearchScraperTool.ts +0 -106
  186. package/ai/tools/importDataTool.ts +0 -133
  187. package/ai/tools/importSkillTool.ts +0 -162
  188. package/ai/tools/index.ts +0 -1858
  189. package/ai/tools/listFilesTool.ts +0 -82
  190. package/ai/tools/listUserSpacesTool.ts +0 -113
  191. package/ai/tools/modelUsageTools.ts +0 -142
  192. package/ai/tools/olmOcrTool.ts +0 -34
  193. package/ai/tools/openaiImageTool.ts +0 -218
  194. package/ai/tools/paddleOcrTool.ts +0 -34
  195. package/ai/tools/prepareTools.ts +0 -23
  196. package/ai/tools/readDocTool.ts +0 -84
  197. package/ai/tools/readFileTool.ts +0 -211
  198. package/ai/tools/readTool.ts +0 -163
  199. package/ai/tools/readXPostTool.ts +0 -233
  200. package/ai/tools/rememberMemoryTool.ts +0 -84
  201. package/ai/tools/remotionVideoTool.ts +0 -151
  202. package/ai/tools/searchDialogMessagesTool.ts +0 -222
  203. package/ai/tools/searchRepoTool.ts +0 -115
  204. package/ai/tools/searchWorkspaceTool.ts +0 -259
  205. package/ai/tools/skillFollowup.ts +0 -86
  206. package/ai/tools/surfWeatherTool.ts +0 -169
  207. package/ai/tools/table/addTableRowTool.ts +0 -217
  208. package/ai/tools/table/createTableTool.ts +0 -315
  209. package/ai/tools/table/rowTools.ts +0 -366
  210. package/ai/tools/table/schemaTools.ts +0 -244
  211. package/ai/tools/table/shareTableTool.ts +0 -148
  212. package/ai/tools/table/toolShared.ts +0 -129
  213. package/ai/tools/toolApiClient.ts +0 -198
  214. package/ai/tools/toolNameAliases.ts +0 -57
  215. package/ai/tools/toolResultError.ts +0 -42
  216. package/ai/tools/toolRunSlice.ts +0 -303
  217. package/ai/tools/toolSchemaCompatibility.ts +0 -53
  218. package/ai/tools/toolVisibility.ts +0 -4
  219. package/ai/tools/types.ts +0 -20
  220. package/ai/tools/uiAskChoiceTool.ts +0 -104
  221. package/ai/tools/updateContentTitleTool.ts +0 -84
  222. package/ai/tools/updateDocTool.ts +0 -105
  223. package/ai/tools/updateUserPreferenceProfileTool.ts +0 -145
  224. package/ai/tools/whisperTool.ts +0 -77
  225. package/ai/tools/writeFileTool.ts +0 -210
  226. package/ai/tools/youtubeScraperTool.ts +0 -116
  227. package/ai/tools/ziweiChartTool.ts +0 -678
  228. package/ai/types.ts +0 -55
  229. package/ai/workflow/workflowExecutor.ts +0 -323
  230. package/ai/workflow/workflowSlice.ts +0 -73
  231. package/ai/workflow/workflowTypes.ts +0 -106
  232. package/connector-experimental/index.ts +0 -5
@@ -1,1278 +0,0 @@
1
- // 文件路径: packages/ai/agent/streamAgentChatTurn.ts
2
- import { extractCustomId } from "core/prefix";
3
- import { createDialogMessageKeyAndId } from "database/keys";
4
- import { DataType } from "create/types";
5
-
6
- import type { RootState } from "app/store";
7
- import { patch, read } from "database/dbSlice";
8
- import { generateRequestBody } from "ai/llm/generateRequestBody";
9
- import {
10
- selectCurrentDialogConfig,
11
- selectDialogConfigByKey,
12
- addActiveController,
13
- removeActiveController,
14
- selectPendingUserInputQueue,
15
- dequeueUserInput,
16
- clearPendingUserInputQueue,
17
- } from "chat/dialog/dialogSlice";
18
- import { removeTransientMessage, selectAllMsgs } from "chat/messages/messageSlice";
19
- import {
20
- selectContextRetention,
21
- selectMaxExecutionTime,
22
- selectCurrentServer,
23
- } from "app/settings/settingSlice";
24
- import { filterAndCleanMessages } from "integrations/openai/filterAndCleanMessages";
25
- import {
26
- getFullChatContextKeys,
27
- deduplicateContextKeys,
28
- } from "ai/agent/getFullChatContextKeys";
29
- import type { Agent } from "app/types";
30
- import { isResponseAPIModel } from "ai/llm/isResponseAPIModel";
31
- import { getModelContextWindow } from "ai/llm/getModelContextWindow";
32
-
33
- import {
34
- sendOpenAICompletionsRequest,
35
- type CompletionMeta,
36
- } from "../chat/sendOpenAICompletionsRequest";
37
- import { sendOpenAIResponseRequest } from "../chat/sendOpenAIResponseRequest";
38
-
39
- import type { AgentRuntimeOptions } from "./types";
40
- import { buildAgentViewMessages } from "./cleanAgentMessages";
41
- import { extractCategorizedMentions, type CategorizedMentions } from "create/editor/utils/slateUtils";
42
- import { resolveReferenceAssets, resolveToolsFromKeys } from "./referenceUtils";
43
- import { estimateTokenCount } from "ai/context/tokenUtils";
44
- import {
45
- applyImageConfigRuntimeOverride,
46
- buildStaticContexts,
47
- compressOldToolResults,
48
- buildDynamicContexts,
49
- mergeContexts,
50
- hasImageInMessages,
51
- mergeAgentToolsWithRuntime,
52
- trimMessagesWithSummary,
53
- validateAccessAndBalance,
54
- } from "./streamAgentChatTurnUtils";
55
- import { buildCliPrompt } from "./cliPrompt";
56
- import { createCliChatTurnStream } from "./cliChatClient";
57
- import { getCliChatSession, startCliChatSession } from "./cliChatClient";
58
- import { messageStreaming, prepareAndPersistUserMessage } from "chat/messages/messageSlice";
59
- import { selectCurrentToken, selectUserId } from "auth/authSlice";
60
- import { persistMessageWithFixedId } from "./persistMessageWithFixedId";
61
- import { updateTotalUsage } from "../chat/updateTotalUsage";
62
- import { createSSEParser } from "../chat/parseMultilineSSE";
63
- import {
64
- extractAgentRuntimeServerBase,
65
- normalizeServerOrigin,
66
- } from "./runtimeServerBase";
67
-
68
- const buildParallelMessageMetadata = (
69
- agentConfig: Pick<Agent, "dbKey" | "name">,
70
- runtimeOptions?: AgentRuntimeOptions,
71
- ) => {
72
- const rawName =
73
- typeof agentConfig?.name === "string" ? agentConfig.name.trim() : "";
74
- return {
75
- agentKey: agentConfig.dbKey,
76
- cybotKey: agentConfig.dbKey,
77
- ...(rawName ? { agentName: rawName } : {}),
78
- ...(runtimeOptions?.parallelSessionId
79
- ? { parallelSessionId: runtimeOptions.parallelSessionId }
80
- : {}),
81
- ...(runtimeOptions?.parallelBranchId
82
- ? { parallelBranchId: runtimeOptions.parallelBranchId }
83
- : {}),
84
- ...(runtimeOptions?.parallelLabel
85
- ? { parallelLabel: runtimeOptions.parallelLabel }
86
- : {}),
87
- ...(runtimeOptions?.parallelIndex !== undefined
88
- ? { parallelIndex: runtimeOptions.parallelIndex }
89
- : {}),
90
- };
91
- };
92
-
93
- const filterMessagesForParallelBranch = (
94
- messages: any[],
95
- runtimeOptions?: AgentRuntimeOptions,
96
- ) => {
97
- if (!runtimeOptions?.parallelSessionId) return messages;
98
-
99
- return messages.filter((message: any) => {
100
- const sessionId = message?.parallelSessionId;
101
- if (sessionId !== runtimeOptions.parallelSessionId) {
102
- return true;
103
- }
104
- return message?.parallelBranchId === runtimeOptions.parallelBranchId;
105
- });
106
- };
107
-
108
- const formatMachineAgentRunError = async (response: Response): Promise<string> => {
109
- const errorText = await response.text();
110
- let payload: any = null;
111
- try {
112
- payload = errorText ? JSON.parse(errorText) : null;
113
- } catch {
114
- payload = null;
115
- }
116
-
117
- const reason = typeof payload?.reason === "string" ? payload.reason : "";
118
- if (response.status === 409) {
119
- if (reason === "bound_machine_unavailable") {
120
- return "绑定的电脑不在线。请确认这台电脑已开机并重新运行连接命令。";
121
- }
122
- if (reason === "connector_offline") {
123
- return "电脑在线,但连接器未连接。请在这台电脑上重新运行连接命令后再试。";
124
- }
125
- if (reason === "missing_capability") {
126
- return "这台电脑没有对应的 CLI 能力。请安装对应 CLI,或把 Agent 绑定到另一台电脑。";
127
- }
128
- }
129
-
130
- const message =
131
- typeof payload?.message === "string" && payload.message.trim()
132
- ? payload.message.trim()
133
- : typeof payload?.error === "string" && payload.error.trim()
134
- ? payload.error.trim()
135
- : errorText.trim();
136
- return message || `Machine agent run failed (${response.status})`;
137
- };
138
-
139
- /** streamAgentChatTurn 参数(聊天轮次专用) */
140
- export interface StreamAgentChatTurnArgs {
141
- agentKey: string;
142
- userInput: string | any[];
143
- serverBase?: string;
144
- dialogKey?: string; // 可选。显式指定目标对话,不传则使用当前活跃对话。
145
- isStreaming?: boolean;
146
- parentMessageId?: string;
147
- runtimeOptions?: AgentRuntimeOptions;
148
- }
149
-
150
- /**
151
- * 真正用于“聊天轮次”的流式 Agent 调用(带 Agent Loop):
152
- * - 每轮检查权限 & 余额
153
- * - 每轮重建上下文 & 消息
154
- * - 调用 Completions/Response API
155
- * - 对 Completions 模型,基于 tool_calls / handoff / pending 决定是否继续
156
- */
157
- export const streamAgentChatTurnHandler = async (
158
- args: StreamAgentChatTurnArgs,
159
- thunkApi: any,
160
- ) => {
161
- const { agentKey, userInput, dialogKey: explicitDialogKey, parentMessageId, runtimeOptions } = args;
162
- const { getState, dispatch, rejectWithValue } = thunkApi;
163
- const state = getState() as RootState;
164
-
165
- // 🚀 额外引入一个 Loop 控制器,用于中止整个 Agent 循环
166
- const loopController = new AbortController();
167
- const isAbortError = (error?: { name?: string } | null) =>
168
- error?.name === "AbortError" || loopController.signal.aborted || thunkApi.signal.aborted;
169
- const onAbort = () => loopController.abort();
170
- thunkApi.signal.addEventListener("abort", onAbort);
171
- let loopKey: string | null = null;
172
- let runtimeDialogKey: string | null = explicitDialogKey ?? null;
173
- let remoteTransientMessageId: string | null = null;
174
- let remoteTransientMessageFinalized = false;
175
-
176
- try {
177
- let totalTurnUsage: any = null;
178
- // 1. 读取 Agent 配置
179
- const agentConfig = (await dispatch(read({ dbKey: agentKey })).unwrap()) as Agent;
180
- if (!agentConfig) {
181
- return rejectWithValue(`Agent config not found for ID: ${agentKey}`);
182
- }
183
-
184
- // ── CLI Agent 专用路由 ────────────────────────────────────────────────
185
- // CLI 共享 prompt / model 这些入口能力,但不复用本地 tool-call 循环。
186
- if (agentConfig.apiSource === "cli") {
187
- const currentState = getState() as RootState;
188
- const w =
189
- typeof globalThis !== "undefined" && (globalThis as any).window
190
- ? (globalThis as any).window
191
- : null;
192
- if (w) w.__LOOP_STOP_REASON__ = null;
193
-
194
- const userText = typeof userInput === "string"
195
- ? userInput
196
- : (userInput as any[]).find((p) => p.type === "text")?.text ?? "";
197
-
198
- const prompt = buildCliPrompt(agentConfig.prompt, userText);
199
-
200
- // 生成消息 key
201
- const dialogConfig =
202
- selectDialogConfigByKey(currentState, explicitDialogKey) ??
203
- selectCurrentDialogConfig(currentState);
204
- if (!dialogConfig) {
205
- return rejectWithValue("Dialog config not found");
206
- }
207
-
208
- const dialogKey = explicitDialogKey || dialogConfig.dbKey;
209
- if (!dialogKey) {
210
- return rejectWithValue("当前对话不存在,无法发送消息。");
211
- }
212
- runtimeDialogKey = dialogKey;
213
- const dialogId = extractCustomId(dialogKey);
214
- loopKey = `loop:${dialogId}`;
215
- dispatch(addActiveController({ messageId: loopKey, controller: loopController, dialogKey }));
216
-
217
- const { key: msgKey, messageId } = createDialogMessageKeyAndId(dialogId);
218
- const cliMessageMetadata = buildParallelMessageMetadata(
219
- agentConfig,
220
- runtimeOptions,
221
- );
222
- const boundMachineId =
223
- typeof (agentConfig as any).runtimeBinding?.machineId === "string"
224
- ? (agentConfig as any).runtimeBinding.machineId.trim()
225
- : "";
226
-
227
- if (boundMachineId) {
228
- const token = selectCurrentToken(currentState);
229
- const authHeader = token ? `Bearer ${token}` : "";
230
- const rawMessages = filterMessagesForParallelBranch(
231
- selectAllMsgs(currentState, dialogId),
232
- runtimeOptions,
233
- );
234
- const visibleMessages = buildAgentViewMessages(
235
- rawMessages as any,
236
- agentConfig.dbKey,
237
- );
238
- const cleanedMessages = filterAndCleanMessages(visibleMessages);
239
- const currentServer = selectCurrentServer(currentState);
240
- remoteTransientMessageId = messageId;
241
- let accumulated = "";
242
- let totalTurnUsage: any = undefined;
243
- const buildMachineAssistantMessage = () => ({
244
- id: messageId,
245
- dbKey: msgKey,
246
- role: "assistant" as const,
247
- content: accumulated,
248
- ...cliMessageMetadata,
249
- userId: selectUserId(getState() as RootState),
250
- });
251
-
252
- dispatch(messageStreaming({
253
- id: messageId,
254
- dialogId,
255
- dbKey: msgKey,
256
- content: "",
257
- role: "assistant",
258
- ...cliMessageMetadata,
259
- }));
260
-
261
- const rejectMachineStream = async (message: string) => {
262
- if (accumulated.length > 0) {
263
- await persistMessageWithFixedId(dispatch, buildMachineAssistantMessage());
264
- } else {
265
- dispatch(removeTransientMessage(messageId));
266
- }
267
- remoteTransientMessageFinalized = true;
268
- return rejectWithValue(message);
269
- };
270
-
271
- const machineResponse = await fetch(`${currentServer.replace(/\/+$/, "")}/api/agent/run`, {
272
- method: "POST",
273
- headers: {
274
- "Content-Type": "application/json",
275
- Accept: "text/event-stream",
276
- ...(authHeader ? { Authorization: authHeader } : {}),
277
- },
278
- body: JSON.stringify({
279
- agentKey,
280
- userInput: userText,
281
- messages: cleanedMessages,
282
- stream: true,
283
- runtimeContext: {
284
- surface: "web",
285
- host: "browser",
286
- runtime: "react",
287
- entrypoint: "chat-dialog",
288
- capabilities: ["streaming", "dialog-ui", "machine-bound-cli"],
289
- },
290
- ...((dialogConfig as any)?.spaceId ? { spaceId: (dialogConfig as any).spaceId } : {}),
291
- }),
292
- signal: loopController.signal,
293
- });
294
-
295
- if (!machineResponse.ok) {
296
- return await rejectMachineStream(
297
- await formatMachineAgentRunError(machineResponse),
298
- );
299
- }
300
-
301
- const reader = machineResponse.body?.getReader();
302
- if (!reader) {
303
- return await rejectMachineStream("无法读取电脑端 Agent 流式响应");
304
- }
305
-
306
- const decoder = new TextDecoder();
307
- const parseSSE = createSSEParser();
308
- const abortMachineStream = async () => {
309
- if (w) w.__LOOP_STOP_REASON__ = "aborted";
310
- if (accumulated.length <= 0) return;
311
- await persistMessageWithFixedId(dispatch, buildMachineAssistantMessage());
312
- };
313
-
314
- try {
315
- while (true) {
316
- const { done, value } = await reader.read();
317
- if (done) break;
318
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
319
- await abortMachineStream();
320
- return;
321
- }
322
-
323
- const payloads = parseSSE(
324
- decoder.decode(value, { stream: true }),
325
- );
326
- for (const payload of payloads) {
327
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
328
- await abortMachineStream();
329
- return;
330
- }
331
- if (payload.type === "error") {
332
- return await rejectMachineStream(
333
- payload.message || "电脑端 Agent 执行失败",
334
- );
335
- }
336
- if (payload.type === "text" && typeof payload.content === "string") {
337
- accumulated += payload.content;
338
- dispatch(messageStreaming({
339
- id: messageId,
340
- dialogId,
341
- dbKey: msgKey,
342
- content: accumulated,
343
- role: "assistant",
344
- ...cliMessageMetadata,
345
- }));
346
- }
347
- if (payload.type === "done") {
348
- totalTurnUsage = payload.usage;
349
- }
350
- }
351
- }
352
-
353
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
354
- await abortMachineStream();
355
- return;
356
- }
357
-
358
- await persistMessageWithFixedId(dispatch, buildMachineAssistantMessage());
359
- remoteTransientMessageFinalized = true;
360
- } finally {
361
- try {
362
- await reader.cancel();
363
- } catch {
364
- // ignore
365
- }
366
- }
367
-
368
- return {
369
- usage: totalTurnUsage ?? undefined,
370
- };
371
- }
372
-
373
- let cliSessionId = dialogConfig.cliSessionId ?? null;
374
-
375
- // 先创建一条空的流式消息(让用户立刻看到 loading 状态)
376
- dispatch(messageStreaming({
377
- id: messageId,
378
- dialogId,
379
- dbKey: msgKey,
380
- content: "",
381
- role: "assistant",
382
- ...cliMessageMetadata,
383
- }));
384
-
385
- const ensureCliSession = async () => {
386
- if (cliSessionId) {
387
- const existing = await getCliChatSession(
388
- { getState },
389
- { sessionId: cliSessionId },
390
- ).catch(() => null);
391
- if (existing?.ok && existing?.session?.sessionId) {
392
- return cliSessionId;
393
- }
394
- }
395
-
396
- const started = await startCliChatSession(
397
- { getState },
398
- {
399
- cliProvider: agentConfig.cliProvider || "copilot",
400
- model: agentConfig.model || undefined,
401
- systemPrompt: agentConfig.prompt || undefined,
402
- },
403
- );
404
-
405
- const newSessionId =
406
- typeof started?.sessionId === "string" ? started.sessionId : null;
407
- if (!newSessionId) {
408
- throw new Error("无法创建 CLI session。");
409
- }
410
-
411
- cliSessionId = newSessionId;
412
- const patchResult = dispatch(
413
- patch({
414
- dbKey: dialogKey,
415
- changes: {
416
- cliSessionId: newSessionId,
417
- },
418
- })
419
- ) as any;
420
- try {
421
- if (typeof patchResult?.unwrap === "function") {
422
- await patchResult.unwrap();
423
- } else {
424
- await patchResult;
425
- }
426
- } catch {
427
- // Best effort only. Session still exists server-side even if dialog persistence fails.
428
- }
429
- return newSessionId;
430
- };
431
-
432
- const initialSessionId = await ensureCliSession();
433
- let resp = await createCliChatTurnStream(
434
- {
435
- getState,
436
- },
437
- {
438
- sessionId: initialSessionId,
439
- prompt,
440
- model: agentConfig.model || undefined,
441
- },
442
- loopController.signal,
443
- );
444
-
445
- if (!resp.ok && resp.status === 404) {
446
- cliSessionId = null;
447
- const renewedSessionId = await ensureCliSession();
448
- resp = await createCliChatTurnStream(
449
- {
450
- getState,
451
- },
452
- {
453
- sessionId: renewedSessionId,
454
- prompt,
455
- model: agentConfig.model || undefined,
456
- },
457
- loopController.signal,
458
- );
459
- }
460
-
461
- if (!resp.ok) {
462
- const err = await resp.json().catch(() => ({ error: resp.statusText }));
463
- return rejectWithValue(err.error || "CLI 执行失败");
464
- }
465
-
466
- // 读取 SSE 流并逐步更新消息内容
467
- const reader = resp.body?.getReader();
468
- if (!reader) {
469
- return rejectWithValue("无法读取流式响应");
470
- }
471
-
472
- let accumulated = "";
473
- const decoder = new TextDecoder();
474
- const buildCliAssistantMessage = () => ({
475
- id: messageId,
476
- dbKey: msgKey,
477
- role: "assistant" as const,
478
- content: accumulated,
479
- ...cliMessageMetadata,
480
- userId: selectUserId(getState() as RootState),
481
- });
482
- const abortCliStream = async () => {
483
- if (w) w.__LOOP_STOP_REASON__ = "aborted";
484
- if (accumulated.length <= 0) {
485
- return;
486
- }
487
- await persistMessageWithFixedId(dispatch, buildCliAssistantMessage());
488
- };
489
-
490
- try {
491
- while (true) {
492
- const { done, value } = await reader.read();
493
- if (done) break;
494
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
495
- await abortCliStream();
496
- return;
497
- }
498
-
499
- const raw = decoder.decode(value, { stream: true });
500
- // 解析 SSE 格式 "data: {...}\n\n"
501
- const lines = raw.split("\n");
502
- for (const line of lines) {
503
- if (!line.startsWith("data: ")) continue;
504
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
505
- await abortCliStream();
506
- return;
507
- }
508
- try {
509
- const payload = JSON.parse(line.slice(6));
510
- if (payload.error) {
511
- return rejectWithValue(payload.error);
512
- }
513
- if (payload.chunk) {
514
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
515
- await abortCliStream();
516
- return;
517
- }
518
- accumulated += payload.chunk;
519
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
520
- await abortCliStream();
521
- return;
522
- }
523
- dispatch(messageStreaming({
524
- id: messageId,
525
- dialogId,
526
- dbKey: msgKey,
527
- content: accumulated,
528
- role: "assistant",
529
- ...cliMessageMetadata,
530
- }));
531
- }
532
- // payload.done 时 stream 自然关闭,while loop 会退出
533
- } catch {
534
- // 忽略解析失败的行
535
- }
536
- }
537
- }
538
-
539
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
540
- await abortCliStream();
541
- return;
542
- }
543
-
544
- // 持久化最终消息:用已有 ID,避免 prepareAndPersistMessage 重新生成 ID 导致重复
545
- await persistMessageWithFixedId(dispatch, buildCliAssistantMessage());
546
- } finally {
547
- try {
548
- await reader.cancel();
549
- } catch {
550
- // ignore
551
- }
552
- }
553
-
554
- return;
555
- }
556
- // ─────────────────────────────────────────────────────────────────────
557
-
558
- const currentDialog =
559
- selectDialogConfigByKey(state, explicitDialogKey) ??
560
- selectCurrentDialogConfig(state);
561
- const activeDialogKey = currentDialog?.dbKey;
562
- const dialogKey = explicitDialogKey || activeDialogKey;
563
-
564
- if (!dialogKey) {
565
- return rejectWithValue("当前对话不存在,无法发送消息。");
566
- }
567
- runtimeDialogKey = dialogKey;
568
- const dialogId = extractCustomId(dialogKey);
569
-
570
- let userInputText = "";
571
- if (typeof userInput === "string") {
572
- userInputText = userInput;
573
- } else if (Array.isArray(userInput)) {
574
- const textPart = userInput.find((p) => p.type === "text");
575
- if (textPart && textPart.text) {
576
- userInputText = textPart.text;
577
- }
578
- }
579
-
580
- const explicitServerBase =
581
- typeof args.serverBase === "string" && args.serverBase.trim()
582
- ? args.serverBase.trim()
583
- : null;
584
- const declaredRuntimeServerBase = extractAgentRuntimeServerBase(agentConfig);
585
- const requestedServerBase = explicitServerBase ?? declaredRuntimeServerBase;
586
- const normalizedRequestedServerBase =
587
- requestedServerBase && normalizeServerOrigin(requestedServerBase);
588
- const normalizedCurrentServer = normalizeServerOrigin(
589
- selectCurrentServer(state),
590
- );
591
- const canAutoRouteRemotely =
592
- !Array.isArray(userInput) &&
593
- !runtimeOptions?.extraTools?.length &&
594
- !runtimeOptions?.editingTarget &&
595
- !runtimeOptions?.imageConfigOverride;
596
- if (requestedServerBase && canAutoRouteRemotely) {
597
- if (
598
- normalizedRequestedServerBase &&
599
- normalizedCurrentServer &&
600
- normalizedRequestedServerBase === normalizedCurrentServer
601
- ) {
602
- // same server as current workspace; keep local flow
603
- } else {
604
- const token = selectCurrentToken(state);
605
- const authHeader = token ? `Bearer ${token}` : "";
606
- const rawMessages = filterMessagesForParallelBranch(
607
- selectAllMsgs(state, dialogId),
608
- runtimeOptions,
609
- );
610
- const visibleMessages = buildAgentViewMessages(
611
- rawMessages as any,
612
- agentConfig.dbKey,
613
- );
614
- const cleanedMessages = filterAndCleanMessages(visibleMessages);
615
- const { key: msgKey, messageId } = createDialogMessageKeyAndId(dialogId);
616
- remoteTransientMessageId = messageId;
617
- const remoteMessageMetadata = buildParallelMessageMetadata(
618
- agentConfig,
619
- runtimeOptions,
620
- );
621
- let accumulated = "";
622
- let totalTurnUsage: any = undefined;
623
- const buildRemoteAssistantMessage = () => ({
624
- id: messageId,
625
- dbKey: msgKey,
626
- role: "assistant" as const,
627
- content: accumulated,
628
- ...remoteMessageMetadata,
629
- userId: selectUserId(getState() as RootState),
630
- });
631
-
632
- loopKey = `loop:${dialogId}`;
633
- dispatch(addActiveController({ messageId: loopKey, controller: loopController, dialogKey }));
634
- dispatch(messageStreaming({
635
- id: messageId,
636
- dialogId,
637
- dbKey: msgKey,
638
- content: "",
639
- role: "assistant",
640
- ...remoteMessageMetadata,
641
- }));
642
-
643
- const rejectRemoteStream = async (message: string) => {
644
- if (accumulated.length > 0) {
645
- await persistMessageWithFixedId(dispatch, buildRemoteAssistantMessage());
646
- } else {
647
- dispatch(removeTransientMessage(messageId));
648
- }
649
- remoteTransientMessageFinalized = true;
650
- return rejectWithValue(message);
651
- };
652
-
653
- const remoteResponse = await fetch(`${requestedServerBase.replace(/\/+$/, "")}/api/agent/run`, {
654
- method: "POST",
655
- headers: {
656
- "Content-Type": "application/json",
657
- Accept: "text/event-stream",
658
- ...(authHeader ? { Authorization: authHeader } : {}),
659
- },
660
- body: JSON.stringify({
661
- agentKey,
662
- userInput: userInputText,
663
- messages: cleanedMessages,
664
- stream: true,
665
- runtimeContext: {
666
- surface: "web",
667
- host: "browser",
668
- runtime: "react",
669
- entrypoint: "chat-dialog",
670
- capabilities: ["streaming", "dialog-ui", "tool-cards"],
671
- },
672
- ...(currentDialog?.spaceId ? { spaceId: currentDialog.spaceId } : {}),
673
- }),
674
- signal: loopController.signal,
675
- });
676
-
677
- if (!remoteResponse.ok) {
678
- const errorText = await remoteResponse.text();
679
- return await rejectRemoteStream(
680
- errorText || `Remote agent run failed (${remoteResponse.status})`,
681
- );
682
- }
683
-
684
- const reader = remoteResponse.body?.getReader();
685
- if (!reader) {
686
- return await rejectRemoteStream("无法读取远端流式响应");
687
- }
688
-
689
- const decoder = new TextDecoder();
690
- const parseSSE = createSSEParser();
691
- const abortRemoteStream = async () => {
692
- if (accumulated.length <= 0) return;
693
- await persistMessageWithFixedId(dispatch, buildRemoteAssistantMessage());
694
- };
695
-
696
- try {
697
- while (true) {
698
- const { done, value } = await reader.read();
699
- if (done) break;
700
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
701
- await abortRemoteStream();
702
- return;
703
- }
704
-
705
- const payloads = parseSSE(
706
- decoder.decode(value, { stream: true }),
707
- );
708
- for (const payload of payloads) {
709
- if (payload.type === "error") {
710
- return await rejectRemoteStream(
711
- payload.message || "远端 Agent 执行失败",
712
- );
713
- }
714
- if (payload.type === "text" && typeof payload.content === "string") {
715
- accumulated += payload.content;
716
- dispatch(messageStreaming({
717
- id: messageId,
718
- dialogId,
719
- dbKey: msgKey,
720
- content: accumulated,
721
- role: "assistant",
722
- ...remoteMessageMetadata,
723
- }));
724
- }
725
- if (payload.type === "done") {
726
- totalTurnUsage = payload.usage;
727
- }
728
- }
729
- }
730
- } finally {
731
- try {
732
- await reader.cancel();
733
- } catch {
734
- // ignore
735
- }
736
- }
737
-
738
- await persistMessageWithFixedId(dispatch, buildRemoteAssistantMessage());
739
- remoteTransientMessageFinalized = true;
740
- return {
741
- usage: totalTurnUsage ?? undefined,
742
- };
743
- }
744
- }
745
-
746
- // Extract Mentions from userInput if it's potentially Slate content
747
- let extractedMentions: CategorizedMentions | undefined;
748
- if (Array.isArray(userInput)) {
749
- // Basic check if it looks like Slate nodes (has children) or just assume safe to traverse
750
- // extractCategorizedMentions handles traversal safely.
751
- extractedMentions = extractCategorizedMentions(userInput as any);
752
- }
753
-
754
- const mentionedTools = extractedMentions?.tools ?? [];
755
-
756
- // 2. 解析引用:包含 tools 的页面自动升级为 instruction
757
- const {
758
- references: normalizedReferences,
759
- contentByKey: referenceContentCache,
760
- referencedTools: referenceTools,
761
- recommendedSkillTools: referenceRecommendedSkillTools,
762
- recommendedSkillHints: referenceRecommendedSkillHints,
763
- skillPromptPatches: referenceSkillPromptPatches,
764
- } = await resolveReferenceAssets(agentConfig.references, dispatch);
765
-
766
- const agentConfigWithReferences = {
767
- ...agentConfig,
768
- references: normalizedReferences,
769
- referencedTools: referenceTools,
770
- recommendedSkillTools: referenceRecommendedSkillTools,
771
- recommendedSkillHints: referenceRecommendedSkillHints,
772
- skillPromptPatches: referenceSkillPromptPatches,
773
- };
774
-
775
- // --- [新增] 提取本次 Handler 启动前的稳定历史消息 ID 集合 ---
776
- const initialRawMsgs = selectAllMsgs(state, dialogId);
777
- const initialHistoryIds = new Set(initialRawMsgs.map((m: any) => m.id));
778
-
779
- const keySets = await getFullChatContextKeys(
780
- state,
781
- dispatch,
782
- agentConfigWithReferences,
783
- userInput,
784
- currentDialog ?? undefined,
785
- );
786
- const finalKeys = deduplicateContextKeys(keySets);
787
- const allContextKeys = new Set<string>([
788
- ...finalKeys.botInstructionsContext,
789
- ...finalKeys.currentInputContext,
790
- ...finalKeys.historyContext,
791
- ...finalKeys.botKnowledgeContext,
792
- ]);
793
-
794
- // 4. 上下文页面里提取 tools 并缓存内容
795
- const {
796
- tools: contextTools,
797
- contentByKey: contextContentCache,
798
- recommendedSkillTools: contextRecommendedSkillTools = [],
799
- recommendedSkillHints: contextRecommendedSkillHints = [],
800
- skillPromptPatches: contextSkillPromptPatches = [],
801
- } = await resolveToolsFromKeys(
802
- Array.from(allContextKeys),
803
- dispatch,
804
- referenceContentCache,
805
- );
806
-
807
- const mergedContentCache = new Map<string, any>([
808
- ...referenceContentCache,
809
- ...contextContentCache,
810
- ]);
811
-
812
- // 4. 合并工具 (Base + Default + Context + Mentioned + Runtime) + 图片配置
813
- const agentConfigWithTools = mergeAgentToolsWithRuntime(
814
- {
815
- ...agentConfigWithReferences,
816
- recommendedSkillTools: [
817
- ...(((agentConfigWithReferences as any).recommendedSkillTools ?? []) as string[]),
818
- ...contextRecommendedSkillTools,
819
- ],
820
- recommendedSkillHints: [
821
- ...(((agentConfigWithReferences as any).recommendedSkillHints ?? []) as string[]),
822
- ...contextRecommendedSkillHints,
823
- ],
824
- skillPromptPatches: [
825
- ...(((agentConfigWithReferences as any).skillPromptPatches ?? []) as string[]),
826
- ...contextSkillPromptPatches,
827
- ],
828
- },
829
- contextTools,
830
- mentionedTools,
831
- runtimeOptions,
832
- state,
833
- );
834
- const agentConfigForCall = applyImageConfigRuntimeOverride(
835
- agentConfigWithTools,
836
- runtimeOptions,
837
- );
838
-
839
- // 如果对话设置了 maxTokens,覆盖 agent 的 max_tokens
840
- const dialogMaxTokens = currentDialog?.maxTokens;
841
- const effectiveAgentConfig = dialogMaxTokens
842
- ? { ...agentConfigForCall, max_tokens: dialogMaxTokens }
843
- : agentConfigForCall;
844
-
845
- const isRespModel = isResponseAPIModel(agentConfigForCall);
846
-
847
- // 🔹 Response-style 模型:与 completions 一样走完整 Agent Loop
848
- if (isRespModel) {
849
- const maxExecutionTime = selectMaxExecutionTime(state);
850
- const MAX_TIME_MS = maxExecutionTime > 0 ? maxExecutionTime : 240_000;
851
- const startTime = Date.now();
852
-
853
- const staticContexts = await buildStaticContexts(
854
- state,
855
- dispatch,
856
- agentConfigForCall,
857
- currentDialog ?? undefined,
858
- mergedContentCache,
859
- );
860
-
861
- let appendTempUserInput = true;
862
- let currentParentMessageId = parentMessageId ?? undefined;
863
-
864
- const w = typeof globalThis !== "undefined" && (globalThis as any).window ? (globalThis as any).window : null;
865
- if (w) w.__LOOP_STOP_REASON__ = null;
866
-
867
- loopKey = `loop:${dialogId}`;
868
- dispatch(addActiveController({ messageId: loopKey, controller: loopController, dialogKey }));
869
-
870
- for (;;) {
871
- const requestParentMessageId = currentParentMessageId;
872
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
873
- if (w) w.__LOOP_STOP_REASON__ = "aborted";
874
- break;
875
- }
876
-
877
- const loopState = getState() as RootState;
878
- const now = Date.now();
879
- if (now - startTime > MAX_TIME_MS) {
880
- if (w) w.__LOOP_STOP_REASON__ = "timeout";
881
- break;
882
- }
883
-
884
- const accessError = validateAccessAndBalance(
885
- agentConfigForCall,
886
- loopState,
887
- );
888
- if (accessError) {
889
- return rejectWithValue(accessError);
890
- }
891
-
892
- const dynamicContexts = await buildDynamicContexts(
893
- loopState,
894
- dispatch,
895
- agentConfigForCall,
896
- userInput,
897
- runtimeOptions,
898
- mergedContentCache,
899
- dialogKey,
900
- );
901
- const contexts = mergeContexts(staticContexts, dynamicContexts);
902
-
903
- const rawMessages = filterMessagesForParallelBranch(
904
- selectAllMsgs(loopState, dialogId),
905
- runtimeOptions,
906
- );
907
- let visibleMessages = buildAgentViewMessages(
908
- rawMessages as any,
909
- agentConfigForCall.dbKey,
910
- );
911
-
912
- if (
913
- appendTempUserInput &&
914
- userInputText &&
915
- userInputText.trim().length > 0
916
- ) {
917
- visibleMessages = [
918
- ...visibleMessages,
919
- {
920
- id: `__tmp_user_${Date.now()}`,
921
- dbKey: "",
922
- role: "user",
923
- content: userInputText,
924
- thinkContent: "",
925
- cybotKey: agentConfigForCall.dbKey,
926
- isStreaming: false,
927
- } as any,
928
- ];
929
- }
930
-
931
- const cleanedMessages = filterAndCleanMessages(visibleMessages);
932
- const ctxWindow =
933
- getModelContextWindow(agentConfigForCall.model) || 128000;
934
- const retention = selectContextRetention(loopState);
935
- const summaryTokenCount = contexts.dialogSummary
936
- ? estimateTokenCount(contexts.dialogSummary)
937
- : 0;
938
- const processedMessages = trimMessagesWithSummary(
939
- compressOldToolResults(cleanedMessages),
940
- ctxWindow,
941
- summaryTokenCount,
942
- retention,
943
- );
944
-
945
- let firstDynamicIdx = processedMessages.findIndex(
946
- (m) => m.id && !initialHistoryIds.has(m.id),
947
- );
948
- if (firstDynamicIdx === -1) firstDynamicIdx = processedMessages.length;
949
-
950
- const stableMessages = processedMessages.slice(0, firstDynamicIdx);
951
- const dynamicMessages = processedMessages.slice(firstDynamicIdx);
952
-
953
- if (appendTempUserInput) {
954
- const agentHasVision =
955
- (agentConfigForCall as any).hasVision === undefined
956
- ? true
957
- : Boolean((agentConfigForCall as any).hasVision);
958
-
959
- if (!agentHasVision && hasImageInMessages(processedMessages)) {
960
- return rejectWithValue(
961
- "当前 Agent 不支持图片输入,请改用文本或文档。",
962
- );
963
- }
964
- }
965
-
966
- const bodyData = generateRequestBody({
967
- agentConfig: effectiveAgentConfig,
968
- messages: dynamicMessages,
969
- stableMessages,
970
- userInput: userInputText,
971
- contexts,
972
- });
973
-
974
- const meta: CompletionMeta = await sendOpenAIResponseRequest({
975
- bodyData,
976
- agentConfig: agentConfigForCall,
977
- thunkApi,
978
- dialogKey,
979
- parentMessageId: currentParentMessageId,
980
- messageMetadata: buildParallelMessageMetadata(
981
- agentConfigForCall,
982
- runtimeOptions,
983
- ),
984
- });
985
-
986
- appendTempUserInput = false;
987
- currentParentMessageId = undefined;
988
- totalTurnUsage = updateTotalUsage(totalTurnUsage, meta.usage);
989
-
990
- if (meta.hasHandedOff) {
991
- if (!requestParentMessageId && meta.messageId) {
992
- dispatch(removeTransientMessage(meta.messageId));
993
- }
994
- if (w) w.__LOOP_STOP_REASON__ = "handoff";
995
- break;
996
- }
997
-
998
- if (meta.hasPendingInteraction) {
999
- if (w) w.__LOOP_STOP_REASON__ = "pending";
1000
- break;
1001
- }
1002
-
1003
- const afterTurnState = getState() as RootState;
1004
- const queuedMessages = selectPendingUserInputQueue(afterTurnState, dialogKey);
1005
- if (queuedMessages.length > 0) {
1006
- const queuedText = queuedMessages[0];
1007
-
1008
- const currentDialogConfig =
1009
- selectDialogConfigByKey(afterTurnState, dialogKey) ??
1010
- selectCurrentDialogConfig(afterTurnState);
1011
- if (!currentDialogConfig) {
1012
- dispatch(clearPendingUserInputQueue({ dialogKey }));
1013
- break;
1014
- }
1015
- await dispatch(
1016
- prepareAndPersistUserMessage({
1017
- userInput: queuedText,
1018
- dialogConfig: currentDialogConfig,
1019
- })
1020
- ).unwrap();
1021
- dispatch(dequeueUserInput({ dialogKey }));
1022
- continue;
1023
- }
1024
-
1025
- if (!meta.hasToolCalls) {
1026
- if (w) w.__LOOP_STOP_REASON__ = "done";
1027
- break;
1028
- }
1029
- }
1030
-
1031
- return {
1032
- usage: totalTurnUsage ?? undefined,
1033
- };
1034
- }
1035
-
1036
- // 🔹 Completions-style 模型:Agent Loop
1037
- const maxExecutionTime = selectMaxExecutionTime(state);
1038
-
1039
- const MAX_TIME_MS = maxExecutionTime > 0 ? maxExecutionTime : 240_000;
1040
- const startTime = Date.now();
1041
-
1042
- // 🚀 优化:在 Loop 外构建静态上下文(只执行一次)
1043
- // 静态上下文包含:botInstructions、botKnowledge、spaceContext、userGlobalPrompt
1044
- // 这些内容在 Loop 期间是稳定的,不需要每轮重新构建
1045
- const staticContexts = await buildStaticContexts(
1046
- state,
1047
- dispatch,
1048
- agentConfigForCall,
1049
- currentDialog ?? undefined,
1050
- mergedContentCache,
1051
- );
1052
-
1053
- let appendTempUserInput = true;
1054
- let currentParentMessageId = parentMessageId ?? undefined;
1055
-
1056
- const w = typeof globalThis !== "undefined" && (globalThis as any).window ? (globalThis as any).window : null;
1057
- if (w) w.__LOOP_STOP_REASON__ = null;
1058
-
1059
- if (!isRespModel) {
1060
- loopKey = `loop:${dialogId}`;
1061
- dispatch(addActiveController({ messageId: loopKey, controller: loopController, dialogKey }));
1062
- }
1063
-
1064
- for (;;) {
1065
- const requestParentMessageId = currentParentMessageId;
1066
- // 每轮开始前检查是否已中止
1067
- if (loopController.signal.aborted || thunkApi.signal.aborted) {
1068
- if (w) w.__LOOP_STOP_REASON__ = "aborted";
1069
- break;
1070
- }
1071
-
1072
- const loopState = getState() as RootState;
1073
- const now = Date.now();
1074
- if (now - startTime > MAX_TIME_MS) {
1075
- if (w) w.__LOOP_STOP_REASON__ = "timeout";
1076
- break;
1077
- }
1078
-
1079
- // 每轮检查权限 & 余额
1080
- const accessError = validateAccessAndBalance(
1081
- agentConfigForCall,
1082
- loopState,
1083
- );
1084
- if (accessError) {
1085
- return rejectWithValue(accessError);
1086
- }
1087
-
1088
- // 🚀 优化:每轮只构建动态上下文(currentInput、history、editingContext、dialogSummary)
1089
- const dynamicContexts = await buildDynamicContexts(
1090
- loopState,
1091
- dispatch,
1092
- agentConfigForCall,
1093
- userInput,
1094
- runtimeOptions,
1095
- mergedContentCache,
1096
- dialogKey,
1097
- );
1098
-
1099
- // 合并静态和动态上下文
1100
- const contexts = mergeContexts(staticContexts, dynamicContexts);
1101
-
1102
- const rawMessages = filterMessagesForParallelBranch(
1103
- selectAllMsgs(loopState, dialogId),
1104
- runtimeOptions,
1105
- );
1106
- let visibleMessages = buildAgentViewMessages(
1107
- rawMessages as any,
1108
- agentConfigForCall.dbKey,
1109
- );
1110
-
1111
- if (
1112
- appendTempUserInput &&
1113
- userInputText &&
1114
- userInputText.trim().length > 0
1115
- ) {
1116
- visibleMessages = [
1117
- ...visibleMessages,
1118
- {
1119
- id: `__tmp_user_${Date.now()}`,
1120
- dbKey: "",
1121
- role: "user",
1122
- content: userInputText,
1123
- thinkContent: "",
1124
- cybotKey: agentConfigForCall.dbKey,
1125
- isStreaming: false,
1126
- } as any,
1127
- ];
1128
- }
1129
-
1130
- const cleanedMessages = filterAndCleanMessages(visibleMessages);
1131
- const ctxWindow =
1132
- getModelContextWindow(agentConfigForCall.model) || 128000;
1133
- const retention = selectContextRetention(loopState);
1134
- const summaryTokenCount = contexts.dialogSummary
1135
- ? estimateTokenCount(contexts.dialogSummary)
1136
- : 0;
1137
- const processedMessages = trimMessagesWithSummary(
1138
- compressOldToolResults(cleanedMessages),
1139
- ctxWindow,
1140
- summaryTokenCount,
1141
- retention,
1142
- );
1143
-
1144
- // --- [优化 P1] 使用 findIndex + slice 确保顺序和无 ID 稳定消息的保留 ---
1145
- let firstDynamicIdx = processedMessages.findIndex(
1146
- (m) => m.id && !initialHistoryIds.has(m.id),
1147
- );
1148
- if (firstDynamicIdx === -1) firstDynamicIdx = processedMessages.length;
1149
-
1150
- const stableMessages = processedMessages.slice(0, firstDynamicIdx);
1151
- const dynamicMessages = processedMessages.slice(firstDynamicIdx);
1152
-
1153
- if (appendTempUserInput) {
1154
- const agentHasVision =
1155
- (agentConfigForCall as any).hasVision === undefined
1156
- ? true
1157
- : Boolean((agentConfigForCall as any).hasVision);
1158
-
1159
- if (!agentHasVision && hasImageInMessages(processedMessages)) {
1160
- return rejectWithValue(
1161
- "当前 Agent 不支持图片输入,请改用文本或文档。",
1162
- );
1163
- }
1164
- }
1165
-
1166
- const bodyData = generateRequestBody({
1167
- agentConfig: effectiveAgentConfig,
1168
- messages: dynamicMessages,
1169
- stableMessages,
1170
- userInput: userInputText,
1171
- contexts,
1172
- });
1173
-
1174
- const meta: CompletionMeta = await sendOpenAICompletionsRequest({
1175
- bodyData,
1176
- agentConfig: agentConfigForCall,
1177
- thunkApi,
1178
- dialogKey,
1179
- parentMessageId: currentParentMessageId,
1180
- messageMetadata: buildParallelMessageMetadata(
1181
- agentConfigForCall,
1182
- runtimeOptions,
1183
- ),
1184
- });
1185
-
1186
- appendTempUserInput = false;
1187
- currentParentMessageId = undefined;
1188
- totalTurnUsage = updateTotalUsage(totalTurnUsage, meta.usage);
1189
-
1190
- // handoff(例如 runStreamingAgent):当前 Agent 停止,后续由子 Agent 自动续跑
1191
- if (meta.hasHandedOff) {
1192
- if (!requestParentMessageId && meta.messageId) {
1193
- dispatch(removeTransientMessage(meta.messageId));
1194
- }
1195
- if (w) w.__LOOP_STOP_REASON__ = "handoff";
1196
- break;
1197
- }
1198
-
1199
- if (meta.hasPendingInteraction) {
1200
- if (w) w.__LOOP_STOP_REASON__ = "pending";
1201
- break;
1202
- }
1203
-
1204
- // 检查是否有用户在 loop 期间发送的排队消息
1205
- const afterTurnState = getState() as RootState;
1206
- const queuedMessages = selectPendingUserInputQueue(afterTurnState, dialogKey);
1207
- if (queuedMessages.length > 0) {
1208
- const queuedText = queuedMessages[0];
1209
-
1210
- const currentDialogConfig =
1211
- selectDialogConfigByKey(afterTurnState, dialogKey) ??
1212
- selectCurrentDialogConfig(afterTurnState);
1213
- if (!currentDialogConfig) {
1214
- // 对话已切换/销毁,无法持久化;清空队列并终止 loop,避免死循环重试
1215
- dispatch(clearPendingUserInputQueue({ dialogKey }));
1216
- break;
1217
- }
1218
- await dispatch(
1219
- prepareAndPersistUserMessage({
1220
- userInput: queuedText,
1221
- dialogConfig: currentDialogConfig,
1222
- })
1223
- ).unwrap();
1224
- // 持久化成功后再出队,避免 persist 失败时丢消息
1225
- dispatch(dequeueUserInput({ dialogKey }));
1226
- // 用户消息已持久化到 DB,下一轮 selectAllMsgs 会自动包含它
1227
- // 不设置 appendTempUserInput,直接继续下一轮
1228
- continue;
1229
- }
1230
-
1231
- if (!meta.hasToolCalls) {
1232
- if (w) w.__LOOP_STOP_REASON__ = "done";
1233
- break;
1234
- }
1235
-
1236
- // 否则:存在 tool_calls 且没有 handoff / pending,基于新的 history 继续下一轮
1237
- }
1238
-
1239
- return {
1240
- usage: totalTurnUsage ?? undefined,
1241
- };
1242
- } catch (error: any) {
1243
- if (isAbortError(error)) {
1244
- if (remoteTransientMessageId && !remoteTransientMessageFinalized) {
1245
- dispatch(removeTransientMessage(remoteTransientMessageId));
1246
- remoteTransientMessageFinalized = true;
1247
- }
1248
- const w =
1249
- typeof globalThis !== "undefined" && (globalThis as any).window
1250
- ? (globalThis as any).window
1251
- : null;
1252
- if (w) w.__LOOP_STOP_REASON__ = "aborted";
1253
- return;
1254
- }
1255
- console.error(
1256
- `Error in streamAgentChatTurn for [${agentKey}]:`,
1257
- error,
1258
- );
1259
- if (remoteTransientMessageId && !remoteTransientMessageFinalized) {
1260
- dispatch(removeTransientMessage(remoteTransientMessageId));
1261
- remoteTransientMessageFinalized = true;
1262
- }
1263
-
1264
- return rejectWithValue(
1265
- error?.message ||
1266
- "An unexpected error occurred in streamAgentChatTurn.",
1267
- );
1268
- } finally {
1269
- if (loopKey && runtimeDialogKey) {
1270
- dispatch(removeActiveController({ messageId: loopKey, dialogKey: runtimeDialogKey }));
1271
- } else if (loopKey) {
1272
- dispatch(removeActiveController(loopKey));
1273
- }
1274
- // 清空排队的用户消息(loop 结束或异常时,未消费的消息不应继续保留)
1275
- dispatch(clearPendingUserInputQueue(runtimeDialogKey ? { dialogKey: runtimeDialogKey } : undefined));
1276
- thunkApi.signal.removeEventListener("abort", onAbort);
1277
- }
1278
- };