From f61389ab073d2b862371720369d6fde14b3a6ebe Mon Sep 17 00:00:00 2001 From: Huarch Date: Mon, 8 Jun 2026 14:14:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=A7=8B=E7=BB=88=E5=85=81=E8=AE=B8=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/chat.ts | 3 ++ src/routes/chatStream.ts | 47 ++++++++++++++++++++++++---- tests/routes/chatStream.test.ts | 55 +++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/src/routes/chat.ts b/src/routes/chat.ts index 557a61a..aac90b1 100644 --- a/src/routes/chat.ts +++ b/src/routes/chat.ts @@ -34,6 +34,7 @@ const payloadSchema = z.object({ message: z.string().min(1).max(10000), session_id: z.string().max(128).optional(), model: z.enum(supportedModels).optional(), + approval_mode: z.enum(["request", "always"]).optional().default("request"), }); const abortPayloadSchema = z.object({ @@ -968,6 +969,7 @@ export const buildChatRouter = ( sessionId: binding.sessionId, created: created || sessionCreated, model: parsed.data.model, + approvalMode: parsed.data.approval_mode, traceId: requestContext.traceId, projectId: requestContext.projectId, }, @@ -1137,6 +1139,7 @@ export const buildChatRouter = ( clientSessionId, message: preparedMessage, model: parsed.data.model, + approvalMode: parsed.data.approval_mode, traceId: requestContext.traceId, projectId: requestContext.projectId, signal: abortController.signal, diff --git a/src/routes/chatStream.ts b/src/routes/chatStream.ts index f59d124..3375421 100644 --- a/src/routes/chatStream.ts +++ b/src/routes/chatStream.ts @@ -10,6 +10,7 @@ export const supportedModels = [ ] as const; export type SupportedModel = (typeof supportedModels)[number]; +export type ApprovalMode = "request" | "always"; type StreamPromptOptions = { runtime: OpencodeRuntimeAdapter; @@ -17,6 +18,7 @@ type StreamPromptOptions = { clientSessionId: string; message: string; model?: SupportedModel; + approvalMode?: ApprovalMode; traceId?: string; projectId?: string; signal?: AbortSignal; @@ -345,6 +347,7 @@ export const streamPromptResponse = async ({ clientSessionId, message, model, + approvalMode = "request", traceId, projectId, signal, @@ -591,10 +594,26 @@ export const streamPromptResponse = async ({ emitProgress({ id: `permission-${event.properties.id}`, phase: "permission", - status: "running", - title: "等待权限确认", - detail: buildPermissionDetail(event), + status: approvalMode === "always" ? "completed" : "running", + title: approvalMode === "always" ? "已自动允许权限请求" : "等待权限确认", + detail: + approvalMode === "always" + ? "当前批准模式为始终允许,已自动允许本次权限请求。" + : buildPermissionDetail(event), }); + if (approvalMode === "always") { + await runtime.replyPermission({ + requestId: event.properties.id, + sessionId, + reply: "always", + }); + write("permission_response", { + session_id: clientSessionId, + request_id: event.properties.id, + reply: "always" satisfies PermissionReply, + }); + continue; + } write("permission_request", { session_id: clientSessionId, request_id: event.properties.id, @@ -620,10 +639,26 @@ export const streamPromptResponse = async ({ emitProgress({ id: `permission-${event.properties.id}`, phase: "permission", - status: "running", - title: "等待权限确认", - detail: buildPermissionV2Detail(event), + status: approvalMode === "always" ? "completed" : "running", + title: approvalMode === "always" ? "已自动允许权限请求" : "等待权限确认", + detail: + approvalMode === "always" + ? "当前批准模式为始终允许,已自动允许本次权限请求。" + : buildPermissionV2Detail(event), }); + if (approvalMode === "always") { + await runtime.replyPermission({ + requestId: event.properties.id, + sessionId, + reply: "always", + }); + write("permission_response", { + session_id: clientSessionId, + request_id: event.properties.id, + reply: "always" satisfies PermissionReply, + }); + continue; + } write("permission_request", { session_id: clientSessionId, request_id: event.properties.id, diff --git a/tests/routes/chatStream.test.ts b/tests/routes/chatStream.test.ts index 7a242f4..875cbcb 100644 --- a/tests/routes/chatStream.test.ts +++ b/tests/routes/chatStream.test.ts @@ -61,6 +61,61 @@ describe("streamPromptResponse", () => { } satisfies Partial); }); + it("auto replies always when approval mode is always", async () => { + const replies: Array> = []; + const runtime = { + subscribeEvents: async () => + createEventStream([ + { + type: "permission.asked", + properties: { + id: "perm-1", + sessionID: "runtime-session-1", + permission: "bash", + patterns: ["npm test"], + metadata: { command: "npm test" }, + always: ["npm test"], + }, + }, + { + type: "session.idle", + properties: { + sessionID: "runtime-session-1", + }, + }, + ]), + prompt: async () => undefined, + messages: async () => [], + replyPermission: async (options: Record) => { + replies.push(options); + }, + } as unknown as OpencodeRuntimeAdapter; + const events: Array<{ event: string; data: Record }> = []; + + await streamPromptResponse({ + runtime, + sessionId: "runtime-session-1", + clientSessionId: "client-session-1", + message: "run tests", + approvalMode: "always", + write: (event, data) => events.push({ event, data }), + }); + + expect(replies).toEqual([ + { + requestId: "perm-1", + sessionId: "runtime-session-1", + reply: "always", + }, + ]); + expect(events.some((item) => item.event === "permission_request")).toBe(false); + expect(events.find((item) => item.event === "permission_response")?.data).toEqual({ + session_id: "client-session-1", + request_id: "perm-1", + reply: "always", + }); + }); + it("forwards opencode v2 permission requests as SSE payloads", async () => { const runtime = { subscribeEvents: async () =>