fix(codex): 添加codex CLI instructions fallback机制

## 问题
- 使用OpenAI API key时,opencode客户端可能因instructions不兼容而报错
- 依赖外部GitHub获取instructions,网络故障时会失败

## 解决方案
1. 将codex CLI标准instructions嵌入到项目中
2. 实现自动fallback机制:
   - 优先使用opencode GitHub的instructions
   - 失败时自动fallback到本地codex CLI instructions
3. 添加辅助函数用于错误检测和手动替换

## 改动
- 新增: internal/service/prompts/codex_cli_instructions.md
  - 从codex项目复制的标准instructions
  - 使用go:embed嵌入到二进制文件

- 修改: internal/service/openai_codex_transform.go
  - 添加embed支持
  - 增强getOpenCodeCodexHeader()的fallback逻辑
  - 新增GetCodexCLIInstructions()公开函数
  - 新增ReplaceWithCodexInstructions()用于手动替换
  - 新增IsInstructionError()用于错误检测

## 优势
- 零停机:GitHub不可用时仍能正常工作
- 离线可用:不依赖外部网络
- 兼容性:使用标准codex CLI instructions
- 部署简单:instructions嵌入到二进制文件
This commit is contained in:
IanShaw027
2026-01-13 11:14:32 +08:00
parent 93db889a10
commit f9713e8733
2 changed files with 343 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
package service
import (
_ "embed"
"encoding/json"
"fmt"
"io"
@@ -16,6 +17,9 @@ const (
codexCacheTTL = 15 * time.Minute
)
//go:embed prompts/codex_cli_instructions.md
var codexCLIInstructions string
var codexModelMap = map[string]string{
"gpt-5.1-codex": "gpt-5.1-codex",
"gpt-5.1-codex-low": "gpt-5.1-codex",
@@ -119,6 +123,13 @@ func applyCodexOAuthTransform(reqBody map[string]any) codexTransformResult {
reqBody["instructions"] = instructions
result.Modified = true
}
} else if existingInstructions == "" {
// If no opencode instructions available, try codex CLI instructions
codexInstructions := strings.TrimSpace(getCodexCLIInstructions())
if codexInstructions != "" {
reqBody["instructions"] = codexInstructions
result.Modified = true
}
}
if input, ok := reqBody["input"].([]any); ok {
@@ -235,13 +246,69 @@ func getOpenCodeCachedPrompt(url, cacheFileName, metaFileName string) string {
}
func getOpenCodeCodexHeader() string {
return getOpenCodeCachedPrompt(opencodeCodexHeaderURL, "opencode-codex-header.txt", "opencode-codex-header-meta.json")
// Try to get from opencode repository first
opencodeInstructions := getOpenCodeCachedPrompt(opencodeCodexHeaderURL, "opencode-codex-header.txt", "opencode-codex-header-meta.json")
// If opencode instructions are available, return them
if opencodeInstructions != "" {
return opencodeInstructions
}
// Fallback to local codex CLI instructions
return getCodexCLIInstructions()
}
func getCodexCLIInstructions() string {
return codexCLIInstructions
}
func GetOpenCodeInstructions() string {
return getOpenCodeCodexHeader()
}
func GetCodexCLIInstructions() string {
return getCodexCLIInstructions()
}
func ReplaceWithCodexInstructions(reqBody map[string]any) bool {
codexInstructions := strings.TrimSpace(getCodexCLIInstructions())
if codexInstructions == "" {
return false
}
existingInstructions, _ := reqBody["instructions"].(string)
if strings.TrimSpace(existingInstructions) != codexInstructions {
reqBody["instructions"] = codexInstructions
return true
}
return false
}
func IsInstructionError(errorMessage string) bool {
if errorMessage == "" {
return false
}
lowerMsg := strings.ToLower(errorMessage)
instructionKeywords := []string{
"instruction",
"instructions",
"system prompt",
"system message",
"invalid prompt",
"prompt format",
}
for _, keyword := range instructionKeywords {
if strings.Contains(lowerMsg, keyword) {
return true
}
}
return false
}
func filterCodexInput(input []any) []any {
filtered := make([]any, 0, len(input))
for _, item := range input {