feat(codex): 移除 opencode 指令回源与缓存

- 不再从 GitHub 拉取 opencode codex_header.txt\n- 删除 ~/.opencode 缓存与异步刷新逻辑\n- 所有 instructions 统一使用内置 codex_cli_instructions.md
This commit is contained in:
yangjianbo
2026-02-07 09:28:32 +08:00
parent ce764bf2d9
commit 55b56328da
2 changed files with 5 additions and 232 deletions

View File

@@ -2,36 +2,7 @@ package service
import (
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
const (
opencodeCodexHeaderURL = "https://raw.githubusercontent.com/anomalyco/opencode/dev/packages/opencode/src/session/prompt/codex_header.txt"
codexCacheTTL = 15 * time.Minute
// 避免冷启动首请求被外网/ DNS / GitHub 卡死。
// http.DefaultClient 默认无超时,网络异常时可能阻塞很久。
opencodeFetchTimeout = 3 * time.Second
// 本地缓存为空时,最小回源间隔(防止并发下反复打 GitHub
opencodeEmptyCacheRefreshInterval = 1 * time.Minute
// 防抖:防止短时间内重复触发异步回源。
opencodeFetchDebounce = 3 * time.Second
)
var opencodeFetchHTTPClient = &http.Client{Timeout: opencodeFetchTimeout}
var (
opencodeFetchMu sync.Mutex
opencodeFetchInFlight bool
opencodeFetchLastStart time.Time
)
//go:embed prompts/codex_cli_instructions.md
@@ -94,12 +65,6 @@ type codexTransformResult struct {
PromptCacheKey string
}
type opencodeCacheMetadata struct {
ETag string `json:"etag"`
LastFetch string `json:"lastFetch,omitempty"`
LastChecked int64 `json:"lastChecked"`
}
func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool) codexTransformResult {
result := codexTransformResult{}
// 工具续链需求会影响存储策略与 input 过滤逻辑。
@@ -233,98 +198,9 @@ func getNormalizedCodexModel(modelID string) string {
return ""
}
func getOpenCodeCachedPrompt(url, cacheFileName, metaFileName string) string {
cacheDir := codexCachePath("")
if cacheDir == "" {
return ""
}
cacheFile := filepath.Join(cacheDir, cacheFileName)
metaFile := filepath.Join(cacheDir, metaFileName)
var cachedContent string
if content, ok := readFile(cacheFile); ok {
cachedContent = content
}
var meta opencodeCacheMetadata
_ = loadJSON(metaFile, &meta)
if meta.LastChecked > 0 {
lastCheckedAt := time.UnixMilli(meta.LastChecked)
if cachedContent != "" {
if time.Since(lastCheckedAt) < codexCacheTTL {
return cachedContent
}
} else {
// 没有任何缓存内容时,回源失败也不应影响请求链路;这里做节流,避免并发下反复回源。
if time.Since(lastCheckedAt) < opencodeEmptyCacheRefreshInterval {
return ""
}
}
}
// 不在请求链路内同步拉取GitHub/DNS/网络异常会导致冷启动首请求卡 1 分钟+)。
// 直接返回当前缓存(可为空),并异步刷新缓存。
scheduleOpencodeCacheRefresh(url, cacheFile, metaFile, meta.ETag)
return cachedContent
}
func scheduleOpencodeCacheRefresh(url, cacheFile, metaFile, etag string) {
opencodeFetchMu.Lock()
if opencodeFetchInFlight {
opencodeFetchMu.Unlock()
return
}
if !opencodeFetchLastStart.IsZero() && time.Since(opencodeFetchLastStart) < opencodeFetchDebounce {
opencodeFetchMu.Unlock()
return
}
opencodeFetchInFlight = true
opencodeFetchLastStart = time.Now()
opencodeFetchMu.Unlock()
go func() {
defer func() {
opencodeFetchMu.Lock()
opencodeFetchInFlight = false
opencodeFetchMu.Unlock()
}()
now := time.Now()
content, newETag, status, err := fetchWithETag(url, etag)
var meta opencodeCacheMetadata
_ = loadJSON(metaFile, &meta)
meta.LastChecked = now.UnixMilli()
switch {
case err == nil && status == http.StatusNotModified:
// 304 表示无需更新缓存文件,只更新检查时间。
if newETag != "" {
meta.ETag = newETag
}
_ = writeJSON(metaFile, meta)
case err == nil && status >= 200 && status < 300 && strings.TrimSpace(content) != "":
_ = writeFile(cacheFile, content)
meta.ETag = newETag
meta.LastFetch = now.UTC().Format(time.RFC3339)
_ = writeJSON(metaFile, meta)
default:
// 拉取失败也记录检查时间,避免高并发下持续回源。
_ = writeJSON(metaFile, meta)
}
}()
}
func getOpenCodeCodexHeader() string {
// 优先从 opencode 仓库缓存获取指令
opencodeInstructions := getOpenCodeCachedPrompt(opencodeCodexHeaderURL, "opencode-codex-header.txt", "opencode-codex-header-meta.json")
// 若 opencode 指令可用,直接返回。
if opencodeInstructions != "" {
return opencodeInstructions
}
// 否则回退使用本地 Codex CLI 指令。
// 兼容保留:历史上这里会从 opencode 仓库拉取 codex_header.txt
// 现在我们与 Codex CLI 一致,直接使用仓库内置的 instructions避免读写缓存与外网依赖。
return getCodexCLIInstructions()
}
@@ -343,7 +219,7 @@ func GetCodexCLIInstructions() string {
// applyInstructions 处理 instructions 字段
// isCodexCLI=true: 仅补充缺失的 instructions使用内置 Codex CLI 指令)
// isCodexCLI=false: 优先使用 opencode 指令覆盖(不可用时回退到内置 Codex CLI 指令
// isCodexCLI=false: 优先使用内置 Codex CLI 指令覆盖
func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
if isCodexCLI {
return applyCodexCLIInstructions(reqBody)
@@ -367,8 +243,8 @@ func applyCodexCLIInstructions(reqBody map[string]any) bool {
return false
}
// applyOpenCodeInstructions 为非 Codex CLI 请求应用 opencode 指令
// 优先使用 opencode 指令覆盖
// applyOpenCodeInstructions 为非 Codex CLI 请求应用内置 Codex CLI 指令(兼容历史函数名)
// 优先使用内置 Codex CLI 指令覆盖
func applyOpenCodeInstructions(reqBody map[string]any) bool {
instructions := strings.TrimSpace(getOpenCodeCodexHeader())
existingInstructions, _ := reqBody["instructions"].(string)
@@ -591,85 +467,3 @@ func normalizeCodexTools(reqBody map[string]any) bool {
return modified
}
func codexCachePath(filename string) string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
cacheDir := filepath.Join(home, ".opencode", "cache")
if filename == "" {
return cacheDir
}
return filepath.Join(cacheDir, filename)
}
func readFile(path string) (string, bool) {
if path == "" {
return "", false
}
data, err := os.ReadFile(path)
if err != nil {
return "", false
}
return string(data), true
}
func writeFile(path, content string) error {
if path == "" {
return fmt.Errorf("empty cache path")
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
return os.WriteFile(path, []byte(content), 0o644)
}
func loadJSON(path string, target any) bool {
data, err := os.ReadFile(path)
if err != nil {
return false
}
if err := json.Unmarshal(data, target); err != nil {
return false
}
return true
}
func writeJSON(path string, value any) error {
if path == "" {
return fmt.Errorf("empty json path")
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
data, err := json.Marshal(value)
if err != nil {
return err
}
return os.WriteFile(path, data, 0o644)
}
func fetchWithETag(url, etag string) (string, string, int, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return "", "", 0, err
}
req.Header.Set("User-Agent", "sub2api-codex")
if etag != "" {
req.Header.Set("If-None-Match", etag)
}
resp, err := opencodeFetchHTTPClient.Do(req)
if err != nil {
return "", "", 0, err
}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", resp.StatusCode, err
}
return string(body), resp.Header.Get("etag"), resp.StatusCode, nil
}

View File

@@ -1,11 +1,7 @@
package service
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
)
@@ -191,23 +187,6 @@ func TestNormalizeCodexModel_Gpt53(t *testing.T) {
func setupCodexCache(t *testing.T) {
t.Helper()
// 使用临时 HOME 避免触发网络拉取 header。
tempDir := t.TempDir()
t.Setenv("HOME", tempDir)
cacheDir := filepath.Join(tempDir, ".opencode", "cache")
require.NoError(t, os.MkdirAll(cacheDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(cacheDir, "opencode-codex-header.txt"), []byte("header"), 0o644))
meta := map[string]any{
"etag": "",
"lastFetch": time.Now().UTC().Format(time.RFC3339),
"lastChecked": time.Now().UnixMilli(),
}
data, err := json.Marshal(meta)
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(cacheDir, "opencode-codex-header-meta.json"), data, 0o644))
}
func TestApplyCodexOAuthTransform_CodexCLI_PreservesExistingInstructions(t *testing.T) {