diff --git a/src/main/agent/continue-host-adapter.ts b/src/main/agent/continue-host-adapter.ts index 8e7b61c5..5ebff759 100644 --- a/src/main/agent/continue-host-adapter.ts +++ b/src/main/agent/continue-host-adapter.ts @@ -120,14 +120,13 @@ const continueHostStreamEventSchema = z.discriminatedUnion('type', [ const continueHostQuestionSchema = z .object({ - requestId: z.string().min(1).max(128), + requestId: z.string().min(1), timestamp: z.number().finite().optional(), - question: z.string().trim().min(1).max(2_000), + question: z.string().trim().min(1), options: z - .array(z.string().trim().min(1).max(200)) - .max(20) + .array(z.string().trim().min(1)) .optional(), - defaultAnswer: z.string().trim().max(2_000).optional() + defaultAnswer: z.string().trim().optional() }) .strict() @@ -925,7 +924,7 @@ export class ContinueHostAdapter { patched = replaceExactly( patched, serverMarker, - 'let j=(0,atn.default)();if(!process.env.GOODBUDDY_CONTINUE_HOST_TOKEN)throw new Error("Missing GoodBuddy host token");j.use((we,Te,ue)=>{we.headers.authorization===`Bearer ${process.env.GOODBUDDY_CONTINUE_HOST_TOKEN}`?ue():Te.status(401).json({error:"Unauthorized"})}),j.use(atn.default.json({limit:"20mb"})),j.post("/goodbuddy/question-answer",(we,Te)=>{let{requestId:ue,answer:ce,isCustomAnswer:de}=we.body??{};typeof ue==="string"&&ue.length>0&&ue.length<=128&&typeof ce==="string"&&ce.length>0&&ce.length<=2e3?Lbe.answerQuestion(ue,ce,de===!0)?Te.json({success:!0}):Te.status(404).json({error:"Question not pending"}):Te.status(400).json({error:"Invalid question answer"})}),j.get("/state"' + 'let j=(0,atn.default)();if(!process.env.GOODBUDDY_CONTINUE_HOST_TOKEN)throw new Error("Missing GoodBuddy host token");j.use((we,Te,ue)=>{we.headers.authorization===`Bearer ${process.env.GOODBUDDY_CONTINUE_HOST_TOKEN}`?ue():Te.status(401).json({error:"Unauthorized"})}),j.use(atn.default.json({limit:"20mb"})),j.post("/goodbuddy/question-answer",(we,Te)=>{let{requestId:ue,answer:ce,isCustomAnswer:de}=we.body??{};typeof ue==="string"&&ue.length>0&&typeof ce==="string"&&ce.length>0?Lbe.answerQuestion(ue,ce,de===!0)?Te.json({success:!0}):Te.status(404).json({error:"Question not pending"}):Te.status(400).json({error:"Invalid question answer"})}),j.get("/state"' ) patched = replaceExactly( patched, diff --git a/src/main/agent/opencode-runtime.test.ts b/src/main/agent/opencode-runtime.test.ts index c5422aa8..7856de11 100644 --- a/src/main/agent/opencode-runtime.test.ts +++ b/src/main/agent/opencode-runtime.test.ts @@ -2075,7 +2075,26 @@ describe("OpenCodeRuntime embedded permission mediation", () => { ); it.each(["parent", "child"] as const)("parses %s OpenCode questions and sends selected answers back", async (owner) => { + const longQuestion = `请选择实现方式\n${"发布说明内容。".repeat(6_000)}`; const setup = runClient([ + { + id: "question-tool-running", + type: "message.part.updated", + properties: { + sessionID: "session-1", + part: { + id: "part-question-1", + callID: "call-question-1", + type: "tool", + tool: "question", + state: { + status: "running", + input: { questions: [] }, + time: { start: 1 }, + }, + }, + }, + }, { id: "question-event", type: "question.asked", @@ -2085,7 +2104,7 @@ describe("OpenCodeRuntime embedded permission mediation", () => { questions: [ { header: "实现方式", - question: "请选择实现方式", + question: longQuestion, options: [ { label: "直接修改", @@ -2106,6 +2125,25 @@ describe("OpenCodeRuntime embedded permission mediation", () => { }, }, }, + { + id: "question-tool-completed", + type: "message.part.updated", + properties: { + sessionID: "session-1", + part: { + id: "part-question-1", + callID: "call-question-1", + type: "tool", + tool: "question", + state: { + status: "completed", + input: { questions: [] }, + output: "Answered", + time: { start: 1, end: 2 }, + }, + }, + }, + }, { id: "idle", type: "session.idle", @@ -2136,7 +2174,7 @@ describe("OpenCodeRuntime embedded permission mediation", () => { questions: [ { header: "实现方式", - question: "请选择实现方式", + question: longQuestion, multiple: false, custom: true, }, diff --git a/src/main/agent/opencode-runtime.ts b/src/main/agent/opencode-runtime.ts index eb0a2c6b..46580f26 100644 --- a/src/main/agent/opencode-runtime.ts +++ b/src/main/agent/opencode-runtime.ts @@ -81,9 +81,6 @@ const MAX_PERMISSION_PATTERN_LENGTH = 1_024; const MAX_PERMISSION_PATTERNS_BYTES = 8 * 1_024; const MAX_PERMISSION_METADATA_BYTES = 8 * 1_024; const MAX_EXECUTION_OUTPUT_BYTES = 1024 * 1024; -const MAX_QUESTION_REQUEST_BYTES = 32 * 1_024; -const MAX_QUESTIONS_PER_REQUEST = 4; -const MAX_QUESTION_OPTIONS = 20; const MAX_NATIVE_AGENTS = runtimeNativeInventoryLimits.agents; const MAX_NATIVE_TOOLS = runtimeNativeInventoryLimits.tools; const MAX_NATIVE_COMMANDS = runtimeNativeInventoryLimits.commands; @@ -384,29 +381,22 @@ function parseQuestionRequest( if ( typeof id !== "string" || id.length === 0 || - id.length > MAX_PERMISSION_NAME_LENGTH || !Array.isArray(questions) || questions.length === 0 || - questions.length > MAX_QUESTIONS_PER_REQUEST || !questions.every( (question) => isRecord(question) && typeof question.question === "string" && question.question.trim().length > 0 && - question.question.length <= 2_000 && typeof question.header === "string" && question.header.trim().length > 0 && - question.header.length <= 120 && Array.isArray(question.options) && - question.options.length <= MAX_QUESTION_OPTIONS && question.options.every( (option) => isRecord(option) && typeof option.label === "string" && option.label.trim().length > 0 && - option.label.length <= 200 && - typeof option.description === "string" && - option.description.length <= 1_000, + typeof option.description === "string", ) && (question.multiple === undefined || typeof question.multiple === "boolean") && @@ -416,22 +406,11 @@ function parseQuestionRequest( (!isRecord(tool) || typeof tool.messageID !== "string" || tool.messageID.length === 0 || - tool.messageID.length > 256 || typeof tool.callID !== "string" || - tool.callID.length === 0 || - tool.callID.length > 256)) + tool.callID.length === 0)) ) { throw new Error("OpenCode 提问请求格式无效"); } - let serialized: string; - try { - serialized = JSON.stringify(properties); - } catch { - throw new Error("OpenCode 提问请求无法序列化"); - } - if (!byteLengthWithin(serialized, MAX_QUESTION_REQUEST_BYTES)) { - throw new Error("OpenCode 提问请求超过安全限制"); - } return properties as QuestionRequest; } @@ -2569,6 +2548,7 @@ export class OpenCodeRuntime implements AgentRuntime { throw new Error("OpenCode 工具调用 ID 格式无效"); } const toolName = part.tool.slice(0, 200); + const nativeControlTool = toolName.trim().toLowerCase() === "question"; const state = part.state.status === "error" ? "failed" : part.state.status; if (state === "failed") { @@ -2609,7 +2589,7 @@ export class OpenCodeRuntime implements AgentRuntime { const retained = subagentProgress.retain(subagent); toolStates.get(callId)!.subagent = retained; yield retained; - } else { + } else if (!nativeControlTool) { yield { requestId: request.requestId, type: "tool", diff --git a/src/renderer/src/AgentQuestionCard.tsx b/src/renderer/src/AgentQuestionCard.tsx index 4ee4ceba..c9ae8283 100644 --- a/src/renderer/src/AgentQuestionCard.tsx +++ b/src/renderer/src/AgentQuestionCard.tsx @@ -122,7 +122,6 @@ export function AgentQuestionCard({ {t('question.otherAnswer')} { const answer = event.target.value setCustom((current) => diff --git a/src/shared/assistant-contracts.test.ts b/src/shared/assistant-contracts.test.ts index 701031e5..0a2e55b3 100644 --- a/src/shared/assistant-contracts.test.ts +++ b/src/shared/assistant-contracts.test.ts @@ -15,7 +15,10 @@ import { projectExecutionSpaceSchema, projectUpdateSchema } from './assistant-contracts' -import { subagentEventSchema } from './contracts' +import { + agentQuestionResponseSchema, + subagentEventSchema +} from './contracts' const untouchedProject: AssistantProject = { id: '00000000-0000-4000-8000-000000000101', @@ -34,6 +37,25 @@ const untouchedProject: AssistantProject = { updatedAt: '2026-08-01T00:00:00.000Z' } +describe('agent question contracts', () => { + it('does not impose GoodBuddy-specific question answer limits', () => { + const answers = Array.from({ length: 5 }, (_, questionIndex) => + Array.from( + { length: 21 }, + (_, optionIndex) => + `${questionIndex}:${optionIndex}:${'回答内容'.repeat(600)}` + ) + ) + + expect( + agentQuestionResponseSchema.parse({ + questionId: `question-${'id'.repeat(100)}`, + answers + }).answers + ).toEqual(answers) + }) +}) + describe('activity history contracts', () => { it('accepts more than 500 records and details longer than 4,000 characters', () => { const records = Array.from({ length: 501 }, (_, index) => ({ diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts index 4def7cdb..8cb5e461 100644 --- a/src/shared/contracts.ts +++ b/src/shared/contracts.ts @@ -305,13 +305,12 @@ export const workspaceOpenPathRequestSchema = z .strict() export const agentQuestionAnswerSchema = z - .array(z.string().trim().min(1).max(2_000)) - .max(20) + .array(z.string().trim().min(1)) export const agentQuestionResponseSchema = z .object({ - questionId: z.string().trim().min(1).max(128), - answers: z.array(agentQuestionAnswerSchema).max(4) + questionId: z.string().trim().min(1), + answers: z.array(agentQuestionAnswerSchema) }) .strict()