feat: handle opencode permission requests

This commit is contained in:
2026-06-08 13:32:50 +08:00
parent 4e31b141e7
commit 05d36aa8ca
5 changed files with 590 additions and 6 deletions
+171 -1
View File
@@ -2,7 +2,7 @@ import type { Event as OpencodeEvent, Part } from "@opencode-ai/sdk/v2";
import { writeLlmRequestAuditLog } from "../audit/llmRequestAudit.js";
import { logger } from "../logger.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import { type PermissionReply, type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
export const supportedModels = [
"deepseek/deepseek-v4-flash",
@@ -33,6 +33,20 @@ type ProgressPayload = {
detail?: string;
};
export type PermissionRequestPayload = {
session_id: string;
request_id: string;
permission: string;
patterns: string[];
metadata: Record<string, unknown>;
always: string[];
tool?: {
messageID: string;
callID: string;
};
created_at: number;
};
const isDevelopmentDebugLoggingEnabled = process.env.NODE_ENV === "development";
const toolLabels: Record<string, string> = {
@@ -158,6 +172,42 @@ const isSessionEvent = (event: OpencodeEvent, sessionId: string) =>
"sessionID" in event.properties &&
event.properties.sessionID === sessionId;
const isPermissionAskedEvent = (
event: OpencodeEvent,
): event is Extract<OpencodeEvent, { type: "permission.asked" }> =>
event.type === "permission.asked";
const isPermissionV2AskedEvent = (
event: OpencodeEvent,
): event is Extract<OpencodeEvent, { type: "permission.v2.asked" }> =>
event.type === "permission.v2.asked";
const isPermissionRepliedEvent = (
event: OpencodeEvent,
): event is Extract<OpencodeEvent, { type: "permission.replied" }> =>
event.type === "permission.replied";
const isPermissionV2RepliedEvent = (
event: OpencodeEvent,
): event is Extract<OpencodeEvent, { type: "permission.v2.replied" }> =>
event.type === "permission.v2.replied";
const buildPermissionDetail = (event: Extract<OpencodeEvent, { type: "permission.asked" }>) => {
const patterns = event.properties.patterns.length
? event.properties.patterns.join(", ")
: event.properties.permission;
return `需要用户确认权限:${event.properties.permission};匹配规则:${patterns}`;
};
const buildPermissionV2Detail = (
event: Extract<OpencodeEvent, { type: "permission.v2.asked" }>,
) => {
const resources = event.properties.resources.length
? event.properties.resources.join(", ")
: event.properties.action;
return `需要用户确认权限:${event.properties.action};资源:${resources}`;
};
export const collectTextContent = (parts: Part[]) =>
parts
.filter((part): part is Extract<Part, { type: "text" }> => part.type === "text")
@@ -529,6 +579,126 @@ export const streamPromptResponse = async ({
});
}
if (isPermissionAskedEvent(event)) {
sawResponseActivity = true;
logDevelopmentDebug("permission request received", {
...debugContext,
requestId: event.properties.id,
permission: event.properties.permission,
patterns: event.properties.patterns,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `permission-${event.properties.id}`,
phase: "permission",
status: "running",
title: "等待权限确认",
detail: buildPermissionDetail(event),
});
write("permission_request", {
session_id: clientSessionId,
request_id: event.properties.id,
permission: event.properties.permission,
patterns: event.properties.patterns,
metadata: event.properties.metadata,
always: event.properties.always,
tool: event.properties.tool,
created_at: Date.now(),
} satisfies PermissionRequestPayload);
continue;
}
if (isPermissionV2AskedEvent(event)) {
sawResponseActivity = true;
logDevelopmentDebug("permission v2 request received", {
...debugContext,
requestId: event.properties.id,
action: event.properties.action,
resources: event.properties.resources,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `permission-${event.properties.id}`,
phase: "permission",
status: "running",
title: "等待权限确认",
detail: buildPermissionV2Detail(event),
});
write("permission_request", {
session_id: clientSessionId,
request_id: event.properties.id,
permission: event.properties.action,
patterns: event.properties.resources,
metadata: event.properties.metadata ?? {},
always: event.properties.save ?? [],
tool: undefined,
created_at: Date.now(),
} satisfies PermissionRequestPayload);
continue;
}
if (isPermissionRepliedEvent(event)) {
sawResponseActivity = true;
logDevelopmentDebug("permission request replied", {
...debugContext,
requestId: event.properties.requestID,
reply: event.properties.reply,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `permission-${event.properties.requestID}`,
phase: "permission",
status: event.properties.reply === "reject" ? "error" : "completed",
title:
event.properties.reply === "reject"
? "权限请求已拒绝"
: "权限请求已允许",
detail:
event.properties.reply === "always"
? "已允许本次请求,并记住同类权限。"
: event.properties.reply === "once"
? "已允许本次请求。"
: "已拒绝本次请求。",
});
write("permission_response", {
session_id: clientSessionId,
request_id: event.properties.requestID,
reply: event.properties.reply satisfies PermissionReply,
});
continue;
}
if (isPermissionV2RepliedEvent(event)) {
sawResponseActivity = true;
logDevelopmentDebug("permission v2 request replied", {
...debugContext,
requestId: event.properties.requestID,
reply: event.properties.reply,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `permission-${event.properties.requestID}`,
phase: "permission",
status: event.properties.reply === "reject" ? "error" : "completed",
title:
event.properties.reply === "reject"
? "权限请求已拒绝"
: "权限请求已允许",
detail:
event.properties.reply === "always"
? "已允许本次请求,并记住同类权限。"
: event.properties.reply === "once"
? "已允许本次请求。"
: "已拒绝本次请求。",
});
write("permission_response", {
session_id: clientSessionId,
request_id: event.properties.requestID,
reply: event.properties.reply satisfies PermissionReply,
});
continue;
}
if (isSkillEvent(event)) {
sawResponseActivity = true;
const { name, reason, payload } = extractSkillAuditInfo(event);