fix: finalize v1.21.0 release workflow

This commit is contained in:
coso
2026-04-28 10:27:36 +08:00
parent 75c47e86e7
commit af597c6fd7
8 changed files with 472 additions and 3 deletions
-3
View File
@@ -330,9 +330,6 @@ jobs:
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as env_file:
env_file.write(f"TAURI_SIGNING_PRIVATE_KEY_PATH={key_path}\n")
env_file.write("TAURI_SIGNING_PRIVATE_KEY<<__TAURI_SIGNING_PRIVATE_KEY__\n")
env_file.write(normalized_key.rstrip("\n") + "\n")
env_file.write("__TAURI_SIGNING_PRIVATE_KEY__\n")
print(f"Normalized updater key written to {key_path}")
PY
+123
View File
@@ -0,0 +1,123 @@
import { act } from "react";
import type { ReactNode } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { RootRouter } from "./RootRouter";
const {
mockFinalizeModuleImportAutoReload,
mockGetRuntimeAppVersion,
mockStartOemCloudStartupLoginIfRequired,
} = vi.hoisted(() => ({
mockFinalizeModuleImportAutoReload: vi.fn(),
mockGetRuntimeAppVersion: vi.fn(() => "0.0.0-test"),
mockStartOemCloudStartupLoginIfRequired: vi.fn(),
}));
vi.mock("./App", () => ({
default: () => <div data-testid="main-app">主应用</div>,
}));
vi.mock("./pages/smart-input", () => ({
SmartInputPage: () => <div data-testid="smart-input-page" />,
}));
vi.mock("./pages/update-notification", () => ({
UpdateNotificationPage: () => <div data-testid="update-notification-page" />,
}));
vi.mock("./pages", () => ({
BrowserRuntimeDebuggerPage: () => (
<div data-testid="browser-runtime-debugger-page" />
),
}));
vi.mock("./features/resource-manager", () => ({
ResourceManagerPage: () => <div data-testid="resource-manager-page" />,
}));
vi.mock("./components/settings-v2/system/chrome-relay/guide-window", () => ({
BrowserConnectorGuideWindow: () => (
<div data-testid="browser-connector-guide-page" />
),
}));
vi.mock("./components/ui/sonner", () => ({
Toaster: () => <div data-testid="toaster" />,
}));
vi.mock("./components/layout/AppCrashBoundary", () => ({
AppCrashBoundary: ({ children }: { children: ReactNode }) => (
<>{children}</>
),
}));
vi.mock("./components/layout/CrashRecoveryPanel.helpers", () => ({
finalizeModuleImportAutoReload: mockFinalizeModuleImportAutoReload,
}));
vi.mock("./lib/appVersion", () => ({
getRuntimeAppVersion: mockGetRuntimeAppVersion,
}));
vi.mock("./lib/oemCloudStartupLogin", () => ({
startOemCloudStartupLoginIfRequired:
mockStartOemCloudStartupLoginIfRequired,
}));
interface MountedRootRouter {
container: HTMLDivElement;
root: Root;
}
const mounted: MountedRootRouter[] = [];
async function renderRootRouter(pathname: string) {
window.history.pushState({}, "", pathname);
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
await act(async () => {
root.render(<RootRouter />);
await Promise.resolve();
});
const page = { container, root };
mounted.push(page);
return page;
}
describe("RootRouter", () => {
beforeEach(() => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
mockStartOemCloudStartupLoginIfRequired.mockResolvedValue({
status: "not_configured",
});
});
afterEach(() => {
for (const item of mounted.splice(0)) {
act(() => item.root.unmount());
item.container.remove();
}
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("打开主应用时应触发启动期云端登录判断", async () => {
const { container } = await renderRootRouter("/");
expect(container.textContent).toContain("主应用");
expect(mockStartOemCloudStartupLoginIfRequired).toHaveBeenCalledTimes(1);
});
it("独立工具窗口不应触发主应用登录流程", async () => {
const { container } = await renderRootRouter("/smart-input");
expect(
container.querySelector('[data-testid="smart-input-page"]'),
).not.toBeNull();
expect(mockStartOemCloudStartupLoginIfRequired).not.toHaveBeenCalled();
});
});
+16
View File
@@ -14,6 +14,7 @@ import { Toaster } from "./components/ui/sonner";
import { AppCrashBoundary } from "./components/layout/AppCrashBoundary";
import { finalizeModuleImportAutoReload } from "./components/layout/CrashRecoveryPanel.helpers";
import { getRuntimeAppVersion } from "./lib/appVersion";
import { startOemCloudStartupLoginIfRequired } from "./lib/oemCloudStartupLogin";
/**
* 根据 URL 路径渲染对应的组件
@@ -27,6 +28,13 @@ import { getRuntimeAppVersion } from "./lib/appVersion";
*/
export function RootRouter() {
const pathname = window.location.pathname;
const isMainAppRoute = ![
"/smart-input",
"/update-notification",
"/browser-runtime-debugger",
"/resource-manager",
"/browser-connector-guide",
].includes(pathname);
useEffect(() => {
if (typeof window === "undefined") {
@@ -41,6 +49,14 @@ export function RootRouter() {
);
}, [pathname]);
useEffect(() => {
if (!isMainAppRoute) {
return;
}
void startOemCloudStartupLoginIfRequired();
}, [isMainAppRoute]);
// 截图对话悬浮窗口路由(也用于语音输入)
if (pathname === "/smart-input") {
return (
@@ -285,6 +285,7 @@ describe("WebSearchSettings", () => {
);
await leaveTip(heroTip);
await switchTab(document.body, "图片搜索");
const pexelsTip = await hoverTip("Pexels 接入说明");
expect(getBodyText()).toContain(
"申请地址:https://www.pexels.com/api/new/",
@@ -312,6 +313,8 @@ describe("WebSearchSettings", () => {
findInput(container, "web-search-provider-priority"),
"multi_search_engine, tavily, bing_search_api",
);
await switchTab(container, "Provider 凭证");
await setInputValue(
findInput(container, "web-search-tavily-key"),
"tavily-new-key",
@@ -328,6 +331,8 @@ describe("WebSearchSettings", () => {
findInput(container, "web-search-google-engine-id"),
"cx-new-id",
);
await switchTab(container, "MSE 聚合");
await setInputValue(
findInput(container, "web-search-mse-custom-engine-name"),
"hn",
@@ -336,6 +341,8 @@ describe("WebSearchSettings", () => {
findInput(container, "web-search-mse-custom-engine-template"),
"https://hn.algolia.com/?q={query}",
);
await switchTab(container, "图片搜索");
await setInputValue(
findInput(container, "web-search-pexels-key"),
"new-key",
@@ -384,6 +391,7 @@ describe("WebSearchSettings", () => {
await flushEffects();
await flushEffects();
await switchTab(container, "图片搜索");
await act(async () => {
findButton(container, "申请 Pexels Key").click();
await flushEffects();
@@ -397,6 +405,7 @@ describe("WebSearchSettings", () => {
await flushEffects();
await flushEffects();
await switchTab(container, "Provider 凭证");
await act(async () => {
findButton(container, "申请 Tavily Key").click();
await flushEffects();
@@ -417,6 +426,7 @@ describe("WebSearchSettings", () => {
await flushEffects();
await flushEffects();
await switchTab(container, "图片搜索");
await act(async () => {
findButton(container, "申请 Pexels Key").click();
await flushEffects();
@@ -436,6 +446,7 @@ describe("WebSearchSettings", () => {
await flushEffects();
await flushEffects();
await switchTab(container, "图片搜索");
await act(async () => {
findButton(container, "申请 Pixabay Key").click();
await flushEffects();
@@ -451,6 +462,7 @@ describe("WebSearchSettings", () => {
await flushEffects();
await flushEffects();
await switchTab(container, "Provider 凭证");
await act(async () => {
findButton(container, "申请 Bing Key").click();
await flushEffects();
@@ -466,6 +478,7 @@ describe("WebSearchSettings", () => {
await flushEffects();
await flushEffects();
await switchTab(container, "Provider 凭证");
await act(async () => {
findButton(container, "申请 Google Key").click();
await flushEffects();
@@ -481,6 +494,7 @@ describe("WebSearchSettings", () => {
await flushEffects();
await flushEffects();
await switchTab(container, "Provider 凭证");
await act(async () => {
findButton(container, "创建 CSE").click();
await flushEffects();
+49
View File
@@ -15,6 +15,7 @@ import {
getClientCreditsDashboard,
getClientProviderOffer,
getClientReferralDashboard,
listPublicOAuthProviders,
listClientPaymentConfigs,
listClientPlans,
listClientProviderOfferModels,
@@ -154,6 +155,54 @@ describe("oemCloudControlPlane desktop auth", () => {
});
});
it("应读取公开 OAuth Provider 目录供启动登录判断使用", async () => {
const fetchMock = vi.fn(async () => ({
ok: true,
status: 200,
json: async () => ({
code: 200,
message: "success",
data: {
items: [
{
provider: "google",
displayName: "Google",
authorizeUrl: "https://user.limeai.run/oauth/google",
redirectUri: "https://user.limeai.run/oauth/callback",
scopes: ["openid", "email"],
enabled: true,
loginHint: "使用 Google 登录",
},
],
},
}),
}));
vi.stubGlobal("fetch", fetchMock);
const providers = await listPublicOAuthProviders("tenant-0001");
expect(fetchMock).toHaveBeenCalledWith(
"https://user.limeai.run/api/v1/public/tenants/tenant-0001/oauth/providers",
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({
Accept: "application/json",
}),
}),
);
expect(providers).toEqual([
{
provider: "google",
displayName: "Google",
authorizeUrl: "https://user.limeai.run/oauth/google",
redirectUri: "https://user.limeai.run/oauth/callback",
scopes: ["openid", "email"],
enabled: true,
loginHint: "使用 Google 登录",
},
]);
});
it("应解析 bootstrap 中缓存的邀请开关与分享事实源", async () => {
window.__LIME_SESSION_TOKEN__ = "session-token-001";
+44
View File
@@ -47,6 +47,16 @@ export interface OemCloudUserSession {
expiresAt: string;
}
export interface OemCloudPublicOAuthProvider {
provider: string;
displayName: string;
authorizeUrl?: string;
redirectUri?: string;
scopes: string[];
enabled: boolean;
loginHint?: string;
}
export interface OemCloudCurrentSession extends Omit<
OemCloudCurrentSessionLike,
"tenant" | "user" | "session"
@@ -1096,6 +1106,28 @@ function parseCurrentSession(value: unknown): OemCloudCurrentSession {
};
}
function parsePublicOAuthProvider(value: unknown): OemCloudPublicOAuthProvider {
if (!isRecord(value)) {
throw new OemCloudControlPlaneError("OAuth Provider 格式非法");
}
const provider = normalizeText(value.provider);
const displayName = normalizeText(value.displayName) ?? provider;
if (!provider || !displayName) {
throw new OemCloudControlPlaneError("OAuth Provider 格式非法");
}
return {
provider,
displayName,
authorizeUrl: normalizeText(value.authorizeUrl) ?? undefined,
redirectUri: normalizeText(value.redirectUri) ?? undefined,
scopes: normalizeStringArray(value.scopes),
enabled: normalizeBoolean(value.enabled, true),
loginHint: normalizeText(value.loginHint) ?? undefined,
};
}
function parseProviderOfferSummary(
value: unknown,
): OemCloudProviderOfferSummary {
@@ -2402,6 +2434,18 @@ export async function pollClientDesktopAuthSession(
);
}
export async function listPublicOAuthProviders(
tenantId: string,
): Promise<OemCloudPublicOAuthProvider[]> {
const response = await requestControlPlane<{ items?: unknown[] }>(
`/v1/public/tenants/${encodeURIComponent(tenantId)}/oauth/providers`,
);
return Array.isArray(response.items)
? response.items.map(parsePublicOAuthProvider)
: [];
}
export async function verifyClientAuthEmailCode(
tenantId: string,
payload: VerifyClientAuthEmailCodePayload,
+123
View File
@@ -0,0 +1,123 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { setStoredOemCloudSessionState } from "@/lib/oemCloudSession";
import { startOemCloudStartupLoginIfRequired } from "@/lib/oemCloudStartupLogin";
const { mockListPublicOAuthProviders, mockStartOemCloudLogin } = vi.hoisted(
() => ({
mockListPublicOAuthProviders: vi.fn(),
mockStartOemCloudLogin: vi.fn(),
}),
);
vi.mock("@/lib/api/oemCloudControlPlane", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/api/oemCloudControlPlane")>();
return {
...actual,
listPublicOAuthProviders: mockListPublicOAuthProviders,
};
});
vi.mock("@/lib/oemCloudLoginLauncher", () => ({
startOemCloudLogin: mockStartOemCloudLogin,
}));
function configureRuntime() {
window.__LIME_OEM_CLOUD__ = {
enabled: true,
baseUrl: "https://user.limeai.run",
tenantId: "tenant-0001",
desktopClientId: "desktop-client",
desktopOauthRedirectUrl: "lime://oauth/callback",
};
}
describe("oemCloudStartupLogin", () => {
beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
delete window.__LIME_BOOTSTRAP__;
delete window.__LIME_OEM_CLOUD__;
delete window.__LIME_SESSION_TOKEN__;
mockListPublicOAuthProviders.mockResolvedValue([
{
provider: "google",
displayName: "Google",
enabled: true,
scopes: ["openid", "email"],
},
]);
mockStartOemCloudLogin.mockResolvedValue({
mode: "desktop_auth",
openedUrl: "https://user.limeai.run/oauth/desktop/authorize",
});
});
afterEach(() => {
localStorage.clear();
sessionStorage.clear();
delete window.__LIME_BOOTSTRAP__;
delete window.__LIME_OEM_CLOUD__;
delete window.__LIME_SESSION_TOKEN__;
vi.restoreAllMocks();
});
it("启动时发现品牌云端配置了 Google 登录且本地无会话,应发起登录", async () => {
configureRuntime();
const result = await startOemCloudStartupLoginIfRequired();
expect(result.status).toBe("started");
expect(mockListPublicOAuthProviders).toHaveBeenCalledWith("tenant-0001");
expect(mockStartOemCloudLogin).toHaveBeenCalledWith(
expect.objectContaining({
baseUrl: "https://user.limeai.run",
tenantId: "tenant-0001",
}),
);
});
it("同一窗口已尝试启动登录后不应重复打开浏览器", async () => {
configureRuntime();
await startOemCloudStartupLoginIfRequired();
const result = await startOemCloudStartupLoginIfRequired();
expect(result.status).toBe("already_attempted");
expect(mockStartOemCloudLogin).toHaveBeenCalledTimes(1);
});
it("已有当前租户会话时不应启动登录", async () => {
configureRuntime();
setStoredOemCloudSessionState({
token: "session-token",
tenant: { id: "tenant-0001" },
user: { id: "user-001" },
session: { id: "session-001", provider: "google" },
});
const result = await startOemCloudStartupLoginIfRequired();
expect(result.status).toBe("has_session");
expect(mockListPublicOAuthProviders).not.toHaveBeenCalled();
expect(mockStartOemCloudLogin).not.toHaveBeenCalled();
});
it("后端未下发 Google Provider 时保持开源默认使用", async () => {
configureRuntime();
mockListPublicOAuthProviders.mockResolvedValue([
{
provider: "github",
displayName: "GitHub",
enabled: true,
scopes: [],
},
]);
const result = await startOemCloudStartupLoginIfRequired();
expect(result.status).toBe("no_google_provider");
expect(mockStartOemCloudLogin).not.toHaveBeenCalled();
});
});
+103
View File
@@ -0,0 +1,103 @@
import {
listPublicOAuthProviders,
type OemCloudPublicOAuthProvider,
} from "@/lib/api/oemCloudControlPlane";
import {
resolveOemCloudRuntimeContext,
type OemCloudRuntimeContext,
} from "@/lib/api/oemCloudRuntime";
import { getStoredOemCloudSessionState } from "@/lib/oemCloudSession";
import { startOemCloudLogin } from "@/lib/oemCloudLoginLauncher";
const STARTUP_LOGIN_ATTEMPT_PREFIX = "lime:oem-cloud-startup-login:v1";
export type OemCloudStartupLoginStatus =
| "not_configured"
| "has_session"
| "already_attempted"
| "no_google_provider"
| "started"
| "failed";
export interface OemCloudStartupLoginResult {
status: OemCloudStartupLoginStatus;
reason?: string;
}
function normalizeProvider(value: string | undefined): string {
return value?.trim().toLowerCase() ?? "";
}
function hasGoogleOAuthProvider(
providers: OemCloudPublicOAuthProvider[],
): boolean {
return providers.some(
(provider) =>
provider.enabled !== false &&
normalizeProvider(provider.provider) === "google",
);
}
function hasCurrentTenantSession(runtime: OemCloudRuntimeContext): boolean {
const storedSession = getStoredOemCloudSessionState();
return Boolean(
storedSession?.token &&
storedSession.session.tenant.id === runtime.tenantId,
);
}
function getStartupLoginAttemptKey(runtime: OemCloudRuntimeContext): string {
return `${STARTUP_LOGIN_ATTEMPT_PREFIX}:${runtime.tenantId}:${runtime.baseUrl}`;
}
function readStartupAttempt(runtime: OemCloudRuntimeContext): boolean {
if (typeof window === "undefined" || !window.sessionStorage) {
return false;
}
return (
window.sessionStorage.getItem(getStartupLoginAttemptKey(runtime)) === "1"
);
}
function markStartupAttempt(runtime: OemCloudRuntimeContext): void {
if (typeof window === "undefined" || !window.sessionStorage) {
return;
}
window.sessionStorage.setItem(getStartupLoginAttemptKey(runtime), "1");
}
export async function startOemCloudStartupLoginIfRequired(
runtime = resolveOemCloudRuntimeContext(),
): Promise<OemCloudStartupLoginResult> {
if (!runtime) {
return { status: "not_configured" };
}
if (hasCurrentTenantSession(runtime)) {
return { status: "has_session" };
}
if (readStartupAttempt(runtime)) {
return { status: "already_attempted" };
}
const providers = await listPublicOAuthProviders(runtime.tenantId);
if (!hasGoogleOAuthProvider(providers)) {
return { status: "no_google_provider" };
}
markStartupAttempt(runtime);
try {
await startOemCloudLogin(runtime);
return { status: "started" };
} catch (error) {
const reason =
error instanceof Error && error.message.trim()
? error.message.trim()
: "启动云端登录失败";
console.warn("启动期云端登录失败:", error);
return { status: "failed", reason };
}
}