@next-open-ai/openbot 0.6.8 → 0.6.66

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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -136
  3. package/apps/desktop/renderer/dist/assets/index-CxDZnMBH.css +10 -0
  4. package/apps/desktop/renderer/dist/assets/index-k47Qiokg.js +93 -0
  5. package/apps/desktop/renderer/dist/index.html +2 -2
  6. package/dist/cli/cli.js +136 -0
  7. package/dist/cli/extension-cmd.d.ts +15 -0
  8. package/dist/cli/extension-cmd.js +107 -0
  9. package/dist/core/agent/agent-dir.d.ts +6 -0
  10. package/dist/core/agent/agent-dir.js +8 -0
  11. package/dist/core/agent/agent-manager.d.ts +27 -6
  12. package/dist/core/agent/agent-manager.js +147 -26
  13. package/dist/core/agent/proxy/adapters/claude-code-adapter.d.ts +2 -0
  14. package/dist/core/agent/proxy/adapters/claude-code-adapter.js +186 -0
  15. package/dist/core/agent/proxy/adapters/coze-adapter.d.ts +2 -0
  16. package/dist/core/agent/proxy/adapters/coze-adapter.js +406 -0
  17. package/dist/core/agent/proxy/adapters/local-adapter.d.ts +2 -0
  18. package/dist/core/agent/proxy/adapters/local-adapter.js +95 -0
  19. package/dist/core/agent/proxy/adapters/openclawx-adapter.d.ts +2 -0
  20. package/dist/core/agent/proxy/adapters/openclawx-adapter.js +115 -0
  21. package/dist/core/agent/proxy/adapters/opencode-adapter.d.ts +11 -0
  22. package/dist/core/agent/proxy/adapters/opencode-adapter.js +786 -0
  23. package/dist/core/agent/proxy/adapters/opencode-free-models.d.ts +20 -0
  24. package/dist/core/agent/proxy/adapters/opencode-free-models.js +14 -0
  25. package/dist/core/agent/proxy/adapters/opencode-local-runner.d.ts +5 -0
  26. package/dist/core/agent/proxy/adapters/opencode-local-runner.js +95 -0
  27. package/dist/core/agent/proxy/index.d.ts +3 -0
  28. package/dist/core/agent/proxy/index.js +18 -0
  29. package/dist/core/agent/proxy/registry.d.ts +7 -0
  30. package/dist/core/agent/proxy/registry.js +13 -0
  31. package/dist/core/agent/proxy/run-for-channel.d.ts +3 -0
  32. package/dist/core/agent/proxy/run-for-channel.js +31 -0
  33. package/dist/core/agent/proxy/types.d.ts +30 -0
  34. package/dist/core/agent/proxy/types.js +1 -0
  35. package/dist/core/agent/run.js +1 -1
  36. package/dist/core/agent/token-usage-log-extension.d.ts +14 -0
  37. package/dist/core/agent/token-usage-log-extension.js +61 -0
  38. package/dist/core/config/agent-reload-pending.d.ts +9 -0
  39. package/dist/core/config/agent-reload-pending.js +67 -0
  40. package/dist/core/config/desktop-config.d.ts +136 -5
  41. package/dist/core/config/desktop-config.js +470 -46
  42. package/dist/core/config/provider-support-default.js +27 -0
  43. package/dist/core/extensions/index.d.ts +1 -0
  44. package/dist/core/extensions/index.js +1 -0
  45. package/dist/core/extensions/load.d.ts +11 -0
  46. package/dist/core/extensions/load.js +101 -0
  47. package/dist/core/inbound-message-preprocess.d.ts +27 -0
  48. package/dist/core/inbound-message-preprocess.js +96 -0
  49. package/dist/core/local-llm-server/download-model.d.ts +16 -0
  50. package/dist/core/local-llm-server/download-model.js +37 -0
  51. package/dist/core/local-llm-server/index.d.ts +32 -0
  52. package/dist/core/local-llm-server/index.js +152 -0
  53. package/dist/core/local-llm-server/llm-context.d.ts +66 -0
  54. package/dist/core/local-llm-server/llm-context.js +270 -0
  55. package/dist/core/local-llm-server/model-resolve.d.ts +27 -0
  56. package/dist/core/local-llm-server/model-resolve.js +90 -0
  57. package/dist/core/local-llm-server/server.d.ts +1 -0
  58. package/dist/core/local-llm-server/server.js +234 -0
  59. package/dist/core/local-llm-server/start-from-config.d.ts +5 -0
  60. package/dist/core/local-llm-server/start-from-config.js +50 -0
  61. package/dist/core/mcp/adapter.d.ts +4 -2
  62. package/dist/core/mcp/adapter.js +10 -4
  63. package/dist/core/mcp/client.d.ts +4 -0
  64. package/dist/core/mcp/client.js +2 -0
  65. package/dist/core/mcp/config.d.ts +14 -3
  66. package/dist/core/mcp/config.js +68 -3
  67. package/dist/core/mcp/index.d.ts +10 -6
  68. package/dist/core/mcp/index.js +7 -3
  69. package/dist/core/mcp/operator.d.ts +28 -2
  70. package/dist/core/mcp/operator.js +131 -30
  71. package/dist/core/mcp/transport/index.d.ts +4 -0
  72. package/dist/core/mcp/transport/index.js +6 -1
  73. package/dist/core/mcp/transport/stdio.d.ts +12 -0
  74. package/dist/core/mcp/transport/stdio.js +147 -29
  75. package/dist/core/mcp/types.d.ts +18 -0
  76. package/dist/core/memory/compaction-extension.d.ts +4 -3
  77. package/dist/core/memory/compaction-extension.js +6 -14
  78. package/dist/core/memory/embedding-types.d.ts +10 -0
  79. package/dist/core/memory/embedding-types.js +5 -0
  80. package/dist/core/memory/embedding.d.ts +2 -1
  81. package/dist/core/memory/embedding.js +38 -6
  82. package/dist/core/memory/index.js +3 -0
  83. package/dist/core/memory/local-embedding-llama.d.ts +13 -0
  84. package/dist/core/memory/local-embedding-llama.js +78 -0
  85. package/dist/core/memory/local-embedding.d.ts +11 -0
  86. package/dist/core/memory/local-embedding.js +69 -0
  87. package/dist/core/memory/persist-compaction-on-close.d.ts +14 -0
  88. package/dist/core/memory/persist-compaction-on-close.js +32 -0
  89. package/dist/core/session-outlet/index.d.ts +19 -0
  90. package/dist/core/session-outlet/index.js +33 -0
  91. package/dist/core/session-outlet/outlet.d.ts +15 -0
  92. package/dist/core/session-outlet/outlet.js +49 -0
  93. package/dist/core/session-outlet/types.d.ts +35 -0
  94. package/dist/core/session-outlet/types.js +5 -0
  95. package/dist/core/tools/bookmark-tool.d.ts +4 -0
  96. package/dist/core/tools/bookmark-tool.js +59 -3
  97. package/dist/core/tools/index.d.ts +3 -1
  98. package/dist/core/tools/index.js +3 -1
  99. package/dist/core/tools/memory-recall-tool.d.ts +6 -0
  100. package/dist/core/tools/memory-recall-tool.js +77 -0
  101. package/dist/core/tools/truncate-result.d.ts +14 -0
  102. package/dist/core/tools/truncate-result.js +27 -0
  103. package/dist/core/tools/web-search/create-web-search-tool.d.ts +17 -0
  104. package/dist/core/tools/web-search/create-web-search-tool.js +87 -0
  105. package/dist/core/tools/web-search/index.d.ts +4 -0
  106. package/dist/core/tools/web-search/index.js +2 -0
  107. package/dist/core/tools/web-search/providers/brave.d.ts +2 -0
  108. package/dist/core/tools/web-search/providers/brave.js +87 -0
  109. package/dist/core/tools/web-search/providers/duck-duck-scrape.d.ts +2 -0
  110. package/dist/core/tools/web-search/providers/duck-duck-scrape.js +47 -0
  111. package/dist/core/tools/web-search/providers/index.d.ts +5 -0
  112. package/dist/core/tools/web-search/providers/index.js +13 -0
  113. package/dist/core/tools/web-search/types.d.ts +35 -0
  114. package/dist/core/tools/web-search/types.js +4 -0
  115. package/dist/gateway/channel/adapters/telegram.js +13 -2
  116. package/dist/gateway/channel/adapters/wechat.d.ts +24 -0
  117. package/dist/gateway/channel/adapters/wechat.js +205 -0
  118. package/dist/gateway/channel/channel-core.d.ts +1 -0
  119. package/dist/gateway/channel/channel-core.js +101 -59
  120. package/dist/gateway/channel/run-agent.d.ts +2 -4
  121. package/dist/gateway/channel/run-agent.js +13 -125
  122. package/dist/gateway/methods/agent-cancel.d.ts +3 -1
  123. package/dist/gateway/methods/agent-cancel.js +16 -2
  124. package/dist/gateway/methods/agent-chat.d.ts +4 -0
  125. package/dist/gateway/methods/agent-chat.js +377 -118
  126. package/dist/gateway/methods/run-scheduled-task.js +9 -7
  127. package/dist/gateway/proxy-run-abort.d.ts +6 -0
  128. package/dist/gateway/proxy-run-abort.js +39 -0
  129. package/dist/gateway/server.js +123 -19
  130. package/dist/server/agent-config/agent-config.controller.d.ts +10 -2
  131. package/dist/server/agent-config/agent-config.controller.js +19 -4
  132. package/dist/server/agent-config/agent-config.module.js +3 -1
  133. package/dist/server/agent-config/agent-config.service.d.ts +91 -6
  134. package/dist/server/agent-config/agent-config.service.js +115 -3
  135. package/dist/server/agents/agents.controller.d.ts +16 -0
  136. package/dist/server/agents/agents.controller.js +62 -1
  137. package/dist/server/agents/agents.gateway.js +1 -1
  138. package/dist/server/agents/agents.service.js +1 -1
  139. package/dist/server/bootstrap.d.ts +1 -0
  140. package/dist/server/bootstrap.js +28 -4
  141. package/dist/server/config/config.controller.d.ts +134 -2
  142. package/dist/server/config/config.controller.js +199 -3
  143. package/dist/server/config/config.module.js +5 -4
  144. package/dist/server/config/config.service.d.ts +32 -2
  145. package/dist/server/config/config.service.js +69 -9
  146. package/dist/server/config/local-models.service.d.ts +67 -0
  147. package/dist/server/config/local-models.service.js +242 -0
  148. package/dist/server/workspace/workspace.service.d.ts +7 -0
  149. package/dist/server/workspace/workspace.service.js +16 -0
  150. package/package.json +10 -2
  151. package/presets/preset-agents.json +128 -0
  152. package/presets/preset-config.json +29 -0
  153. package/presets/preset-providers.json +180 -0
  154. package/presets/recommended-local-models.json +36 -0
  155. package/presets/workspaces/code-assistant/skills/code-review/SKILL.md +19 -0
  156. package/presets/workspaces/code-assistant/skills/code-runner/SKILL.md +21 -0
  157. package/presets/workspaces/code-assistant/skills/git-helper/SKILL.md +29 -0
  158. package/presets/workspaces/creator-assistant/skills/.gitkeep +0 -0
  159. package/presets/workspaces/creator-assistant/skills/creator-tools/SKILL.md +15 -0
  160. package/presets/workspaces/doc-assistant/skills/doc-processor/SKILL.md +21 -0
  161. package/presets/workspaces/download-assistant/skills/downloader/SKILL.md +20 -0
  162. package/presets/workspaces/file-assistant/skills/file-converter/SKILL.md +21 -0
  163. package/presets/workspaces/file-assistant/skills/file-organizer/SKILL.md +17 -0
  164. package/presets/workspaces/file-assistant/skills/file-search/SKILL.md +22 -0
  165. package/presets/workspaces/morning-briefing/skills/news-fetcher/SKILL.md +16 -0
  166. package/presets/workspaces/morning-briefing/skills/web-summarizer/SKILL.md +20 -0
  167. package/presets/workspaces/news-assistant/skills/news-fetcher/SKILL.md +16 -0
  168. package/presets/workspaces/news-assistant/skills/web-summarizer/SKILL.md +20 -0
  169. package/presets/workspaces/office-automation/skills/rpa-helper/SKILL.md +9 -0
  170. package/presets/workspaces/self-media-bot/skills/self-media-tools/SKILL.md +9 -0
  171. package/skills/url-bookmark/SKILL.md +12 -12
  172. package/apps/desktop/renderer/dist/assets/index-LCp1YPVA.css +0 -10
  173. package/apps/desktop/renderer/dist/assets/index-l5fpDsHs.js +0 -89
@@ -0,0 +1,786 @@
1
+ /**
2
+ * OpenCode AgentProxy 适配器:使用 [@opencode-ai/sdk](https://opencode.ai/docs/zh-cn/sdk/) 对接官方 Server。
3
+ * 支持在消息中使用 //init、//undo、//redo、//share、//help 等 TUI 常见指令。
4
+ *
5
+ * 认证:SDK 文档未要求 Basic Auth。仅当用户在配置中填写了密码(对应 opencode serve 的 OPENCODE_SERVER_PASSWORD)
6
+ * 时,才通过自定义 fetch 注入 HTTP Basic;未填密码时直接使用 SDK 默认 fetch,无需处理 Request/duplex。
7
+ *
8
+ * 服务端默认模型:在启动 opencode serve 之前,在 OpenCode 配置里设置 model 即可。
9
+ * - 全局:编辑 ~/.config/opencode/opencode.json,添加 "model": "providerID/modelID"。
10
+ * - 当前项目:在项目根目录创建 opencode.json,添加 "model": "providerID/modelID"。
11
+ * - 环境变量:OPENCODE_CONFIG_CONTENT='{"model":"opencode/minimax-m2.5-free"}' opencode serve
12
+ * 并确保该 provider 已通过 /connect 或环境变量配置好 API Key。详见 https://opencode.ai/docs/config
13
+ *
14
+ * OpenCode Zen 免费模型(截图中的 Select model → OpenCode Zen):
15
+ * - 在 TUI 中 /connect → 选择 OpenCode Zen → 到 opencode.ai/auth 登录并粘贴 API Key。
16
+ * - 在配置中设置 model 为以下之一即可作为默认模型(格式 opencode/<model-id>):
17
+ * - opencode/minimax-m2.5-free — MiniMax M2.5 Free
18
+ * - opencode/glm-5-free — GLM 5 Free
19
+ * - opencode/kimi-k2.5-free — Kimi K2.5 Free
20
+ * - opencode/big-pickle — Big Pickle
21
+ * 本适配器请求不传 model 时由 OpenCode 服务端使用上述配置的默认模型。详见 https://opencode.ai/docs/zen
22
+ */
23
+ import { join, resolve } from "path";
24
+ import { createOpencodeClient } from "@opencode-ai/sdk";
25
+ import { getOpenbotWorkspaceDir } from "../../agent-dir.js";
26
+ import { ensureLocalOpencodeRunning } from "./opencode-local-runner.js";
27
+ /** 单次请求与事件流超时。长任务(如 init 分析大项目、多轮工具调用)可能较久,故设 15 分钟 */
28
+ const REQUEST_TIMEOUT_MS = 15 * 60 * 1000; // 15 min
29
+ /** 仅在使用自定义 fetch(带密码)时用作 Basic 认证用户名 */
30
+ const DEFAULT_SERVER_USERNAME = "opencode";
31
+ /** /help 的说明文案 */
32
+ const OPENCODE_HELP_TEXT = `**OpenCode 指令**(在消息开头输入,如 \`/init\`)
33
+
34
+ - **/init** — 分析当前项目并创建 AGENTS.md,便于 OpenCode 理解项目结构
35
+ - **/undo** — 撤销上一步修改
36
+ - **/redo** — 重做已撤销的修改
37
+ - **/share** — 分享当前会话,获取分享链接
38
+ - **/help** — 显示本说明
39
+
40
+ 直接输入普通内容将作为对话发送给 OpenCode。`;
41
+ function getSelfGatewayPort() {
42
+ const p = process.env.PORT?.trim();
43
+ if (!p)
44
+ return null;
45
+ const n = parseInt(p, 10);
46
+ return Number.isFinite(n) && n > 0 ? n : null;
47
+ }
48
+ function isLoopback(address) {
49
+ const a = address.toLowerCase();
50
+ return a === "127.0.0.1" || a === "localhost" || a === "::1" || a === "::ffff:127.0.0.1";
51
+ }
52
+ function getOpenCodeConfig(config) {
53
+ const oc = config.opencode;
54
+ if (oc?.port == null)
55
+ return null;
56
+ const port = Number(oc.port);
57
+ if (Number.isNaN(port) || port <= 0)
58
+ return null;
59
+ const mode = oc.mode === "local" || oc.mode === "remote" ? oc.mode : "remote";
60
+ const address = mode === "local"
61
+ ? "127.0.0.1"
62
+ : (oc.address != null && String(oc.address).trim()) || "";
63
+ if (mode === "remote" && !address)
64
+ return null;
65
+ const normalizedAddress = address.replace(/^https?:\/\//, "");
66
+ const selfPort = getSelfGatewayPort();
67
+ if (selfPort != null && port === selfPort && isLoopback(normalizedAddress)) {
68
+ throw new Error("OpenCode 地址不能指向本机当前 Gateway 端口(" +
69
+ normalizedAddress +
70
+ ":" +
71
+ port +
72
+ "),否则会造成请求死锁。");
73
+ }
74
+ const baseUrl = `http://${normalizedAddress}:${port}`.replace(/\/$/, "");
75
+ const username = (oc.username != null && String(oc.username).trim())
76
+ ? String(oc.username).trim()
77
+ : DEFAULT_SERVER_USERNAME;
78
+ const modelStr = (oc.model != null && String(oc.model).trim())
79
+ ? String(oc.model).trim()
80
+ : (config.model || "opencode/default");
81
+ const providerID = (config.provider && String(config.provider).trim()) || (modelStr.includes("/") ? modelStr.split("/")[0].trim() : modelStr.includes("-") ? modelStr.split("-")[0].trim() : "opencode");
82
+ const modelID = modelStr.includes("/") ? modelStr.slice(modelStr.indexOf("/") + 1).trim() : modelStr;
83
+ return {
84
+ baseUrl,
85
+ username,
86
+ password: oc.password != null ? String(oc.password).trim() : undefined,
87
+ model: { providerID, modelID: modelID || "default" },
88
+ };
89
+ }
90
+ /** 与 Claude Code 一致:未显式配置时使用智能体工作区路径 ~/.openbot/workspace/<workspace>/ */
91
+ function getOpencodeWorkingDirectory(config) {
92
+ const custom = config.opencode?.workingDirectory;
93
+ if (typeof custom === "string" && custom.trim()) {
94
+ return resolve(custom.trim());
95
+ }
96
+ const w = config.workspace;
97
+ if (typeof w !== "string" || !w.trim())
98
+ return undefined;
99
+ return join(getOpenbotWorkspaceDir(), w.trim());
100
+ }
101
+ function buildAuthHeaders(oc) {
102
+ const user = oc.username || DEFAULT_SERVER_USERNAME;
103
+ const pass = oc.password ?? "";
104
+ return {
105
+ "Content-Type": "application/json",
106
+ Authorization: "Basic " + Buffer.from(`${user}:${pass}`).toString("base64"),
107
+ };
108
+ }
109
+ /**
110
+ * 在 Node/undici 下带 body 的请求需要 duplex,SDK 默认 fetch 未设置。无密码时用此 fetch 仅补 duplex。
111
+ * @param externalSignal 用户中止时传入,与超时合并
112
+ */
113
+ function createDuplexFetch(timeoutMs, externalSignal) {
114
+ return (input, init) => {
115
+ let signal = init?.signal ?? AbortSignal.timeout(timeoutMs);
116
+ if (externalSignal) {
117
+ if (externalSignal.aborted)
118
+ signal = externalSignal;
119
+ else {
120
+ const c = new AbortController();
121
+ const tid = setTimeout(() => c.abort(), timeoutMs);
122
+ externalSignal.addEventListener("abort", () => {
123
+ clearTimeout(tid);
124
+ c.abort();
125
+ }, { once: true });
126
+ signal = c.signal;
127
+ }
128
+ }
129
+ if (input instanceof Request) {
130
+ const method = input.method.toUpperCase();
131
+ const hasBody = input.body != null || ["POST", "PUT", "PATCH"].includes(method);
132
+ const req = new Request(input.url, {
133
+ method: input.method,
134
+ headers: input.headers,
135
+ body: input.body,
136
+ mode: input.mode,
137
+ credentials: input.credentials,
138
+ cache: input.cache,
139
+ redirect: input.redirect,
140
+ referrer: input.referrer,
141
+ integrity: input.integrity,
142
+ signal,
143
+ ...(hasBody && { duplex: "half" }),
144
+ });
145
+ return fetch(req);
146
+ }
147
+ const method = (init?.method ?? "GET").toString().toUpperCase();
148
+ const hasBody = init?.body != null || ["POST", "PUT", "PATCH"].includes(method);
149
+ return fetch(input, { ...init, signal, ...(hasBody && { duplex: "half" }) });
150
+ };
151
+ }
152
+ /**
153
+ * 仅当用户配置了密码时使用:创建带 HTTP Basic 的 fetch,并满足 Node/undici 对带 body 请求的 duplex 要求。
154
+ * @param externalSignal 用户中止时传入,与超时合并
155
+ */
156
+ function createAuthFetch(authHeaders, timeoutMs, externalSignal) {
157
+ return (input, init) => {
158
+ let signal = init?.signal ?? AbortSignal.timeout(timeoutMs);
159
+ if (externalSignal) {
160
+ if (externalSignal.aborted)
161
+ signal = externalSignal;
162
+ else {
163
+ const c = new AbortController();
164
+ const tid = setTimeout(() => c.abort(), timeoutMs);
165
+ externalSignal.addEventListener("abort", () => {
166
+ clearTimeout(tid);
167
+ c.abort();
168
+ }, { once: true });
169
+ signal = c.signal;
170
+ }
171
+ }
172
+ if (input instanceof Request) {
173
+ const cloned = input.clone();
174
+ const headers = new Headers(cloned.headers);
175
+ for (const [k, v] of Object.entries(authHeaders))
176
+ headers.set(k, v);
177
+ const method = cloned.method.toUpperCase();
178
+ const hasBody = cloned.body != null || ["POST", "PUT", "PATCH"].includes(method);
179
+ const req = new Request(cloned.url, {
180
+ method: cloned.method,
181
+ headers,
182
+ body: cloned.body,
183
+ mode: cloned.mode,
184
+ credentials: cloned.credentials,
185
+ cache: cloned.cache,
186
+ redirect: cloned.redirect,
187
+ referrer: cloned.referrer,
188
+ integrity: cloned.integrity,
189
+ signal,
190
+ ...(hasBody && { duplex: "half" }),
191
+ });
192
+ return fetch(req);
193
+ }
194
+ const headers = new Headers(init?.headers);
195
+ for (const [k, v] of Object.entries(authHeaders))
196
+ headers.set(k, v);
197
+ const method = (init?.method ?? "GET").toString().toUpperCase();
198
+ const hasBody = init?.body != null || ["POST", "PUT", "PATCH"].includes(method);
199
+ return fetch(input, { ...init, headers, signal, ...(hasBody && { duplex: "half" }) });
200
+ };
201
+ }
202
+ /** 按 channel 的 sessionId 复用 OpenCode session,便于 //undo、//redo 等生效 */
203
+ const opencodeSessionCache = new Map();
204
+ function getCachedSessionId(channelSessionId) {
205
+ return opencodeSessionCache.get(channelSessionId);
206
+ }
207
+ function setCachedSessionId(channelSessionId, opencodeSessionId) {
208
+ opencodeSessionCache.set(channelSessionId, opencodeSessionId);
209
+ }
210
+ /** 解析消息:若以 / 或 // 开头则返回 { command, args },否则返回 null。支持常用 /command 形式。 */
211
+ function parseSlashCommand(message) {
212
+ const trimmed = message.trim();
213
+ const isSlash = trimmed.startsWith("/");
214
+ if (!isSlash)
215
+ return null;
216
+ const rest = trimmed.startsWith("//") ? trimmed.slice(2).trim() : trimmed.slice(1).trim();
217
+ if (!rest)
218
+ return null;
219
+ const space = rest.indexOf(" ");
220
+ const command = space >= 0 ? rest.slice(0, space).toLowerCase() : rest.toLowerCase();
221
+ const args = space >= 0 ? rest.slice(space + 1).trim() : "";
222
+ return command ? { command, args } : null;
223
+ }
224
+ /** 从 session.prompt / session.command 返回的 parts 提取文本(含所有 type,用于兼容旧逻辑) */
225
+ function partsToText(parts) {
226
+ if (!Array.isArray(parts))
227
+ return "";
228
+ return parts
229
+ .map((p) => (typeof p?.text === "string" ? p.text : typeof p?.content === "string" ? p.content : ""))
230
+ .filter(Boolean)
231
+ .join("");
232
+ }
233
+ /** 仅取 type=text 的 part 拼接为助手正文,与流式时只推 text 保持一致,不包含 reasoning/step-start/step-finish */
234
+ function partsToReplyText(parts) {
235
+ if (!Array.isArray(parts))
236
+ return "";
237
+ return parts
238
+ .filter((p) => p?.type === "text")
239
+ .map((p) => (typeof p.text === "string" ? p.text : typeof p.content === "string" ? p.content : ""))
240
+ .filter(Boolean)
241
+ .join("")
242
+ .trim();
243
+ }
244
+ /** 日志用:可序列化对象,避免循环引用、过长字符串和不可序列化字段 */
245
+ function safeForLog(obj, maxStrLen = 2000) {
246
+ if (obj === null || obj === undefined)
247
+ return obj;
248
+ if (typeof obj !== "object") {
249
+ const s = String(obj);
250
+ return s.length > maxStrLen ? s.slice(0, maxStrLen) + "…" : s;
251
+ }
252
+ if (Array.isArray(obj))
253
+ return obj.map((v) => safeForLog(v, maxStrLen));
254
+ const seen = new WeakSet();
255
+ function toPlain(o) {
256
+ if (o === null || o === undefined)
257
+ return o;
258
+ if (typeof o !== "object")
259
+ return typeof o === "string" && o.length > maxStrLen ? o.slice(0, maxStrLen) + "…" : o;
260
+ if (seen.has(o))
261
+ return "[Circular]";
262
+ seen.add(o);
263
+ if (typeof o.toJSON === "function")
264
+ return toPlain(o.toJSON());
265
+ if (Array.isArray(o))
266
+ return o.map(toPlain);
267
+ const out = {};
268
+ for (const k of Object.keys(o)) {
269
+ try {
270
+ out[k] = toPlain(o[k]);
271
+ }
272
+ catch {
273
+ out[k] = "[?]";
274
+ }
275
+ }
276
+ return out;
277
+ }
278
+ return toPlain(obj);
279
+ }
280
+ /** 打印 SDK 接口的完整返回,便于排查 */
281
+ function logSdkResponse(api, res) {
282
+ console.log(`[OpenCode] SDK ${api} 返回:`, JSON.stringify(safeForLog(res), null, 2));
283
+ }
284
+ /** 将 SDK/API 抛出的错误转为可读字符串,避免 [object Object] */
285
+ function formatErrorMessage(err) {
286
+ if (err instanceof Error)
287
+ return err.message;
288
+ const o = err;
289
+ if (Array.isArray(o?.error) && o.error.length > 0 && typeof o.error[0]?.message === "string") {
290
+ return o.error[0].message;
291
+ }
292
+ if (typeof o?.data?.message === "string")
293
+ return o.data.message;
294
+ if (typeof o?.message === "string")
295
+ return o.message;
296
+ if (typeof err === "string")
297
+ return err;
298
+ try {
299
+ return JSON.stringify(err);
300
+ }
301
+ catch {
302
+ return "未知错误";
303
+ }
304
+ }
305
+ /** 从多种可能的 API 响应结构中取出 parts 或纯文本 */
306
+ function extractPartsOrText(res) {
307
+ const data = res?.data ?? res;
308
+ if (!data)
309
+ return "";
310
+ const parts = data?.info?.parts ?? data?.parts;
311
+ if (Array.isArray(parts))
312
+ return partsToText(parts);
313
+ if (typeof data?.content === "string")
314
+ return data.content;
315
+ if (typeof data?.text === "string")
316
+ return data.text;
317
+ if (typeof data?.message === "string")
318
+ return data.message;
319
+ return "";
320
+ }
321
+ /** 从 command 类接口返回中优先取 type=text 的 part 作为主回复,避免被 reasoning 等淹没;无则退回 partsToText */
322
+ function extractCommandReplyText(res) {
323
+ const data = res?.data ?? res;
324
+ if (!data)
325
+ return "";
326
+ const parts = data?.info?.parts ?? data?.parts;
327
+ if (!Array.isArray(parts)) {
328
+ if (typeof data?.content === "string")
329
+ return data.content;
330
+ if (typeof data?.text === "string")
331
+ return data.text;
332
+ return "";
333
+ }
334
+ const textPart = parts.find((p) => p?.type === "text");
335
+ if (textPart && (typeof textPart.text === "string"))
336
+ return textPart.text.trim();
337
+ return partsToText(parts);
338
+ }
339
+ const POLL_INTERVAL_MS = 1500;
340
+ /** 流式回退轮询超时。长任务若事件流提前断开,需给服务端足够时间生成完整回复 */
341
+ const POLL_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
342
+ /**
343
+ * 服务端 POST /session/{id}/message 返回 204,助手回复异步生成。轮询 session.messages() 取最新 assistant 消息的文本。
344
+ */
345
+ async function pollForAssistantMessage(session, sessionId) {
346
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
347
+ let lastListRes;
348
+ let pollCount = 0;
349
+ while (Date.now() < deadline) {
350
+ pollCount++;
351
+ const listRes = await session.messages({ path: { id: sessionId } });
352
+ lastListRes = listRes;
353
+ if (pollCount === 1)
354
+ logSdkResponse("session.messages(poll, first)", listRes);
355
+ const list = listRes?.data ?? listRes;
356
+ if (!Array.isArray(list) || list.length === 0) {
357
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
358
+ continue;
359
+ }
360
+ // 找最后一条 user 的位置,只认「在它之后出现的 assistant」为本轮回复(避免并发多轮时拿错)
361
+ let lastUserIndex = -1;
362
+ for (let i = list.length - 1; i >= 0; i--) {
363
+ if (list[i]?.info?.role === "user") {
364
+ lastUserIndex = i;
365
+ break;
366
+ }
367
+ }
368
+ for (let i = lastUserIndex + 1; i < list.length; i++) {
369
+ const item = list[i];
370
+ if (item?.info?.role !== "assistant")
371
+ continue;
372
+ const parts = item?.info?.parts ?? item?.parts;
373
+ if (!Array.isArray(parts))
374
+ continue;
375
+ const text = partsToReplyText(parts);
376
+ if (text)
377
+ return text;
378
+ // 已有 assistant 但 parts 为空,可能仍在生成,继续轮询
379
+ break;
380
+ }
381
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
382
+ }
383
+ console.log("[OpenCode] session.messages(poll) 超时,最后一次返回:", JSON.stringify(safeForLog(lastListRes)));
384
+ return "";
385
+ }
386
+ export const opencodeAdapter = {
387
+ type: "opencode",
388
+ async runStream(options, config, callbacks) {
389
+ const oc = getOpenCodeConfig(config);
390
+ if (!oc) {
391
+ throw new Error("OpenCode adapter: missing opencode.port or (remote 模式下缺少 address) in agent config");
392
+ }
393
+ if (config.opencode?.mode === "local" && config.opencode.port != null) {
394
+ await ensureLocalOpencodeRunning(Number(config.opencode.port), config.opencode.model?.trim() || undefined, getOpencodeWorkingDirectory(config));
395
+ }
396
+ const hasPassword = Boolean(oc.password?.trim());
397
+ const userSignal = options.signal;
398
+ const fetchImpl = hasPassword
399
+ ? createAuthFetch(buildAuthHeaders(oc), REQUEST_TIMEOUT_MS, userSignal)
400
+ : createDuplexFetch(REQUEST_TIMEOUT_MS, userSignal);
401
+ const client = createOpencodeClient({
402
+ baseUrl: oc.baseUrl,
403
+ fetch: fetchImpl,
404
+ throwOnError: true,
405
+ });
406
+ const channelSessionId = options.sessionId;
407
+ let sessionId = getCachedSessionId(channelSessionId);
408
+ const ensureSession = async () => {
409
+ if (sessionId)
410
+ return sessionId;
411
+ const createRes = await client.session.create({ body: {} });
412
+ logSdkResponse("session.create", createRes);
413
+ const id = createRes.data?.id ?? createRes.id;
414
+ if (typeof id !== "string" || !id)
415
+ throw new Error("OpenCode 创建 Session 失败:返回缺少 id");
416
+ sessionId = id;
417
+ setCachedSessionId(channelSessionId, sessionId);
418
+ return sessionId;
419
+ };
420
+ const slash = parseSlashCommand(options.message);
421
+ try {
422
+ if (slash) {
423
+ const { command, args } = slash;
424
+ await ensureSession();
425
+ if (command === "help") {
426
+ if (OPENCODE_HELP_TEXT)
427
+ callbacks.onChunk(OPENCODE_HELP_TEXT);
428
+ callbacks.onDone();
429
+ return;
430
+ }
431
+ if (command === "init") {
432
+ try {
433
+ const initRes = await client.session.command({
434
+ path: { id: sessionId },
435
+ body: { command: "init", arguments: args || "" },
436
+ });
437
+ logSdkResponse("session.command(init)", initRes);
438
+ const text = extractCommandReplyText(initRes) || "执行成功:已初始化项目并生成 AGENTS.md。";
439
+ callbacks.onChunk(text);
440
+ }
441
+ catch (initErr) {
442
+ console.warn("[OpenCode] init failed:", initErr);
443
+ const msg = formatErrorMessage(initErr);
444
+ const isAbort = msg.includes("aborted") || msg.includes("AbortError");
445
+ callbacks.onChunk(isAbort ? "执行失败:请求已取消或超时。" : `执行失败:${msg}`);
446
+ }
447
+ callbacks.onDone();
448
+ return;
449
+ }
450
+ if (command === "undo" || command === "redo") {
451
+ try {
452
+ if (command === "redo") {
453
+ const unrevertRes = await client.session.unrevert({
454
+ path: { id: sessionId },
455
+ });
456
+ logSdkResponse("session.unrevert(redo)", unrevertRes);
457
+ callbacks.onChunk("已重做。");
458
+ }
459
+ else {
460
+ const listRes = await client.session.messages({
461
+ path: { id: sessionId },
462
+ });
463
+ const list = listRes?.data ?? listRes;
464
+ const items = Array.isArray(list) ? list : [];
465
+ const lastAssistant = [...items].reverse().find((m) => m?.info?.role === "assistant");
466
+ const messageID = lastAssistant?.info?.id;
467
+ if (!messageID) {
468
+ callbacks.onChunk("没有可撤销的助手消息。");
469
+ callbacks.onDone();
470
+ return;
471
+ }
472
+ await client.session.revert({
473
+ path: { id: sessionId },
474
+ body: { messageID },
475
+ });
476
+ logSdkResponse("session.revert(undo)", { messageID });
477
+ callbacks.onChunk("已撤销。");
478
+ }
479
+ }
480
+ catch (undoRedoErr) {
481
+ console.warn("[OpenCode] undo/redo failed:", undoRedoErr);
482
+ callbacks.onChunk(`执行失败:${formatErrorMessage(undoRedoErr)}`);
483
+ }
484
+ callbacks.onDone();
485
+ return;
486
+ }
487
+ if (command === "share") {
488
+ const shareRes = await client.session.share({ path: { id: sessionId } });
489
+ logSdkResponse("session.share", shareRes);
490
+ const data = shareRes.data ?? shareRes;
491
+ const url = data?.share?.url ?? data?.shareURL ?? data?.shareUrl ?? data?.url ?? "";
492
+ callbacks.onChunk(url ? `分享链接:${url}` : "分享完成(请查看 OpenCode 返回的链接)。");
493
+ callbacks.onDone();
494
+ return;
495
+ }
496
+ // 其他未单独处理的 /xxx 当作普通指令发给 OpenCode command
497
+ try {
498
+ const cmdRes = await client.session.command({
499
+ path: { id: sessionId },
500
+ body: { command, arguments: args || "", agent: "build" },
501
+ });
502
+ logSdkResponse("session.command(/xxx)", cmdRes);
503
+ const text = extractCommandReplyText(cmdRes) || "已执行。";
504
+ callbacks.onChunk(text);
505
+ }
506
+ catch (cmdErr) {
507
+ console.warn("[OpenCode] command failed:", cmdErr);
508
+ callbacks.onChunk(`执行失败:${formatErrorMessage(cmdErr)}`);
509
+ }
510
+ callbacks.onDone();
511
+ return;
512
+ }
513
+ // 普通对话:使用 promptAsync + event 流式接收 message.part.updated(delta)与 session.idle 结束
514
+ sessionId = await ensureSession();
515
+ const opencodeSessionId = sessionId;
516
+ const subscribeResult = await client.event.subscribe({
517
+ signal: userSignal,
518
+ });
519
+ const eventStream = subscribeResult?.stream;
520
+ if (!eventStream || typeof eventStream[Symbol.asyncIterator] !== "function") {
521
+ throw new Error("OpenCode event.subscribe() 未返回可迭代 stream");
522
+ }
523
+ try {
524
+ await client.session.promptAsync({
525
+ path: { id: opencodeSessionId },
526
+ body: { parts: [{ type: "text", text: options.message }] },
527
+ });
528
+ }
529
+ catch (promptErr) {
530
+ callbacks.onChunk(`请求失败:${formatErrorMessage(promptErr)}`);
531
+ callbacks.onDone();
532
+ return;
533
+ }
534
+ // 取刚发送的用户消息 ID,流式时只转发助手消息的 part,避免把用户问题当助手内容回显
535
+ let userMessageID = null;
536
+ try {
537
+ const listRes = await client.session.messages({ path: { id: opencodeSessionId } });
538
+ const list = listRes?.data ?? listRes;
539
+ if (Array.isArray(list) && list.length > 0) {
540
+ // 列表可能是新→旧或旧→新,取「最后一条 user」为刚发的
541
+ for (let i = list.length - 1; i >= 0; i--) {
542
+ const item = list[i];
543
+ if (item?.info?.role === "user" && item?.info?.id) {
544
+ userMessageID = item.info.id;
545
+ break;
546
+ }
547
+ }
548
+ if (userMessageID == null) {
549
+ for (let i = 0; i < list.length; i++) {
550
+ const item = list[i];
551
+ if (item?.info?.role === "user" && item?.info?.id) {
552
+ userMessageID = item.info.id;
553
+ break;
554
+ }
555
+ }
556
+ }
557
+ }
558
+ }
559
+ catch (_) {
560
+ // 忽略,仅用于过滤用户 part
561
+ }
562
+ let hadAnyChunk = false;
563
+ let lastTextLength = 0;
564
+ let eventCount = 0;
565
+ // reasoning 只缓存在见到 text 或 session.idle 时再整体输出一次,避免「先部分后完整」重复显示
566
+ const reasoningBuffer = new Map();
567
+ // 正文累积后去掉 [step-start]/[step-finish] 再推送,避免服务端把这类标签混在 text 里
568
+ let textAccumulated = "";
569
+ let lastEmittedCleanLen = 0;
570
+ const stepMarkerRe = /\n?\[step-(?:start|finish)\]\n?/g;
571
+ const emitStrippedText = () => {
572
+ const cleaned = textAccumulated.replace(stepMarkerRe, "");
573
+ const toEmit = cleaned.slice(lastEmittedCleanLen);
574
+ if (toEmit) {
575
+ hadAnyChunk = true;
576
+ callbacks.onChunk(toEmit);
577
+ lastEmittedCleanLen = cleaned.length;
578
+ }
579
+ };
580
+ const flushReasoningForMessage = (messageID) => {
581
+ const raw = reasoningBuffer.get(messageID);
582
+ if (!raw?.trim())
583
+ return;
584
+ reasoningBuffer.delete(messageID);
585
+ const formatted = `\n\n---\nreasoning: ${raw.slice(0, 2000)}${raw.length > 2000 ? "…" : ""}\n---\n\n`;
586
+ hadAnyChunk = true;
587
+ callbacks.onChunk(formatted);
588
+ };
589
+ try {
590
+ for await (const event of eventStream) {
591
+ if (userSignal?.aborted)
592
+ break;
593
+ eventCount++;
594
+ const ev = event;
595
+ if (ev.type !== "message.part.updated" || (ev.properties?.part?.type !== "text")) {
596
+ const partType = ev.properties?.part?.type ?? "(no part)";
597
+ console.log(`[OpenCode] event #${eventCount} type=${ev.type} part.type=${partType}`);
598
+ }
599
+ const info = ev.properties?.info;
600
+ if (ev.type === "message.updated" && info?.sessionID === opencodeSessionId && info?.role === "user" && info?.id) {
601
+ userMessageID = info.id;
602
+ }
603
+ if (ev.type === "message.part.updated" && ev.properties?.part?.sessionID === opencodeSessionId) {
604
+ const part = ev.properties.part;
605
+ if (userMessageID != null && part.messageID === userMessageID)
606
+ continue;
607
+ const partType = part.type ?? "";
608
+ const delta = ev.properties.delta;
609
+ const fullText = typeof part?.text === "string" ? part.text : "";
610
+ if (partType === "text") {
611
+ const msgId = part.messageID ?? "";
612
+ flushReasoningForMessage(msgId);
613
+ if (typeof delta === "string" && delta) {
614
+ textAccumulated += delta;
615
+ emitStrippedText();
616
+ }
617
+ else if (fullText.length > lastTextLength) {
618
+ textAccumulated = fullText;
619
+ lastTextLength = fullText.length;
620
+ emitStrippedText();
621
+ }
622
+ }
623
+ else if (partType === "reasoning" && fullText.trim()) {
624
+ const msgId = part.messageID ?? "";
625
+ reasoningBuffer.set(msgId, fullText);
626
+ }
627
+ // step-start、step-finish、tool_call 等不推给前端
628
+ }
629
+ if (ev.type === "session.idle" && ev.properties?.sessionID === opencodeSessionId) {
630
+ for (const msgId of reasoningBuffer.keys())
631
+ flushReasoningForMessage(msgId);
632
+ console.log(`[OpenCode] session.idle after ${eventCount} events, hadAnyChunk=${hadAnyChunk}`);
633
+ break;
634
+ }
635
+ }
636
+ }
637
+ catch (streamErr) {
638
+ const errMsg = streamErr instanceof Error ? streamErr.message : String(streamErr);
639
+ if (streamErr?.name !== "AbortError") {
640
+ console.warn(`[OpenCode] event stream error after ${eventCount} events, hadAnyChunk=${hadAnyChunk}:`, errMsg);
641
+ }
642
+ // 流提前断开(如超时、网络):若已有部分内容已下发,前端能看到;若 hadAnyChunk 为 false 则下面轮询补全
643
+ }
644
+ if (!hadAnyChunk) {
645
+ console.log("[OpenCode] no chunks from stream, falling back to pollForAssistantMessage");
646
+ const fallback = await pollForAssistantMessage(client.session, opencodeSessionId);
647
+ if (fallback)
648
+ callbacks.onChunk(fallback);
649
+ }
650
+ callbacks.onDone();
651
+ }
652
+ catch (e) {
653
+ const msg = e instanceof Error ? e.message : String(e);
654
+ console.error(`[OpenCode] error: ${msg}`);
655
+ throw e;
656
+ }
657
+ },
658
+ async runCollect(options, config) {
659
+ const oc = getOpenCodeConfig(config);
660
+ if (!oc) {
661
+ throw new Error("OpenCode adapter: missing opencode.port or (remote 模式下缺少 address) in agent config");
662
+ }
663
+ if (config.opencode?.mode === "local" && config.opencode.port != null) {
664
+ await ensureLocalOpencodeRunning(Number(config.opencode.port), config.opencode.model?.trim() || undefined, getOpencodeWorkingDirectory(config));
665
+ }
666
+ const hasPassword = oc.password != null && String(oc.password).trim() !== "";
667
+ const client = createOpencodeClient({
668
+ baseUrl: oc.baseUrl,
669
+ ...(hasPassword
670
+ ? { fetch: createAuthFetch(buildAuthHeaders(oc), REQUEST_TIMEOUT_MS) }
671
+ : {}),
672
+ throwOnError: true,
673
+ });
674
+ const channelSessionId = options.sessionId;
675
+ let sessionId = getCachedSessionId(channelSessionId);
676
+ const ensureSession = async () => {
677
+ if (sessionId)
678
+ return sessionId;
679
+ const createRes = await client.session.create({ body: {} });
680
+ logSdkResponse("session.create", createRes);
681
+ const id = createRes.data?.id ?? createRes.id;
682
+ if (typeof id !== "string" || !id)
683
+ throw new Error("OpenCode 创建 Session 失败:返回缺少 id");
684
+ sessionId = id;
685
+ setCachedSessionId(channelSessionId, sessionId);
686
+ return sessionId;
687
+ };
688
+ const slash = parseSlashCommand(options.message);
689
+ if (slash) {
690
+ const { command, args } = slash;
691
+ await ensureSession();
692
+ if (command === "help")
693
+ return OPENCODE_HELP_TEXT.trim();
694
+ if (command === "init") {
695
+ try {
696
+ const initRes = await client.session.command({
697
+ path: { id: sessionId },
698
+ body: { command: "init", arguments: args || "" },
699
+ });
700
+ logSdkResponse("session.command(init) runCollect", initRes);
701
+ return extractCommandReplyText(initRes) || "执行成功:已初始化项目并生成 AGENTS.md。";
702
+ }
703
+ catch (e) {
704
+ console.warn("[OpenCode] init failed (runCollect):", e);
705
+ const msg = formatErrorMessage(e);
706
+ const isAbort = msg.includes("aborted") || msg.includes("AbortError");
707
+ return isAbort ? "执行失败:请求已取消或超时。" : `执行失败:${msg}`;
708
+ }
709
+ }
710
+ if (command === "undo" || command === "redo") {
711
+ try {
712
+ if (command === "redo") {
713
+ await client.session.unrevert({ path: { id: sessionId } });
714
+ logSdkResponse("session.unrevert(redo) runCollect", {});
715
+ return "已重做。";
716
+ }
717
+ const listRes = await client.session.messages({
718
+ path: { id: sessionId },
719
+ });
720
+ const list = listRes?.data ?? listRes;
721
+ const items = Array.isArray(list) ? list : [];
722
+ const lastAssistant = [...items].reverse().find((m) => m?.info?.role === "assistant");
723
+ const messageID = lastAssistant?.info?.id;
724
+ if (!messageID)
725
+ return "没有可撤销的助手消息。";
726
+ await client.session.revert({
727
+ path: { id: sessionId },
728
+ body: { messageID },
729
+ });
730
+ logSdkResponse("session.revert(undo) runCollect", { messageID });
731
+ return "已撤销。";
732
+ }
733
+ catch (e) {
734
+ return `执行失败:${formatErrorMessage(e)}`;
735
+ }
736
+ }
737
+ if (command === "share") {
738
+ const shareRes = await client.session.share({ path: { id: sessionId } });
739
+ logSdkResponse("session.share (runCollect)", shareRes);
740
+ const data = shareRes.data ?? shareRes;
741
+ const url = data?.share?.url ?? data?.shareURL ?? data?.shareUrl ?? data?.url ?? "";
742
+ return url ? `分享链接:${url}` : "分享完成。";
743
+ }
744
+ try {
745
+ const cmdRes = await client.session.command({
746
+ path: { id: sessionId },
747
+ body: { command, arguments: args || "", agent: "build" },
748
+ });
749
+ logSdkResponse("session.command(/xxx) runCollect", cmdRes);
750
+ return extractCommandReplyText(cmdRes) || "已执行。";
751
+ }
752
+ catch (e) {
753
+ return `执行失败:${formatErrorMessage(e)}`;
754
+ }
755
+ }
756
+ sessionId = await ensureSession();
757
+ let text = "";
758
+ try {
759
+ const promptRes = await client.session.prompt({
760
+ path: { id: sessionId },
761
+ body: { parts: [{ type: "text", text: options.message }] }, // 不传 model,用 OpenCode 服务端默认
762
+ });
763
+ logSdkResponse("session.prompt (runCollect)", promptRes);
764
+ text = extractPartsOrText(promptRes);
765
+ }
766
+ catch (promptErr) {
767
+ const msg = promptErr instanceof Error ? promptErr.message : String(promptErr);
768
+ const cause = promptErr instanceof Error ? promptErr?.cause : undefined;
769
+ const isClosedOrTimeout = msg.includes("fetch failed") ||
770
+ msg.includes("other side closed") ||
771
+ msg.includes("aborted due to timeout") ||
772
+ msg.includes("TimeoutError") ||
773
+ cause?.code === "UND_ERR_SOCKET";
774
+ if (isClosedOrTimeout) {
775
+ console.warn("[OpenCode] session.prompt (runCollect) 被关闭或超时,改为轮询:", msg);
776
+ text = await pollForAssistantMessage(client.session, sessionId);
777
+ }
778
+ else {
779
+ throw promptErr;
780
+ }
781
+ }
782
+ if (!text)
783
+ text = await pollForAssistantMessage(client.session, sessionId);
784
+ return text.trim() || "(无文本回复或请求超时)";
785
+ },
786
+ };