feat: validate Team workspace connections

This commit is contained in:
zhoujun0601
2026-08-13 09:03:47 -04:00
parent 6adbb2d714
commit eb7f9fd66c
10 changed files with 345 additions and 28 deletions
+82 -4
View File
@@ -46,6 +46,10 @@ type Account = {
displayName: string;
email?: string;
planType?: string;
expectedKind: "any" | "personal" | "team";
actualKind: "unknown" | "personal" | "team";
validationStatus: "pending" | "matched" | "mismatch" | "unknown";
possibleDuplicate: boolean;
connected: boolean;
};
type Dash = {
@@ -358,7 +362,7 @@ function Dashboard() {
{d.displayName} · {d.account.email || "尚未连接"}
</b>
</div>
<span className="badge">{d.account.planType || "未识别套餐"}</span>
<span className="badge">{planLabel(d.account.planType)}</span>
<span className="fresh">
更新于{" "}
{d.fetchedAt
@@ -683,12 +687,29 @@ function CodexSettings() {
const [xs, setXs] = useState<Account[]>([]),
[result, setResult] = useState<any>(null),
[active, setActive] = useState(0),
[newKind, setNewKind] = useState<"personal" | "team">("team"),
[busy, setBusy] = useState(false),
[err, setErr] = useState("");
const load = () => api<Account[]>("accounts").then(setXs);
const load = () => api<Account[]>("accounts").then((accounts) => { setXs(accounts); return accounts; });
useEffect(() => {
load();
}, []);
useEffect(() => {
if (!result || !active) return;
const started = Date.now();
const timer = setInterval(async () => {
const accounts = await load();
const account = accounts.find((x) => x.id === active);
if (
account &&
["matched", "mismatch"].includes(account.validationStatus)
)
clearInterval(timer);
else if (Date.now() - started >= 120000)
clearInterval(timer);
}, 2000);
return () => clearInterval(timer);
}, [result, active]);
const login = async (id: number) => {
try {
setBusy(true);
@@ -708,6 +729,7 @@ function CodexSettings() {
setErr("");
const x = await post<Account>("accounts", {
displayName: `账号 ${xs.length + 1}`,
expectedKind: newKind,
});
await load();
setActive(x.id);
@@ -727,6 +749,18 @@ function CodexSettings() {
个人订阅和 Team 工作区请分别添加为独立连接;同一邮箱可以添加多次。
</p>
</div>
<label>
新连接类型
<select
value={newKind}
onChange={(e) =>
setNewKind(e.target.value as "personal" | "team")
}
>
<option value="team">Team / Business 工作区</option>
<option value="personal">个人订阅</option>
</select>
</label>
<button disabled={busy} onClick={add}>
{busy && active === 0 ? "服务启动中…" : "添加账号"}
</button>
@@ -742,15 +776,41 @@ function CodexSettings() {
onBlur={async (e) => {
const name = e.target.value.trim();
if (name && name !== x.displayName) {
await put(`accounts/${x.id}`, { displayName: name });
await put(`accounts/${x.id}`, {
displayName: name,
expectedKind: x.expectedKind,
});
load();
}
}}
/>
<small>
{x.email || "尚未登录"} · {x.planType || "未识别套餐"}
{x.email || "尚未登录"} · {planLabel(x.planType)}
</small>
<small className={x.validationStatus === "mismatch" ? "error" : ""}>
{validationLabel(x)}
</small>
{x.possibleDuplicate && (
<small className="hint">
同一邮箱已有相同类型连接,请确认没有重复授权同一工作区
</small>
)}
</div>
<select
aria-label="预期连接类型"
value={x.expectedKind}
onChange={async (e) => {
await put(`accounts/${x.id}`, {
displayName: x.displayName,
expectedKind: e.target.value,
});
load();
}}
>
<option value="any">不校验</option>
<option value="personal">个人订阅</option>
<option value="team">Team / Business</option>
</select>
<button
className="secondary"
disabled={busy}
@@ -1056,6 +1116,24 @@ const duration = (v?: number) =>
: v < 3600
? `${Math.floor(v / 60)} 分 ${v % 60} 秒`
: `${(v / 3600).toFixed(1)} 小时`;
const planLabel = (plan?: string) => {
if (!plan) return "未识别套餐";
const labels: Record<string, string> = {
free: "Free", go: "Go", plus: "Plus", pro: "Pro", prolite: "Pro Lite",
team: "Team", business: "Business",
self_serve_business_prolite: "Business Pro Lite",
self_serve_business_usage_based: "Business(按量)",
enterprise: "Enterprise", edu: "Edu",
};
return labels[plan.toLowerCase()] || plan;
};
const validationLabel = (account: Account) => {
if (account.expectedKind === "any") return "未启用套餐类型校验";
if (account.validationStatus === "pending") return `等待验证${account.expectedKind === "team" ? " Team / Business" : "个人订阅"}`;
if (account.validationStatus === "matched") return account.actualKind === "team" ? "已连接 Team / Business 工作区" : "已连接个人订阅";
if (account.validationStatus === "mismatch") return `类型不匹配:实际为 ${planLabel(account.planType)},请退出后重新授权`;
return `无法验证工作区类型:${planLabel(account.planType)}`;
};
const relative = (ts: number) => {
const s = Math.max(0, ts - Math.floor(Date.now() / 1000));
if (!s) return "即将重置";
+1 -1
View File
@@ -483,7 +483,7 @@ header p {
}
.account-row {
display: grid;
grid-template-columns: auto minmax(180px, 1fr) auto auto auto;
grid-template-columns: auto minmax(180px, 1fr) auto auto auto auto;
gap: 10px;
align-items: center;
padding: 14px;
+11 -1
View File
@@ -12,7 +12,17 @@ const responses: Record<string, unknown> = {
notifyBefore: true,
notifyAfter: true,
},
accounts: [{ id: 1, displayName: "默认账号", email: "test@example.com", planType: "Plus", connected: true }],
accounts: [{
id: 1,
displayName: "默认账号",
email: "test@example.com",
planType: "plus",
expectedKind: "personal",
actualKind: "personal",
validationStatus: "matched",
possibleDuplicate: false,
connected: true,
}],
"settings/telegram": { token: "", configured: true, enabled: true, menuEnabled: true },
"settings/smtp": {
host: "smtp.example.com",