Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions apps/codex-plus-launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,13 @@ impl Default for LauncherDataService {

#[async_trait::async_trait]
impl BridgeDataService for LauncherDataService {
async fn provider_guard_status(&self) -> anyhow::Result<Value> {
let status = tokio::task::spawn_blocking(|| codex_plus_data::inspect_provider_guard(None))
.await
.map_err(|error| anyhow::anyhow!("provider guard status task failed: {error}"))??;
Ok(serde_json::to_value(status)?)
}

async fn delete(&self, session: SessionRef) -> anyhow::Result<DeleteResult> {
let db_paths = self.candidate_db_paths();
let backup_store = codex_plus_data::BackupStore::new(self.backup_dir.clone());
Expand Down
60 changes: 37 additions & 23 deletions apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,29 +561,8 @@ pub fn import_ccs_providers() -> CommandResult<SettingsPayload> {

let store = SettingsStore::default();
let mut settings = store.load().unwrap_or_default();
let mut existing_keys: Vec<String> = settings
.relay_profiles
.iter()
.map(codex_plus_core::ccs_import::imported_provider_identity)
.collect();
let mut existing_ids: Vec<String> = settings
.relay_profiles
.iter()
.map(|profile| profile.id.clone())
.collect();
let mut imported = 0usize;

for provider in providers {
let key = codex_plus_core::ccs_import::provider_identity_from_ccs(&provider);
if existing_keys.iter().any(|existing| existing == &key) {
continue;
}
let profile = codex_plus_core::ccs_import::relay_profile_from_ccs(&provider, &existing_ids);
existing_ids.push(profile.id.clone());
existing_keys.push(key);
settings.relay_profiles.push(profile);
imported += 1;
}
let imported =
codex_plus_core::ccs_import::append_new_relay_profiles_from_ccs(&mut settings, &providers);

if imported == 0 {
return settings_payload("没有新的 cc-switch 供应商配置需要导入。", "设置读取失败");
Expand Down Expand Up @@ -1150,6 +1129,41 @@ pub async fn load_provider_sync_targets() -> CommandResult<Value> {
}
}

#[tauri::command]
pub async fn load_provider_guard_status() -> CommandResult<Value> {
let result = tauri::async_runtime::spawn_blocking(|| codex_plus_data::inspect_provider_guard(None))
.await
.map_err(|error| anyhow::anyhow!("provider guard status task failed: {error}"));
match result {
Ok(Ok(status)) => ok(
"Provider Guard 状态已加载。",
serde_json::to_value(status).unwrap_or_else(|_| json!({})),
),
Ok(Err(error)) | Err(error) => {
failed(&format!("Provider Guard 状态加载失败:{error}"), json!({}))
}
}
}

#[tauri::command]
pub async fn repair_provider_guard(confirmed: bool) -> CommandResult<Value> {
if !confirmed {
return failed("必须在原生管理器中确认后才能执行 Provider Guard 修复。", json!({}));
}
let result = tauri::async_runtime::spawn_blocking(|| codex_plus_data::repair_provider_guard(None))
.await
.map_err(|error| anyhow::anyhow!("provider guard repair task failed: {error}"));
match result {
Ok(Ok(repair)) => ok(
"Provider Guard 已完成备份和修复。",
serde_json::to_value(repair).unwrap_or_else(|_| json!({})),
),
Ok(Err(error)) | Err(error) => {
failed(&format!("Provider Guard 修复失败:{error}"), json!({}))
}
}
}

fn merge_manual_provider_sync_targets(
targets: &mut codex_plus_data::ProviderSyncTargetList,
manual: &[String],
Expand Down
2 changes: 2 additions & 0 deletions apps/codex-plus-manager/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ pub fn run() {
commands::forget_zed_remote_project,
commands::delete_local_session,
commands::load_provider_sync_targets,
commands::load_provider_guard_status,
commands::repair_provider_guard,
commands::preview_session_index_cleanup,
commands::apply_session_index_cleanup,
commands::sync_providers_now,
Expand Down
116 changes: 115 additions & 1 deletion apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,28 @@ type ProviderSyncTargetsPayload = {

type ProviderSyncTargetsResult = CommandResult<ProviderSyncTargetsPayload>;

type ProviderGuardFinding = {
code: string;
severity: "warning" | "critical" | string;
message: string;
};

type ProviderGuardStatusPayload = {
level: "ok" | "warning" | "critical" | string;
stableProvider: string;
currentProvider: string;
stableProviderConfigured: boolean;
totalThreads: number;
databasesScanned: number;
providerBuckets: Array<{ provider: string; threads: number }>;
endpoint: { kind: string; loopback: boolean; port?: number | null };
findings: ProviderGuardFinding[];
canRepair: boolean;
repairRequiresNativeConfirmation: boolean;
};

type ProviderGuardResult = CommandResult<ProviderGuardStatusPayload>;

type ProviderSyncProgress = {
active: boolean;
percent: number;
Expand Down Expand Up @@ -845,6 +867,7 @@ export function App() {
message: t("尚未检查官方远端插件缓存。"),
});
const [providerSyncTargets, setProviderSyncTargets] = useState<ProviderSyncTargetsResult | null>(null);
const [providerGuard, setProviderGuard] = useState<ProviderGuardResult | null>(null);
const [selectedProviderSyncTarget, setSelectedProviderSyncTarget] = useState("");
const [removeOwnedData, setRemoveOwnedData] = useState(false);
const [relaySwitching, setRelaySwitching] = useState(false);
Expand Down Expand Up @@ -1217,6 +1240,7 @@ export function App() {
await refreshSettings(true);
await refreshLocalSessions(true);
await refreshProviderSyncTargets(true);
await refreshProviderGuard(true);
}
if (next === "zedRemote") {
await refreshSettings(true);
Expand Down Expand Up @@ -1532,6 +1556,35 @@ export function App() {
return result;
};

const refreshProviderGuard = async (silent = false) => {
const result = await run(() => call<ProviderGuardResult>("load_provider_guard_status"));
if (result) {
setProviderGuard(result);
if (!silent && !isSuccessStatus(result.status)) showNotice(t("Provider Guard"), result.message, result.status);
}
return result;
};

const repairProviderGuard = async () => {
if (!providerGuard?.canRepair) {
showNotice(t("Provider Guard"), t("当前配置不满足安全修复条件,请先配置 model_providers.custom。"), "failed");
return;
}
const confirmed = window.confirm(
t("修复前会备份 config.toml、会话文件和 SQLite 索引,并将稳定供应商 ID 设为 custom。是否继续?"),
);
if (!confirmed) return;
const result = await run(() =>
call<CommandResult<{ guard?: ProviderGuardStatusPayload }>>("repair_provider_guard", { confirmed: true }),
);
if (result) {
showNotice(t("Provider Guard"), result.message, result.status);
await refreshProviderGuard(true);
await refreshProviderSyncTargets(true);
await refreshLocalSessions(true);
}
};

const syncProvidersNow = async () => {
if (providerSyncProgress.active) return;
setProviderSyncProgress({
Expand Down Expand Up @@ -1948,6 +2001,7 @@ export function App() {
await refreshRelay(true);
await refreshEnvConflicts(true);
await refreshProviderSyncTargets(true);
await refreshProviderGuard(true);
await refreshPendingProviderImport(true);
await refreshRemotePluginMarketplace(true);
})();
Expand Down Expand Up @@ -2080,6 +2134,8 @@ export function App() {
},
syncProvidersNow,
refreshProviderSyncTargets,
refreshProviderGuard,
repairProviderGuard,
setProviderSyncTarget: (provider: string) => {
setSelectedProviderSyncTarget(provider);
setSettingsForm((current) => ({ ...current, providerSyncLastSelectedProvider: provider }));
Expand Down Expand Up @@ -2141,7 +2197,7 @@ export function App() {
disableWatcher: () => watcherAction("disable_watcher"),
toggleTheme: () => setTheme((current) => (current === "dark" ? "light" : "dark")),
}),
[route, launchForm, settingsForm, settings, removeOwnedData, update, updateInstallProgress.active, logs, diagnostics, theme, relayFiles, localSessions, zedRemoteProjects, selectedProviderSyncTarget, envConflicts, relayEnvironment, ccsProviders],
[route, launchForm, settingsForm, settings, removeOwnedData, update, updateInstallProgress.active, logs, diagnostics, theme, relayFiles, localSessions, zedRemoteProjects, selectedProviderSyncTarget, providerGuard, envConflicts, relayEnvironment, ccsProviders],
);
const hasUpdate = update?.updateAvailable === true;

Expand Down Expand Up @@ -2251,6 +2307,7 @@ export function App() {
sessions={localSessions}
providerSyncProgress={providerSyncProgress}
providerSyncTargets={providerSyncTargets}
providerGuard={providerGuard}
selectedProviderSyncTarget={selectedProviderSyncTarget}
onFormChange={setSettingsForm}
actions={actions}
Expand Down Expand Up @@ -2374,6 +2431,8 @@ type Actions = {
saveManualCodexAppPath: () => Promise<void>;
syncProvidersNow: () => Promise<void>;
refreshProviderSyncTargets: (silent?: boolean) => Promise<ProviderSyncTargetsResult | null>;
refreshProviderGuard: (silent?: boolean) => Promise<ProviderGuardResult | null>;
repairProviderGuard: () => Promise<void>;
setProviderSyncTarget: (provider: string) => void;
setLaunchMode: (launchMode: LaunchMode) => Promise<void>;
refreshRelay: () => Promise<void>;
Expand Down Expand Up @@ -3192,6 +3251,7 @@ function SessionsScreen({
sessions,
providerSyncProgress,
providerSyncTargets,
providerGuard,
selectedProviderSyncTarget,
onFormChange,
actions,
Expand All @@ -3201,6 +3261,7 @@ function SessionsScreen({
sessions: LocalSessionsResult | null;
providerSyncProgress: ProviderSyncProgress;
providerSyncTargets: ProviderSyncTargetsResult | null;
providerGuard: ProviderGuardResult | null;
selectedProviderSyncTarget: string;
onFormChange: (value: BackendSettings) => void;
actions: Actions;
Expand Down Expand Up @@ -3262,6 +3323,59 @@ function SessionsScreen({

return (
<>
<Panel>
<CardHead
title={t("Provider Guard")}
detail={t("固定稳定供应商 ID,检查会话分桶,并阻止脚本市场静默修改配置或 SQLite")}
/>
<CardContent>
<div className="metric-list">
<Metric label={t("安全状态")} value={providerGuard?.level ?? t("尚未检查")} />
<Metric label={t("当前 provider")} value={providerGuard?.currentProvider ?? "-"} />
<Metric label={t("稳定 provider")} value={providerGuard?.stableProvider ?? "custom"} />
<Metric label={t("索引会话")} value={tf("{0} 个", [providerGuard?.totalThreads ?? 0])} />
<Metric
label={t("接口类型")}
value={providerGuard?.endpoint ? `${providerGuard.endpoint.kind}${providerGuard.endpoint.port ? `:${providerGuard.endpoint.port}` : ""}` : "-"}
/>
</div>
{(providerGuard?.providerBuckets ?? []).length ? (
<div className="hint-line">
<Info className="h-4 w-4" />
<span>
{t("会话分桶:")}
{providerGuard?.providerBuckets.map((bucket) => `${bucket.provider}=${bucket.threads}`).join(",")}
</span>
</div>
) : null}
{(providerGuard?.findings ?? []).map((finding) => (
<div className="hint-line" key={finding.code}>
{finding.severity === "critical" ? <ShieldAlert className="h-4 w-4" /> : <Info className="h-4 w-4" />}
<span>{finding.message}</span>
</div>
))}
{!providerGuard?.findings?.length && providerGuard ? (
<div className="hint-line">
<ShieldCheck className="h-4 w-4" />
<span>{t("配置与会话分桶保持稳定。")}</span>
</div>
) : null}
<Toolbar>
<Button onClick={() => void actions.refreshProviderGuard()} variant="outline">
<RefreshCw className="h-4 w-4" />
{t("重新检查")}
</Button>
<Button disabled={!providerGuard?.canRepair} onClick={() => void actions.repairProviderGuard()}>
<ShieldCheck className="h-4 w-4" />
{t("备份并修复")}
</Button>
</Toolbar>
<div className="hint-line">
<ShieldCheck className="h-4 w-4" />
<span>{t("修复只能从原生管理器执行;脚本市场仅拥有只读检查权限。")}</span>
</div>
</CardContent>
</Panel>
<Panel>
<CardHead title={t("会话管理")} detail={t("读取 Codex 本地 SQLite 会话库,会删除数据库记录和对应 rollout 文件")} />
<CardContent>
Expand Down
17 changes: 17 additions & 0 deletions apps/codex-plus-manager/src/i18n-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@

// Plain strings: t("中文") -> EN_PLAIN["中文"].
export const EN_PLAIN: Record<string, string> = {
"Provider Guard": "Provider Guard",
"当前配置不满足安全修复条件,请先配置 model_providers.custom。":
"The current configuration cannot be repaired safely. Configure model_providers.custom first.",
"修复前会备份 config.toml、会话文件和 SQLite 索引,并将稳定供应商 ID 设为 custom。是否继续?":
"Before repairing, Codex++ will back up config.toml, session files, and SQLite indexes, then set the stable provider ID to custom. Continue?",
"固定稳定供应商 ID,检查会话分桶,并阻止脚本市场静默修改配置或 SQLite":
"Keep a stable provider ID, inspect session buckets, and prevent marketplace scripts from silently changing config or SQLite.",
"安全状态": "Safety status",
"稳定 provider": "Stable provider",
"索引会话": "Indexed sessions",
"接口类型": "Endpoint type",
"会话分桶:": "Session buckets: ",
"配置与会话分桶保持稳定。": "Configuration and session buckets are stable.",
"重新检查": "Check again",
"备份并修复": "Back up and repair",
"修复只能从原生管理器执行;脚本市场仅拥有只读检查权限。":
"Repairs can only run from the native manager; marketplace scripts have read-only inspection access.",
"API Key 模式下扩展插件市场请求,尽量显示完整插件列表;官方/混合模式通常不需要。":
"Expands plugin marketplace requests in API Key mode to show the full plugin list. Usually unnecessary in official/mixed mode.",
"API Key 环境变量": "API Key environment variable",
Expand Down
32 changes: 4 additions & 28 deletions assets/inject/renderer-inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -4737,11 +4737,7 @@
}

function codexPlusModelNames() {
return uniqueValues([
codexModelCatalog.default_model,
codexModelCatalog.model,
...(Array.isArray(codexModelCatalog.models) ? codexModelCatalog.models : []),
]);
return uniqueValues(Array.isArray(codexModelCatalog.models) ? codexModelCatalog.models : []);
}

async function loadCodexModelCatalog(force = false) {
Expand All @@ -4750,27 +4746,7 @@
codexModelCatalogPromise = postJson("/codex-model-catalog", {})
.then(async (result) => {
codexModelCatalog = result && typeof result === "object" ? result : { status: "failed", model: "", default_model: "", model_provider: "", provider_name: "", models: [], sources: [], responses_api: { status: "unknown", message: "" } };
if ((!codexModelCatalog.models || codexModelCatalog.models.length === 0) && codexModelCatalog.status === "not_configured") {
try {
const settingsPromise = postJson("/settings/get", {});
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("fallback timeout")), 3000));
const settingsResp = await Promise.race([settingsPromise, timeoutPromise]);
if (settingsResp && settingsResp.relayProfiles && Array.isArray(settingsResp.relayProfiles)) {
const activeId = settingsResp.activeRelayId || "";
const profile = settingsResp.relayProfiles.find(p => p.id === activeId) || settingsResp.relayProfiles[0];
if (profile && profile.modelList) {
const extraModels = profile.modelList.split(/[\r\n,]+/).map(s => s.trim()).filter(Boolean);
if (extraModels.length > 0) {
codexModelCatalog.models = extraModels;
codexModelCatalog.default_model = codexModelCatalog.default_model || extraModels[0];
sendCodexPlusDiagnostic("model_catalog_fallback_applied", { count: extraModels.length });
}
}
}
} catch (fallbackError) {
sendCodexPlusDiagnostic("model_catalog_fallback_error", { error: String(fallbackError?.message || fallbackError) });
}
}

codexModelCatalogLoadedAt = Date.now();
renderCodexPlusMenu();
scheduleCodexModelWhitelistRefresh();
Expand Down Expand Up @@ -4798,9 +4774,9 @@
slug: modelName,
name: modelName,
displayName: modelName,
description: codexModelCatalog.provider_name || codexModelCatalog.model_provider || "Custom model",
description: codexModelCatalog.provider_name || codexModelCatalog.model_provider || "Provider model",
hidden: false,
isDefault: (codexModelCatalog.default_model || codexModelCatalog.model) === modelName,
isDefault: codexModelCatalog.default_model === modelName,
defaultReasoningEffort: "medium",
supportedReasoningEfforts: modelReasoningEfforts(),
};
Expand Down
Loading