openai_tool_corrector.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. package service
  2. import (
  3. "bytes"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "sync"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  9. "github.com/tidwall/gjson"
  10. "github.com/tidwall/sjson"
  11. )
  12. // codexToolNameMapping 定义 Codex 原生工具名称到 OpenCode 工具名称的映射
  13. var codexToolNameMapping = map[string]string{
  14. "apply_patch": "edit",
  15. "applyPatch": "edit",
  16. "update_plan": "todowrite",
  17. "updatePlan": "todowrite",
  18. "read_plan": "todoread",
  19. "readPlan": "todoread",
  20. "search_files": "grep",
  21. "searchFiles": "grep",
  22. "list_files": "glob",
  23. "listFiles": "glob",
  24. "read_file": "read",
  25. "readFile": "read",
  26. "write_file": "write",
  27. "writeFile": "write",
  28. "execute_bash": "bash",
  29. "executeBash": "bash",
  30. "exec_bash": "bash",
  31. "execBash": "bash",
  32. // Some clients output generic fetch names.
  33. "fetch": "webfetch",
  34. "web_fetch": "webfetch",
  35. "webFetch": "webfetch",
  36. }
  37. // ToolCorrectionStats 记录工具修正的统计信息(导出用于 JSON 序列化)
  38. type ToolCorrectionStats struct {
  39. TotalCorrected int `json:"total_corrected"`
  40. CorrectionsByTool map[string]int `json:"corrections_by_tool"`
  41. }
  42. // CodexToolCorrector 处理 Codex 工具调用的自动修正
  43. type CodexToolCorrector struct {
  44. stats ToolCorrectionStats
  45. mu sync.RWMutex
  46. }
  47. // NewCodexToolCorrector 创建新的工具修正器
  48. func NewCodexToolCorrector() *CodexToolCorrector {
  49. return &CodexToolCorrector{
  50. stats: ToolCorrectionStats{
  51. CorrectionsByTool: make(map[string]int),
  52. },
  53. }
  54. }
  55. // CorrectToolCallsInSSEData 修正 SSE 数据中的工具调用
  56. // 返回修正后的数据和是否进行了修正
  57. func (c *CodexToolCorrector) CorrectToolCallsInSSEData(data string) (string, bool) {
  58. if data == "" || data == "\n" {
  59. return data, false
  60. }
  61. correctedBytes, corrected := c.CorrectToolCallsInSSEBytes([]byte(data))
  62. if !corrected {
  63. return data, false
  64. }
  65. return string(correctedBytes), true
  66. }
  67. // CorrectToolCallsInSSEBytes 修正 SSE JSON 数据中的工具调用(字节路径)。
  68. // 返回修正后的数据和是否进行了修正。
  69. func (c *CodexToolCorrector) CorrectToolCallsInSSEBytes(data []byte) ([]byte, bool) {
  70. if len(bytes.TrimSpace(data)) == 0 {
  71. return data, false
  72. }
  73. if !mayContainToolCallPayload(data) {
  74. return data, false
  75. }
  76. if !gjson.ValidBytes(data) {
  77. // 不是有效 JSON,直接返回原数据
  78. return data, false
  79. }
  80. updated := data
  81. corrected := false
  82. collect := func(changed bool, next []byte) {
  83. if changed {
  84. corrected = true
  85. updated = next
  86. }
  87. }
  88. if next, changed := c.correctToolCallsArrayAtPath(updated, "tool_calls"); changed {
  89. collect(changed, next)
  90. }
  91. if next, changed := c.correctFunctionAtPath(updated, "function_call"); changed {
  92. collect(changed, next)
  93. }
  94. if next, changed := c.correctToolCallsArrayAtPath(updated, "delta.tool_calls"); changed {
  95. collect(changed, next)
  96. }
  97. if next, changed := c.correctFunctionAtPath(updated, "delta.function_call"); changed {
  98. collect(changed, next)
  99. }
  100. choicesCount := int(gjson.GetBytes(updated, "choices.#").Int())
  101. for i := 0; i < choicesCount; i++ {
  102. prefix := "choices." + strconv.Itoa(i)
  103. if next, changed := c.correctToolCallsArrayAtPath(updated, prefix+".message.tool_calls"); changed {
  104. collect(changed, next)
  105. }
  106. if next, changed := c.correctFunctionAtPath(updated, prefix+".message.function_call"); changed {
  107. collect(changed, next)
  108. }
  109. if next, changed := c.correctToolCallsArrayAtPath(updated, prefix+".delta.tool_calls"); changed {
  110. collect(changed, next)
  111. }
  112. if next, changed := c.correctFunctionAtPath(updated, prefix+".delta.function_call"); changed {
  113. collect(changed, next)
  114. }
  115. }
  116. if !corrected {
  117. return data, false
  118. }
  119. return updated, true
  120. }
  121. func mayContainToolCallPayload(data []byte) bool {
  122. // 快速路径:多数 token / 文本事件不包含工具字段,避免进入 JSON 解析热路径。
  123. return bytes.Contains(data, []byte(`"tool_calls"`)) ||
  124. bytes.Contains(data, []byte(`"function_call"`)) ||
  125. bytes.Contains(data, []byte(`"function":{"name"`))
  126. }
  127. // correctToolCallsArrayAtPath 修正指定路径下 tool_calls 数组中的工具名称。
  128. func (c *CodexToolCorrector) correctToolCallsArrayAtPath(data []byte, toolCallsPath string) ([]byte, bool) {
  129. count := int(gjson.GetBytes(data, toolCallsPath+".#").Int())
  130. if count <= 0 {
  131. return data, false
  132. }
  133. updated := data
  134. corrected := false
  135. for i := 0; i < count; i++ {
  136. functionPath := toolCallsPath + "." + strconv.Itoa(i) + ".function"
  137. if next, changed := c.correctFunctionAtPath(updated, functionPath); changed {
  138. updated = next
  139. corrected = true
  140. }
  141. }
  142. return updated, corrected
  143. }
  144. // correctFunctionAtPath 修正指定路径下单个函数调用的工具名称和参数。
  145. func (c *CodexToolCorrector) correctFunctionAtPath(data []byte, functionPath string) ([]byte, bool) {
  146. namePath := functionPath + ".name"
  147. nameResult := gjson.GetBytes(data, namePath)
  148. if !nameResult.Exists() || nameResult.Type != gjson.String {
  149. return data, false
  150. }
  151. name := strings.TrimSpace(nameResult.Str)
  152. if name == "" {
  153. return data, false
  154. }
  155. updated := data
  156. corrected := false
  157. // 查找并修正工具名称
  158. if correctName, found := codexToolNameMapping[name]; found {
  159. if next, err := sjson.SetBytes(updated, namePath, correctName); err == nil {
  160. updated = next
  161. c.recordCorrection(name, correctName)
  162. corrected = true
  163. name = correctName // 使用修正后的名称进行参数修正
  164. }
  165. }
  166. // 修正工具参数(基于工具名称)
  167. if next, changed := c.correctToolParametersAtPath(updated, functionPath+".arguments", name); changed {
  168. updated = next
  169. corrected = true
  170. }
  171. return updated, corrected
  172. }
  173. // correctToolParametersAtPath 修正指定路径下 arguments 参数。
  174. func (c *CodexToolCorrector) correctToolParametersAtPath(data []byte, argumentsPath, toolName string) ([]byte, bool) {
  175. if toolName != "bash" && toolName != "edit" {
  176. return data, false
  177. }
  178. args := gjson.GetBytes(data, argumentsPath)
  179. if !args.Exists() {
  180. return data, false
  181. }
  182. switch args.Type {
  183. case gjson.String:
  184. argsJSON := strings.TrimSpace(args.Str)
  185. if !gjson.Valid(argsJSON) {
  186. return data, false
  187. }
  188. if !gjson.Parse(argsJSON).IsObject() {
  189. return data, false
  190. }
  191. nextArgsJSON, corrected := c.correctToolArgumentsJSON(argsJSON, toolName)
  192. if !corrected {
  193. return data, false
  194. }
  195. next, err := sjson.SetBytes(data, argumentsPath, nextArgsJSON)
  196. if err != nil {
  197. return data, false
  198. }
  199. return next, true
  200. case gjson.JSON:
  201. if !args.IsObject() || !gjson.Valid(args.Raw) {
  202. return data, false
  203. }
  204. nextArgsJSON, corrected := c.correctToolArgumentsJSON(args.Raw, toolName)
  205. if !corrected {
  206. return data, false
  207. }
  208. next, err := sjson.SetRawBytes(data, argumentsPath, []byte(nextArgsJSON))
  209. if err != nil {
  210. return data, false
  211. }
  212. return next, true
  213. default:
  214. return data, false
  215. }
  216. }
  217. // correctToolArgumentsJSON 修正工具参数 JSON(对象字符串),返回修正后的 JSON 与是否变更。
  218. func (c *CodexToolCorrector) correctToolArgumentsJSON(argsJSON, toolName string) (string, bool) {
  219. if !gjson.Valid(argsJSON) {
  220. return argsJSON, false
  221. }
  222. if !gjson.Parse(argsJSON).IsObject() {
  223. return argsJSON, false
  224. }
  225. updated := argsJSON
  226. corrected := false
  227. // 根据工具名称应用特定的参数修正规则
  228. switch toolName {
  229. case "bash":
  230. // OpenCode bash 支持 workdir;有些来源会输出 work_dir。
  231. if !gjson.Get(updated, "workdir").Exists() {
  232. if next, changed := moveJSONField(updated, "work_dir", "workdir"); changed {
  233. updated = next
  234. corrected = true
  235. logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'work_dir' to 'workdir' in bash tool")
  236. }
  237. } else {
  238. if next, changed := deleteJSONField(updated, "work_dir"); changed {
  239. updated = next
  240. corrected = true
  241. logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Removed duplicate 'work_dir' parameter from bash tool")
  242. }
  243. }
  244. case "edit":
  245. // OpenCode edit 参数为 filePath/oldString/newString(camelCase)。
  246. if !gjson.Get(updated, "filePath").Exists() {
  247. if next, changed := moveJSONField(updated, "file_path", "filePath"); changed {
  248. updated = next
  249. corrected = true
  250. logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'file_path' to 'filePath' in edit tool")
  251. } else if next, changed := moveJSONField(updated, "path", "filePath"); changed {
  252. updated = next
  253. corrected = true
  254. logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'path' to 'filePath' in edit tool")
  255. } else if next, changed := moveJSONField(updated, "file", "filePath"); changed {
  256. updated = next
  257. corrected = true
  258. logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'file' to 'filePath' in edit tool")
  259. }
  260. }
  261. if next, changed := moveJSONField(updated, "old_string", "oldString"); changed {
  262. updated = next
  263. corrected = true
  264. logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'old_string' to 'oldString' in edit tool")
  265. }
  266. if next, changed := moveJSONField(updated, "new_string", "newString"); changed {
  267. updated = next
  268. corrected = true
  269. logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'new_string' to 'newString' in edit tool")
  270. }
  271. if next, changed := moveJSONField(updated, "replace_all", "replaceAll"); changed {
  272. updated = next
  273. corrected = true
  274. logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'replace_all' to 'replaceAll' in edit tool")
  275. }
  276. }
  277. return updated, corrected
  278. }
  279. func moveJSONField(input, from, to string) (string, bool) {
  280. if gjson.Get(input, to).Exists() {
  281. return input, false
  282. }
  283. src := gjson.Get(input, from)
  284. if !src.Exists() {
  285. return input, false
  286. }
  287. next, err := sjson.SetRaw(input, to, src.Raw)
  288. if err != nil {
  289. return input, false
  290. }
  291. next, err = sjson.Delete(next, from)
  292. if err != nil {
  293. return input, false
  294. }
  295. return next, true
  296. }
  297. func deleteJSONField(input, path string) (string, bool) {
  298. if !gjson.Get(input, path).Exists() {
  299. return input, false
  300. }
  301. next, err := sjson.Delete(input, path)
  302. if err != nil {
  303. return input, false
  304. }
  305. return next, true
  306. }
  307. // recordCorrection 记录一次工具名称修正
  308. func (c *CodexToolCorrector) recordCorrection(from, to string) {
  309. c.mu.Lock()
  310. defer c.mu.Unlock()
  311. c.stats.TotalCorrected++
  312. key := fmt.Sprintf("%s->%s", from, to)
  313. c.stats.CorrectionsByTool[key]++
  314. logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Corrected tool call: %s -> %s (total: %d)",
  315. from, to, c.stats.TotalCorrected)
  316. }
  317. // GetStats 获取工具修正统计信息
  318. func (c *CodexToolCorrector) GetStats() ToolCorrectionStats {
  319. c.mu.RLock()
  320. defer c.mu.RUnlock()
  321. // 返回副本以避免并发问题
  322. statsCopy := ToolCorrectionStats{
  323. TotalCorrected: c.stats.TotalCorrected,
  324. CorrectionsByTool: make(map[string]int, len(c.stats.CorrectionsByTool)),
  325. }
  326. for k, v := range c.stats.CorrectionsByTool {
  327. statsCopy.CorrectionsByTool[k] = v
  328. }
  329. return statsCopy
  330. }
  331. // ResetStats 重置统计信息
  332. func (c *CodexToolCorrector) ResetStats() {
  333. c.mu.Lock()
  334. defer c.mu.Unlock()
  335. c.stats.TotalCorrected = 0
  336. c.stats.CorrectionsByTool = make(map[string]int)
  337. }
  338. // CorrectToolName 直接修正工具名称(用于非 SSE 场景)
  339. func CorrectToolName(name string) (string, bool) {
  340. if correctName, found := codexToolNameMapping[name]; found {
  341. return correctName, true
  342. }
  343. return name, false
  344. }
  345. // GetToolNameMapping 获取工具名称映射表
  346. func GetToolNameMapping() map[string]string {
  347. // 返回副本以避免外部修改
  348. mapping := make(map[string]string, len(codexToolNameMapping))
  349. for k, v := range codexToolNameMapping {
  350. mapping[k] = v
  351. }
  352. return mapping
  353. }