From 82314ce018eda45a2748fbacca3ec37ef66c122a Mon Sep 17 00:00:00 2001 From: Sanyela Date: Wed, 3 Dec 2025 14:07:51 +0800 Subject: [PATCH 01/10] fix: remove content deduplication to fix token counting and display --- src/claude/claude-kiro.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index e30b48d..9d94689 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -1098,7 +1098,6 @@ async initializeAuth(forceRefresh = false) { const stream = response.data; let buffer = ''; - const processedPositions = new Set(); // 避免重复处理 for await (const chunk of stream) { buffer += chunk.toString(); @@ -1107,16 +1106,12 @@ async initializeAuth(forceRefresh = false) { const { events, remaining } = this.parseAwsEventStreamBuffer(buffer); buffer = remaining; - // 只 yield 新的事件 + // yield 所有事件(不再去重,因为重复内容是有效的) for (const event of events) { - const eventKey = `${event.type}:${event.data}`; - if (!processedPositions.has(eventKey)) { - processedPositions.add(eventKey); - if (event.type === 'content' && event.data) { - yield { type: 'content', content: event.data }; - } else if (event.type === 'toolUse') { - yield { type: 'toolUse', toolUse: event.data }; - } + if (event.type === 'content' && event.data) { + yield { type: 'content', content: event.data }; + } else if (event.type === 'toolUse') { + yield { type: 'toolUse', toolUse: event.data }; } } } From c56ab5780c06692f21eccf2f0572280d0bfab657 Mon Sep 17 00:00:00 2001 From: Sanyela Date: Wed, 3 Dec 2025 14:16:59 +0800 Subject: [PATCH 02/10] fix: correct tool calls parsing for streaming - support structured and bracket formats --- src/claude/claude-kiro.js | 84 +++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index 9d94689..86e5679 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -1022,22 +1022,12 @@ async initializeAuth(forceRefresh = false) { let searchStart = 0; while (true) { - // 查找 {"content": 或 {"toolUse" 的起始位置 - const contentStart = remaining.indexOf('{"content":', searchStart); - const toolUseStart = remaining.indexOf('{"toolUse":', searchStart); - - let jsonStart = -1; - if (contentStart >= 0 && toolUseStart >= 0) { - jsonStart = Math.min(contentStart, toolUseStart); - } else if (contentStart >= 0) { - jsonStart = contentStart; - } else if (toolUseStart >= 0) { - jsonStart = toolUseStart; - } - + // 查找 JSON 对象的起始位置 + // Kiro 返回格式: {"content":"..."} 或 {"name":"xxx","toolUseId":"xxx",...} + const jsonStart = remaining.indexOf('{', searchStart); if (jsonStart < 0) break; - // 查找对应的 } 结束位置 + // 查找对应的 } 结束位置(简单匹配,不处理嵌套) const jsonEnd = remaining.indexOf('}', jsonStart); if (jsonEnd < 0) { // 不完整的 JSON,保留在缓冲区 @@ -1048,10 +1038,24 @@ async initializeAuth(forceRefresh = false) { const jsonStr = remaining.substring(jsonStart, jsonEnd + 1); try { const parsed = JSON.parse(jsonStr); - if (parsed.content !== undefined) { - events.push({ type: 'content', data: parsed.content }); - } else if (parsed.toolUse !== undefined) { - events.push({ type: 'toolUse', data: parsed.toolUse }); + // 处理 content 事件 + if (parsed.content !== undefined && !parsed.followupPrompt) { + // 处理转义字符 + let decodedContent = parsed.content; + decodedContent = decodedContent.replace(/(? 0) { + for (const btc of bracketToolCalls) { + toolCalls.push({ + toolUseId: btc.id || `tool_${uuidv4()}`, + name: btc.function.name, + input: JSON.parse(btc.function.arguments || '{}') + }); } } @@ -1228,11 +1270,13 @@ async initializeAuth(forceRefresh = false) { index: blockIndex, delta: { type: "input_json_delta", - partial_json: JSON.stringify(tc.input || {}) + partial_json: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input || {}) } }; yield { type: "content_block_stop", index: blockIndex }; + + outputTokens += this.countTextTokens(JSON.stringify(tc.input || {})); } } From 28ba5cfef6104d8c3247d873ea9a7e022479cad4 Mon Sep 17 00:00:00 2001 From: Sanyela Date: Wed, 3 Dec 2025 14:22:14 +0800 Subject: [PATCH 03/10] fix: ensure content is never empty when sending toolResults --- src/claude/claude-kiro.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index 86e5679..59cb417 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -742,8 +742,9 @@ async initializeAuth(forceRefresh = false) { currentContent = this.getContentText(currentMessage); } - if (!currentContent && currentToolResults.length === 0 && currentToolUses.length === 0) { - currentContent = 'Continue'; + // Kiro API 要求 content 不能为空,即使有 toolResults + if (!currentContent) { + currentContent = currentToolResults.length > 0 ? 'Tool results provided.' : 'Continue'; } } From 55059fc6c7e7f7db79a9340f5ae6657fb8af1c13 Mon Sep 17 00:00:00 2001 From: Sanyela Date: Wed, 3 Dec 2025 14:25:45 +0800 Subject: [PATCH 04/10] fix: only include history when non-empty --- src/claude/claude-kiro.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index 59cb417..a8f6473 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -752,10 +752,14 @@ async initializeAuth(forceRefresh = false) { conversationState: { chatTriggerType: KIRO_CONSTANTS.CHAT_TRIGGER_TYPE_MANUAL, conversationId: conversationId, - currentMessage: {}, // Will be populated as userInputMessage - history: history + currentMessage: {} // Will be populated as userInputMessage } }; + + // 只有当 history 非空时才添加(API 可能不接受空数组) + if (history.length > 0) { + request.conversationState.history = history; + } // currentMessage 始终是 userInputMessage 类型 // 注意:API 不接受 null 值,空字段应该完全不包含 From 14b4882e1005d4fd61c982998fea078d7af194b2 Mon Sep 17 00:00:00 2001 From: Sanyela Date: Wed, 3 Dec 2025 14:30:26 +0800 Subject: [PATCH 05/10] fix: tools calling failed due to nested JSON parsing --- src/claude/claude-kiro.js | 46 ++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index a8f6473..ad5fce0 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -1028,14 +1028,49 @@ async initializeAuth(forceRefresh = false) { while (true) { // 查找 JSON 对象的起始位置 - // Kiro 返回格式: {"content":"..."} 或 {"name":"xxx","toolUseId":"xxx",...} + // Kiro 返回格式: {"content":"..."} 或 {"name":"xxx","toolUseId":"xxx","input":{...},...} const jsonStart = remaining.indexOf('{', searchStart); if (jsonStart < 0) break; - // 查找对应的 } 结束位置(简单匹配,不处理嵌套) - const jsonEnd = remaining.indexOf('}', jsonStart); + // 正确处理嵌套的 {} - 使用括号计数法 + let braceCount = 0; + let jsonEnd = -1; + let inString = false; + let escapeNext = false; + + for (let i = jsonStart; i < remaining.length; i++) { + const char = remaining[i]; + + if (escapeNext) { + escapeNext = false; + continue; + } + + if (char === '\\') { + escapeNext = true; + continue; + } + + if (char === '"') { + inString = !inString; + continue; + } + + if (!inString) { + if (char === '{') { + braceCount++; + } else if (char === '}') { + braceCount--; + if (braceCount === 0) { + jsonEnd = i; + break; + } + } + } + } + if (jsonEnd < 0) { - // 不完整的 JSON,保留在缓冲区 + // 不完整的 JSON,保留在缓冲区等待更多数据 remaining = remaining.substring(jsonStart); break; } @@ -1063,7 +1098,8 @@ async initializeAuth(forceRefresh = false) { }); } } catch (e) { - // JSON 解析失败,可能是不完整的,继续搜索 + // JSON 解析失败,跳过这个位置继续搜索 + console.debug('[Kiro] JSON parse failed for:', jsonStr.substring(0, 100)); } searchStart = jsonEnd + 1; From fc3e7ccba4f5050ed41956eb52ca1681c5ea0003 Mon Sep 17 00:00:00 2001 From: Sanyela Date: Wed, 3 Dec 2025 14:33:10 +0800 Subject: [PATCH 06/10] fix: skip AWS Event Stream binary headers when parsing JSON --- src/claude/claude-kiro.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index ad5fce0..fe51470 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -1027,9 +1027,20 @@ async initializeAuth(forceRefresh = false) { let searchStart = 0; while (true) { - // 查找 JSON 对象的起始位置 - // Kiro 返回格式: {"content":"..."} 或 {"name":"xxx","toolUseId":"xxx","input":{...},...} - const jsonStart = remaining.indexOf('{', searchStart); + // 查找真正的 JSON payload 起始位置 + // AWS Event Stream 包含二进制头部,我们只搜索有效的 JSON 模式 + // Kiro 返回格式: {"content":"..."} 或 {"name":"xxx","toolUseId":"xxx",...} 或 {"followupPrompt":"..."} + + // 搜索所有可能的 JSON payload 开头模式 + const contentStart = remaining.indexOf('{"content":', searchStart); + const nameStart = remaining.indexOf('{"name":', searchStart); + const followupStart = remaining.indexOf('{"followupPrompt":', searchStart); + + // 找到最早出现的有效 JSON 模式 + const candidates = [contentStart, nameStart, followupStart].filter(pos => pos >= 0); + if (candidates.length === 0) break; + + const jsonStart = Math.min(...candidates); if (jsonStart < 0) break; // 正确处理嵌套的 {} - 使用括号计数法 @@ -1099,7 +1110,6 @@ async initializeAuth(forceRefresh = false) { } } catch (e) { // JSON 解析失败,跳过这个位置继续搜索 - console.debug('[Kiro] JSON parse failed for:', jsonStr.substring(0, 100)); } searchStart = jsonEnd + 1; From 9a2f0b0c52a4b85ba20bf7eea44eac59aa5e82f3 Mon Sep 17 00:00:00 2001 From: Sanyela Date: Wed, 3 Dec 2025 14:37:20 +0800 Subject: [PATCH 07/10] fix: filter duplicate consecutive content events from Kiro stream --- src/claude/claude-kiro.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index fe51470..3249063 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -1153,6 +1153,7 @@ async initializeAuth(forceRefresh = false) { const stream = response.data; let buffer = ''; + let lastContentEvent = null; // 用于检测连续重复的 content 事件 for await (const chunk of stream) { buffer += chunk.toString(); @@ -1161,9 +1162,15 @@ async initializeAuth(forceRefresh = false) { const { events, remaining } = this.parseAwsEventStreamBuffer(buffer); buffer = remaining; - // yield 所有事件(不再去重,因为重复内容是有效的) + // yield 所有事件,但过滤连续完全相同的 content 事件(Kiro API 有时会重复发送) for (const event of events) { if (event.type === 'content' && event.data) { + // 检查是否与上一个 content 事件完全相同 + if (lastContentEvent === event.data) { + // 跳过重复的内容 + continue; + } + lastContentEvent = event.data; yield { type: 'content', content: event.data }; } else if (event.type === 'toolUse') { yield { type: 'toolUse', toolUse: event.data }; From 55ae7664ec6f14ceef99d3bffc8381ae73701c7d Mon Sep 17 00:00:00 2001 From: Sanyela Date: Wed, 3 Dec 2025 14:42:22 +0800 Subject: [PATCH 08/10] fix: handle multi-part toolUse events (input/stop sent separately) --- src/claude/claude-kiro.js | 88 +++++++++++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index 3249063..e2fb5a3 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -1032,12 +1032,18 @@ async initializeAuth(forceRefresh = false) { // Kiro 返回格式: {"content":"..."} 或 {"name":"xxx","toolUseId":"xxx",...} 或 {"followupPrompt":"..."} // 搜索所有可能的 JSON payload 开头模式 + // Kiro 返回的 toolUse 可能分多个事件: + // 1. {"name":"xxx","toolUseId":"xxx"} - 开始 + // 2. {"input":"..."} - input 数据(可能多次) + // 3. {"stop":true} - 结束 const contentStart = remaining.indexOf('{"content":', searchStart); const nameStart = remaining.indexOf('{"name":', searchStart); const followupStart = remaining.indexOf('{"followupPrompt":', searchStart); + const inputStart = remaining.indexOf('{"input":', searchStart); + const stopStart = remaining.indexOf('{"stop":', searchStart); // 找到最早出现的有效 JSON 模式 - const candidates = [contentStart, nameStart, followupStart].filter(pos => pos >= 0); + const candidates = [contentStart, nameStart, followupStart, inputStart, stopStart].filter(pos => pos >= 0); if (candidates.length === 0) break; const jsonStart = Math.min(...candidates); @@ -1096,7 +1102,7 @@ async initializeAuth(forceRefresh = false) { decodedContent = decodedContent.replace(/(? 0) { From 43d491fb5be300e12ca7fe26faf1c4d18fe7ec99 Mon Sep 17 00:00:00 2001 From: Sanyela Date: Wed, 3 Dec 2025 14:46:09 +0800 Subject: [PATCH 09/10] fix: deduplicate toolResults to avoid Kiro API validation error --- src/claude/claude-kiro.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index e2fb5a3..af43308 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -637,7 +637,16 @@ async initializeAuth(forceRefresh = false) { userInputMessage.images = images; } if (toolResults.length > 0) { - userInputMessage.userInputMessageContext = { toolResults }; + // 去重 toolResults - Kiro API 不接受重复的 toolUseId + const uniqueToolResults = []; + const seenIds = new Set(); + for (const tr of toolResults) { + if (!seenIds.has(tr.toolUseId)) { + seenIds.add(tr.toolUseId); + uniqueToolResults.push(tr); + } + } + userInputMessage.userInputMessageContext = { toolResults: uniqueToolResults }; } history.push({ userInputMessage }); @@ -777,7 +786,16 @@ async initializeAuth(forceRefresh = false) { // 构建 userInputMessageContext,只包含非空字段 const userInputMessageContext = {}; if (currentToolResults.length > 0) { - userInputMessageContext.toolResults = currentToolResults; + // 去重 toolResults - Kiro API 不接受重复的 toolUseId + const uniqueToolResults = []; + const seenToolUseIds = new Set(); + for (const tr of currentToolResults) { + if (!seenToolUseIds.has(tr.toolUseId)) { + seenToolUseIds.add(tr.toolUseId); + uniqueToolResults.push(tr); + } + } + userInputMessageContext.toolResults = uniqueToolResults; } if (Object.keys(toolsContext).length > 0 && toolsContext.tools) { userInputMessageContext.tools = toolsContext.tools; From 399435e39d5311852d64c55794b2b08b0dd4e6f3 Mon Sep 17 00:00:00 2001 From: Sanyela Date: Wed, 3 Dec 2025 15:02:41 +0800 Subject: [PATCH 10/10] fix: merge toolUse events with same toolUseId instead of creating separate blocks --- src/claude/claude-kiro.js | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index af43308..0ba0bac 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -1309,24 +1309,31 @@ async initializeAuth(forceRefresh = false) { }; } else if (event.type === 'toolUse') { const tc = event.toolUse; - // 工具调用开始事件(包含 name 和 toolUseId) + // 工具调用事件(包含 name 和 toolUseId) if (tc.name && tc.toolUseId) { - // 如果有未完成的工具调用,先保存它 - if (currentToolCall) { - try { - currentToolCall.input = JSON.parse(currentToolCall.input); - } catch (e) { - // input 不是有效 JSON,保持原样 + // 检查是否是同一个工具调用的续传(相同 toolUseId) + if (currentToolCall && currentToolCall.toolUseId === tc.toolUseId) { + // 同一个工具调用,累积 input + currentToolCall.input += tc.input || ''; + } else { + // 不同的工具调用 + // 如果有未完成的工具调用,先保存它 + if (currentToolCall) { + try { + currentToolCall.input = JSON.parse(currentToolCall.input); + } catch (e) { + // input 不是有效 JSON,保持原样 + } + toolCalls.push(currentToolCall); } - toolCalls.push(currentToolCall); + // 开始新的工具调用 + currentToolCall = { + toolUseId: tc.toolUseId, + name: tc.name, + input: tc.input || '' + }; } - // 开始新的工具调用 - currentToolCall = { - toolUseId: tc.toolUseId, - name: tc.name, - input: tc.input || '' - }; - // 如果这个事件同时包含 stop,直接完成 + // 如果这个事件包含 stop,完成工具调用 if (tc.stop) { try { currentToolCall.input = JSON.parse(currentToolCall.input);