diff --git a/.opencode/tools/render_junctions.ts b/.opencode/tools/render_junctions.ts new file mode 100644 index 0000000..acb9ffc --- /dev/null +++ b/.opencode/tools/render_junctions.ts @@ -0,0 +1,16 @@ +import { tool } from "@opencode-ai/plugin"; + +export default tool({ + description: "在前端地图上对 junctions 图层应用分区渲染。", + args: { + reason: tool.schema + .string() + .describe("Why this junction rendering action is needed for the user request."), + render_ref: tool.schema + .string() + .describe("Reference to a stored junction rendering payload resolved by the Agent service."), + }, + async execute() { + return "已在地图上应用节点分区渲染。"; + }, +}); diff --git a/src/results/store.ts b/src/results/store.ts index f34e0ad..eff647e 100644 --- a/src/results/store.ts +++ b/src/results/store.ts @@ -45,6 +45,7 @@ export type StoreResultInput = { export type RetrievalContext = { actorKey: string; + clientSessionId?: string; maxItems?: number; projectId?: string; }; @@ -105,18 +106,33 @@ export class ResultReferenceStore { return record; } - async getAuthorized(resultRef: string, context: RetrievalContext) { + async getAuthorized(resultRef: string, context: RetrievalContext) { + const record = await this.readAuthorizedRecord(resultRef, context); + if (!record) { + return null; + } + const data = projectData(record.data, context.maxItems ?? config.RESULT_REF_MAX_RETRIEVAL_ITEMS); + return { + ok: true, + result_ref: record.resultRef, + result_size_bytes: record.sizeBytes, + stored_at: record.createdAt, + data, + preview: record.preview, + }; + } + + async getFullAuthorized(resultRef: string, context: RetrievalContext) { const record = await this.readAuthorizedRecord(resultRef, context); if (!record) { return null; } - const data = projectData(record.data, context.maxItems ?? config.RESULT_REF_MAX_RETRIEVAL_ITEMS); return { ok: true, result_ref: record.resultRef, result_size_bytes: record.sizeBytes, stored_at: record.createdAt, - data, + data: record.data, preview: record.preview, }; } @@ -173,6 +189,12 @@ export class ResultReferenceStore { if ((record.projectId ?? "") !== (context.projectId ?? "")) { return null; } + if ( + context.clientSessionId && + record.clientSessionId !== context.clientSessionId + ) { + return null; + } return record; } } diff --git a/src/routes/chat.ts b/src/routes/chat.ts index 13999e9..ed59f78 100644 --- a/src/routes/chat.ts +++ b/src/routes/chat.ts @@ -5,9 +5,11 @@ import { z } from "zod"; import { type LearningOrchestrator } from "../learning/orchestrator.js"; import { logger } from "../logger.js"; import { MemoryStore } from "../memory/store.js"; +import { type ResultReferenceStore } from "../results/store.js"; import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js"; import { type ChatSessionBridge } from "../chat/sessionBridge.js"; import { writeLlmRequestAuditLog } from "../audit/llmRequestAudit.js"; +import { toActorKey } from "../utils/fileStore.js"; const supportedModels = [ "deepseek/deepseek-v4-flash", @@ -36,9 +38,47 @@ export const buildChatRouter = ( runtime: OpencodeRuntimeAdapter, memoryStore: MemoryStore, learningOrchestrator: LearningOrchestrator, + resultReferenceStore: ResultReferenceStore, ) => { const chatRouter = Router(); + chatRouter.get("/render-ref/:renderRef", async (req, res) => { + const renderRef = req.params.renderRef?.trim(); + const userId = req.header("x-user-id")?.trim(); + const projectId = req.header("x-project-id") ?? undefined; + const clientSessionId = + typeof req.query.session_id === "string" + ? req.query.session_id.trim() + : undefined; + + if (!userId) { + res.status(400).json({ + message: "x-user-id is required", + }); + return; + } + + if (!renderRef) { + res.status(400).json({ + message: "render_ref is required", + }); + return; + } + + const result = await resultReferenceStore.getFullAuthorized(renderRef, { + actorKey: toActorKey(userId), + clientSessionId, + projectId, + }); + + if (!result) { + res.status(404).json({ message: "render_ref not found" }); + return; + } + + res.json(result); + }); + chatRouter.post("/abort", async (req, res) => { const parsed = abortPayloadSchema.safeParse(req.body); if (!parsed.success) { @@ -1037,6 +1077,7 @@ const toolLabels: Record = { view_history: "历史数据面板", view_scada: "SCADA 面板", show_chart: "图表渲染", + render_junctions: "节点渲染", }; const buildPromptWithLearningContext = async ( diff --git a/src/server.ts b/src/server.ts index 1cbe423..7ee9166 100644 --- a/src/server.ts +++ b/src/server.ts @@ -172,7 +172,13 @@ app.post("/internal/tools/session-search", async (req, res) => { app.use( "/api/v1/agent/chat", - buildChatRouter(sessionBridge, opencodeRuntime, memoryStore, learningOrchestrator), + buildChatRouter( + sessionBridge, + opencodeRuntime, + memoryStore, + learningOrchestrator, + resultReferenceStore, + ), ); const bootstrap = async () => {