diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index fecd211..06d715f 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -53,7 +53,12 @@ def get_settings() -> dict: """返回当前配置概况(Key 脱敏)。""" from app.config import settings from app.services import preferences - from app.services.ai_provider import ai_configured, current_ai_model, current_codex_command + from app.services.ai_provider import ( + ai_configured, + current_ai_model, + current_codex_command, + current_codex_reasoning_effort, + ) key = secrets_store.get_tickflow_key() ai_provider = secrets_store.get_ai_config("ai_provider", settings.ai_provider) @@ -76,6 +81,7 @@ def get_settings() -> dict: "ai_configured": ai_configured(ai_provider), "ai_model": current_ai_model(), "ai_codex_command": current_codex_command(), + "ai_codex_reasoning_effort": current_codex_reasoning_effort(), "ai_user_agent": secrets_store.get_ai_config("ai_user_agent", settings.ai_user_agent), } @@ -234,6 +240,7 @@ class AiSettingsIn(BaseModel): api_key: str | None = None model: str = "" codex_command: str = "" + codex_reasoning_effort: str = "" user_agent: str = "" @@ -241,7 +248,15 @@ class AiSettingsIn(BaseModel): def save_ai_settings(req: AiSettingsIn) -> dict: """保存 AI 配置(全部持久化到 secrets.json)""" from app.config import settings - from app.services.ai_provider import ai_configured, current_ai_model, current_ai_provider, current_codex_command, normalize_codex_command + from app.services.ai_provider import ( + ai_configured, + current_ai_model, + current_ai_provider, + current_codex_command, + current_codex_reasoning_effort, + normalize_codex_command, + normalize_codex_reasoning_effort, + ) updates: dict = {} if req.provider: @@ -268,8 +283,11 @@ def save_ai_settings(req: AiSettingsIn) -> dict: codex_command = normalize_codex_command(req.codex_command) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc + codex_reasoning_effort = normalize_codex_reasoning_effort(req.codex_reasoning_effort) updates["ai_codex_command"] = codex_command + updates["ai_codex_reasoning_effort"] = codex_reasoning_effort settings.ai_codex_command = codex_command + settings.ai_codex_reasoning_effort = codex_reasoning_effort # user_agent 允许清空(回到默认浏览器 UA),故无条件持久化 updates["ai_user_agent"] = req.user_agent settings.ai_user_agent = req.user_agent @@ -283,6 +301,7 @@ def save_ai_settings(req: AiSettingsIn) -> dict: "ai_provider": provider, "ai_model": current_ai_model(), "ai_codex_command": current_codex_command(), + "ai_codex_reasoning_effort": current_codex_reasoning_effort(), "ai_configured": ai_configured(provider), } @@ -295,13 +314,14 @@ def clear_ai_settings() -> dict: """ from app.config import settings - secrets_store.clear("ai_provider", "ai_base_url", "ai_api_key", "ai_model", "ai_codex_command") + secrets_store.clear("ai_provider", "ai_base_url", "ai_api_key", "ai_model", "ai_codex_command", "ai_codex_reasoning_effort") # 同步重置运行时内存(provider 回默认值,其余置空) settings.ai_provider = "openai_compat" settings.ai_base_url = "" settings.ai_api_key = "" settings.ai_model = "" settings.ai_codex_command = "codex" + settings.ai_codex_reasoning_effort = "" return {"ok": True} diff --git a/backend/app/api/strategy.py b/backend/app/api/strategy.py index 9223a4c..3886c0f 100644 --- a/backend/app/api/strategy.py +++ b/backend/app/api/strategy.py @@ -564,7 +564,11 @@ async def ai_test(request: Request): ) return {"ok": True, "model": current_ai_model() or current_ai_provider(), "response": text[:80]} except Exception as e: - return {"ok": False, "error": str(e)} + return { + "ok": False, + "error": str(e) or repr(e), + "error_type": type(e).__name__, + } def _build_prompt(req: BuildRequest) -> str: diff --git a/backend/app/config.py b/backend/app/config.py index 524b40a..3ac9e0c 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -81,6 +81,7 @@ class Settings(BaseSettings): ai_api_key: str = "" ai_model: str = "gpt-5.5" ai_codex_command: str = "codex" + ai_codex_reasoning_effort: str = "" # 默认浏览器风格 UA,绕过 Cloudflare 等 CDN/WAF 的 Bot 拦截(Issue #8)。 # 用户可在 AI 设置页按需修改。 ai_user_agent: str = ( diff --git a/backend/app/services/ai_provider.py b/backend/app/services/ai_provider.py index 301488d..4ab4387 100644 --- a/backend/app/services/ai_provider.py +++ b/backend/app/services/ai_provider.py @@ -5,11 +5,15 @@ import asyncio import os import re import shutil +import stat +import subprocess import sys import tempfile +import time import tomllib -from collections.abc import AsyncIterator, Sequence +from collections.abc import AsyncIterator, Callable, Sequence from pathlib import Path +from types import TracebackType from app import secrets_store from app.config import settings @@ -17,8 +21,47 @@ from app.config import settings OPENAI_COMPAT_PROVIDER = "openai_compat" CODEX_CLI_PROVIDER = "codex_cli" CODEX_DEFAULT_COMMAND = "codex" -CODEX_SERVICE_TIER_FALLBACK = "fast" -CODEX_SUPPORTED_SERVICE_TIERS = {"fast", "flex"} +CODEX_SUPPORTED_REASONING_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh"} + +_CODEX_ENV_ALLOWLIST = ( + "PATH", + "PATHEXT", + "SYSTEMROOT", + "WINDIR", + "COMSPEC", + "HOME", + "USERPROFILE", + "HOMEDRIVE", + "HOMEPATH", + "APPDATA", + "LOCALAPPDATA", + "PROGRAMDATA", + "PROGRAMFILES", + "PROGRAMFILES(X86)", + "TEMP", + "TMP", + "TMPDIR", + "SHELL", + "USER", + "LOGNAME", + "LANG", + "LC_ALL", + "LC_CTYPE", + "TZ", + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "NO_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + "no_proxy", + "SSL_CERT_FILE", + "SSL_CERT_DIR", + "REQUESTS_CA_BUNDLE", + "CURL_CA_BUNDLE", + "NODE_EXTRA_CA_CERTS", +) Message = dict[str, str] @@ -42,6 +85,15 @@ def current_codex_command() -> str: ) +def current_codex_reasoning_effort() -> str: + return normalize_codex_reasoning_effort( + secrets_store.get_ai_config( + "ai_codex_reasoning_effort", + settings.ai_codex_reasoning_effort, + ) + ) + + def is_codex_cli_provider(provider: str | None = None) -> bool: return (provider or current_ai_provider()) == CODEX_CLI_PROVIDER @@ -49,12 +101,20 @@ def is_codex_cli_provider(provider: str | None = None) -> bool: def normalize_codex_model(model: str) -> str: value = model.strip() aliases = { - "gpt5": "gpt-5", "gpt5.5": "gpt-5.5", + "gpt5.6": "gpt-5.6-sol", + "gpt5.6-sol": "gpt-5.6-sol", + "gpt5.6-terra": "gpt-5.6-terra", + "gpt5.6-luna": "gpt-5.6-luna", } return aliases.get(value.lower(), value) +def normalize_codex_reasoning_effort(effort: str | None) -> str: + value = (effort or "").strip().lower() + return value if value in CODEX_SUPPORTED_REASONING_EFFORTS else "" + + def normalize_codex_command(command: str | None, *, strict: bool = True) -> str: value = (command or "").strip() if not value or value.lower() == CODEX_DEFAULT_COMMAND: @@ -363,8 +423,8 @@ async def _run_codex_cli( timeout: float, ) -> str: prompt = _codex_prompt(messages, max_tokens=max_tokens) - with tempfile.TemporaryDirectory(prefix="tickflow-codex-run-") as run_dir: - run_path = Path(run_dir) + run_path = Path(tempfile.mkdtemp(prefix="tickflow-codex-run-")) + try: codex_home_path = run_path / "codex-home" workspace_path = run_path / "workspace" codex_home_path.mkdir() @@ -389,37 +449,107 @@ async def _run_codex_cli( args.extend(["--model", model]) args.extend(["--cd", str(workspace_path), "-"]) - env = os.environ.copy() - env.setdefault("NO_COLOR", "1") - env["CODEX_HOME"] = str(codex_home_path) + env = _codex_process_env(codex_home_path) - proc = await asyncio.create_subprocess_exec( - *args, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - env=env, + returncode, stdout, stderr = await asyncio.to_thread( + _run_codex_process, + args, + prompt, + env, + timeout, ) - try: - stdout, stderr = await asyncio.wait_for( - proc.communicate(prompt.encode("utf-8")), - timeout=timeout, - ) - except TimeoutError as exc: - proc.kill() - await proc.wait() - raise RuntimeError("Codex CLI 调用超时, 请稍后重试或检查本机 Codex 登录状态") from exc out = _clean_process_text(stdout) err = _clean_process_text(stderr) final_message = _read_output_file(output_path) - if proc.returncode != 0: - detail = err or out or f"exit code {proc.returncode}" + if returncode != 0: + detail = err or out or f"exit code {returncode}" raise RuntimeError(f"Codex CLI 调用失败: {detail[-1200:]}") result = final_message or out if not result: raise RuntimeError("Codex CLI 未返回内容") return result + finally: + await asyncio.to_thread(_remove_tree_best_effort, run_path) + + +def _run_codex_process( + args: Sequence[str], + prompt: str, + env: dict[str, str], + timeout: float, +) -> tuple[int, bytes, bytes]: + try: + proc = subprocess.run( + list(args), + input=prompt.encode("utf-8"), + capture_output=True, + env=env, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired as exc: + raise RuntimeError("Codex CLI 调用超时, 请稍后重试或检查本机 Codex 登录状态") from exc + return proc.returncode, proc.stdout, proc.stderr + + +def _codex_process_env(codex_home_path: Path) -> dict[str, str]: + """Pass only OS, locale, certificate, and proxy settings to Codex.""" + env: dict[str, str] = {} + seen: set[str] = set() + for name in _CODEX_ENV_ALLOWLIST: + normalized = name.casefold() if os.name == "nt" else name + if normalized in seen: + continue + value = os.environ.get(name) + if value: + env[name] = value + seen.add(normalized) + env["NO_COLOR"] = "1" + env["CODEX_HOME"] = str(codex_home_path) + return env + + +def _remove_tree_best_effort(path: Path) -> None: + _remove_auth_files(path) + for attempt in range(4): + try: + shutil.rmtree(path, onerror=_make_writable_and_retry) + return + except FileNotFoundError: + return + except OSError: + if attempt == 3: + break + time.sleep(0.2 * (attempt + 1)) + _remove_auth_files(path) + shutil.rmtree(path, ignore_errors=True) + _remove_auth_files(path) + + +def _remove_auth_files(path: Path) -> None: + try: + auth_files = list(path.rglob("auth.json")) + except OSError: + return + for auth_file in auth_files: + try: + os.chmod(auth_file, stat.S_IWRITE) + auth_file.unlink(missing_ok=True) + except OSError: + pass + + +def _make_writable_and_retry( + func: Callable[[str], object], + path: str, + exc_info: tuple[type[BaseException], BaseException, TracebackType], +) -> None: + try: + os.chmod(path, stat.S_IWRITE) + func(path) + except OSError: + raise exc_info[1] from None def _codex_prompt(messages: Sequence[Message], *, max_tokens: int) -> str: @@ -539,10 +669,16 @@ def _write_compatible_codex_config(path: Path) -> None: config = _read_codex_config() lines: list[str] = [] - tier = str(config.get("service_tier") or "").strip() - if tier not in CODEX_SUPPORTED_SERVICE_TIERS: - tier = CODEX_SERVICE_TIER_FALLBACK - lines.append(_toml_string("service_tier", tier)) + model = current_ai_model() or normalize_codex_model(str(config.get("model") or "")) + if model: + lines.append(_toml_string("model", model)) + + effort = current_codex_reasoning_effort() or normalize_codex_reasoning_effort( + str(config.get("model_reasoning_effort") or "") + ) + if effort: + lines.append(_toml_string("model_reasoning_effort", effort)) + lines.append(_toml_string("approval_policy", "never")) lines.append(_toml_string("sandbox_mode", "read-only")) diff --git a/backend/tests/test_ai_provider.py b/backend/tests/test_ai_provider.py index c73b3d1..5ff5c05 100644 --- a/backend/tests/test_ai_provider.py +++ b/backend/tests/test_ai_provider.py @@ -3,6 +3,7 @@ from __future__ import annotations import httpx import openai +from app.services import ai_provider from app.services.ai_provider import ( _format_openai_error, _is_temperature_rejected, @@ -140,3 +141,23 @@ def test_is_temperature_rejected_false_for_non_400(): ) exc = openai.AuthenticationError("unauthorized", response=response, body=None) assert _is_temperature_rejected(exc) is False + + +def test_codex_process_env_excludes_application_secrets(monkeypatch, tmp_path): + monkeypatch.setenv("PATH", "test-path") + monkeypatch.setenv("HTTPS_PROXY", "http://proxy.example") + monkeypatch.setenv("TICKFLOW_API_KEY", "tickflow-secret") + monkeypatch.setenv("AI_API_KEY", "ai-secret") + monkeypatch.setenv("OPENAI_API_KEY", "openai-secret") + monkeypatch.setenv("AUTH_PASSWORD", "password-secret") + + env = ai_provider._codex_process_env(tmp_path / "codex-home") + + assert env["PATH"] == "test-path" + assert env["HTTPS_PROXY"] == "http://proxy.example" + assert env["NO_COLOR"] == "1" + assert env["CODEX_HOME"] == str(tmp_path / "codex-home") + assert "TICKFLOW_API_KEY" not in env + assert "AI_API_KEY" not in env + assert "OPENAI_API_KEY" not in env + assert "AUTH_PASSWORD" not in env diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 7065dc7..66e1867 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -703,6 +703,7 @@ export interface SettingsState { ai_configured?: boolean ai_model: string ai_codex_command?: string + ai_codex_reasoning_effort?: string ai_user_agent: string } @@ -896,8 +897,8 @@ export const api = { ), /** 保存 AI 配置 */ - saveAiSettings: (ai: { provider?: string; base_url?: string; api_key?: string; model?: string; codex_command?: string; user_agent?: string }) => - request<{ ok: boolean; ai_provider?: string; ai_model?: string; ai_codex_command?: string; ai_configured?: boolean }>('/api/settings/ai', { + saveAiSettings: (ai: { provider?: string; base_url?: string; api_key?: string; model?: string; codex_command?: string; codex_reasoning_effort?: string; user_agent?: string }) => + request<{ ok: boolean; ai_provider?: string; ai_model?: string; ai_codex_command?: string; ai_codex_reasoning_effort?: string; ai_configured?: boolean }>('/api/settings/ai', { method: 'POST', body: JSON.stringify(ai), }), diff --git a/frontend/src/pages/settings/AI.tsx b/frontend/src/pages/settings/AI.tsx index 3048355..961984c 100644 --- a/frontend/src/pages/settings/AI.tsx +++ b/frontend/src/pages/settings/AI.tsx @@ -15,22 +15,40 @@ const INPUT_CLS = const CODEX_PROVIDER = 'codex_cli' const OPENAI_PROVIDER = 'openai_compat' -const CUSTOM_CODEX_MODEL = '__custom__' const CODEX_COMMAND = 'codex' +const DEFAULT_CODEX_MODEL = 'gpt-5.6-sol' +const DEFAULT_CODEX_REASONING_EFFORT = 'xhigh' +const SAVED_CODEX_OPTION_VALUE = '__saved_codex_config__' +const CODEX_REASONING_LABELS: Record = { + high: '高', + xhigh: '极高', +} -const CODEX_MODEL_OPTIONS = [ - { label: 'Codex 默认(推荐)', value: '', hint: '使用当前 Codex CLI 支持的默认模型' }, - { label: 'gpt-5.5', value: 'gpt-5.5', hint: '高能力模型' }, - { label: 'gpt-5', value: 'gpt-5', hint: '通用模型' }, +type CodexModelOption = { label: string; value: string; model: string; effort: string; hint: string } + +const CODEX_MODEL_OPTIONS: CodexModelOption[] = [ + { label: 'GPT-5.6 Sol · 极高(推荐)', value: 'gpt-5.6-sol:xhigh', model: 'gpt-5.6-sol', effort: 'xhigh', hint: '旗舰档,适合复杂金融分析与专业任务' }, + { label: 'GPT-5.6 Terra · 极高', value: 'gpt-5.6-terra:xhigh', model: 'gpt-5.6-terra', effort: 'xhigh', hint: '平衡智能、速度与使用成本' }, + { label: 'GPT-5.6 Luna · 极高', value: 'gpt-5.6-luna:xhigh', model: 'gpt-5.6-luna', effort: 'xhigh', hint: '适合成本敏感与高频分析任务' }, + { label: 'gpt-5.5 · 高', value: 'gpt-5.5:high', model: 'gpt-5.5', effort: 'high', hint: '使用 gpt-5.5 + high 推理档' }, + { label: 'gpt-5.5 · 极高', value: 'gpt-5.5:xhigh', model: 'gpt-5.5', effort: 'xhigh', hint: '使用 gpt-5.5 + xhigh 推理档' }, + { label: '跟随本机 Codex 默认', value: '', model: '', effort: '', hint: '使用本机 Codex CLI 配置的默认模型与推理强度' }, ] +const codexModelLabel = (model?: string, effort?: string) => { + if (!model && !effort) return '默认模型' + const modelLabel = model || '默认模型' + const effortLabel = effort ? CODEX_REASONING_LABELS[effort] ?? effort : '' + return effortLabel ? `${modelLabel} · ${effortLabel}` : modelLabel +} + const PRESETS: { label: string; provider?: string; url: string; model: string; codexCommand?: string; website: string; websiteLabel: string; description: string; partner?: boolean; promo?: string; custom?: boolean }[] = [ { label: '自定义', url: '', model: '', website: '', websiteLabel: '', description: '不自动填充任何配置,完全手动填写 API 地址、模型和密钥。', custom: true }, { label: 'DeepSeek', url: 'https://api.deepseek.com', model: 'deepseek-v4-pro', website: 'https://www.deepseek.com/', websiteLabel: 'deepseek.com', description: 'DeepSeek 官方 OpenAI 兼容接口。' }, { label: '通义千问', url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-3.6plus', website: 'https://tongyi.aliyun.com/', websiteLabel: 'tongyi.aliyun.com', description: '阿里云 DashScope 兼容模式接口。' }, { label: '智谱 GLM', url: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-5.2', website: 'https://open.bigmodel.cn/', websiteLabel: 'open.bigmodel.cn', description: '智谱 AI 官方 OpenAI 兼容接口。' }, { label: 'Kimi', url: 'https://api.moonshot.cn/v1', model: 'kimi-k2.7-code', website: 'https://platform.moonshot.cn/', websiteLabel: 'platform.moonshot.cn', description: '月之暗面 Moonshot 官方 OpenAI 兼容接口,支持超长上下文。' }, - { label: 'Codex CLI', provider: CODEX_PROVIDER, url: '', model: '', codexCommand: CODEX_COMMAND, website: 'https://developers.openai.com/codex/noninteractive', websiteLabel: 'codex exec', description: '调用本机 Codex CLI 的 codex exec, 适合已登录 ChatGPT/Codex 的本地环境。' }, + { label: 'Codex CLI', provider: CODEX_PROVIDER, url: '', model: DEFAULT_CODEX_MODEL, codexCommand: CODEX_COMMAND, website: 'https://developers.openai.com/codex/noninteractive', websiteLabel: 'codex exec', description: '调用本机 Codex CLI 的 codex exec, 适合已登录 ChatGPT/Codex 的本地环境。' }, { label: '炸鸡中转站', url: 'https://api.zhaji.dev/v1', model: 'gpt-5.5', website: 'https://api.zhaji.dev', websiteLabel: 'api.zhaji.dev', description: 'OpenAI 兼容中转服务,适合直接使用国际模型。', partner: true, promo: '通过链接邀请注册赠送免费额度 · 国际模型最低0.02倍率' }, ] @@ -43,7 +61,7 @@ export function SettingsAIPanel() { const [baseUrl, setBaseUrl] = useState('') const [apiKey, setApiKey] = useState('') const [model, setModel] = useState('') - const [codexCustomModel, setCodexCustomModel] = useState(false) + const [codexReasoningEffort, setCodexReasoningEffort] = useState('') const [codexCommand, setCodexCommand] = useState(CODEX_COMMAND) const [customUa, setCustomUa] = useState(false) const [userAgent, setUserAgent] = useState('') @@ -59,17 +77,39 @@ export function SettingsAIPanel() { // 选中的预设: 精确匹配 provider+url/codexCommand; 匹配不上时默认"自定义" const matchedPreset = PRESETS.find(p => (p.provider ?? OPENAI_PROVIDER) === provider && (isCodexProvider ? p.codexCommand === codexCommand : p.url === baseUrl)) const selectedPreset = matchedPreset ?? PRESETS.find(p => p.custom) - const codexModelSelectValue = codexCustomModel ? CUSTOM_CODEX_MODEL : model + const savedCodexModel = savedCodexProvider ? (s?.ai_model ?? '') : '' + const savedCodexEffort = savedCodexProvider ? (s?.ai_codex_reasoning_effort ?? '') : '' + const savedCodexOptionKnown = CODEX_MODEL_OPTIONS.some(option => + option.model === savedCodexModel && option.effort === savedCodexEffort, + ) + const savedCodexOption: CodexModelOption | null = + (savedCodexModel || savedCodexEffort) && !savedCodexOptionKnown + ? { + label: `${codexModelLabel(savedCodexModel, savedCodexEffort)}(当前配置)`, + value: SAVED_CODEX_OPTION_VALUE, + model: savedCodexModel, + effort: savedCodexEffort, + hint: '保留项目中已保存的模型与推理档;此兼容项不可编辑', + } + : null + const codexModelOptions = savedCodexOption + ? [savedCodexOption, ...CODEX_MODEL_OPTIONS] + : CODEX_MODEL_OPTIONS + const selectedCodexModelOption = codexModelOptions.find(option => + option.model === model && option.effort === codexReasoningEffort, + ) ?? CODEX_MODEL_OPTIONS[0] + const codexModelSelectValue = selectedCodexModelOption.value const canSave = isCodexProvider ? true : !!baseUrl.trim() && !!model.trim() useEffect(() => { if (!s) return // 未配置过 AI (无 api_key): 字段留空, 默认选中"自定义"预设, 不预填充后端默认值 const unconfigured = !s.has_ai_key && !s.ai_configured - setProvider(s.ai_provider ?? OPENAI_PROVIDER) + const savedProvider = s.ai_provider ?? OPENAI_PROVIDER + setProvider(savedProvider) setBaseUrl(unconfigured ? '' : (s.ai_base_url ?? '')) setModel(unconfigured ? '' : (s.ai_model ?? '')) - setCodexCustomModel(!unconfigured && !!s.ai_model && !CODEX_MODEL_OPTIONS.some(o => o.value === s.ai_model)) + setCodexReasoningEffort(unconfigured ? '' : (s.ai_codex_reasoning_effort ?? '')) setCodexCommand(s.ai_codex_command ?? CODEX_COMMAND) const ua = s.ai_user_agent ?? '' setCustomUa(!!ua) @@ -82,6 +122,7 @@ export function SettingsAIPanel() { api_key: apiKey || undefined, model, codex_command: isCodexProvider ? CODEX_COMMAND : codexCommand, + codex_reasoning_effort: isCodexProvider ? codexReasoningEffort : '', user_agent: customUa ? userAgent : '', }) @@ -96,6 +137,7 @@ export function SettingsAIPanel() { ai_base_url: baseUrl, ai_model: result.ai_model ?? model, ai_codex_command: result.ai_codex_command ?? (isCodexProvider ? CODEX_COMMAND : codexCommand), + ai_codex_reasoning_effort: result.ai_codex_reasoning_effort ?? (isCodexProvider ? codexReasoningEffort : ''), ai_configured: result.ai_configured ?? (isCodexProvider ? true : (apiKey ? true : prev.ai_configured)), ...(apiKey ? { has_ai_key: true, @@ -115,7 +157,7 @@ export function SettingsAIPanel() { setBaseUrl('') setApiKey('') setModel('') - setCodexCustomModel(false) + setCodexReasoningEffort('') setCodexCommand(CODEX_COMMAND) setTestResult(null) qc.setQueryData(QK.settings, prev => prev ? { @@ -124,6 +166,7 @@ export function SettingsAIPanel() { ai_base_url: '', ai_model: '', ai_codex_command: CODEX_COMMAND, + ai_codex_reasoning_effort: '', has_ai_key: false, ai_configured: false, ai_api_key_masked: '', @@ -149,13 +192,13 @@ export function SettingsAIPanel() { setProvider(OPENAI_PROVIDER) setBaseUrl('') setModel('') - setCodexCustomModel(false) + setCodexReasoningEffort('') return } setProvider(p.provider ?? OPENAI_PROVIDER) setBaseUrl(p.url) setModel(p.model) - setCodexCustomModel(false) + setCodexReasoningEffort(p.provider === CODEX_PROVIDER ? DEFAULT_CODEX_REASONING_EFFORT : '') if (p.codexCommand) setCodexCommand(CODEX_COMMAND) } @@ -193,7 +236,7 @@ export function SettingsAIPanel() {
{configured ? (savedCodexProvider - ? `${s?.ai_codex_command ?? CODEX_COMMAND} · ${s?.ai_model || '默认模型'}` + ? `${s?.ai_codex_command ?? CODEX_COMMAND} · ${codexModelLabel(s?.ai_model, s?.ai_codex_reasoning_effort)}` : `${s?.ai_model} · ${s?.ai_api_key_masked}`) : (isCodexProvider ? '使用本机 codex exec, 此处无需填写 API Key。' : '配置 API Key 后即可使用 AI 功能。')}
@@ -256,39 +299,23 @@ export function SettingsAIPanel() { o.value === model)?.hint} + label="模型 / 推理档" + hint={selectedCodexModelOption.hint} > - {codexCustomModel && ( - setModel(e.target.value)} - placeholder="例如 gpt-5.5" - className={`${INPUT_CLS} mt-2`} - /> - )} ) : ( @@ -298,7 +325,7 @@ export function SettingsAIPanel() { setBaseUrl(e.target.value)} placeholder="https://api.zhaji.dev/v1" className={INPUT_CLS} /> - setModel(e.target.value)} placeholder="gpt-5.5" className={INPUT_CLS} /> + setModel(e.target.value)} placeholder="gpt-5.6-sol" className={INPUT_CLS} /> diff --git a/packaging/tickflow-logo.svg b/packaging/tickflow-logo.svg new file mode 100644 index 0000000..aa13b43 --- /dev/null +++ b/packaging/tickflow-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + +