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
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions apps/codex-plus-manager/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ serde.workspace = true
serde_json.workspace = true
tauri = { version = "2", features = ["protocol-asset", "custom-protocol", "tray-icon"] }
tauri-plugin-dialog = "2"
tokio.workspace = true
toml_edit.workspace = true

[build-dependencies]
Expand Down
209 changes: 187 additions & 22 deletions apps/codex-plus-manager/src-tauri/src/gateway_commands.rs

Large diffs are not rendered by default.

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 @@ -403,6 +403,8 @@ pub fn run() {
gateway_commands::reset_gateway_key_health,
gateway_commands::create_gateway_provider,
gateway_commands::update_gateway_provider,
gateway_commands::get_native_auth_state,
gateway_commands::set_native_auth_account,
gateway_commands::add_gateway_provider_key,
gateway_commands::replace_gateway_provider_key,
gateway_commands::delete_gateway_provider_key,
Expand Down
8 changes: 7 additions & 1 deletion apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ import {
normalizeAggregateDefaultRelayId,
} from "./aggregate-members";
import { relayAuthForLiveDraft, shouldBackfillRelayProfileBeforeSwitch } from "./relay-live-files";
import { NativeAuthControl, SavedAuthAccountField } from "./NativeAuthControls";
import { relayBillingCacheKey, relayBillingProbeEligible, type RelayBillingProbe } from "./relay-billing";
import { RelayBillingLine, RelayBillingRetry, useRelayBillingProbe } from "./RelayBillingLine";
import { apiKeyPreview, createRelayApiKey, normalizeRelayApiKeys, relayProfileWithNormalizedApiKeys, relayProfileWithUpdatedApiKey, type RelayApiKey } from "./provider-api-keys";
Expand Down Expand Up @@ -622,6 +623,7 @@ export type RelayProfile = {
relayMode: RelayMode;
sessionProvider?: RelaySessionProvider;
officialMixApiKey: boolean;
officialAccountRef?: string;
hideOfficialUsageAlert: boolean;
testModel: string;
configContents: string;
Expand Down Expand Up @@ -8075,6 +8077,7 @@ function RelayScreen({
)}
/>
{connection}
<NativeAuthControl />
<EnvConflictNotice envConflicts={envConflicts} actions={actions} />
<SshRemoteLoopbackNotice notice={envConflicts?.sshRemoteLoopback ?? null} />
{!normalized.relayProfilesEnabled ? (
Expand Down Expand Up @@ -13339,7 +13342,7 @@ function RelayProfileEditor({
mode: "pureApi" as const,
icon: KeyRound,
title: t("纯 API"),
description: t("只使用 API 凭据,不依赖官方登录。"),
description: t("使用 API 凭据,原生登录账号独立选择。"),
},
]).map(({ mode, icon: Icon, title, description }) => (
<button
Expand All @@ -13359,6 +13362,9 @@ function RelayProfileEditor({
</button>
))}
</div>
{editorMode !== "pureApi" ? (
<SavedAuthAccountField onChange={(officialAccountRef) => updateDraft({ officialAccountRef })} value={profile.officialAccountRef ?? ""} />
) : null}
{showApiFields ? (
<div className="relay-inline-connection">
<div className="relay-inline-connection-head">
Expand Down
24 changes: 24 additions & 0 deletions apps/codex-plus-manager/src/GatewayPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -2846,6 +2846,30 @@ body.gw-sorting * {
font-weight: 500;
}

.shell .gw-quota-window[data-tone="unknown"] {
color: var(--gw-mute);
}

.shell .gw-quota-windows[data-compact] {
gap: 5px 16px;
margin-top: 4px;
min-width: 0;
}

.shell .gw-quota-windows[data-compact] .gw-quota-window {
gap: 6px;
font-size: 11.5px;
white-space: nowrap;
}

.shell .gw-quota-windows[data-compact] .gw-quota-window b {
min-width: 19px;
}

.shell .gw-quota-windows[data-compact] .gw-quota-window i {
width: 48px;
}

.shell .gw-quota-window i {
width: 56px;
}
Expand Down
4 changes: 2 additions & 2 deletions apps/codex-plus-manager/src/GatewayPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ export function GatewayPage({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// 路由页定时刷新供应商状态:静默读取,不占用 busy,也不弹错误。
// 订阅额度在后台预取;供应商页也要及时收到首次查询和授权后的结果。
useEffect(() => {
if (tab !== "routing") return;
if (tab !== "routing" && tab !== "providers") return;
let cancelled = false;
const timer = window.setInterval(() => {
const started = gatewayGeneration();
Expand Down
34 changes: 24 additions & 10 deletions apps/codex-plus-manager/src/GatewayProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
providerEnabled,
providerInput,
skippedAgents,
subscriptionBlockReason,
maskAccountText,
type GatewayAgentId,
type GatewayPayload,
type GatewayPoolEntry,
Expand All @@ -48,7 +50,7 @@ import {
type SubscriptionLoginStart,
type SubscriptionView,
} from "./gateway-model";
import { AgentIcon, Avatar, KeyCount, MemberStatusLabel, QuotaBadge, QuotaWindows, Switch, type Notify, type Run } from "./gateway-ui";
import { AgentIcon, Avatar, KeyCount, MemberStatusLabel, QuotaWindows, Switch, type Notify, type Run } from "./gateway-ui";
import { ProviderFormDialog, type ProviderFormState } from "./ProviderFormDialog";
import { ProviderExtrasDialog, ProviderLimitsDialog, ProviderNewApiAccessDialog, keyLimitSummary, priceSummary } from "./ProviderExtrasDialog";
import { ProviderWindowsDialog, windowSummary, withUnlistedWindows } from "./ProviderWindowsDialog";
Expand Down Expand Up @@ -619,7 +621,7 @@ function PoolRow({
if (await run(`newapi-access:${entry.id}`, "save_gateway_provider_newapi_access", { id: entry.id, token, userId, clear }, title)) setAccessOpen(false);
};
const sub = (entry.account
? [t("订阅登录"), entry.lentToCodex ? t("Codex 正用它登录") : "", tf("{0} 个模型", [entry.models.length])]
? [t("订阅登录"), tf("{0} 个模型", [entry.models.length])]
: [protocolLabel(entry.protocol), presetHost(entry.baseUrl), tf("{0} 个模型", [entry.models.length])]
).filter(Boolean).join(" · ");
// 价格、代理各开各的弹窗。
Expand Down Expand Up @@ -664,6 +666,7 @@ function PoolRow({
const price = priceSummary(entry);
// ChatGPT 订阅账号、Copilot 和 SuperGrok 能查额度(ChatGPT API 账号按量计费,没有额度窗口)。
const quotaSupported = entry.account === "chatgpt" || entry.account === "copilot" || entry.account === "grok";
const testBlocked = subscriptionBlockReason(entry);
const saveExtras = async (nextPrice: ProviderPrice | null, modelPrices: Record<string, ProviderPrice> | null, proxy: string) => {
const title = extrasOpen === "proxy" ? t("保存代理") : t("保存价格");
if (await run(`extras:${entry.id}`, "save_gateway_provider_extras", { id: entry.id, price: nextPrice, modelPrices, proxy }, title)) setExtrasOpen(null);
Expand Down Expand Up @@ -699,6 +702,9 @@ function PoolRow({
<div className="gw-pool-main">
<strong>{maskAccountName(entry.name)}</strong>
<small>{sub}</small>
{quotaSupported ? entry.quota
? <QuotaWindows compact maxAgeSeconds={entry.subscriptionPolicy?.maxAgeSeconds} quota={entry.quota} />
: <small>{t("额度待刷新")}</small> : null}
</div>
<span className="gw-pool-status">
{newModels ? (
Expand All @@ -712,23 +718,22 @@ function PoolRow({
: status.state === "idle" && billing.probe?.rate ? <RelayBillingPoolLabel probe={billing.probe} />
// 只有一个 Key(订阅登录就是一个账号)时「🔑 1」没信息量,写「可用」。
: status.text === "{0} 个 Key" && Number(status.args[0]) <= 1 ? t("可用") : <MemberStatusLabel status={status} />}
{enabled && entry.quota ? <QuotaBadge quota={entry.quota} /> : null}
{enabled && billing.probe ? <RelayBillingPoolBadge failure={billing.failure} loading={billing.loading} onRefresh={billing.refresh} probe={billing.probe} /> : null}
{vendorBalance.balance ? <ProviderBalancePoolBadge balance={vendorBalance.balance} loading={vendorBalance.loading} onRefresh={vendorBalance.refresh} /> : null}
{enabled && !billing.probe && billing.settled ? (
<RelayBillingRetry failure={billing.failure} loading={billing.loading} onRefresh={billing.refresh} />
) : null}
</span>
<span className="gw-pool-actions">
{/* 编辑放在标题栏(#2782);订阅账号没有可编辑的连接,留同宽的空位让开关对齐。 */}
{entry.account ? (
{/* 普通订阅没有可编辑的连接;ChatGPT 订阅可编辑独立的额度策略。 */}
{entry.account && entry.account !== "chatgpt" ? (
<span aria-hidden="true" className="gw-pool-edit-slot" />
) : (
<button
aria-label={tf("编辑 {0}", [maskAccountName(entry.name)])}
className="gw-icon-button gw-pool-edit"
onClick={onEdit}
title={t("编辑名称、地址、协议与模型")}
title={entry.account === "chatgpt" ? t("编辑订阅额度策略") : t("编辑名称、地址、协议与模型")}
type="button"
>
<Pencil className="h-4 w-4" />
Expand Down Expand Up @@ -823,7 +828,7 @@ function PoolRow({
<div className="gw-detail-row">
<span className="gw-detail-label">{t("额度")}</span>
<div className="gw-detail-value gw-chip-row">
{entry.quota ? <QuotaWindows quota={entry.quota} /> : <span className="gw-muted">{t("还没查到,每 10 分钟查一次,用过一次也会更新")}</span>}
{entry.quota ? <QuotaWindows maxAgeSeconds={entry.subscriptionPolicy?.maxAgeSeconds} quota={entry.quota} /> : <span className="gw-muted">{t("尚未取得官方用量")}</span>}
</div>
<span className="gw-detail-actions">
<button
Expand All @@ -838,6 +843,15 @@ function PoolRow({
</span>
</div>
) : null}
{entry.account === "chatgpt" ? (
<div className="gw-detail-row">
<span className="gw-detail-label">{t("订阅额度策略")}</span>
<div className="gw-detail-value">
{t("按官方窗口限制")}
</div>
<span className="gw-detail-actions"><EditIcon disabled={busy !== null} label={t("编辑订阅额度策略")} onClick={onEdit} /></span>
</div>
) : null}
{/* 价格、窗口、思考等级、代理、限速不常改:收进「更多设置」,展开详情先看到模型、Key、额度。 */}
<details className="gw-detail-advanced gw-pool-more">
<summary>
Expand Down Expand Up @@ -927,9 +941,9 @@ function PoolRow({
<span className="gw-detail-foot-actions">
<button
className="gw-text-button gw-sync-button"
disabled={busy !== null}
disabled={busy !== null || Boolean(testBlocked)}
onClick={() => setTestKey(null)}
title={t("发一条很短的请求,看这家供应商能不能用、多快回复;不计入路由与统计")}
title={testBlocked ? t(maskAccountText(testBlocked)) : t("发一条很短的请求,看这家供应商能不能用、多快回复;不计入路由与统计")}
type="button"
>
<Zap className="h-3.5 w-3.5" />
Expand All @@ -949,7 +963,7 @@ function PoolRow({
</div>
</div>
) : null}
{testKey !== undefined ? <ProviderTestDialog entry={entry} keyId={testKey} onClose={() => setTestKey(undefined)} /> : null}
{testKey !== undefined ? <ProviderTestDialog entry={entry} keyId={testKey} onEditSubscription={onEdit} onClose={() => setTestKey(undefined)} /> : null}
{metaSync ? (
<ProviderMetaSyncDialog
busy={busy !== null}
Expand Down
113 changes: 113 additions & 0 deletions apps/codex-plus-manager/src/NativeAuthControls.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
.native-auth-control {
margin: 8px 0 12px;
font-size: 12px;
min-width: 0;
}
.native-auth-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
min-width: 0;
}
.native-auth-row > label {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
min-width: 0;
}
.native-auth-row select,
.subscription-policy select,
.subscription-policy input[type="number"] {
min-width: 0;
max-width: 100%;
border: 1px solid hsl(var(--input));
border-radius: 6px;
padding: 6px 8px;
background: hsl(var(--background));
color: hsl(var(--foreground));
font: inherit;
}
.native-auth-row select {
width: min(320px, 100%);
}
:root .shell .native-auth-current > .cx-row-main {
flex: 0 0 auto;
padding-left: 0;
}
:root .shell .native-auth-current > .cx-row-controls {
flex: 0 1 auto;
max-width: 430px;
margin-left: auto;
min-width: 0;
}
:root .shell .native-auth-current .app-select {
flex: 0 1 auto;
width: auto;
max-width: 350px;
min-width: 0;
}
:root .shell .native-auth-current .app-select-trigger {
height: 30px;
min-height: 30px;
padding: 0 8px 0 10px;
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
font-size: 13px;
}
:root .shell .native-auth-current .app-select-trigger:hover,
:root .shell .native-auth-current .app-select-trigger[aria-expanded="true"] {
border-color: hsl(var(--border));
background: hsl(var(--accent) / 0.5);
}
.native-auth-current .native-auth-action {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 32px;
width: 32px;
height: 32px;
padding: 0;
border: 0;
border-radius: 6px;
background: transparent;
color: hsl(var(--muted-foreground));
cursor: pointer;
}
.native-auth-current .native-auth-action:hover:not(:disabled) {
background: hsl(var(--accent));
color: hsl(var(--foreground));
}
.native-auth-current .native-auth-action:disabled {
opacity: 0.4;
cursor: default;
}
.native-auth-action > svg {
width: 14px;
height: 14px;
flex: none;
}
.native-auth-error {
margin: 6px 0 0;
color: hsl(var(--destructive));
overflow-wrap: anywhere;
}
.subscription-policy {
display: grid;
gap: 10px;
}
.subscription-policy label {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
}
.subscription-policy input[type="number"] {
width: 100px;
}
.subscription-policy .subscription-permission {
justify-content: flex-start;
}
Loading
Loading