| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- package service
- import (
- "bytes"
- "fmt"
- "strconv"
- "strings"
- "sync"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "github.com/tidwall/gjson"
- "github.com/tidwall/sjson"
- )
- // codexToolNameMapping 定义 Codex 原生工具名称到 OpenCode 工具名称的映射
- var codexToolNameMapping = map[string]string{
- "apply_patch": "edit",
- "applyPatch": "edit",
- "update_plan": "todowrite",
- "updatePlan": "todowrite",
- "read_plan": "todoread",
- "readPlan": "todoread",
- "search_files": "grep",
- "searchFiles": "grep",
- "list_files": "glob",
- "listFiles": "glob",
- "read_file": "read",
- "readFile": "read",
- "write_file": "write",
- "writeFile": "write",
- "execute_bash": "bash",
- "executeBash": "bash",
- "exec_bash": "bash",
- "execBash": "bash",
- // Some clients output generic fetch names.
- "fetch": "webfetch",
- "web_fetch": "webfetch",
- "webFetch": "webfetch",
- }
- // ToolCorrectionStats 记录工具修正的统计信息(导出用于 JSON 序列化)
- type ToolCorrectionStats struct {
- TotalCorrected int `json:"total_corrected"`
- CorrectionsByTool map[string]int `json:"corrections_by_tool"`
- }
- // CodexToolCorrector 处理 Codex 工具调用的自动修正
- type CodexToolCorrector struct {
- stats ToolCorrectionStats
- mu sync.RWMutex
- }
- // NewCodexToolCorrector 创建新的工具修正器
- func NewCodexToolCorrector() *CodexToolCorrector {
- return &CodexToolCorrector{
- stats: ToolCorrectionStats{
- CorrectionsByTool: make(map[string]int),
- },
- }
- }
- // CorrectToolCallsInSSEData 修正 SSE 数据中的工具调用
- // 返回修正后的数据和是否进行了修正
- func (c *CodexToolCorrector) CorrectToolCallsInSSEData(data string) (string, bool) {
- if data == "" || data == "\n" {
- return data, false
- }
- correctedBytes, corrected := c.CorrectToolCallsInSSEBytes([]byte(data))
- if !corrected {
- return data, false
- }
- return string(correctedBytes), true
- }
- // CorrectToolCallsInSSEBytes 修正 SSE JSON 数据中的工具调用(字节路径)。
- // 返回修正后的数据和是否进行了修正。
- func (c *CodexToolCorrector) CorrectToolCallsInSSEBytes(data []byte) ([]byte, bool) {
- if len(bytes.TrimSpace(data)) == 0 {
- return data, false
- }
- if !mayContainToolCallPayload(data) {
- return data, false
- }
- if !gjson.ValidBytes(data) {
- // 不是有效 JSON,直接返回原数据
- return data, false
- }
- updated := data
- corrected := false
- collect := func(changed bool, next []byte) {
- if changed {
- corrected = true
- updated = next
- }
- }
- if next, changed := c.correctToolCallsArrayAtPath(updated, "tool_calls"); changed {
- collect(changed, next)
- }
- if next, changed := c.correctFunctionAtPath(updated, "function_call"); changed {
- collect(changed, next)
- }
- if next, changed := c.correctToolCallsArrayAtPath(updated, "delta.tool_calls"); changed {
- collect(changed, next)
- }
- if next, changed := c.correctFunctionAtPath(updated, "delta.function_call"); changed {
- collect(changed, next)
- }
- choicesCount := int(gjson.GetBytes(updated, "choices.#").Int())
- for i := 0; i < choicesCount; i++ {
- prefix := "choices." + strconv.Itoa(i)
- if next, changed := c.correctToolCallsArrayAtPath(updated, prefix+".message.tool_calls"); changed {
- collect(changed, next)
- }
- if next, changed := c.correctFunctionAtPath(updated, prefix+".message.function_call"); changed {
- collect(changed, next)
- }
- if next, changed := c.correctToolCallsArrayAtPath(updated, prefix+".delta.tool_calls"); changed {
- collect(changed, next)
- }
- if next, changed := c.correctFunctionAtPath(updated, prefix+".delta.function_call"); changed {
- collect(changed, next)
- }
- }
- if !corrected {
- return data, false
- }
- return updated, true
- }
- func mayContainToolCallPayload(data []byte) bool {
- // 快速路径:多数 token / 文本事件不包含工具字段,避免进入 JSON 解析热路径。
- return bytes.Contains(data, []byte(`"tool_calls"`)) ||
- bytes.Contains(data, []byte(`"function_call"`)) ||
- bytes.Contains(data, []byte(`"function":{"name"`))
- }
- // correctToolCallsArrayAtPath 修正指定路径下 tool_calls 数组中的工具名称。
- func (c *CodexToolCorrector) correctToolCallsArrayAtPath(data []byte, toolCallsPath string) ([]byte, bool) {
- count := int(gjson.GetBytes(data, toolCallsPath+".#").Int())
- if count <= 0 {
- return data, false
- }
- updated := data
- corrected := false
- for i := 0; i < count; i++ {
- functionPath := toolCallsPath + "." + strconv.Itoa(i) + ".function"
- if next, changed := c.correctFunctionAtPath(updated, functionPath); changed {
- updated = next
- corrected = true
- }
- }
- return updated, corrected
- }
- // correctFunctionAtPath 修正指定路径下单个函数调用的工具名称和参数。
- func (c *CodexToolCorrector) correctFunctionAtPath(data []byte, functionPath string) ([]byte, bool) {
- namePath := functionPath + ".name"
- nameResult := gjson.GetBytes(data, namePath)
- if !nameResult.Exists() || nameResult.Type != gjson.String {
- return data, false
- }
- name := strings.TrimSpace(nameResult.Str)
- if name == "" {
- return data, false
- }
- updated := data
- corrected := false
- // 查找并修正工具名称
- if correctName, found := codexToolNameMapping[name]; found {
- if next, err := sjson.SetBytes(updated, namePath, correctName); err == nil {
- updated = next
- c.recordCorrection(name, correctName)
- corrected = true
- name = correctName // 使用修正后的名称进行参数修正
- }
- }
- // 修正工具参数(基于工具名称)
- if next, changed := c.correctToolParametersAtPath(updated, functionPath+".arguments", name); changed {
- updated = next
- corrected = true
- }
- return updated, corrected
- }
- // correctToolParametersAtPath 修正指定路径下 arguments 参数。
- func (c *CodexToolCorrector) correctToolParametersAtPath(data []byte, argumentsPath, toolName string) ([]byte, bool) {
- if toolName != "bash" && toolName != "edit" {
- return data, false
- }
- args := gjson.GetBytes(data, argumentsPath)
- if !args.Exists() {
- return data, false
- }
- switch args.Type {
- case gjson.String:
- argsJSON := strings.TrimSpace(args.Str)
- if !gjson.Valid(argsJSON) {
- return data, false
- }
- if !gjson.Parse(argsJSON).IsObject() {
- return data, false
- }
- nextArgsJSON, corrected := c.correctToolArgumentsJSON(argsJSON, toolName)
- if !corrected {
- return data, false
- }
- next, err := sjson.SetBytes(data, argumentsPath, nextArgsJSON)
- if err != nil {
- return data, false
- }
- return next, true
- case gjson.JSON:
- if !args.IsObject() || !gjson.Valid(args.Raw) {
- return data, false
- }
- nextArgsJSON, corrected := c.correctToolArgumentsJSON(args.Raw, toolName)
- if !corrected {
- return data, false
- }
- next, err := sjson.SetRawBytes(data, argumentsPath, []byte(nextArgsJSON))
- if err != nil {
- return data, false
- }
- return next, true
- default:
- return data, false
- }
- }
- // correctToolArgumentsJSON 修正工具参数 JSON(对象字符串),返回修正后的 JSON 与是否变更。
- func (c *CodexToolCorrector) correctToolArgumentsJSON(argsJSON, toolName string) (string, bool) {
- if !gjson.Valid(argsJSON) {
- return argsJSON, false
- }
- if !gjson.Parse(argsJSON).IsObject() {
- return argsJSON, false
- }
- updated := argsJSON
- corrected := false
- // 根据工具名称应用特定的参数修正规则
- switch toolName {
- case "bash":
- // OpenCode bash 支持 workdir;有些来源会输出 work_dir。
- if !gjson.Get(updated, "workdir").Exists() {
- if next, changed := moveJSONField(updated, "work_dir", "workdir"); changed {
- updated = next
- corrected = true
- logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'work_dir' to 'workdir' in bash tool")
- }
- } else {
- if next, changed := deleteJSONField(updated, "work_dir"); changed {
- updated = next
- corrected = true
- logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Removed duplicate 'work_dir' parameter from bash tool")
- }
- }
- case "edit":
- // OpenCode edit 参数为 filePath/oldString/newString(camelCase)。
- if !gjson.Get(updated, "filePath").Exists() {
- if next, changed := moveJSONField(updated, "file_path", "filePath"); changed {
- updated = next
- corrected = true
- logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'file_path' to 'filePath' in edit tool")
- } else if next, changed := moveJSONField(updated, "path", "filePath"); changed {
- updated = next
- corrected = true
- logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'path' to 'filePath' in edit tool")
- } else if next, changed := moveJSONField(updated, "file", "filePath"); changed {
- updated = next
- corrected = true
- logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'file' to 'filePath' in edit tool")
- }
- }
- if next, changed := moveJSONField(updated, "old_string", "oldString"); changed {
- updated = next
- corrected = true
- logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'old_string' to 'oldString' in edit tool")
- }
- if next, changed := moveJSONField(updated, "new_string", "newString"); changed {
- updated = next
- corrected = true
- logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'new_string' to 'newString' in edit tool")
- }
- if next, changed := moveJSONField(updated, "replace_all", "replaceAll"); changed {
- updated = next
- corrected = true
- logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Renamed 'replace_all' to 'replaceAll' in edit tool")
- }
- }
- return updated, corrected
- }
- func moveJSONField(input, from, to string) (string, bool) {
- if gjson.Get(input, to).Exists() {
- return input, false
- }
- src := gjson.Get(input, from)
- if !src.Exists() {
- return input, false
- }
- next, err := sjson.SetRaw(input, to, src.Raw)
- if err != nil {
- return input, false
- }
- next, err = sjson.Delete(next, from)
- if err != nil {
- return input, false
- }
- return next, true
- }
- func deleteJSONField(input, path string) (string, bool) {
- if !gjson.Get(input, path).Exists() {
- return input, false
- }
- next, err := sjson.Delete(input, path)
- if err != nil {
- return input, false
- }
- return next, true
- }
- // recordCorrection 记录一次工具名称修正
- func (c *CodexToolCorrector) recordCorrection(from, to string) {
- c.mu.Lock()
- defer c.mu.Unlock()
- c.stats.TotalCorrected++
- key := fmt.Sprintf("%s->%s", from, to)
- c.stats.CorrectionsByTool[key]++
- logger.LegacyPrintf("service.openai_tool_corrector", "[CodexToolCorrector] Corrected tool call: %s -> %s (total: %d)",
- from, to, c.stats.TotalCorrected)
- }
- // GetStats 获取工具修正统计信息
- func (c *CodexToolCorrector) GetStats() ToolCorrectionStats {
- c.mu.RLock()
- defer c.mu.RUnlock()
- // 返回副本以避免并发问题
- statsCopy := ToolCorrectionStats{
- TotalCorrected: c.stats.TotalCorrected,
- CorrectionsByTool: make(map[string]int, len(c.stats.CorrectionsByTool)),
- }
- for k, v := range c.stats.CorrectionsByTool {
- statsCopy.CorrectionsByTool[k] = v
- }
- return statsCopy
- }
- // ResetStats 重置统计信息
- func (c *CodexToolCorrector) ResetStats() {
- c.mu.Lock()
- defer c.mu.Unlock()
- c.stats.TotalCorrected = 0
- c.stats.CorrectionsByTool = make(map[string]int)
- }
- // CorrectToolName 直接修正工具名称(用于非 SSE 场景)
- func CorrectToolName(name string) (string, bool) {
- if correctName, found := codexToolNameMapping[name]; found {
- return correctName, true
- }
- return name, false
- }
- // GetToolNameMapping 获取工具名称映射表
- func GetToolNameMapping() map[string]string {
- // 返回副本以避免外部修改
- mapping := make(map[string]string, len(codexToolNameMapping))
- for k, v := range codexToolNameMapping {
- mapping[k] = v
- }
- return mapping
- }
|