From 6f15b5d7e3c8b8f3a2940243679d9e94ccecbe72 Mon Sep 17 00:00:00 2001 From: Huarch Date: Wed, 29 Apr 2026 15:31:39 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=AF=B9=20copilot=20?= =?UTF-8?q?=E7=9A=84=E5=85=BC=E5=AE=B9=E3=80=82=E6=9B=B4=E6=96=B0=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=E5=92=8C=E6=96=87=E6=A1=A3=EF=BC=8C=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20session=5Fid=20=E4=BB=A3=E6=9B=BF=20conver?= =?UTF-8?q?sationId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .opencode/skills/examples.md | 26 +++++++++++++------------- .opencode/skills/runbook.md | 22 +++++++++++----------- README.md | 4 ++-- dist/chat/sessionBridge.js | 6 +++--- dist/routes/chat.js | 22 +++++++++++----------- dist/server.js | 2 +- dist/session/registry.js | 6 +++--- src/chat/sessionBridge.ts | 9 +++++---- src/routes/chat.ts | 22 +++++++++++----------- src/server.ts | 2 +- src/session/registry.ts | 10 +++++----- 11 files changed, 66 insertions(+), 65 deletions(-) diff --git a/.opencode/skills/examples.md b/.opencode/skills/examples.md index 2cc1c86..85e6c21 100644 --- a/.opencode/skills/examples.md +++ b/.opencode/skills/examples.md @@ -1,15 +1,15 @@ -# 示例(基于 chat/stream 工具调用链) +# 示例(基于 opencode Agent chat/stream 工具调用链) -## 示例 1:前端发起对话,LLM 触发工具调用 +## 示例 1:前端发起对话,opencode agent 触发工具调用 用户意图:查询设备 `170490` 在时间范围内的 `monitored_value`。 -前端调用 `POST /api/v1/copilot/chat/stream`: +前端调用 `POST /api/v1/agent/chat/stream`: ```json { "message": "请查询设备170490在最近24小时的monitored_value历史数据", - "conversationId": "conv-demo-001" + "session_id": "agent-demo-001" } ``` @@ -18,7 +18,7 @@ - `x-project-id: ` 服务端内部行为: -- LLM 选择工具 `dynamic_http_call` +- opencode agent 选择工具 `dynamic_http_call` - 工具参数示例: ```json { @@ -33,7 +33,7 @@ } ``` -## 示例 2:LLM 多步规划 + 多次工具调用 +## 示例 2:opencode agent 多步规划 + 多次工具调用 用户消息: - “先查这个设备历史数据,再给我异常点摘要。” @@ -41,14 +41,14 @@ 典型链路: - 第一步工具调用:查询历史数据接口。 - 第二步(可选)工具调用:查询补充数据接口。 -- LLM 汇总工具结果,持续通过 SSE 输出 token,最终返回 `done`。 +- opencode agent 汇总工具结果,持续通过 SSE 输出 token,最终返回 `done`。 ## 示例 3:前端工具 — 定位要素 用户消息: - "帮我找到管道 P-001 和 P-002" -LLM 调用工具 `locate_features`: +opencode agent 调用工具 `locate_features`: ```json { "ids": ["P-001", "P-002"], @@ -56,7 +56,7 @@ LLM 调用工具 `locate_features`: } ``` -前端收到 SSE 事件后缩放地图并高亮管道。LLM 回复文字:"已在地图上定位到管道 P-001 和 P-002。" +前端收到 SSE 事件后缩放地图并高亮管道。opencode agent 回复文字:"已在地图上定位到管道 P-001 和 P-002。" ## 示例 4:前端工具 — 对话内图表 @@ -64,10 +64,10 @@ LLM 调用工具 `locate_features`: - "展示节点 J-001 最近一天的压力变化曲线" 典型链路: -1. LLM 先调用 `dynamic_http_call` 查询数据 +1. opencode agent 先调用 `dynamic_http_call` 查询数据 2. 拿到数据后,调用 `show_chart` 将处理好的数据传给前端渲染 -第一步 — LLM 调用 `dynamic_http_call`: +第一步 — opencode agent 调用 `dynamic_http_call`: ```json { "path": "/api/v1/composite/element-simulation", @@ -80,7 +80,7 @@ LLM 调用工具 `locate_features`: } ``` -第二步 — LLM 处理数据后调用 `show_chart`: +第二步 — opencode agent 处理数据后调用 `show_chart`: ```json { "title": "节点 J-001 压力变化", @@ -103,7 +103,7 @@ LLM 调用工具 `locate_features`: 用户消息: - "我想看看 J-001 的监测数据" -LLM 调用工具 `view_scada`: +opencode agent 调用工具 `view_scada`: ```json { "device_ids": ["J-001"], diff --git a/.opencode/skills/runbook.md b/.opencode/skills/runbook.md index 1e21adc..7a3f913 100644 --- a/.opencode/skills/runbook.md +++ b/.opencode/skills/runbook.md @@ -3,17 +3,17 @@ ## 1) 总体原则 - Skills 负责“告诉模型可做什么”。 -- `chat/stream` 内部启动 LLM 会话,并注册工具 `dynamic_http_call`。 -- LLM 通过工具调用后端能力,不直接发 HTTP。 -- Sidecar 执行器负责“代表当前用户调真实后端 API”(动态路径,无白名单)。 +- `chat/stream` 内部启动 opencode 会话,并注册工具 `dynamic_http_call`。 +- opencode agent 通过工具调用后端能力,不直接发 HTTP。 +- TJWaterAgent 执行器负责“代表当前用户调真实后端 API”(动态路径,无白名单)。 ## 2) 请求入口(前端) -- `POST /api/v1/copilot/chat/stream`(唯一前端入口) +- `POST /api/v1/agent/chat/stream`(唯一前端入口) 不提供 `/execute` 对外调用路径,统一通过 `chat/stream` + 工具调用链执行。 -## 3) 工具参数约定(LLM 调用工具时) +## 3) 工具参数约定(opencode agent 调用工具时) ```json { @@ -53,18 +53,18 @@ 前端工具(`locate_features`, `view_history`, `view_scada`, `show_chart`)的调用链与 `dynamic_http_call` 不同: ``` -用户消息 → LLM → function calling → 调用前端工具 (如 locate_features) +用户消息 → opencode agent → tool calling → 调用前端工具 (如 locate_features) ↓ tool handler: 1) 推送 SSE tool_call 事件到前端 - 2) 返回简短确认给 LLM("已定位到管道") + 2) 返回简短确认给 opencode agent("已定位到管道") ↓ 前端同时收到: - SSE event: tool_call → 前端执行操作(定位地图/打开面板/渲染图表) - - SSE event: token → 渲染 LLM 文字回复 + - SSE event: token → 渲染 opencode agent 文字回复 ``` 关键区别: -- `dynamic_http_call`:Sidecar 代理 HTTP 请求,结果返回给 LLM 做后续分析。 -- 前端工具:Sidecar 仅推送 SSE 事件,前端直接执行,结果不返回 LLM。 -- `show_chart`:LLM 先通过 `dynamic_http_call` 查询数据,处理为 x_data + series 格式后调用 `show_chart`,前端直接渲染图表,不再请求后端。 +- `dynamic_http_call`:TJWaterAgent 代理 HTTP 请求,结果返回给 opencode agent 做后续分析。 +- 前端工具:TJWaterAgent 仅推送 SSE 事件,前端直接执行,结果不返回 opencode agent。 +- `show_chart`:opencode agent 先通过 `dynamic_http_call` 查询数据,处理为 x_data + series 格式后调用 `show_chart`,前端直接渲染图表,不再请求后端。 diff --git a/README.md b/README.md index 1ef26cb..413305e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ TJWaterAgent/ 1. 启动 HTTP 服务。 2. 通过 `@opencode-ai/sdk` 启动内嵌 opencode server,或连接外部 opencode server。 -3. 管理 `conversationId -> opencode sessionId` 的映射。 +3. 管理前端 `session_id -> opencode sessionId` 的映射。 4. 保存并传递用户 `Authorization`、`x-project-id`、`x-trace-id`。 5. 把 opencode 输出适配成前端需要的 SSE 事件。 6. 为 `.opencode/tools/dynamic_http_call.ts` 提供内部回调接口。 @@ -93,7 +93,7 @@ src/ platform/ ``` -这里保存从 `TJWaterCopilot` 迁移过来的技能树,并保持原有树结构,符合渐进式披露设计。 +这里保存 TJWater 技能树,并保持树结构,符合渐进式披露设计。 agent 需要某个领域知识时再按需加载对应 skill,不把整棵技能树作为 always-loaded prompt 一次性注入。 diff --git a/dist/chat/sessionBridge.js b/dist/chat/sessionBridge.js index 3d3ebb2..a962574 100644 --- a/dist/chat/sessionBridge.js +++ b/dist/chat/sessionBridge.js @@ -11,7 +11,7 @@ export class ChatSessionBridge { } async resolve(context) { const requestContext = { - conversationId: context.conversationId?.trim() || `conv-${randomUUID().slice(0, 12)}`, + clientSessionId: context.clientSessionId?.trim() || `agent-${randomUUID().slice(0, 12)}`, accessToken: context.accessToken, projectId: context.projectId, traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`, @@ -27,13 +27,13 @@ export class ChatSessionBridge { } catch (error) { logger.warn({ - conversationId: requestContext.conversationId, + clientSessionId: requestContext.clientSessionId, sessionId: current.sessionId, err: error, }, "existing opencode session lookup failed, creating a new session"); } } - const session = await this.runtime.createSession(requestContext.conversationId); + const session = await this.runtime.createSession(requestContext.clientSessionId); const binding = this.registry.upsert(requestContext, session.id); this.sessionContexts.set(binding.sessionId, requestContext); return { binding, requestContext, created: true }; diff --git a/dist/routes/chat.js b/dist/routes/chat.js index 3d63e98..0b7d7a4 100644 --- a/dist/routes/chat.js +++ b/dist/routes/chat.js @@ -3,7 +3,7 @@ import { z } from "zod"; import { logger } from "../logger.js"; const payloadSchema = z.object({ message: z.string().min(1).max(10000), - conversation_id: z.string().max(128).optional(), + session_id: z.string().max(128).optional(), }); export const buildChatRouter = (sessionBridge, runtime) => { const chatRouter = Router(); @@ -24,19 +24,19 @@ export const buildChatRouter = (sessionBridge, runtime) => { const projectId = req.header("x-project-id") ?? undefined; const traceId = req.header("x-trace-id") ?? undefined; const { binding, requestContext, created } = await sessionBridge.resolve({ - conversationId: parsed.data.conversation_id, + clientSessionId: parsed.data.session_id, accessToken, projectId, traceId, }); logger.info({ - conversationId: requestContext.conversationId, + clientSessionId: requestContext.clientSessionId, sessionId: binding.sessionId, created, traceId: requestContext.traceId, projectId: requestContext.projectId, }, "processing chat request"); - // 当前先走“发送 prompt 后回读最近消息”的兼容实现。 + // 当前先走“发送 prompt 后回读最近消息”的 opencode SDK 实现。 // 后续切到真正的 opencode 事件流时,只需要替换这里的取数方式。 const messages = await runtime.sendPrompt(binding.sessionId, parsed.data.message); const assistantMessage = messages.find((message) => message.info.role === "assistant"); @@ -45,38 +45,38 @@ export const buildChatRouter = (sessionBridge, runtime) => { res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.setHeader("X-Accel-Buffering", "no"); - const conversationId = requestContext.conversationId; + const clientSessionId = requestContext.clientSessionId; const parts = assistantMessage?.parts ?? []; const textContent = collectTextContent(parts); if (textContent) { res.write(toSse("token", { - conversationId, + session_id: clientSessionId, content: textContent, })); } for (const toolCall of collectToolCalls(parts)) { res.write(toSse("tool_call", { - conversationId, + session_id: clientSessionId, tool: toolCall.tool, params: toolCall.params, })); } if (assistantMessage?.info.role === "assistant" && assistantMessage.info.error) { res.write(toSse("error", { - conversationId, + session_id: clientSessionId, message: getErrorMessage(assistantMessage.info.error), detail: assistantMessage.info.error.name, })); } else if (!assistantMessage) { res.write(toSse("error", { - conversationId, + session_id: clientSessionId, message: "assistant response unavailable", detail: "no assistant message found after prompt", })); } else { - res.write(toSse("done", { conversationId })); + res.write(toSse("done", { session_id: clientSessionId })); } res.end(); } @@ -92,7 +92,7 @@ export const buildChatRouter = (sessionBridge, runtime) => { return chatRouter; }; const toSse = (event, data) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; -// 先把 opencode 的 Part 结构压平成前端当前消费的 SSE 语义。 +// 先把 opencode 的 Part 结构压平成 Agent API 的 SSE 语义。 const collectTextContent = (parts) => parts .filter((part) => part.type === "text") .map((part) => part.text) diff --git a/dist/server.js b/dist/server.js index 3f4a279..b4f33d1 100644 --- a/dist/server.js +++ b/dist/server.js @@ -66,7 +66,7 @@ app.post("/internal/tools/dynamic-http-call", async (req, res) => { }); } }); -app.use("/api/v1/copilot/chat", buildChatRouter(sessionBridge, opencodeRuntime)); +app.use("/api/v1/agent/chat", buildChatRouter(sessionBridge, opencodeRuntime)); const server = app.listen(config.PORT, config.HOST, () => { logger.info({ host: config.HOST, port: config.PORT }, "TJWaterAgent listening"); }); diff --git a/dist/session/registry.js b/dist/session/registry.js index d66f731..982222f 100644 --- a/dist/session/registry.js +++ b/dist/session/registry.js @@ -7,7 +7,7 @@ export class SessionRegistry { } upsert(context, sessionId) { const binding = { - conversationId: context.conversationId, + clientSessionId: context.clientSessionId, sessionId, lastUsedAt: Date.now(), }; @@ -43,11 +43,11 @@ export class SessionRegistry { return expired; } makeKey(context) { - // 会话隔离不能只看 conversationId;同一浏览器会话切换用户或项目时必须映射到不同 opencode session。 + // 会话隔离不能只看前端 session_id;同一浏览器会话切换用户或项目时必须映射到不同 opencode session。 const digest = crypto .createHash("sha256") .update([ - context.conversationId, + context.clientSessionId, context.accessToken ?? "", context.projectId ?? "", ].join("|")) diff --git a/src/chat/sessionBridge.ts b/src/chat/sessionBridge.ts index dfe8d86..062d640 100644 --- a/src/chat/sessionBridge.ts +++ b/src/chat/sessionBridge.ts @@ -18,7 +18,7 @@ export class ChatSessionBridge { ) {} async resolve(context: { - conversationId?: string; + clientSessionId?: string; accessToken?: string; projectId?: string; traceId?: string; @@ -28,7 +28,8 @@ export class ChatSessionBridge { created: boolean; }> { const requestContext: ChatRequestContext = { - conversationId: context.conversationId?.trim() || `conv-${randomUUID().slice(0, 12)}`, + clientSessionId: + context.clientSessionId?.trim() || `agent-${randomUUID().slice(0, 12)}`, accessToken: context.accessToken, projectId: context.projectId, traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`, @@ -46,7 +47,7 @@ export class ChatSessionBridge { } catch (error) { logger.warn( { - conversationId: requestContext.conversationId, + clientSessionId: requestContext.clientSessionId, sessionId: current.sessionId, err: error, }, @@ -55,7 +56,7 @@ export class ChatSessionBridge { } } - const session = await this.runtime.createSession(requestContext.conversationId); + const session = await this.runtime.createSession(requestContext.clientSessionId); const binding = this.registry.upsert(requestContext, session.id); this.sessionContexts.set(binding.sessionId, requestContext); return { binding, requestContext, created: true }; diff --git a/src/routes/chat.ts b/src/routes/chat.ts index 39eca9a..8d97c42 100644 --- a/src/routes/chat.ts +++ b/src/routes/chat.ts @@ -8,7 +8,7 @@ import { type ChatSessionBridge } from "../chat/sessionBridge.js"; const payloadSchema = z.object({ message: z.string().min(1).max(10000), - conversation_id: z.string().max(128).optional(), + session_id: z.string().max(128).optional(), }); export const buildChatRouter = ( @@ -36,7 +36,7 @@ export const buildChatRouter = ( const traceId = req.header("x-trace-id") ?? undefined; const { binding, requestContext, created } = await sessionBridge.resolve({ - conversationId: parsed.data.conversation_id, + clientSessionId: parsed.data.session_id, accessToken, projectId, traceId, @@ -44,7 +44,7 @@ export const buildChatRouter = ( logger.info( { - conversationId: requestContext.conversationId, + clientSessionId: requestContext.clientSessionId, sessionId: binding.sessionId, created, traceId: requestContext.traceId, @@ -53,7 +53,7 @@ export const buildChatRouter = ( "processing chat request", ); - // 当前先走“发送 prompt 后回读最近消息”的兼容实现。 + // 当前先走“发送 prompt 后回读最近消息”的 opencode SDK 实现。 // 后续切到真正的 opencode 事件流时,只需要替换这里的取数方式。 const messages = await runtime.sendPrompt(binding.sessionId, parsed.data.message); const assistantMessage = messages.find( @@ -66,13 +66,13 @@ export const buildChatRouter = ( res.setHeader("Connection", "keep-alive"); res.setHeader("X-Accel-Buffering", "no"); - const conversationId = requestContext.conversationId; + const clientSessionId = requestContext.clientSessionId; const parts = assistantMessage?.parts ?? []; const textContent = collectTextContent(parts); if (textContent) { res.write( toSse("token", { - conversationId, + session_id: clientSessionId, content: textContent, }), ); @@ -81,7 +81,7 @@ export const buildChatRouter = ( for (const toolCall of collectToolCalls(parts)) { res.write( toSse("tool_call", { - conversationId, + session_id: clientSessionId, tool: toolCall.tool, params: toolCall.params, }), @@ -91,7 +91,7 @@ export const buildChatRouter = ( if (assistantMessage?.info.role === "assistant" && assistantMessage.info.error) { res.write( toSse("error", { - conversationId, + session_id: clientSessionId, message: getErrorMessage(assistantMessage.info.error), detail: assistantMessage.info.error.name, }), @@ -99,13 +99,13 @@ export const buildChatRouter = ( } else if (!assistantMessage) { res.write( toSse("error", { - conversationId, + session_id: clientSessionId, message: "assistant response unavailable", detail: "no assistant message found after prompt", }), ); } else { - res.write(toSse("done", { conversationId })); + res.write(toSse("done", { session_id: clientSessionId })); } res.end(); @@ -125,7 +125,7 @@ export const buildChatRouter = ( const toSse = (event: string, data: Record) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; -// 先把 opencode 的 Part 结构压平成前端当前消费的 SSE 语义。 +// 先把 opencode 的 Part 结构压平成 Agent API 的 SSE 语义。 const collectTextContent = (parts: Part[]) => parts .filter((part): part is Extract => part.type === "text") diff --git a/src/server.ts b/src/server.ts index d15caab..cb91656 100644 --- a/src/server.ts +++ b/src/server.ts @@ -77,7 +77,7 @@ app.post("/internal/tools/dynamic-http-call", async (req, res) => { } }); -app.use("/api/v1/copilot/chat", buildChatRouter(sessionBridge, opencodeRuntime)); +app.use("/api/v1/agent/chat", buildChatRouter(sessionBridge, opencodeRuntime)); const server = app.listen(config.PORT, config.HOST, () => { logger.info( diff --git a/src/session/registry.ts b/src/session/registry.ts index 47ca49e..eec891e 100644 --- a/src/session/registry.ts +++ b/src/session/registry.ts @@ -1,13 +1,13 @@ import crypto from "node:crypto"; export type SessionBinding = { - conversationId: string; + clientSessionId: string; sessionId: string; lastUsedAt: number; }; export type SessionContext = { - conversationId: string; + clientSessionId: string; accessToken?: string; projectId?: string; }; @@ -22,7 +22,7 @@ export class SessionRegistry { upsert(context: SessionContext, sessionId: string): SessionBinding { const binding: SessionBinding = { - conversationId: context.conversationId, + clientSessionId: context.clientSessionId, sessionId, lastUsedAt: Date.now(), }; @@ -62,12 +62,12 @@ export class SessionRegistry { } private makeKey(context: SessionContext): string { - // 会话隔离不能只看 conversationId;同一浏览器会话切换用户或项目时必须映射到不同 opencode session。 + // 会话隔离不能只看前端 session_id;同一浏览器会话切换用户或项目时必须映射到不同 opencode session。 const digest = crypto .createHash("sha256") .update( [ - context.conversationId, + context.clientSessionId, context.accessToken ?? "", context.projectId ?? "", ].join("|"),