diff --git a/README.md b/README.md index d611d17..9f72edf 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,12 @@ profile env 默认放在: node ./scripts/run-profile.mjs default ``` +如果已经由 state 记录了当前活跃 profile,也可以直接不传 profile 参数: + +```bash +node ./scripts/run-profile.mjs --prefer-state-profile +``` + ## 如何恢复 Windows: diff --git a/scripts/run-profile.mjs b/scripts/run-profile.mjs index 7eb8cdd..22828dd 100644 --- a/scripts/run-profile.mjs +++ b/scripts/run-profile.mjs @@ -55,7 +55,15 @@ function parseEnvFile(content) { function getProfileName(options) { const positional = options._?.[0]; - return `${options.profile || positional || process.env.CODEX_RETRY_GATEWAY_PROFILE || "default"}`; + return `${options.profile || positional || process.env.CODEX_RETRY_GATEWAY_PROFILE || ""}`.trim(); +} + +function resolveDefaultRequestedProfileName(existingState) { + const stateProfileName = `${existingState?.profile_name || ""}`.trim(); + if (stateProfileName) { + return stateProfileName; + } + return process.env.CODEX_RETRY_GATEWAY_DEFAULT_PROFILE || "default"; } function resolvePreferredProfileName({ requestedProfileName, existingState, preferStateProfile, profilesDir }) { @@ -246,7 +254,6 @@ async function ensureCodexPointsToGateway({ paths, codexConfigPath, providerCont async function main() { const options = parseOptions(process.argv, { booleanFlags: ["no-codex-config-update", "prefer-state-profile"] }); - const requestedProfileName = getProfileName(options); const profilesDir = options.profilesDir || DEFAULT_PROFILES_DIR; const stateRoot = options.stateRoot || process.env.CODEX_RETRY_GATEWAY_STATE_ROOT || DEFAULT_STATE_ROOT; const codexConfigPath = @@ -261,6 +268,7 @@ async function main() { await ensureDirectory(paths.backupDir); const existingState = await readJsonFile(paths.statePath); + const requestedProfileName = getProfileName(options) || resolveDefaultRequestedProfileName(existingState); const profileName = resolvePreferredProfileName({ requestedProfileName, existingState, diff --git a/ui-src/src/App.tsx b/ui-src/src/App.tsx index 1764c54..ec476a4 100644 --- a/ui-src/src/App.tsx +++ b/ui-src/src/App.tsx @@ -310,6 +310,17 @@ function durationSeconds(value?: number | null) { return typeof value === "number" && Number.isFinite(value) ? `${(value / 1000).toFixed(2)} s` : "-"; } +function secondsSince(value: string | null | undefined, nowMs: number) { + if (!value) { + return "-"; + } + const atMs = new Date(value).getTime(); + if (Number.isNaN(atMs)) { + return "-"; + } + return `${Math.max(0, (nowMs - atMs) / 1000).toFixed(1)} s`; +} + function bytesFormat(value?: number | null) { if (typeof value !== "number" || !Number.isFinite(value)) { return "-"; @@ -478,6 +489,7 @@ export default function App() { const [latestLogSeq, setLatestLogSeq] = useState(0); const [requestQuery, setRequestQuery] = useState(""); const [requestFilter, setRequestFilter] = useState("all"); + const [nowMs, setNowMs] = useState(() => Date.now()); const [ruleForm, setRuleForm] = useState(ruleFormFromStatus(null)); const [profileForm, setProfileForm] = useState(defaultProfileForm); const [ruleMessage, setRuleMessage] = useState({ text: "", tone: "" }); @@ -668,6 +680,7 @@ export default function App() { if (page !== "requests") { return; } + setNowMs(Date.now()); const timer = window.setTimeout(() => { loadRequests().catch((error) => { setRuleMessage({ text: error?.message || String(error), tone: "error" }); @@ -676,6 +689,16 @@ export default function App() { return () => window.clearTimeout(timer); }, [page, requestQuery, requestFilter]); + useEffect(() => { + if (page !== "requests") { + return; + } + const timer = window.setInterval(() => { + setNowMs(Date.now()); + }, 1000); + return () => window.clearInterval(timer); + }, [page]); + async function saveProfile(event: FormEvent) { event.preventDefault(); setProfileMessage({ text: "正在保存 profile...", tone: "" }); @@ -1019,7 +1042,7 @@ export default function App() { {entry.response_stream - ? `${numberFormat(entry.stream_chunk_count)} chunk / ${bytesFormat(entry.response_bytes_received)}` + ? `${numberFormat(entry.stream_chunk_count)} chunk / ${bytesFormat(entry.response_bytes_received)} / ${secondsSince(entry.last_activity_at || entry.usage_last_updated_at, nowMs)}` : "-"}