feat(codex): 移除 opencode 指令回源与缓存
- 不再从 GitHub 拉取 opencode codex_header.txt\n- 删除 ~/.opencode 缓存与异步刷新逻辑\n- 所有 instructions 统一使用内置 codex_cli_instructions.md
This commit is contained in:
@@ -2,36 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"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
|
//go:embed prompts/codex_cli_instructions.md
|
||||||
@@ -94,12 +65,6 @@ type codexTransformResult struct {
|
|||||||
PromptCacheKey string
|
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 {
|
func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool) codexTransformResult {
|
||||||
result := codexTransformResult{}
|
result := codexTransformResult{}
|
||||||
// 工具续链需求会影响存储策略与 input 过滤逻辑。
|
// 工具续链需求会影响存储策略与 input 过滤逻辑。
|
||||||
@@ -233,98 +198,9 @@ func getNormalizedCodexModel(modelID string) string {
|
|||||||
return ""
|
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 {
|
func getOpenCodeCodexHeader() string {
|
||||||
// 优先从 opencode 仓库缓存获取指令。
|
// 兼容保留:历史上这里会从 opencode 仓库拉取 codex_header.txt。
|
||||||
opencodeInstructions := getOpenCodeCachedPrompt(opencodeCodexHeaderURL, "opencode-codex-header.txt", "opencode-codex-header-meta.json")
|
// 现在我们与 Codex CLI 一致,直接使用仓库内置的 instructions,避免读写缓存与外网依赖。
|
||||||
|
|
||||||
// 若 opencode 指令可用,直接返回。
|
|
||||||
if opencodeInstructions != "" {
|
|
||||||
return opencodeInstructions
|
|
||||||
}
|
|
||||||
|
|
||||||
// 否则回退使用本地 Codex CLI 指令。
|
|
||||||
return getCodexCLIInstructions()
|
return getCodexCLIInstructions()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +219,7 @@ func GetCodexCLIInstructions() string {
|
|||||||
|
|
||||||
// applyInstructions 处理 instructions 字段
|
// applyInstructions 处理 instructions 字段
|
||||||
// isCodexCLI=true: 仅补充缺失的 instructions(使用内置 Codex CLI 指令)
|
// isCodexCLI=true: 仅补充缺失的 instructions(使用内置 Codex CLI 指令)
|
||||||
// isCodexCLI=false: 优先使用 opencode 指令覆盖(不可用时回退到内置 Codex CLI 指令)
|
// isCodexCLI=false: 优先使用内置 Codex CLI 指令覆盖
|
||||||
func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
|
func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
|
||||||
if isCodexCLI {
|
if isCodexCLI {
|
||||||
return applyCodexCLIInstructions(reqBody)
|
return applyCodexCLIInstructions(reqBody)
|
||||||
@@ -367,8 +243,8 @@ func applyCodexCLIInstructions(reqBody map[string]any) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyOpenCodeInstructions 为非 Codex CLI 请求应用 opencode 指令
|
// applyOpenCodeInstructions 为非 Codex CLI 请求应用内置 Codex CLI 指令(兼容历史函数名)
|
||||||
// 优先使用 opencode 指令覆盖
|
// 优先使用内置 Codex CLI 指令覆盖
|
||||||
func applyOpenCodeInstructions(reqBody map[string]any) bool {
|
func applyOpenCodeInstructions(reqBody map[string]any) bool {
|
||||||
instructions := strings.TrimSpace(getOpenCodeCodexHeader())
|
instructions := strings.TrimSpace(getOpenCodeCodexHeader())
|
||||||
existingInstructions, _ := reqBody["instructions"].(string)
|
existingInstructions, _ := reqBody["instructions"].(string)
|
||||||
@@ -591,85 +467,3 @@ func normalizeCodexTools(reqBody map[string]any) bool {
|
|||||||
|
|
||||||
return modified
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -191,23 +187,6 @@ func TestNormalizeCodexModel_Gpt53(t *testing.T) {
|
|||||||
|
|
||||||
func setupCodexCache(t *testing.T) {
|
func setupCodexCache(t *testing.T) {
|
||||||
t.Helper()
|
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) {
|
func TestApplyCodexOAuthTransform_CodexCLI_PreservesExistingInstructions(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user