fix: restore agent app studio cloud upload login

This commit is contained in:
coso
2026-05-21 02:56:49 +08:00
parent d960630bb4
commit 00df2cfd59
23 changed files with 886 additions and 62 deletions
+2
View File
@@ -255,6 +255,8 @@ await lime.agent.startTask({
2026-05-16 10:08 补充:`contentFactorySdkRegression.test.ts` 已改为只从 `src/features/agent-app/sdk/index.ts` 导入 SDK 类型和 helper,证明 Lime-side 内容工厂回归消费的是 SDK-only public surface,而不是 `capabilityContract.ts` / `capabilityAdapters.ts` 等内部深文件。该调整仍不代表外部 `content-factory-app` package 已迁移。
2026-05-21 补充:Agent App Studio 云端发布链路已收敛到宿主开放平台基础能力,不把发布业务代理进宿主。Studio 通过 `lime.cloudSession.getAccessToken` 显式获取宿主当前 session token,未登录或过期时通过 `lime.cloudSession.requestLogin` 拉起宿主登录。Lime GUI / DevBridge 模式下登录不再使用 `lime://oauth/callback` 作为浏览器回跳,而是在宿主侧启动 `127.0.0.1:<port>/oauth/callback` 一次性本机回调桥;Rust 命令 `start_oem_cloud_oauth_callback_bridge` 收到回调后 emit `oem-cloud-oauth-callback`,前端完成 session 写入并继续 Studio 上传。该命令必须作为 bridge truth 走真实 DevBridge / Rust,不允许在浏览器模式优先回退 mock,否则会出现“返回了 localhost 地址但没有真实监听服务”的假成功。2026-05-21 实测 `content-factory-app` 本地目录 `/Users/coso/Documents/dev/ai/limecloud/agentapp/docs/examples/content-factory-app` 可直接识别并一键上传,发布为开发构建 `0.8.0+studio.20260520182133563`,Release `agent-app-release-2368`。
迁移规则:
1. App package 业务文件不能直接构造 `lime.agentApp.bridge` message。
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "lime",
"version": "1.45.0",
"version": "1.45.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lime",
"version": "1.45.0",
"version": "1.45.1",
"license": "GPL-3.0-only",
"workspaces": [
"packages/*"
@@ -13915,7 +13915,7 @@
},
"packages/lime-cli-npm": {
"name": "@limecloud/lime-cli",
"version": "1.45.0",
"version": "1.45.1",
"cpu": [
"x64",
"arm64"
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "lime",
"private": true,
"workspaces": ["packages/*"],
"version": "1.45.0",
"version": "1.45.1",
"description": "AI content workspace for Chinese creators: desktop writing, research, prompt management, knowledge base, and multi-model workflows.",
"keywords": [
"ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@limecloud/lime-cli",
"version": "1.45.0",
"version": "1.45.1",
"description": "Lime 官方任务 CLI",
"bin": {
"lime": "scripts/run.js"
+18 -18
View File
@@ -5131,7 +5131,7 @@ dependencies = [
[[package]]
name = "lime"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"anyhow",
"arboard",
@@ -5238,7 +5238,7 @@ dependencies = [
[[package]]
name = "lime-agent"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"anyhow",
"aster-core",
@@ -5267,7 +5267,7 @@ dependencies = [
[[package]]
name = "lime-browser-runtime"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"chrono",
"futures",
@@ -5284,7 +5284,7 @@ dependencies = [
[[package]]
name = "lime-cli"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"clap",
"lime-core",
@@ -5296,7 +5296,7 @@ dependencies = [
[[package]]
name = "lime-config"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"async-trait",
"lime-core",
@@ -5312,7 +5312,7 @@ dependencies = [
[[package]]
name = "lime-core"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"aster-models",
"async-trait",
@@ -5366,7 +5366,7 @@ dependencies = [
[[package]]
name = "lime-gateway"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"aes",
"axum 0.7.9",
@@ -5396,7 +5396,7 @@ dependencies = [
[[package]]
name = "lime-infra"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"chrono",
"dashmap 5.5.3",
@@ -5416,7 +5416,7 @@ dependencies = [
[[package]]
name = "lime-knowledge"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"chrono",
"hex",
@@ -5429,7 +5429,7 @@ dependencies = [
[[package]]
name = "lime-mcp"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"async-trait",
"dirs 5.0.1",
@@ -5445,7 +5445,7 @@ dependencies = [
[[package]]
name = "lime-media-runtime"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -5478,7 +5478,7 @@ dependencies = [
[[package]]
name = "lime-processor"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"async-trait",
"lime-core",
@@ -5497,7 +5497,7 @@ dependencies = [
[[package]]
name = "lime-providers"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"anyhow",
"async-stream",
@@ -5552,7 +5552,7 @@ dependencies = [
[[package]]
name = "lime-server"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"aster-core",
"async-stream",
@@ -5596,7 +5596,7 @@ dependencies = [
[[package]]
name = "lime-server-utils"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"axum 0.7.9",
"futures",
@@ -5611,7 +5611,7 @@ dependencies = [
[[package]]
name = "lime-services"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"anyhow",
"aster-core",
@@ -5656,7 +5656,7 @@ dependencies = [
[[package]]
name = "lime-skills"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"async-trait",
"dirs 5.0.1",
@@ -5674,7 +5674,7 @@ dependencies = [
[[package]]
name = "lime-websocket"
version = "1.45.0"
version = "1.45.1"
dependencies = [
"axum 0.7.9",
"chrono",
+2 -2
View File
@@ -9,7 +9,7 @@ exclude = [
resolver = "2"
[workspace.package]
version = "1.45.0"
version = "1.45.1"
edition = "2021"
authors = ["coso"]
repository = "https://github.com/aiclientproxy/lime"
@@ -199,7 +199,7 @@ version = "2.4.9"
[package]
name = "lime"
version = "1.45.0"
version = "1.45.1"
description = "AI API Proxy Desktop App"
authors = ["you"]
edition = "2021"
+1
View File
@@ -1639,6 +1639,7 @@ pub fn run() {
commands::external_tools_cmd::open_codex_cli_login,
commands::external_tools_cmd::open_codex_cli_logout,
commands::external_tools_cmd::open_external_url,
commands::external_tools_cmd::start_oem_cloud_oauth_callback_bridge,
commands::external_tools_cmd::get_external_tools,
// Voice Input commands
crate::voice::commands::get_voice_input_config,
@@ -3,9 +3,20 @@
//! 管理 Codex CLI 等外部工具的状态检查和配置
//! 这些工具有自己的认证系统,不通过 Lime 凭证池管理
use axum::extract::{Query, State};
use axum::http::{StatusCode, Uri};
use axum::response::{Html, IntoResponse};
use axum::routing::get;
use axum::Router;
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr};
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use tauri::{AppHandle, Emitter};
use tokio::net::TcpListener;
use tokio::process::Command;
use tokio::sync::{oneshot, Mutex};
/// Codex CLI 状态
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -24,6 +35,70 @@ pub struct CodexCliStatus {
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OemCloudOAuthCallbackBridgeStartResponse {
pub callback_url: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OemCloudOAuthCallbackBridgePayload {
pub source_path: String,
pub tenant_id: Option<String>,
pub token: Option<String>,
pub next: Option<String>,
pub error: Option<String>,
pub device_code: Option<String>,
pub status: Option<String>,
}
const OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT: &str = "oem-cloud-oauth-callback";
const OEM_CLOUD_OAUTH_CALLBACK_PATH: &str = "/oauth/callback";
const OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_TTL: Duration = Duration::from_secs(10 * 60);
const OEM_CLOUD_OAUTH_CALLBACK_HTML: &str = r#"<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lime 登录回调</title>
<script>
(function () {
if (window.location.hash && window.location.hash.length > 1) {
var params = new URLSearchParams(window.location.hash.slice(1));
var search = new URLSearchParams(window.location.search);
params.forEach(function (value, key) {
if (!search.has(key)) search.set(key, value);
});
window.location.replace(window.location.pathname + "?" + search.toString());
}
})();
</script>
</head>
<body>
<p>Lime 登录结果已返回,可以关闭此页面。</p>
</body>
</html>"#;
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct OemCloudOAuthCallbackBridgeQuery {
#[serde(alias = "tenant_id")]
tenant_id: Option<String>,
token: Option<String>,
next: Option<String>,
error: Option<String>,
#[serde(alias = "device_code")]
device_code: Option<String>,
status: Option<String>,
}
#[derive(Clone)]
struct OemCloudOAuthCallbackBridgeState {
app: AppHandle,
shutdown_tx: Arc<Mutex<Option<oneshot::Sender<()>>>>,
}
/// 检查 Lime CLI 状态
#[tauri::command]
pub async fn check_codex_cli_status() -> Result<CodexCliStatus, String> {
@@ -140,6 +215,114 @@ pub async fn open_external_url(url: String) -> Result<(), String> {
Ok(())
}
fn normalize_callback_bridge_base_url(addr: SocketAddr) -> Result<String, String> {
if !matches!(addr.ip(), IpAddr::V4(ip) if ip.is_loopback()) {
return Err("OAuth 本地回调桥必须绑定到 IPv4 loopback 地址".to_string());
}
Ok(format!(
"http://127.0.0.1:{}{}",
addr.port(),
OEM_CLOUD_OAUTH_CALLBACK_PATH
))
}
fn normalize_callback_bridge_value(value: Option<String>) -> Option<String> {
value.and_then(|value| {
let normalized = value.trim();
if normalized.is_empty() {
None
} else {
Some(normalized.to_string())
}
})
}
fn build_oem_cloud_oauth_callback_payload(
uri: &Uri,
query: OemCloudOAuthCallbackBridgeQuery,
) -> OemCloudOAuthCallbackBridgePayload {
OemCloudOAuthCallbackBridgePayload {
source_path: uri.path().to_string(),
tenant_id: normalize_callback_bridge_value(query.tenant_id),
token: normalize_callback_bridge_value(query.token),
next: normalize_callback_bridge_value(query.next),
error: normalize_callback_bridge_value(query.error),
device_code: normalize_callback_bridge_value(query.device_code),
status: normalize_callback_bridge_value(query.status),
}
}
fn should_emit_oem_cloud_oauth_callback(payload: &OemCloudOAuthCallbackBridgePayload) -> bool {
payload.tenant_id.is_some()
|| payload.token.is_some()
|| payload.error.is_some()
|| payload.device_code.is_some()
|| payload.status.is_some()
}
async fn handle_oem_cloud_oauth_callback_bridge_request(
State(state): State<OemCloudOAuthCallbackBridgeState>,
Query(query): Query<OemCloudOAuthCallbackBridgeQuery>,
uri: Uri,
) -> impl IntoResponse {
let payload = build_oem_cloud_oauth_callback_payload(&uri, query);
if should_emit_oem_cloud_oauth_callback(&payload) {
if let Err(error) = state
.app
.emit(OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT, payload)
{
tracing::warn!("[OAuthCallbackBridge] 发送 OAuth 回调事件失败: {}", error);
}
if let Some(shutdown_tx) = state.shutdown_tx.lock().await.take() {
let _ = shutdown_tx.send(());
}
return (StatusCode::OK, Html(OEM_CLOUD_OAUTH_CALLBACK_HTML)).into_response();
}
(StatusCode::OK, Html(OEM_CLOUD_OAUTH_CALLBACK_HTML)).into_response()
}
/// 启动一次性 OEM Cloud OAuth 本机回调桥。
#[tauri::command]
pub async fn start_oem_cloud_oauth_callback_bridge(
app: AppHandle,
) -> Result<OemCloudOAuthCallbackBridgeStartResponse, String> {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.map_err(|error| format!("无法启动 OAuth 本地回调桥: {error}"))?;
let addr = listener
.local_addr()
.map_err(|error| format!("无法读取 OAuth 本地回调地址: {error}"))?;
let callback_url = normalize_callback_bridge_base_url(addr)?;
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
let state = OemCloudOAuthCallbackBridgeState {
app,
shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))),
};
let router = Router::new()
.route(
OEM_CLOUD_OAUTH_CALLBACK_PATH,
get(handle_oem_cloud_oauth_callback_bridge_request),
)
.with_state(state);
tauri::async_runtime::spawn(async move {
let server = axum::serve(listener, router).with_graceful_shutdown(async move {
tokio::select! {
_ = shutdown_rx => {}
_ = tokio::time::sleep(OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_TTL) => {}
}
});
if let Err(error) = server.await {
tracing::warn!("[OAuthCallbackBridge] OAuth 本地回调桥运行失败: {}", error);
}
});
Ok(OemCloudOAuthCallbackBridgeStartResponse { callback_url })
}
/// 外部工具列表
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalTool {
+1 -1
View File
@@ -117,7 +117,7 @@ pub async fn handle_command(
return Ok(result);
}
if let Some(result) = external_tools::try_handle(cmd, args.as_ref()).await? {
if let Some(result) = external_tools::try_handle(state, cmd, args.as_ref()).await? {
return Ok(result);
}
@@ -1,7 +1,9 @@
use super::{args_or_default, get_string_arg};
use super::{args_or_default, get_string_arg, require_app_handle};
use crate::dev_bridge::DevBridgeState;
use serde_json::Value as JsonValue;
pub(super) async fn try_handle(
state: &DevBridgeState,
cmd: &str,
args: Option<&JsonValue>,
) -> Result<Option<JsonValue>, Box<dyn std::error::Error>> {
@@ -15,6 +17,16 @@ pub(super) async fn try_handle(
.map_err(|error| format!("打开外部链接失败: {error}"))?,
)?
}
"start_oem_cloud_oauth_callback_bridge" => {
let app_handle = require_app_handle(state)?;
serde_json::to_value(
crate::commands::external_tools_cmd::start_oem_cloud_oauth_callback_bridge(
app_handle,
)
.await
.map_err(|error| format!("启动 OAuth 本地回调桥失败: {error}"))?,
)?
}
_ => return Ok(None),
};
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lime",
"version": "1.45.0",
"version": "1.45.1",
"identifier": "com.limecloud.lime.headless",
"build": {
"beforeDevCommand": "npm run dev:web-bridge",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lime",
"version": "1.45.0",
"version": "1.45.1",
"identifier": "com.limecloud.lime",
"build": {
"beforeDevCommand": "node scripts/start-tauri-dev-server.mjs",
@@ -1144,6 +1144,125 @@ describe("createAgentAppCapabilityDispatcher", () => {
expect(loginLauncherMocks.startOemCloudLogin).toHaveBeenCalledTimes(1);
});
it("lime.cloudSession.requestLogin 支持强制刷新已有宿主会话", async () => {
const dispatch = buildDispatcherWithCloudSessionCapability();
window.__LIME_OEM_CLOUD__ = {
enabled: true,
baseUrl: "https://user.limeai.run",
tenantId: "tenant-0001",
sessionToken: "stale-host-session-token",
};
loginLauncherMocks.startOemCloudLogin.mockImplementation(async () => {
window.__LIME_SESSION_TOKEN__ = "fresh-host-session-token";
window.__LIME_OEM_CLOUD__ = {
enabled: true,
baseUrl: "https://user.limeai.run",
tenantId: "tenant-0001",
sessionToken: "fresh-host-session-token",
};
return {
mode: "desktop_auth",
openedUrl: "lime://oauth/callback",
};
});
await expect(
dispatch({
appId: "content-factory-app",
entryKey: "dashboard",
capability: "lime.cloudSession",
method: "requestLogin",
input: { force: true },
rawPayload: {
capability: "lime.cloudSession",
method: "requestLogin",
input: { force: true },
},
}),
).resolves.toMatchObject({
tenantId: "tenant-0001",
hasSession: true,
controlPlaneBaseUrl: "https://user.limeai.run/api",
});
expect(loginLauncherMocks.startOemCloudLogin).toHaveBeenCalledTimes(1);
});
it("lime.cloudSession.getAccessToken 不返回已过期的宿主会话令牌", async () => {
const dispatch = buildDispatcherWithCloudSessionCapability();
const expiredToken = [
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
"eyJzdWIiOiJ1c2VyLTAwMDEiLCJ0ZW5hbnRJZCI6InRlbmFudC0wMDAxIiwiZXhwIjoxfQ",
"signature",
].join(".");
window.__LIME_OEM_CLOUD__ = {
enabled: true,
baseUrl: "https://user.limeai.run",
tenantId: "tenant-0001",
sessionToken: expiredToken,
};
await expect(
dispatch({
appId: "content-factory-app",
entryKey: "dashboard",
capability: "lime.cloudSession",
method: "getAccessToken",
rawPayload: {
capability: "lime.cloudSession",
method: "getAccessToken",
},
}),
).rejects.toMatchObject({
code: "SESSION_REQUIRED",
});
});
it("lime.cloudSession.requestLogin 遇到过期宿主会话令牌时会刷新", async () => {
const dispatch = buildDispatcherWithCloudSessionCapability();
const expiredToken = [
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
"eyJzdWIiOiJ1c2VyLTAwMDEiLCJ0ZW5hbnRJZCI6InRlbmFudC0wMDAxIiwiZXhwIjoxfQ",
"signature",
].join(".");
window.__LIME_OEM_CLOUD__ = {
enabled: true,
baseUrl: "https://user.limeai.run",
tenantId: "tenant-0001",
sessionToken: expiredToken,
};
loginLauncherMocks.startOemCloudLogin.mockImplementation(async () => {
window.__LIME_SESSION_TOKEN__ = "fresh-host-session-token";
window.__LIME_OEM_CLOUD__ = {
enabled: true,
baseUrl: "https://user.limeai.run",
tenantId: "tenant-0001",
sessionToken: "fresh-host-session-token",
};
return {
mode: "desktop_auth",
openedUrl: "lime://oauth/callback",
};
});
await expect(
dispatch({
appId: "content-factory-app",
entryKey: "dashboard",
capability: "lime.cloudSession",
method: "requestLogin",
rawPayload: {
capability: "lime.cloudSession",
method: "requestLogin",
},
}),
).resolves.toMatchObject({
tenantId: "tenant-0001",
hasSession: true,
controlPlaneBaseUrl: "https://user.limeai.run/api",
});
expect(loginLauncherMocks.startOemCloudLogin).toHaveBeenCalledTimes(1);
});
it("应通过 lime.models / lime.usage 投影 AgentRuntime 模型与用量事实", async () => {
const dispatch = buildRuntimeProjectionDispatcher();
@@ -569,6 +569,14 @@ function readOptionalInputRecord(
return {};
}
function readBooleanOption(
request: AgentAppHostBridgeCapabilityRequest,
key: string,
): boolean {
const input = readOptionalInputRecord(request);
return input[key] === true;
}
function readStringParam(
request: AgentAppHostBridgeCapabilityRequest,
key: string,
@@ -1154,6 +1162,36 @@ function buildCloudSessionSnapshot() {
};
}
function decodeJwtPayload(token: string): Record<string, unknown> | null {
const payload = token.split(".")[1];
if (!payload) {
return null;
}
try {
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(
base64.length + ((4 - (base64.length % 4)) % 4),
"=",
);
const decoded = atob(padded);
const parsed = JSON.parse(decoded);
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
}
function isExpiredJwt(token: string, nowMs = Date.now()): boolean {
const payload = decodeJwtPayload(token);
const exp = payload?.exp;
if (typeof exp !== "number") {
return false;
}
return exp * 1000 <= nowMs + 60_000;
}
async function dispatchCloudSession(
request: AgentAppHostBridgeCapabilityRequest,
): Promise<unknown> {
@@ -1162,7 +1200,11 @@ async function dispatchCloudSession(
}
if (request.method === "getAccessToken") {
const runtime = resolveOemCloudRuntimeContext();
if (!runtime?.sessionToken || !runtime.tenantId) {
if (
!runtime?.sessionToken ||
!runtime.tenantId ||
isExpiredJwt(runtime.sessionToken)
) {
throw new AgentAppCapabilityDispatcherError(
"SESSION_REQUIRED",
"Host cloud session is not available.",
@@ -1182,7 +1224,12 @@ async function dispatchCloudSession(
"Host cloud login is not configured.",
);
}
if (!runtime.sessionToken) {
const force = readBooleanOption(request, "force");
if (
force ||
!runtime.sessionToken ||
isExpiredJwt(runtime.sessionToken)
) {
await startOemCloudLogin(runtime, { waitForCompletion: true });
}
return buildCloudSessionSnapshot();
+39 -5
View File
@@ -31,6 +31,7 @@ const controlPlaneMocks = vi.hoisted(() => ({
const shellOpenMock = vi.hoisted(() => vi.fn());
const systemBrowserMocks = vi.hoisted(() => ({
openExternalUrlWithSystemBrowser: vi.fn(),
startOemCloudOAuthCallbackBridge: vi.fn(),
}));
const tauriRuntimeMocks = vi.hoisted(() => ({
hasTauriInvokeCapability: vi.fn(),
@@ -86,8 +87,11 @@ vi.mock("@tauri-apps/plugin-shell", () => ({
}));
vi.mock("@/lib/api/externalUrl", () => ({
OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT: "oem-cloud-oauth-callback",
openExternalUrlWithSystemBrowser:
systemBrowserMocks.openExternalUrlWithSystemBrowser,
startOemCloudOAuthCallbackBridge:
systemBrowserMocks.startOemCloudOAuthCallbackBridge,
}));
vi.mock("@/lib/tauri-runtime", () => ({
@@ -257,6 +261,10 @@ describe("useOemCloudAccess", () => {
systemBrowserMocks.openExternalUrlWithSystemBrowser.mockResolvedValue(
undefined,
);
systemBrowserMocks.startOemCloudOAuthCallbackBridge.mockReset();
systemBrowserMocks.startOemCloudOAuthCallbackBridge.mockResolvedValue({
callbackUrl: "http://127.0.0.1:18081/oauth/callback",
});
tauriRuntimeMocks.hasTauriInvokeCapability.mockReturnValue(true);
tauriRuntimeMocks.hasTauriRuntimeMarkers.mockReturnValue(true);
});
@@ -651,18 +659,28 @@ describe("useOemCloudAccess", () => {
await latestState?.handleGoogleLogin();
});
expect(
systemBrowserMocks.startOemCloudOAuthCallbackBridge,
).toHaveBeenCalledTimes(1);
expect(
controlPlaneMocks.createClientDesktopAuthSession,
).toHaveBeenCalledWith("tenant-0001", {
clientId: "desktop-client",
provider: "google",
desktopRedirectUri: "lime://oauth/callback",
desktopRedirectUri: "http://127.0.0.1:18081/oauth/callback",
});
expect(
systemBrowserMocks.openExternalUrlWithSystemBrowser,
).toHaveBeenCalledWith(
"https://user.limeai.run/oauth/desktop/device-code-001/signin",
);
expect(
controlPlaneMocks.createClientDesktopAuthSession,
).toHaveBeenCalledWith("tenant-0001", {
clientId: "desktop-client",
provider: "google",
desktopRedirectUri: "http://127.0.0.1:18081/oauth/callback",
});
expect(shellOpenMock).not.toHaveBeenCalled();
expect(controlPlaneMocks.pollClientDesktopAuthSession).toHaveBeenCalledWith(
"device-code-001",
@@ -759,7 +777,7 @@ describe("useOemCloudAccess", () => {
expect(parsedUrl.searchParams.get("tenant")).toBe("tenant-0001");
expect(parsedUrl.searchParams.get("tenantId")).toBe("tenant-0001");
expect(parsedUrl.searchParams.get("redirectUrl")).toBe(
"lime://oauth/callback",
"http://127.0.0.1:18081/oauth/callback",
);
expect(parsedUrl.searchParams.get("redirect")).toBe("/welcome");
expect(latestState?.errorMessage).toBeNull();
@@ -814,13 +832,16 @@ describe("useOemCloudAccess", () => {
}
});
it("Google 桌面登录不再接受 localhost 本地回调配置", async () => {
it("Tauri 桌面登录会用本机回调桥覆盖静态 localhost 回调配置", async () => {
window.__LIME_OEM_CLOUD__ = {
enabled: true,
baseUrl: "https://user.limeai.run",
tenantId: "tenant-0001",
desktopOauthRedirectUrl: "http://localhost:17834/callback",
};
controlPlaneMocks.createClientDesktopAuthSession.mockRejectedValue(
new Error("desktop client not found"),
);
const container = document.createElement("div");
document.body.appendChild(container);
@@ -836,9 +857,22 @@ describe("useOemCloudAccess", () => {
await latestState?.handleGoogleLogin();
});
expect(
systemBrowserMocks.startOemCloudOAuthCallbackBridge,
).toHaveBeenCalledTimes(1);
expect(
controlPlaneMocks.createClientDesktopAuthSession,
).not.toHaveBeenCalled();
expect(latestState?.errorMessage).toContain("localhost 本地回调");
).toHaveBeenCalledWith("tenant-0001", {
clientId: "desktop-client",
provider: "google",
desktopRedirectUri: "http://127.0.0.1:18081/oauth/callback",
});
const openedUrl = systemBrowserMocks.openExternalUrlWithSystemBrowser.mock
.calls[0]?.[0] as string;
const parsedUrl = new URL(openedUrl);
expect(parsedUrl.searchParams.get("redirectUrl")).toBe(
"http://127.0.0.1:18081/oauth/callback",
);
expect(latestState?.errorMessage).toBeNull();
});
});
+18 -1
View File
@@ -1,6 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { safeInvoke } from "@/lib/dev-bridge";
import { openExternalUrlWithSystemBrowser } from "./externalUrl";
import {
openExternalUrlWithSystemBrowser,
startOemCloudOAuthCallbackBridge,
} from "./externalUrl";
vi.mock("@/lib/dev-bridge", () => ({
safeInvoke: vi.fn(),
@@ -22,4 +25,18 @@ describe("externalUrl API", () => {
url: "https://user.limeai.run/login",
});
});
it("应通过 native 命令启动 OEM Cloud OAuth 本机回调桥", async () => {
vi.mocked(safeInvoke).mockResolvedValueOnce({
callbackUrl: "http://127.0.0.1:18081/oauth/callback",
});
await expect(startOemCloudOAuthCallbackBridge()).resolves.toEqual({
callbackUrl: "http://127.0.0.1:18081/oauth/callback",
});
expect(safeInvoke).toHaveBeenCalledWith(
"start_oem_cloud_oauth_callback_bridge",
);
});
});
+23
View File
@@ -1,7 +1,30 @@
import { safeInvoke } from "@/lib/dev-bridge";
export const OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT =
"oem-cloud-oauth-callback";
export interface OemCloudOAuthCallbackBridgeStartResponse {
callbackUrl: string;
}
export interface OemCloudOAuthCallbackBridgePayload {
sourcePath: string;
tenantId?: string | null;
token?: string | null;
next?: string | null;
error?: string | null;
deviceCode?: string | null;
status?: string | null;
}
export async function openExternalUrlWithSystemBrowser(
url: string,
): Promise<void> {
await safeInvoke("open_external_url", { url });
}
export async function startOemCloudOAuthCallbackBridge(): Promise<OemCloudOAuthCallbackBridgeStartResponse> {
return safeInvoke<OemCloudOAuthCallbackBridgeStartResponse>(
"start_oem_cloud_oauth_callback_bridge",
);
}
@@ -32,6 +32,14 @@ describe("mockPriorityCommands", () => {
false,
);
expect(shouldPreferMockInBrowser("agent_app_stop_ui_runtime")).toBe(false);
expect(
shouldPreferMockInBrowser("start_oem_cloud_oauth_callback_bridge"),
).toBe(false);
expect(
shouldDisallowMockFallbackInBrowser(
"start_oem_cloud_oauth_callback_bridge",
),
).toBe(true);
});
it("图层设计工程目录命令在浏览器模式必须走真实桥接", () => {
@@ -59,6 +59,7 @@ const mockPriorityCommands = new Set<string>([
const bridgeTruthCommands = new Set<string>([
"aster_agent_init",
"aster_agent_status",
"start_oem_cloud_oauth_callback_bridge",
"agent_generate_title",
"get_or_create_default_project",
"workspace_list",
+2 -1
View File
@@ -126,7 +126,8 @@
"memory_runtime_prefetch_for_turn"
],
"systemUtilityCommands": [
"open_external_url"
"open_external_url",
"start_oem_cloud_oauth_callback_bridge"
],
"fileBrowserCommands": [
"list_dir",
+189
View File
@@ -14,11 +14,16 @@ const controlPlaneMocks = vi.hoisted(() => ({
}));
const systemBrowserMocks = vi.hoisted(() => ({
openExternalUrlWithSystemBrowser: vi.fn(),
startOemCloudOAuthCallbackBridge: vi.fn(),
}));
const tauriRuntimeMocks = vi.hoisted(() => ({
hasTauriInvokeCapability: vi.fn(),
hasTauriRuntimeMarkers: vi.fn(),
}));
const devBridgeMocks = vi.hoisted(() => ({
isDevBridgeAvailable: vi.fn(),
safeListen: vi.fn(),
}));
vi.mock("@/lib/api/oemCloudControlPlane", async (importOriginal) => {
const actual =
@@ -38,8 +43,11 @@ vi.mock("@tauri-apps/plugin-shell", () => ({
}));
vi.mock("@/lib/api/externalUrl", () => ({
OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT: "oem-cloud-oauth-callback",
openExternalUrlWithSystemBrowser:
systemBrowserMocks.openExternalUrlWithSystemBrowser,
startOemCloudOAuthCallbackBridge:
systemBrowserMocks.startOemCloudOAuthCallbackBridge,
}));
vi.mock("@/lib/tauri-runtime", () => ({
@@ -47,6 +55,15 @@ vi.mock("@/lib/tauri-runtime", () => ({
hasTauriRuntimeMarkers: tauriRuntimeMocks.hasTauriRuntimeMarkers,
}));
vi.mock("@/lib/dev-bridge", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/dev-bridge")>();
return {
...actual,
isDevBridgeAvailable: devBridgeMocks.isDevBridgeAvailable,
safeListen: devBridgeMocks.safeListen,
};
});
function createOpenedWindow() {
return {
closed: false,
@@ -73,16 +90,28 @@ describe("oemCloudLoginLauncher", () => {
systemBrowserMocks.openExternalUrlWithSystemBrowser.mockResolvedValue(
undefined,
);
systemBrowserMocks.startOemCloudOAuthCallbackBridge.mockReset();
systemBrowserMocks.startOemCloudOAuthCallbackBridge.mockResolvedValue({
callbackUrl: "http://127.0.0.1:18081/oauth/callback",
});
controlPlaneMocks.createClientDesktopAuthSession.mockReset();
controlPlaneMocks.pollClientDesktopAuthSession.mockReset();
tauriRuntimeMocks.hasTauriInvokeCapability.mockReset();
tauriRuntimeMocks.hasTauriRuntimeMarkers.mockReset();
tauriRuntimeMocks.hasTauriInvokeCapability.mockReturnValue(false);
tauriRuntimeMocks.hasTauriRuntimeMarkers.mockReturnValue(false);
devBridgeMocks.isDevBridgeAvailable.mockReset();
devBridgeMocks.isDevBridgeAvailable.mockReturnValue(false);
devBridgeMocks.safeListen.mockReset();
devBridgeMocks.safeListen.mockResolvedValue(() => undefined);
});
afterEach(() => {
localStorage.clear();
Object.defineProperty(window, "parent", {
configurable: true,
value: window,
});
vi.restoreAllMocks();
});
@@ -165,6 +194,109 @@ describe("oemCloudLoginLauncher", () => {
expect(windowOpenSpy).toHaveBeenCalledTimes(1);
});
it("Tauri 桌面登录应启动本机回调桥并把 callbackUrl 传给 desktop auth session", async () => {
tauriRuntimeMocks.hasTauriInvokeCapability.mockReturnValue(true);
tauriRuntimeMocks.hasTauriRuntimeMarkers.mockReturnValue(true);
controlPlaneMocks.createClientDesktopAuthSession.mockResolvedValue({
authSessionId: "auth-session-001",
deviceCode: "device-001",
tenantId: "tenant-0001",
clientId: "desktop-client",
clientName: "Lime Desktop",
provider: "google",
desktopRedirectUri: "http://127.0.0.1:18081/oauth/callback",
status: "pending_login",
expiresInSeconds: 600,
pollIntervalSeconds: 1,
authorizeUrl: "https://user.limeai.run/oauth/desktop/device-001/signin",
});
controlPlaneMocks.pollClientDesktopAuthSession.mockReturnValue(
new Promise(() => undefined),
);
await startOemCloudLogin(
{
baseUrl: "https://user.limeai.run",
controlPlaneBaseUrl: "https://user.limeai.run/api",
sceneBaseUrl: "https://user.limeai.run/scene-api",
gatewayBaseUrl: "https://llm.limeai.run",
tenantId: "tenant-0001",
sessionToken: null,
hubProviderName: null,
loginPath: "/login",
desktopClientId: "desktop-client",
desktopOauthRedirectUrl: "lime://oauth/callback",
desktopOauthNextPath: "/welcome",
},
{ waitForCompletion: false },
);
expect(
systemBrowserMocks.startOemCloudOAuthCallbackBridge,
).toHaveBeenCalledTimes(1);
expect(
controlPlaneMocks.createClientDesktopAuthSession,
).toHaveBeenCalledWith("tenant-0001", {
clientId: "desktop-client",
provider: "google",
desktopRedirectUri: "http://127.0.0.1:18081/oauth/callback",
});
});
it("DevBridge 浏览器模式应使用宿主命令打开登录页并启用本机回调桥", async () => {
devBridgeMocks.isDevBridgeAvailable.mockReturnValue(true);
controlPlaneMocks.createClientDesktopAuthSession.mockResolvedValue({
authSessionId: "auth-session-001",
deviceCode: "device-001",
tenantId: "tenant-0001",
clientId: "desktop-client",
clientName: "Lime Desktop",
provider: "google",
desktopRedirectUri: "http://127.0.0.1:18081/oauth/callback",
status: "pending_login",
expiresInSeconds: 600,
pollIntervalSeconds: 1,
authorizeUrl: "https://user.limeai.run/oauth/desktop/device-001/signin",
});
controlPlaneMocks.pollClientDesktopAuthSession.mockReturnValue(
new Promise(() => undefined),
);
await startOemCloudLogin(
{
baseUrl: "https://user.limeai.run",
controlPlaneBaseUrl: "https://user.limeai.run/api",
sceneBaseUrl: "https://user.limeai.run/scene-api",
gatewayBaseUrl: "https://llm.limeai.run",
tenantId: "tenant-0001",
sessionToken: null,
hubProviderName: null,
loginPath: "/login",
desktopClientId: "desktop-client",
desktopOauthRedirectUrl: "lime://oauth/callback",
desktopOauthNextPath: "/welcome",
},
{ waitForCompletion: false },
);
expect(
systemBrowserMocks.startOemCloudOAuthCallbackBridge,
).toHaveBeenCalledTimes(1);
expect(
controlPlaneMocks.createClientDesktopAuthSession,
).toHaveBeenCalledWith("tenant-0001", {
clientId: "desktop-client",
provider: "google",
desktopRedirectUri: "http://127.0.0.1:18081/oauth/callback",
});
expect(
systemBrowserMocks.openExternalUrlWithSystemBrowser,
).toHaveBeenCalledWith(
"https://user.limeai.run/oauth/desktop/device-001/signin",
);
expect(shellOpenMock).not.toHaveBeenCalled();
});
it("浏览器弹窗被拦截时应抛出可感知错误", async () => {
vi.spyOn(window, "open").mockReturnValue(null);
@@ -326,4 +458,61 @@ describe("oemCloudLoginLauncher", () => {
);
await Promise.resolve();
});
it("requestLogin fallback 打开普通登录页后应等待本机回调完成", async () => {
tauriRuntimeMocks.hasTauriInvokeCapability.mockReturnValue(true);
tauriRuntimeMocks.hasTauriRuntimeMarkers.mockReturnValue(true);
controlPlaneMocks.createClientDesktopAuthSession.mockRejectedValue(
new Error("desktop client not found"),
);
const loginPromise = startOemCloudLogin(
{
baseUrl: "https://user.limeai.run",
controlPlaneBaseUrl: "https://user.limeai.run/api",
sceneBaseUrl: "https://user.limeai.run/scene-api",
gatewayBaseUrl: "https://llm.limeai.run",
tenantId: "tenant-0001",
sessionToken: null,
hubProviderName: null,
loginPath: "/login",
desktopClientId: "desktop-client",
desktopOauthRedirectUrl: "lime://oauth/callback",
desktopOauthNextPath: "/welcome",
},
{ waitForCompletion: true },
);
await vi.waitFor(() => {
expect(
systemBrowserMocks.openExternalUrlWithSystemBrowser,
).toHaveBeenCalledTimes(1);
});
const openedUrl = systemBrowserMocks.openExternalUrlWithSystemBrowser.mock
.calls[0]?.[0] as string;
expect(new URL(openedUrl).searchParams.get("redirectUrl")).toBe(
"http://127.0.0.1:18081/oauth/callback",
);
setStoredOemCloudSessionState({
token: "session-token",
tenant: { id: "tenant-0001", slug: "tenant-0001" },
user: { id: "user-001" },
session: { id: "session-001", provider: "google" },
});
window.dispatchEvent(
new CustomEvent("lime:oem-cloud-oauth-completed", {
detail: {
tenantId: "tenant-0001",
nextPath: "/welcome",
provider: "google",
},
}),
);
await expect(loginPromise).resolves.toEqual({
mode: "login_url",
openedUrl,
});
});
});
+208 -24
View File
@@ -6,8 +6,14 @@ import {
createClientDesktopAuthSession,
pollClientDesktopAuthSession,
} from "@/lib/api/oemCloudControlPlane";
import { openExternalUrlWithSystemBrowser } from "@/lib/api/externalUrl";
import {
OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT,
type OemCloudOAuthCallbackBridgePayload,
openExternalUrlWithSystemBrowser,
startOemCloudOAuthCallbackBridge,
} from "@/lib/api/externalUrl";
import {
DEFAULT_OEM_CLOUD_DESKTOP_OAUTH_NEXT_PATH,
resolveOemCloudRuntimeContext,
type OemCloudRuntimeContext,
} from "@/lib/api/oemCloudRuntime";
@@ -21,6 +27,7 @@ import {
hasTauriInvokeCapability,
hasTauriRuntimeMarkers,
} from "@/lib/tauri-runtime";
import { isDevBridgeAvailable, safeListen } from "@/lib/dev-bridge";
const DESKTOP_AUTH_LEGACY_CLIENT_IDS: Record<string, string[]> = {
"limehub-desktop": ["lobehub-desktop"],
@@ -152,9 +159,7 @@ export async function openExternalUrl(
url: string,
options: OpenExternalUrlOptions = {},
): Promise<void> {
const shouldUseTauriShell =
hasTauriInvokeCapability() || hasTauriRuntimeMarkers();
if (shouldUseTauriShell) {
if (hasHostDesktopBridge()) {
let nativeOpenError: unknown = null;
try {
await openExternalUrlWithSystemBrowser(url);
@@ -274,7 +279,7 @@ function isDesktopClientNotFound(error: unknown) {
);
}
function isLoopbackDesktopOauthRedirectUrl(value: string) {
export function isLoopbackDesktopOauthRedirectUrl(value: string) {
try {
const parsed = new URL(value);
if (!["http:", "https:"].includes(parsed.protocol)) {
@@ -287,6 +292,42 @@ function isLoopbackDesktopOauthRedirectUrl(value: string) {
}
}
function shouldUseOAuthCallbackBridge() {
return hasHostDesktopBridge();
}
function hasHostDesktopBridge() {
return (
hasTauriInvokeCapability() ||
hasTauriRuntimeMarkers() ||
isDevBridgeAvailable()
);
}
function withDesktopOauthRedirectUrl(
runtime: OemCloudRuntimeContext,
desktopOauthRedirectUrl: string,
): OemCloudRuntimeContext {
return {
...runtime,
desktopOauthRedirectUrl,
};
}
async function resolveLoginRuntime(runtime: OemCloudRuntimeContext) {
if (!shouldUseOAuthCallbackBridge()) {
return runtime;
}
const { callbackUrl } = await startOemCloudOAuthCallbackBridge();
const normalizedCallbackUrl = callbackUrl?.trim();
if (!normalizedCallbackUrl) {
return runtime;
}
return withDesktopOauthRedirectUrl(runtime, normalizedCallbackUrl);
}
function resolveDesktopClientIdCandidates(clientId: string) {
const primaryClientId = clientId.trim();
const fallbackClientIds =
@@ -297,12 +338,6 @@ function resolveDesktopClientIdCandidates(clientId: string) {
async function createGoogleDesktopAuthSession(
runtime: OemCloudRuntimeContext,
): Promise<OemCloudDesktopAuthSessionStartResponse> {
if (isLoopbackDesktopOauthRedirectUrl(runtime.desktopOauthRedirectUrl)) {
throw new Error(
"桌面登录回跳地址仍是 localhost 本地回调,请改为 lime://oauth/callback。",
);
}
const payload: CreateClientDesktopAuthSessionPayload = {
clientId: runtime.desktopClientId,
provider: "google",
@@ -477,6 +512,121 @@ function isGoogleOauthCompletionForRuntime(
);
}
function normalizeCallbackBridgeText(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim();
return normalized ? normalized : null;
}
function toDesktopOAuthPayloadFromBridgeEvent(
payload: OemCloudOAuthCallbackBridgePayload | null,
) {
return {
tenantId: normalizeCallbackBridgeText(payload?.tenantId),
token: normalizeCallbackBridgeText(payload?.token),
nextPath:
normalizeCallbackBridgeText(payload?.next) ??
DEFAULT_OEM_CLOUD_DESKTOP_OAUTH_NEXT_PATH,
error: normalizeCallbackBridgeText(payload?.error),
};
}
function subscribeOauthCallbackBridge(
runtime: OemCloudRuntimeContext,
onComplete: () => void,
onError: (error: Error) => void,
) {
if (typeof window === "undefined" || !shouldUseOAuthCallbackBridge()) {
return () => undefined;
}
let disposed = false;
let nativeUnlisten: (() => void) | null = null;
const handleCallback = (
payload: OemCloudOAuthCallbackBridgePayload | null,
) => {
if (disposed) {
return;
}
const callbackPayload = toDesktopOAuthPayloadFromBridgeEvent(payload);
if (callbackPayload.error) {
onError(new Error(callbackPayload.error));
return;
}
if (!callbackPayload.tenantId || !callbackPayload.token) {
return;
}
void completeOemCloudDesktopOAuthLogin(callbackPayload)
.then(() => {
if (disposed) {
return;
}
if (
isGoogleOauthCompletionForRuntime(
{
tenantId: callbackPayload.tenantId!,
nextPath: callbackPayload.nextPath,
provider: "google",
},
runtime,
)
) {
onComplete();
}
})
.catch((error) => {
onError(
error instanceof Error
? error
: new Error("OAuth 本地回调同步失败"),
);
});
};
const handleWindowCallback = (event: Event) => {
const detail =
event instanceof CustomEvent
? (event.detail as OemCloudOAuthCallbackBridgePayload)
: null;
handleCallback(detail);
};
window.addEventListener(
OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT,
handleWindowCallback,
);
void safeListen<OemCloudOAuthCallbackBridgePayload>(
OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT,
(event) => {
handleCallback(event.payload);
},
)
.then((unlisten) => {
if (disposed) {
unlisten();
return;
}
nativeUnlisten = unlisten;
})
.catch(() => undefined);
return () => {
disposed = true;
nativeUnlisten?.();
window.removeEventListener(
OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT,
handleWindowCallback,
);
};
}
function monitorGoogleDesktopAuthCompletion(
runtime: OemCloudRuntimeContext,
authSession: OemCloudDesktopAuthSessionStartResponse,
@@ -519,6 +669,17 @@ export async function openConfiguredOemCloudLoginUrl(
};
}
function shouldWaitForLoginUrlCompletion(
runtime: OemCloudRuntimeContext,
options: OemCloudLoginLaunchOptions,
) {
return (
options.waitForCompletion === true &&
shouldUseOAuthCallbackBridge() &&
isLoopbackDesktopOauthRedirectUrl(runtime.desktopOauthRedirectUrl)
);
}
export async function startOemCloudLogin(
runtime = resolveOemCloudRuntimeContext(),
options: OemCloudLoginLaunchOptions = {},
@@ -526,33 +687,56 @@ export async function startOemCloudLogin(
if (!runtime) {
throw new Error("当前版本未配置云端登录入口。");
}
const loginRuntime = await resolveLoginRuntime(runtime);
let oauthCompleted = false;
let disposeOauthCompletedListener: () => void = () => undefined;
const oauthCompletedPromise =
typeof window === "undefined"
? new Promise<void>(() => undefined)
: new Promise<void>((resolve) => {
disposeOauthCompletedListener = subscribeOauthCompleted(
runtime,
: new Promise<void>((resolve, reject) => {
const complete = () => {
oauthCompleted = true;
disposeOauthCompletedListener();
resolve();
};
const fail = (error: Error) => {
oauthCompleted = true;
disposeOauthCompletedListener();
reject(error);
};
const disposeOauthCompleted = subscribeOauthCompleted(
loginRuntime,
() => {
oauthCompleted = true;
disposeOauthCompletedListener();
resolve();
complete();
},
);
const disposeCallbackBridge = subscribeOauthCallbackBridge(
loginRuntime,
complete,
fail,
);
disposeOauthCompletedListener = () => {
disposeOauthCompleted();
disposeCallbackBridge();
};
});
let authSession: OemCloudDesktopAuthSessionStartResponse | null = null;
try {
authSession = await createGoogleDesktopAuthSession(runtime);
authSession = await createGoogleDesktopAuthSession(loginRuntime);
} catch (error) {
disposeOauthCompletedListener();
if (error instanceof Error && /localhost 本地回调/.test(error.message)) {
options.browserTarget?.close();
throw error;
const result = await openConfiguredOemCloudLoginUrl(loginRuntime, options);
if (shouldWaitForLoginUrlCompletion(loginRuntime, options)) {
try {
await oauthCompletedPromise;
} finally {
disposeOauthCompletedListener();
}
} else {
disposeOauthCompletedListener();
}
return openConfiguredOemCloudLoginUrl(runtime, options);
return result;
}
await openExternalUrl(authSession.authorizeUrl, {
@@ -561,7 +745,7 @@ export async function startOemCloudLogin(
});
const completionPromise = monitorGoogleDesktopAuthCompletion(
runtime,
loginRuntime,
authSession,
oauthCompletedPromise,
() => oauthCompleted,
+3
View File
@@ -92,6 +92,9 @@ export const fileSystemMocks: Record<string, (args?: any) => any> = {
}
return {};
},
start_oem_cloud_oauth_callback_bridge: () => ({
callbackUrl: "http://127.0.0.1:1420/oauth/callback",
}),
delete_file: () => ({ success: true }),
create_file: () => ({ success: true }),
create_directory: () => ({ success: true }),