From 1ca4b80addbbd4e9b59efdbe130f3d1b4da25ac1 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:04:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(decision):=20add=20validateJSONFormat=20to?= =?UTF-8?q?=20catch=20common=20AI=20errors=20Adds=20comprehensive=20JSON?= =?UTF-8?q?=20validation=20before=20parsing=20to=20catch=20common=20AI=20o?= =?UTF-8?q?utput=20errors:=201.=20Format=20validation:=20Ensures=20JSON=20?= =?UTF-8?q?starts=20with=20[{=20(decision=20array)=202.=20Range=20symbol?= =?UTF-8?q?=20detection:=20Rejects=20~=20symbols=20(e.g.,=20"leverage:=203?= =?UTF-8?q?~5")=203.=20Thousands=20separator=20detection:=20Rejects=20comm?= =?UTF-8?q?as=20in=20numbers=20(e.g.,=20"98,000")=20Execution=20order=20(c?= =?UTF-8?q?ritical=20for=20fullwidth=20character=20fix):=201.=20Extract=20?= =?UTF-8?q?JSON=20from=20response=202.=20fixMissingQuotes=20-=20normalize?= =?UTF-8?q?=20fullwidth=20=E2=86=92=20halfwidth=20=E2=9C=85=203.=20validat?= =?UTF-8?q?eJSONFormat=20-=20check=20for=20common=20errors=20=E2=9C=85=204?= =?UTF-8?q?.=20Parse=20JSON=20This=20validation=20layer=20provides=20early?= =?UTF-8?q?=20error=20detection=20and=20clearer=20error=20messages=20for?= =?UTF-8?q?=20debugging=20AI=20response=20issues.=20Added=20helper=20funct?= =?UTF-8?q?ion:=20-=20min(a,=20b=20int)=20int=20-=20returns=20smaller=20of?= =?UTF-8?q?=20two=20integers=20Co-Authored-By:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 50 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index 9a75df38..9619cc61 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -444,12 +444,17 @@ func extractDecisions(response string) ([]Decision, error) { jsonContent := strings.TrimSpace(response[arrayStart : arrayEnd+1]) - // 🔧 修复常见的JSON格式错误:缺少引号的字段值 + // 🔧 先修复全角字符和引号问题(必须在验证之前!) + // 修复常见的JSON格式错误:全角字符、缺少引号的字段值等 // 匹配: "reasoning": 内容"} 或 "reasoning": 内容} (没有引号) // 修复为: "reasoning": "内容"} - // 使用简单的字符串扫描而不是正则表达式 jsonContent = fixMissingQuotes(jsonContent) + // 🔧 验证 JSON 格式(检测常见错误) + if err := validateJSONFormat(jsonContent); err != nil { + return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response) + } + // 解析JSON var decisions []Decision if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil { @@ -478,6 +483,47 @@ func fixMissingQuotes(jsonStr string) string { return jsonStr } +// validateJSONFormat 验证 JSON 格式,检测常见错误 +func validateJSONFormat(jsonStr string) error { + trimmed := strings.TrimSpace(jsonStr) + + // 检查是否是决策对象数组(必须以 [{ 或 [ { 开头) + if !strings.HasPrefix(trimmed, "[{") && !strings.HasPrefix(trimmed, "[ {") { + // 检查是否是纯数字/范围数组(常见错误) + if strings.HasPrefix(trimmed, "[") && !strings.Contains(trimmed[:min(20, len(trimmed))], "{") { + return fmt.Errorf("不是有效的决策数组(必须包含对象 {}),实际内容: %s", trimmed[:min(50, len(trimmed))]) + } + return fmt.Errorf("JSON 必须以 [{ 开头(决策对象数组),实际: %s", trimmed[:min(20, len(trimmed))]) + } + + // 检查是否包含范围符号 ~(LLM 常见错误) + if strings.Contains(jsonStr, "~") { + return fmt.Errorf("JSON 中不可包含范围符号 ~,所有数字必须是精确的单一值") + } + + // 检查是否包含千位分隔符(如 98,000) + // 使用简单的模式匹配:数字+逗号+3位数字 + for i := 0; i < len(jsonStr)-4; i++ { + if jsonStr[i] >= '0' && jsonStr[i] <= '9' && + jsonStr[i+1] == ',' && + jsonStr[i+2] >= '0' && jsonStr[i+2] <= '9' && + jsonStr[i+3] >= '0' && jsonStr[i+3] <= '9' && + jsonStr[i+4] >= '0' && jsonStr[i+4] <= '9' { + return fmt.Errorf("JSON 数字不可包含千位分隔符逗号,发现: %s", jsonStr[i:min(i+10, len(jsonStr))]) + } + } + + return nil +} + +// min 返回两个整数中的较小值 +func min(a, b int) int { + if a < b { + return a + } + return b +} + // validateDecisions 验证所有决策(需要账户信息和杠杆配置) func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { for i, decision := range decisions {