diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74f260c4f..d2cea7e1f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,7 +76,11 @@ jobs: fi if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then - echo "Release $TAG already exists" + gh release edit "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --title "Lime $TAG" \ + --notes-file "$NOTES_FILE" + echo "Release $TAG already exists; notes refreshed" exit 0 fi @@ -115,7 +119,7 @@ jobs: runs-on: ${{ matrix.platform }} env: - LIME_UPDATER_PUBLIC_KEY: ${{ secrets.LIME_UPDATER_PUBLIC_KEY }} + LIME_UPDATER_PUBLIC_KEY_RAW: ${{ secrets.LIME_UPDATER_PUBLIC_KEY }} LIME_UPDATES_BASE_URL: ${{ secrets.LIME_UPDATES_BASE_URL || vars.LIME_UPDATES_BASE_URL }} TAURI_SIGNING_PRIVATE_KEY_RAW: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} @@ -161,7 +165,7 @@ jobs: id: updater_mode shell: bash run: | - if [ -n "${LIME_UPDATER_PUBLIC_KEY}" ] && [ -n "${TAURI_SIGNING_PRIVATE_KEY_RAW}" ] && [ -n "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD}" ]; then + if [ -n "${LIME_UPDATER_PUBLIC_KEY_RAW}" ] && [ -n "${TAURI_SIGNING_PRIVATE_KEY_RAW}" ] && [ -n "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD}" ]; then echo "enabled=true" >> "$GITHUB_OUTPUT" echo "LIME_ENABLE_UPDATER_ARTIFACTS=true" >> "$GITHUB_ENV" echo "Updater artifacts enabled" @@ -172,6 +176,83 @@ jobs: exit 1 fi + - name: Normalize updater public key + if: steps.updater_mode.outputs.enabled == 'true' + shell: bash + run: | + python - <<'PY' + import base64 + import binascii + import os + + raw = os.environ.get("LIME_UPDATER_PUBLIC_KEY_RAW", "") + if not raw.strip(): + raise SystemExit("LIME_UPDATER_PUBLIC_KEY secret is empty") + + candidates = [] + + def add(value: str) -> None: + normalized = value.replace("\r\n", "\n") + if normalized and normalized not in candidates: + candidates.append(normalized) + + add(raw) + if "\\n" in raw and "\n" not in raw: + add(raw.replace("\\r\\n", "\n").replace("\\n", "\n")) + + def to_tauri_pubkey(value: str) -> str: + text = value.strip() + compact = "".join(text.split()) + + # Tauri signer public key file content is already base64 encoded. + try: + decoded = base64.b64decode(compact, validate=True).decode("utf-8") + except (binascii.Error, UnicodeDecodeError, ValueError): + decoded = "" + if decoded: + decoded_lines = [line.strip() for line in decoded.splitlines() if line.strip()] + if len(decoded_lines) >= 2: + return compact + + # Also accept the decoded minisign public key file content. + lines = [line.strip() for line in text.splitlines() if line.strip()] + if len(lines) >= 2: + normalized_text = "\n".join(lines[:2]) + "\n" + return base64.b64encode(normalized_text.encode("utf-8")).decode("ascii") + + # Some setups paste only the raw minisign public key line. + if len(compact) >= 40: + try: + key_bytes = base64.b64decode(compact, validate=True) + except (binascii.Error, ValueError): + key_bytes = b"" + if len(key_bytes) == 42: + normalized_text = f"untrusted comment: minisign public key\n{compact}\n" + return base64.b64encode(normalized_text.encode("utf-8")).decode("ascii") + + return "" + + normalized_key = next( + ( + key + for candidate in candidates + for key in [to_tauri_pubkey(candidate)] + if key + ), + "", + ) + if not normalized_key: + raise SystemExit( + "Unable to normalize LIME_UPDATER_PUBLIC_KEY into Tauri updater pubkey content. " + "Expected the public key value generated by `npx tauri signer generate --write-keys`." + ) + + with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as env_file: + env_file.write(f"LIME_UPDATER_PUBLIC_KEY={normalized_key}\n") + + print("Normalized updater public key verified for Tauri config and runtime") + PY + - name: Sync version from tag to tauri configs shell: bash run: | @@ -181,11 +262,21 @@ jobs: cd src-tauri node -e " const fs = require('fs'); + const updaterPublicKey = process.env.LIME_UPDATER_PUBLIC_KEY?.trim(); + const createUpdaterArtifacts = process.env.LIME_ENABLE_UPDATER_ARTIFACTS === 'true'; + if (createUpdaterArtifacts && !updaterPublicKey) { + throw new Error('LIME_UPDATER_PUBLIC_KEY is required when updater artifacts are enabled'); + } for (const file of ['tauri.conf.json', 'tauri.conf.headless.json']) { const conf = JSON.parse(fs.readFileSync(file, 'utf8')); conf.version = '$VERSION'; conf.bundle = conf.bundle || {}; - conf.bundle.createUpdaterArtifacts = process.env.LIME_ENABLE_UPDATER_ARTIFACTS === 'true'; + conf.bundle.createUpdaterArtifacts = createUpdaterArtifacts; + if (createUpdaterArtifacts) { + conf.plugins = conf.plugins || {}; + conf.plugins.updater = conf.plugins.updater || {}; + conf.plugins.updater.pubkey = updaterPublicKey; + } fs.writeFileSync(file, JSON.stringify(conf, null, 2) + '\n'); } " diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 522c76254..f6ff56335 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -89,8 +89,10 @@ - `cargo test --manifest-path "src-tauri/Cargo.toml"` — 1109 passed / 0 failed / 2 ignored - `cargo clippy --manifest-path "src-tauri/Cargo.toml" --all-targets --all-features` - `npm run lint` - - `npm test` — 43 个 Vitest smart 批次通过 + - `npm test` — 44 个 Vitest smart 批次通过 - `npm run test:contracts` + - `npm run smoke:agent-runtime-tool-surface` + - `npm run smoke:agent-runtime-tool-surface-page` - `git diff --check` - `cargo test` 通过,当前存在 1 条预存 warning: - `write_auxiliary_runtime_projection_fixture` 的 `dead_code` @@ -99,9 +101,7 @@ - `crates/skills/src/lime_llm_provider.rs` 的 `too_many_arguments` - `crates/agent/src/session_execution_runtime.rs` 的 `needless_lifetimes` - `src/services/runtime_evidence_pack_service.rs` 的 `dead_code` -- GUI 主路径校验未完全通过: - - `npm run verify:gui-smoke` 已通过 `bridge:health`、`smoke:workspace-ready`、`smoke:browser-runtime`、`smoke:site-adapters`、`smoke:agent-service-skill-entry`、`smoke:agent-runtime-tool-surface`。 - - `smoke:agent-runtime-tool-surface-page` 在等待 Harness 按钮出现时超时;单独复测时托管 Chrome 会话在提交后丢失,需继续定位真实页面发送后的会话保持 / Harness 入口暴露问题。 +- GUI 主路径补充复测已通过:`smoke:agent-runtime-tool-surface` 与 `smoke:agent-runtime-tool-surface-page` 均确认 Harness 入口在执行态可见,修复此前等待 Harness 按钮超时的问题。 --- diff --git a/src/lib/api/oemCloudControlPlane.test.ts b/src/lib/api/oemCloudControlPlane.test.ts index 23926b042..e4391f580 100644 --- a/src/lib/api/oemCloudControlPlane.test.ts +++ b/src/lib/api/oemCloudControlPlane.test.ts @@ -16,7 +16,6 @@ import { getPublicAuthCatalog, getClientProviderOffer, getClientReferralDashboard, - listPublicOAuthProviders, listClientPaymentConfigs, listClientPlans, listClientProviderOfferModels, @@ -156,59 +155,6 @@ 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 登录", - }, - ], - authPolicy: { - required: true, - startupTrigger: "oauth", - primaryProvider: "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("应读取公开登录目录与启动策略", async () => { const fetchMock = vi.fn(async () => ({ ok: true, @@ -238,7 +184,7 @@ describe("oemCloudControlPlane desktop auth", () => { const catalog = await getPublicAuthCatalog("tenant-0001"); expect(fetchMock).toHaveBeenCalledWith( - "https://user.limeai.run/api/v1/public/tenants/tenant-0001/oauth/providers", + "https://user.limeai.run/api/v1/public/tenants/tenant-0001/client/auth-catalog", expect.objectContaining({ method: "GET", headers: expect.objectContaining({ diff --git a/src/lib/api/oemCloudControlPlane.ts b/src/lib/api/oemCloudControlPlane.ts index ba7db0c8c..343299845 100644 --- a/src/lib/api/oemCloudControlPlane.ts +++ b/src/lib/api/oemCloudControlPlane.ts @@ -47,7 +47,7 @@ export interface OemCloudUserSession { expiresAt: string; } -export interface OemCloudPublicOAuthProvider { +export interface OemCloudAuthCatalogProvider { provider: string; displayName: string; authorizeUrl?: string; @@ -66,7 +66,7 @@ export interface OemCloudAuthPolicy { } export interface OemCloudPublicAuthCatalog { - providers: OemCloudPublicOAuthProvider[]; + providers: OemCloudAuthCatalogProvider[]; authPolicy: OemCloudAuthPolicy; } @@ -1120,15 +1120,15 @@ function parseCurrentSession(value: unknown): OemCloudCurrentSession { }; } -function parsePublicOAuthProvider(value: unknown): OemCloudPublicOAuthProvider { +function parseAuthCatalogProvider(value: unknown): OemCloudAuthCatalogProvider { if (!isRecord(value)) { - throw new OemCloudControlPlaneError("OAuth Provider 格式非法"); + throw new OemCloudControlPlaneError("登录方式格式非法"); } const provider = normalizeText(value.provider); const displayName = normalizeText(value.displayName) ?? provider; if (!provider || !displayName) { - throw new OemCloudControlPlaneError("OAuth Provider 格式非法"); + throw new OemCloudControlPlaneError("登录方式格式非法"); } return { @@ -1157,7 +1157,7 @@ function parsePublicAuthCatalog(value: unknown): OemCloudPublicAuthCatalog { const record = isRecord(value) ? value : {}; return { providers: Array.isArray(record.items) - ? record.items.map(parsePublicOAuthProvider) + ? record.items.map(parseAuthCatalogProvider) : [], authPolicy: parseAuthPolicy(record.authPolicy), }; @@ -2470,18 +2470,12 @@ export async function pollClientDesktopAuthSession( ); } -export async function listPublicOAuthProviders( - tenantId: string, -): Promise { - return (await getPublicAuthCatalog(tenantId)).providers; -} - export async function getPublicAuthCatalog( tenantId: string, ): Promise { return parsePublicAuthCatalog( await requestControlPlane( - `/v1/public/tenants/${encodeURIComponent(tenantId)}/oauth/providers`, + `/v1/public/tenants/${encodeURIComponent(tenantId)}/client/auth-catalog`, ), ); } diff --git a/src/lib/governance/legacySurfaceCatalog.json b/src/lib/governance/legacySurfaceCatalog.json index f0d27d388..8dea27777 100644 --- a/src/lib/governance/legacySurfaceCatalog.json +++ b/src/lib/governance/legacySurfaceCatalog.json @@ -3054,6 +3054,17 @@ "listPosterMaterialsByMood(", "usePosterMaterial(" ] + }, + { + "id": "oem-cloud-public-oauth-provider-list-surface", + "classification": "dead-candidate", + "description": "云端启动登录不允许恢复旧公开 OAuth Provider 列表入口,客户端只能通过 auth catalog/authPolicy 判断登录策略", + "patterns": [ + "listPublicOAuthProviders(", + "OemCloudPublicOAuthProvider", + "/oauth/providers" + ], + "allowedPaths": [] } ], "rustText": [ diff --git a/src/lib/oemCloudStartupLogin.ts b/src/lib/oemCloudStartupLogin.ts index c2382f500..12d02e469 100644 --- a/src/lib/oemCloudStartupLogin.ts +++ b/src/lib/oemCloudStartupLogin.ts @@ -1,6 +1,6 @@ import { getPublicAuthCatalog, - type OemCloudPublicOAuthProvider, + type OemCloudAuthCatalogProvider, } from "@/lib/api/oemCloudControlPlane"; import { resolveOemCloudRuntimeContext, @@ -30,8 +30,8 @@ function normalizeProvider(value: string | undefined): string { return value?.trim().toLowerCase() ?? ""; } -function hasGoogleOAuthProvider( - providers: OemCloudPublicOAuthProvider[], +function hasGoogleAuthProvider( + providers: OemCloudAuthCatalogProvider[], ): boolean { return providers.some( (provider) => @@ -54,7 +54,7 @@ function shouldStartGoogleOauth( return { status: "unsupported_policy" }; } - if (!hasGoogleOAuthProvider(catalog.providers)) { + if (!hasGoogleAuthProvider(catalog.providers)) { return { status: "no_google_provider" }; }