mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore: finalize v1.21.0 release updates
This commit is contained in:
@@ -315,9 +315,9 @@ jobs:
|
||||
key_path.chmod(0o600)
|
||||
|
||||
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(f"TAURI_SIGNING_PRIVATE_KEY={normalized_key}\n")
|
||||
|
||||
print(f"Normalized updater key written to {key_path}")
|
||||
print(f"Normalized updater key verified and staged for Tauri signing from {key_path}")
|
||||
PY
|
||||
|
||||
- name: Validate updater signing key
|
||||
@@ -327,7 +327,6 @@ jobs:
|
||||
PROBE_FILE="$RUNNER_TEMP/tauri-signing-probe.txt"
|
||||
printf 'lime-release-signing-probe\n' > "$PROBE_FILE"
|
||||
npx tauri signer sign \
|
||||
-f "$TAURI_SIGNING_PRIVATE_KEY_PATH" \
|
||||
-p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \
|
||||
"$PROBE_FILE" >/dev/null
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
- `install_browser_connector_extension_cmd`
|
||||
- `open_browser_extensions_page_cmd`
|
||||
- `open_browser_remote_debugging_page_cmd`
|
||||
- `open_browser_connector_guide_window`
|
||||
- `disconnect_browser_connector_session`
|
||||
|
||||
这些命令属于当前设置主路径,不应再在页面组件里散落裸 `invoke`。
|
||||
|
||||
@@ -219,5 +219,5 @@ describe("Harness eval history window", () => {
|
||||
expect(report.signals).not.toContain(
|
||||
"样本数不足 2,当前仅形成 trend seed,还不能判断长期退化。",
|
||||
);
|
||||
}, 30_000);
|
||||
}, 90_000);
|
||||
});
|
||||
|
||||
@@ -990,6 +990,7 @@ async function waitForBridgeHealth(options, startedByScript) {
|
||||
let lastExitedChildLogAt = 0;
|
||||
let lastError = null;
|
||||
let lastHeartbeatAt = startedAt;
|
||||
let lastHealthDuringCompileAt = 0;
|
||||
|
||||
console.log(`[bridge:health] 开始检查: ${options.healthUrl}`);
|
||||
|
||||
@@ -1056,6 +1057,20 @@ async function waitForBridgeHealth(options, startedByScript) {
|
||||
const elapsed = Date.now() - startedAt;
|
||||
const status =
|
||||
payload && typeof payload === "object" ? payload.status : undefined;
|
||||
const smokeRustcProcesses = listActiveGuiSmokeGroupProcesses(
|
||||
startedByScript,
|
||||
).filter((item) => item.command.includes("/bin/rustc"));
|
||||
if (startedByScript && smokeRustcProcesses.length > 0) {
|
||||
if (elapsed - lastHealthDuringCompileAt >= GUI_SMOKE_BRIDGE_HEARTBEAT_MS) {
|
||||
lastHealthDuringCompileAt = elapsed;
|
||||
const heartbeat = describeGuiSmokeHeartbeat(startedByScript);
|
||||
console.log(
|
||||
`[bridge:health] 检测到健康响应但本轮 headless Tauri 仍在编译,继续等待本轮 DevBridge 稳定就绪${heartbeat ? `;进程组: ${heartbeat}` : ""}`,
|
||||
);
|
||||
}
|
||||
await sleep(options.intervalMs);
|
||||
continue;
|
||||
}
|
||||
console.log(
|
||||
`[bridge:health] 就绪: ${options.healthUrl} (${elapsed}ms)${status ? ` status=${status}` : ""}`,
|
||||
);
|
||||
@@ -1369,7 +1384,21 @@ async function main() {
|
||||
|
||||
runCommand(
|
||||
npmCommand,
|
||||
["run", "smoke:agent-runtime-tool-surface-page"],
|
||||
[
|
||||
"run",
|
||||
"smoke:agent-runtime-tool-surface-page",
|
||||
"--",
|
||||
"--app-url",
|
||||
options.appUrl,
|
||||
"--health-url",
|
||||
options.healthUrl,
|
||||
"--invoke-url",
|
||||
options.invokeUrl,
|
||||
"--timeout-ms",
|
||||
String(options.timeoutMs),
|
||||
"--interval-ms",
|
||||
String(options.intervalMs),
|
||||
],
|
||||
"smoke:agent-runtime-tool-surface-page",
|
||||
options.timeoutMs + 30_000,
|
||||
);
|
||||
|
||||
@@ -49,10 +49,13 @@ fn should_use_bearer_auth_for_anthropic_host(host: &str) -> bool {
|
||||
|
||||
[
|
||||
"bigmodel.cn/api/anthropic",
|
||||
"api.z.ai/api/anthropic",
|
||||
"moonshot.cn/anthropic",
|
||||
"moonshot.ai/anthropic",
|
||||
"minimaxi.com/anthropic",
|
||||
"minimax.io/anthropic",
|
||||
"coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
"coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
||||
"token-plan-cn.xiaomimimo.com/anthropic",
|
||||
]
|
||||
.iter()
|
||||
@@ -409,6 +412,12 @@ mod tests {
|
||||
assert!(should_use_bearer_auth_for_anthropic_host(
|
||||
"https://open.bigmodel.cn/api/anthropic"
|
||||
));
|
||||
assert!(should_use_bearer_auth_for_anthropic_host(
|
||||
"https://api.z.ai/api/anthropic"
|
||||
));
|
||||
assert!(should_use_bearer_auth_for_anthropic_host(
|
||||
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -416,6 +425,9 @@ mod tests {
|
||||
assert!(!should_use_bearer_auth_for_anthropic_host(
|
||||
"https://api.anthropic.com"
|
||||
));
|
||||
assert!(!should_use_bearer_auth_for_anthropic_host(
|
||||
"https://api.kimi.com/coding/"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -480,6 +492,12 @@ mod tests {
|
||||
assert!(supports_automatic_prompt_cache_for_host(
|
||||
"https://api.minimaxi.com/anthropic"
|
||||
));
|
||||
assert!(supports_automatic_prompt_cache_for_host(
|
||||
"https://coding.dashscope.aliyuncs.com/apps/anthropic"
|
||||
));
|
||||
assert!(supports_automatic_prompt_cache_for_host(
|
||||
"https://api.kimi.com/coding/"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -296,8 +296,14 @@ mod tests {
|
||||
fn test_known_official_anthropic_compatible_hosts_default_to_automatic() {
|
||||
let hosts = [
|
||||
"https://open.bigmodel.cn/api/anthropic",
|
||||
"https://api.z.ai/api/anthropic",
|
||||
"https://api.moonshot.cn/anthropic",
|
||||
"https://api.moonshot.ai/anthropic",
|
||||
"https://api.kimi.com/coding/",
|
||||
"https://api.minimaxi.com/anthropic",
|
||||
"https://api.minimax.io/anthropic",
|
||||
"https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
||||
"https://token-plan-cn.xiaomimimo.com/anthropic",
|
||||
];
|
||||
|
||||
|
||||
@@ -53,8 +53,14 @@ mod tests {
|
||||
fn known_official_anthropic_compatible_hosts_should_match() {
|
||||
let hosts = [
|
||||
"https://open.bigmodel.cn/api/anthropic",
|
||||
"https://api.z.ai/api/anthropic",
|
||||
"https://api.moonshot.cn/anthropic",
|
||||
"https://api.moonshot.ai/anthropic",
|
||||
"https://api.kimi.com/coding/",
|
||||
"https://api.minimaxi.com/anthropic",
|
||||
"https://api.minimax.io/anthropic",
|
||||
"https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
||||
"https://token-plan-cn.xiaomimimo.com/anthropic",
|
||||
];
|
||||
|
||||
|
||||
@@ -2373,6 +2373,38 @@ impl ModelRegistryService {
|
||||
return &["zai"];
|
||||
}
|
||||
|
||||
if host.contains("moonshot.cn") {
|
||||
return &["moonshotai-cn", "moonshotai", "kimi-for-coding"];
|
||||
}
|
||||
|
||||
if host.contains("moonshot.ai") {
|
||||
return &["moonshotai", "kimi-for-coding"];
|
||||
}
|
||||
|
||||
if host.contains("api.kimi.com") {
|
||||
return &["kimi-for-coding"];
|
||||
}
|
||||
|
||||
if host.contains("minimaxi.com") {
|
||||
return &["minimax-cn", "minimax"];
|
||||
}
|
||||
|
||||
if host.contains("minimax.io") {
|
||||
return &["minimax"];
|
||||
}
|
||||
|
||||
if host.contains("coding-intl.dashscope.aliyuncs.com") {
|
||||
return &["alibaba", "alibaba-cn"];
|
||||
}
|
||||
|
||||
if host.contains("dashscope-intl.aliyuncs.com") {
|
||||
return &["alibaba", "alibaba-cn"];
|
||||
}
|
||||
|
||||
if host.contains("dashscope.aliyuncs.com") {
|
||||
return &["alibaba-cn", "alibaba"];
|
||||
}
|
||||
|
||||
if host.contains("anthropic.com") {
|
||||
return &["anthropic"];
|
||||
}
|
||||
@@ -3708,6 +3740,22 @@ mod tests {
|
||||
),
|
||||
["zhipuai"]
|
||||
);
|
||||
assert_eq!(
|
||||
ModelRegistryService::infer_provider_ids_from_api_host(
|
||||
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
|
||||
),
|
||||
["alibaba", "alibaba-cn"]
|
||||
);
|
||||
assert_eq!(
|
||||
ModelRegistryService::infer_provider_ids_from_api_host(
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1/"
|
||||
),
|
||||
["alibaba", "alibaba-cn"]
|
||||
);
|
||||
assert_eq!(
|
||||
ModelRegistryService::infer_provider_ids_from_api_host("https://api.kimi.com/coding/"),
|
||||
["kimi-for-coding"]
|
||||
);
|
||||
assert_eq!(
|
||||
ModelRegistryService::infer_provider_ids_from_api_host("https://api.openai.com/v1"),
|
||||
["openai"]
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
"contains": "moonshot.ai",
|
||||
"providers": ["moonshotai", "kimi-for-coding"]
|
||||
},
|
||||
{
|
||||
"contains": "api.kimi.com",
|
||||
"providers": ["kimi-for-coding"]
|
||||
},
|
||||
{
|
||||
"contains": "siliconflow.cn",
|
||||
"providers": ["siliconflow-cn", "siliconflow"]
|
||||
@@ -78,7 +82,7 @@
|
||||
},
|
||||
{
|
||||
"contains": "minimaxi.com",
|
||||
"providers": ["minimax"]
|
||||
"providers": ["minimax-cn", "minimax"]
|
||||
},
|
||||
{
|
||||
"contains": "minimax.io",
|
||||
@@ -88,6 +92,14 @@
|
||||
"contains": "xiaomimimo.com",
|
||||
"providers": ["xiaomi"]
|
||||
},
|
||||
{
|
||||
"contains": "coding-intl.dashscope.aliyuncs.com",
|
||||
"providers": ["alibaba", "alibaba-cn"]
|
||||
},
|
||||
{
|
||||
"contains": "dashscope-intl.aliyuncs.com",
|
||||
"providers": ["alibaba", "alibaba-cn"]
|
||||
},
|
||||
{
|
||||
"contains": "dashscope.aliyuncs.com",
|
||||
"providers": ["alibaba-cn", "alibaba"]
|
||||
|
||||
@@ -1561,6 +1561,7 @@ pub fn run() {
|
||||
commands::browser_connector_cmd::install_browser_connector_extension_cmd,
|
||||
commands::browser_connector_cmd::open_browser_extensions_page_cmd,
|
||||
commands::browser_connector_cmd::open_browser_remote_debugging_page_cmd,
|
||||
commands::browser_connector_cmd::open_browser_connector_guide_window,
|
||||
// Browser profile commands
|
||||
commands::browser_profile_cmd::list_browser_profiles_cmd,
|
||||
commands::browser_profile_cmd::save_browser_profile_cmd,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
//! 浏览器连接器命令
|
||||
|
||||
use crate::app::AppState;
|
||||
use crate::services::browser_connector_guide_window::{
|
||||
normalize_browser_connector_guide_mode,
|
||||
open_browser_connector_guide_window as open_browser_connector_guide_window_service,
|
||||
};
|
||||
use crate::services::browser_connector_service::{
|
||||
get_browser_connector_install_status, get_browser_connector_settings,
|
||||
install_browser_connector_extension, sync_browser_connector_auto_config_if_installed,
|
||||
@@ -37,6 +41,12 @@ pub struct SetBrowserActionCapabilityEnabledRequest {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct OpenBrowserConnectorGuideWindowRequest {
|
||||
#[serde(default)]
|
||||
pub mode: Option<String>,
|
||||
}
|
||||
|
||||
fn normalize_bridge_host(host: &str) -> String {
|
||||
match host.trim() {
|
||||
"" | "0.0.0.0" | "::" | "[::]" => "127.0.0.1".to_string(),
|
||||
@@ -201,3 +211,14 @@ pub async fn open_browser_extensions_page_cmd() -> Result<bool, String> {
|
||||
pub async fn open_browser_remote_debugging_page_cmd() -> Result<bool, String> {
|
||||
open_chrome_url("chrome://inspect/#remote-debugging")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_browser_connector_guide_window(
|
||||
app: AppHandle,
|
||||
request: Option<OpenBrowserConnectorGuideWindowRequest>,
|
||||
) -> Result<(), String> {
|
||||
let mode = request.and_then(|request| request.mode);
|
||||
let mode = normalize_browser_connector_guide_mode(mode.as_deref());
|
||||
open_browser_connector_guide_window_service(&app, mode)
|
||||
.map_err(|error| format!("打开浏览器连接器引导窗口失败: {error}"))
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
- `file_browser_service.rs` - Tauri 命令桥接(纯逻辑已迁移到 `crates/services/src/file_browser_service.rs`)
|
||||
- `update_check_service.rs` - 兼容导出层(纯逻辑已迁移到 `crates/services/src/update_check_service.rs`)
|
||||
- `update_window.rs` - 更新提醒独立窗口管理(底部居中 Toast 窗口定位)
|
||||
- `browser_connector_guide_window.rs` - 浏览器连接器引导独立窗口管理
|
||||
- `browser_runtime_window.rs` - 浏览器运行时调试独立窗口管理
|
||||
- `general_chat/` - 通用对话服务模块(会话管理、消息存储)
|
||||
- `api_key_provider_service.rs` - API Key Provider 服务
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
//! 浏览器连接器引导独立窗口管理
|
||||
|
||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum BrowserConnectorGuideWindowError {
|
||||
#[error("窗口创建失败: {0}")]
|
||||
CreateFailed(String),
|
||||
#[error("窗口操作失败: {0}")]
|
||||
OperationFailed(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BrowserConnectorGuideMode {
|
||||
Extension,
|
||||
Cdp,
|
||||
}
|
||||
|
||||
const GUIDE_WINDOW_LABEL_PREFIX: &str = "browser-connector-guide";
|
||||
const GUIDE_WINDOW_ROUTE: &str = "/browser-connector-guide";
|
||||
const WINDOW_WIDTH: f64 = 900.0;
|
||||
const WINDOW_HEIGHT: f64 = 700.0;
|
||||
const MIN_WINDOW_WIDTH: f64 = 760.0;
|
||||
const MIN_WINDOW_HEIGHT: f64 = 560.0;
|
||||
|
||||
impl BrowserConnectorGuideMode {
|
||||
fn as_query_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Extension => "extension",
|
||||
Self::Cdp => "cdp",
|
||||
}
|
||||
}
|
||||
|
||||
fn window_title(self) -> &'static str {
|
||||
match self {
|
||||
Self::Extension => "浏览器扩展连接引导",
|
||||
Self::Cdp => "浏览器直连配置引导",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_browser_connector_guide_mode(mode: Option<&str>) -> BrowserConnectorGuideMode {
|
||||
match mode.map(str::trim) {
|
||||
Some("cdp") => BrowserConnectorGuideMode::Cdp,
|
||||
_ => BrowserConnectorGuideMode::Extension,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_browser_connector_guide_label(mode: BrowserConnectorGuideMode) -> String {
|
||||
format!("{GUIDE_WINDOW_LABEL_PREFIX}-{}", mode.as_query_value())
|
||||
}
|
||||
|
||||
fn build_browser_connector_guide_route(mode: BrowserConnectorGuideMode) -> String {
|
||||
format!(
|
||||
"{GUIDE_WINDOW_ROUTE}?mode={}",
|
||||
urlencoding::encode(mode.as_query_value())
|
||||
)
|
||||
}
|
||||
|
||||
pub fn open_browser_connector_guide_window(
|
||||
app: &AppHandle,
|
||||
mode: BrowserConnectorGuideMode,
|
||||
) -> Result<(), BrowserConnectorGuideWindowError> {
|
||||
let label = build_browser_connector_guide_label(mode);
|
||||
let route = build_browser_connector_guide_route(mode);
|
||||
|
||||
if let Some(window) = app.get_webview_window(&label) {
|
||||
let route_literal = serde_json::to_string(&route).map_err(|error| {
|
||||
BrowserConnectorGuideWindowError::OperationFailed(format!("窗口路由编码失败: {error}"))
|
||||
})?;
|
||||
let js = format!("window.location.replace({route_literal});");
|
||||
window.eval(&js).map_err(|error| {
|
||||
BrowserConnectorGuideWindowError::OperationFailed(format!("导航失败: {error}"))
|
||||
})?;
|
||||
let _ = window.unminimize();
|
||||
window.show().map_err(|error| {
|
||||
BrowserConnectorGuideWindowError::OperationFailed(format!("显示窗口失败: {error}"))
|
||||
})?;
|
||||
window.set_focus().map_err(|error| {
|
||||
BrowserConnectorGuideWindowError::OperationFailed(format!("聚焦窗口失败: {error}"))
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
WebviewWindowBuilder::new(app, &label, WebviewUrl::App(route.into()))
|
||||
.title(mode.window_title())
|
||||
.inner_size(WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
|
||||
.resizable(true)
|
||||
.visible(true)
|
||||
.focused(true)
|
||||
.center()
|
||||
.build()
|
||||
.map_err(|error| BrowserConnectorGuideWindowError::CreateFailed(format!("{error}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn guide_mode_should_fallback_to_extension() {
|
||||
assert_eq!(
|
||||
normalize_browser_connector_guide_mode(None),
|
||||
BrowserConnectorGuideMode::Extension
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_browser_connector_guide_mode(Some("unknown")),
|
||||
BrowserConnectorGuideMode::Extension
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cdp_mode_should_build_stable_route_and_label() {
|
||||
let mode = normalize_browser_connector_guide_mode(Some("cdp"));
|
||||
|
||||
assert_eq!(
|
||||
build_browser_connector_guide_label(mode),
|
||||
"browser-connector-guide-cdp"
|
||||
);
|
||||
assert_eq!(
|
||||
build_browser_connector_guide_route(mode),
|
||||
"/browser-connector-guide?mode=cdp"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ pub mod artifact_prompt_service;
|
||||
pub mod artifact_request_metadata_service;
|
||||
pub mod auto_memory_service;
|
||||
pub mod automation_service;
|
||||
pub mod browser_connector_guide_window;
|
||||
pub mod browser_connector_service;
|
||||
pub mod browser_environment_service;
|
||||
pub mod browser_profile_service;
|
||||
|
||||
@@ -4260,10 +4260,17 @@ export function AgentChatWorkspace({
|
||||
}),
|
||||
[sessionId, taskCenterTransitionTopicId],
|
||||
);
|
||||
const hasHomeConversationActivity =
|
||||
hasDisplayMessages ||
|
||||
hasPendingA2UIForm ||
|
||||
isPreparingSend ||
|
||||
isSending ||
|
||||
queuedTurns.length > 0;
|
||||
const shouldRenderTaskCenterEmbeddedHome = Boolean(
|
||||
agentEntry === "claw" &&
|
||||
sessionId &&
|
||||
!taskCenterSessionSwitchPending &&
|
||||
!hasHomeConversationActivity &&
|
||||
taskCenterEmbeddedHomeSessionIds.has(sessionId),
|
||||
);
|
||||
useEffect(() => {
|
||||
@@ -4271,27 +4278,18 @@ export function AgentChatWorkspace({
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
hasDisplayMessages ||
|
||||
hasPendingA2UIForm ||
|
||||
isPreparingSend ||
|
||||
isSending ||
|
||||
queuedTurns.length > 0
|
||||
) {
|
||||
if (hasHomeConversationActivity) {
|
||||
clearTaskCenterEmbeddedHomeSession(sessionId);
|
||||
}
|
||||
}, [
|
||||
clearTaskCenterEmbeddedHomeSession,
|
||||
hasDisplayMessages,
|
||||
hasPendingA2UIForm,
|
||||
isPreparingSend,
|
||||
isSending,
|
||||
queuedTurns.length,
|
||||
hasHomeConversationActivity,
|
||||
sessionId,
|
||||
taskCenterEmbeddedHomeSessionIds,
|
||||
]);
|
||||
const suppressHomeNavbarUtilityActions =
|
||||
shouldUseBrowserWorkspaceHomeChrome || shouldRenderTaskCenterEmbeddedHome;
|
||||
(shouldUseBrowserWorkspaceHomeChrome && !hasHomeConversationActivity) ||
|
||||
shouldRenderTaskCenterEmbeddedHome;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
||||
@@ -2032,6 +2032,9 @@ describe("AgentChatPage 侧栏显示控制", () => {
|
||||
});
|
||||
await flushEffects();
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="toggle-harness"]'),
|
||||
).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="chat-sidebar"]')).toBeNull();
|
||||
|
||||
clickButton(container, "toggle-history");
|
||||
|
||||
@@ -373,16 +373,46 @@ describe("ModelSelector", () => {
|
||||
apiHost: "https://open.bigmodel.cn/api/anthropic",
|
||||
model: "glm-5.1",
|
||||
},
|
||||
{
|
||||
label: "Z.AI Anthropic",
|
||||
apiHost: "https://api.z.ai/api/anthropic",
|
||||
model: "glm-4.7",
|
||||
},
|
||||
{
|
||||
label: "Kimi Anthropic",
|
||||
apiHost: "https://api.moonshot.cn/anthropic",
|
||||
model: "kimi-k2.5",
|
||||
},
|
||||
{
|
||||
label: "Kimi Global Anthropic",
|
||||
apiHost: "https://api.moonshot.ai/anthropic",
|
||||
model: "kimi-k2.5",
|
||||
},
|
||||
{
|
||||
label: "Kimi Code Subscription",
|
||||
apiHost: "https://api.kimi.com/coding/",
|
||||
model: "k2p5",
|
||||
},
|
||||
{
|
||||
label: "MiniMax Anthropic",
|
||||
apiHost: "https://api.minimaxi.com/anthropic",
|
||||
model: "minimax-m1",
|
||||
},
|
||||
{
|
||||
label: "MiniMax Global Anthropic",
|
||||
apiHost: "https://api.minimax.io/anthropic",
|
||||
model: "minimax-m1",
|
||||
},
|
||||
{
|
||||
label: "Alibaba Coding Anthropic",
|
||||
apiHost: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
model: "qwen3-coder-plus",
|
||||
},
|
||||
{
|
||||
label: "Alibaba Global Coding Anthropic",
|
||||
apiHost: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
||||
model: "qwen3-coder-plus",
|
||||
},
|
||||
{
|
||||
label: "MiMo Anthropic",
|
||||
apiHost: "https://token-plan-cn.xiaomimimo.com/anthropic",
|
||||
|
||||
@@ -288,10 +288,67 @@ describe("ApiKeyProviderSection 模型管理布局", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent ?? "").toContain("DeepSeek");
|
||||
expect(container.textContent ?? "").toContain("Kimi Coding Plan");
|
||||
expect(container.textContent ?? "").toContain("Kimi API(国内按量)");
|
||||
expect(container.textContent ?? "").toContain("GLM Coding Plan(国内)");
|
||||
expect(container.textContent ?? "").not.toContain("Kimi Code 会员(订阅)");
|
||||
expect(container.textContent ?? "").not.toContain("Z.AI Coding Plan(海外)");
|
||||
expect(container.querySelector('[data-testid="model-add-catalog"]')?.className).toContain(
|
||||
"overflow-y-auto",
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
container
|
||||
.querySelector<HTMLButtonElement>('[data-template-id="glm-cn-coding-plan"]')
|
||||
?.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.textContent ?? "").toContain("https://open.bigmodel.cn/api/anthropic");
|
||||
});
|
||||
|
||||
it("海外分类应展示国内厂商的国际订阅入口", async () => {
|
||||
createHookState();
|
||||
const container = renderSection();
|
||||
|
||||
await act(async () => {
|
||||
findByTestId<HTMLButtonElement>("add-model-button").click();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
findByTestId<HTMLButtonElement>("model-catalog-category-overseas").click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const text = container.textContent ?? "";
|
||||
expect(text).toContain("Kimi Code 会员(订阅)");
|
||||
expect(text).toContain("Kimi API(海外按量)");
|
||||
expect(text).toContain("Z.AI Coding Plan(海外)");
|
||||
expect(text).toContain("MiniMax Coding Plan(海外)");
|
||||
expect(text).toContain("Alibaba Coding Plan(海外)");
|
||||
expect(text).not.toContain("GLM Coding Plan(国内)");
|
||||
expect(
|
||||
container.querySelector('[data-template-id="kimi-code-subscription"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-template-id="zai-coding-plan"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-template-id="minimax-coding-plan-global"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-template-id="alibaba-coding-plan-global"]'),
|
||||
).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
container
|
||||
.querySelector<HTMLButtonElement>('[data-template-id="kimi-code-subscription"]')
|
||||
?.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.textContent ?? "").toContain("https://api.kimi.com/coding/");
|
||||
});
|
||||
|
||||
it("自定义供应商可在添加流程内完成创建、加 Key、写入模型并激活", async () => {
|
||||
|
||||
@@ -49,6 +49,8 @@ export type ModelCatalogCategory =
|
||||
| "aggregator"
|
||||
| "overseas"
|
||||
| "local";
|
||||
type ProviderRegion = "cn" | "global";
|
||||
type ProviderBillingMode = "payg" | "coding_plan" | "subscription";
|
||||
|
||||
interface ProviderTemplate {
|
||||
id: string;
|
||||
@@ -62,7 +64,10 @@ interface ProviderTemplate {
|
||||
defaultModels: string[];
|
||||
iconProviderId?: string;
|
||||
systemProviderId?: string;
|
||||
providerResourceId?: string;
|
||||
isCustom?: boolean;
|
||||
region?: ProviderRegion;
|
||||
billingMode?: ProviderBillingMode;
|
||||
}
|
||||
|
||||
interface FormState {
|
||||
@@ -94,58 +99,160 @@ const CATEGORY_OPTIONS: Array<{ value: ModelCatalogCategory; label: string }> =
|
||||
|
||||
const FEATURED_TEMPLATES: ProviderTemplate[] = [
|
||||
{
|
||||
id: "kimi-coding-plan",
|
||||
name: "Kimi Coding Plan",
|
||||
description: "Anthropic 协议编码套餐,适合 Claude Code / OpenClaw 工作流",
|
||||
category: "recommended",
|
||||
id: "kimi-code-subscription",
|
||||
name: "Kimi Code 会员(订阅)",
|
||||
description: "Kimi Code 官方订阅入口,Anthropic 协议,适合 Claude Code / OpenClaw",
|
||||
category: "overseas",
|
||||
type: "anthropic-compatible",
|
||||
apiHost: "https://api.kimi.com/coding/",
|
||||
recommended: true,
|
||||
apiKeyUrl: "https://www.kimi.com/code",
|
||||
defaultModels: ["k2p5"],
|
||||
iconProviderId: "moonshotai",
|
||||
providerResourceId: "kimi-for-coding",
|
||||
region: "global",
|
||||
billingMode: "subscription",
|
||||
},
|
||||
{
|
||||
id: "kimi-api-cn",
|
||||
name: "Kimi API(国内按量)",
|
||||
description: "Moonshot 中国区 Anthropic 协议 API,适合按量接入 Kimi 模型",
|
||||
category: "cn",
|
||||
type: "anthropic-compatible",
|
||||
apiHost: "https://api.moonshot.cn/anthropic",
|
||||
recommended: true,
|
||||
apiKeyUrl: "https://platform.moonshot.cn/console/api-keys",
|
||||
defaultModels: ["k2p5"],
|
||||
defaultModels: ["kimi-k2.5"],
|
||||
iconProviderId: "moonshotai",
|
||||
providerResourceId: "moonshotai-cn",
|
||||
region: "cn",
|
||||
billingMode: "payg",
|
||||
},
|
||||
{
|
||||
id: "kimi-api-global",
|
||||
name: "Kimi API(海外按量)",
|
||||
description: "Moonshot 国际区 Anthropic 协议 API,适合海外账号按量接入",
|
||||
category: "overseas",
|
||||
type: "anthropic-compatible",
|
||||
apiHost: "https://api.moonshot.ai/anthropic",
|
||||
recommended: true,
|
||||
apiKeyUrl: "https://platform.moonshot.ai/console/api-keys",
|
||||
defaultModels: ["kimi-k2.5"],
|
||||
iconProviderId: "moonshotai",
|
||||
providerResourceId: "moonshotai",
|
||||
region: "global",
|
||||
billingMode: "payg",
|
||||
},
|
||||
{
|
||||
id: "minimax-coding-plan",
|
||||
name: "MiniMax Coding Plan",
|
||||
description: "Anthropic 协议编码套餐,默认使用 MiniMax-M2.7",
|
||||
category: "recommended",
|
||||
name: "MiniMax Coding Plan(国内)",
|
||||
description: "MiniMax 中国区 Anthropic 协议编码套餐,默认使用 MiniMax-M2.7",
|
||||
category: "cn",
|
||||
type: "anthropic-compatible",
|
||||
apiHost: "https://api.minimaxi.com/anthropic",
|
||||
recommended: true,
|
||||
apiKeyUrl: "https://platform.minimaxi.com/user-center/basic-information/interface-key",
|
||||
defaultModels: ["MiniMax-M2.7"],
|
||||
iconProviderId: "minimax",
|
||||
iconProviderId: "minimax-cn",
|
||||
providerResourceId: "minimax-cn",
|
||||
region: "cn",
|
||||
billingMode: "coding_plan",
|
||||
},
|
||||
{
|
||||
id: "glm-coding-plan",
|
||||
name: "GLM Coding Plan",
|
||||
description: "智谱 Coding Plan,Anthropic/Claude API 兼容编码入口",
|
||||
category: "recommended",
|
||||
id: "minimax-coding-plan-global",
|
||||
name: "MiniMax Coding Plan(海外)",
|
||||
description: "MiniMax 国际区 Anthropic 协议编码套餐,使用海外订阅入口",
|
||||
category: "overseas",
|
||||
type: "anthropic-compatible",
|
||||
apiHost: "https://api.minimax.io/anthropic",
|
||||
recommended: true,
|
||||
apiKeyUrl: "https://platform.minimax.io/user-center/basic-information/interface-key",
|
||||
defaultModels: ["MiniMax-M2.7"],
|
||||
iconProviderId: "minimax",
|
||||
providerResourceId: "minimax",
|
||||
region: "global",
|
||||
billingMode: "coding_plan",
|
||||
},
|
||||
{
|
||||
id: "glm-cn-coding-plan",
|
||||
name: "GLM Coding Plan(国内)",
|
||||
description: "智谱 BigModel 中国区 Anthropic/Claude API 兼容编码入口",
|
||||
category: "cn",
|
||||
type: "anthropic-compatible",
|
||||
apiHost: "https://open.bigmodel.cn/api/anthropic",
|
||||
recommended: true,
|
||||
apiKeyUrl: "https://open.bigmodel.cn/usercenter/apikeys",
|
||||
defaultModels: ["glm-4.7"],
|
||||
iconProviderId: "zhipuai",
|
||||
providerResourceId: "zhipuai-coding-plan",
|
||||
region: "cn",
|
||||
billingMode: "coding_plan",
|
||||
},
|
||||
{
|
||||
id: "zai-coding-plan",
|
||||
name: "Z.AI Coding Plan(海外)",
|
||||
description: "Z.AI 国际区 Anthropic/Claude API 兼容编码入口",
|
||||
category: "overseas",
|
||||
type: "anthropic-compatible",
|
||||
apiHost: "https://api.z.ai/api/anthropic",
|
||||
recommended: true,
|
||||
apiKeyUrl: "https://z.ai/manage-apikey/apikey-list",
|
||||
defaultModels: ["glm-4.7"],
|
||||
iconProviderId: "zai",
|
||||
providerResourceId: "zai-coding-plan",
|
||||
region: "global",
|
||||
billingMode: "coding_plan",
|
||||
},
|
||||
{
|
||||
id: "mimo-coding-plan",
|
||||
name: "MiMo Coding Plan",
|
||||
description: "小米 MiMo Token Plan,兼容 Claude Code 的 Anthropic 协议",
|
||||
category: "recommended",
|
||||
category: "cn",
|
||||
type: "anthropic-compatible",
|
||||
apiHost: "https://token-plan-cn.xiaomimimo.com/anthropic",
|
||||
recommended: true,
|
||||
apiKeyUrl: "https://mimo.mi.com/",
|
||||
defaultModels: ["mimo-v2.5-pro"],
|
||||
iconProviderId: "xiaomi",
|
||||
providerResourceId: "xiaomi",
|
||||
region: "cn",
|
||||
billingMode: "subscription",
|
||||
},
|
||||
{
|
||||
id: "alibaba-coding-plan-cn",
|
||||
name: "Alibaba Coding Plan(国内)",
|
||||
description: "阿里云百炼中国区 Claude Code Coding Plan 专用 Anthropic 入口",
|
||||
category: "cn",
|
||||
type: "anthropic-compatible",
|
||||
apiHost: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
recommended: true,
|
||||
apiKeyUrl: "https://bailian.console.aliyun.com/",
|
||||
defaultModels: ["qwen3-coder-plus"],
|
||||
iconProviderId: "alibaba-cn",
|
||||
providerResourceId: "alibaba-cn",
|
||||
region: "cn",
|
||||
billingMode: "coding_plan",
|
||||
},
|
||||
{
|
||||
id: "alibaba-coding-plan-global",
|
||||
name: "Alibaba Coding Plan(海外)",
|
||||
description: "阿里云 Model Studio 国际区 Claude Code Coding Plan 专用 Anthropic 入口",
|
||||
category: "overseas",
|
||||
type: "anthropic-compatible",
|
||||
apiHost: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
||||
recommended: true,
|
||||
apiKeyUrl: "https://modelstudio.console.alibabacloud.com/",
|
||||
defaultModels: ["qwen3-coder-plus"],
|
||||
iconProviderId: "alibaba",
|
||||
providerResourceId: "alibaba",
|
||||
region: "global",
|
||||
billingMode: "coding_plan",
|
||||
},
|
||||
{
|
||||
id: "aihubmix-recommended",
|
||||
name: "AiHubMix",
|
||||
description: "聚合 Claude、OpenAI、Gemini 的常用中转服务",
|
||||
category: "recommended",
|
||||
category: "aggregator",
|
||||
type: "openai",
|
||||
apiHost: "https://aihubmix.com",
|
||||
recommended: true,
|
||||
@@ -157,7 +264,7 @@ const FEATURED_TEMPLATES: ProviderTemplate[] = [
|
||||
id: "openrouter-recommended",
|
||||
name: "OpenRouter",
|
||||
description: "海外模型聚合平台,可按模型 ID 灵活接入",
|
||||
category: "recommended",
|
||||
category: "overseas",
|
||||
type: "openai",
|
||||
apiHost: "https://openrouter.ai/api/v1/",
|
||||
recommended: true,
|
||||
@@ -180,7 +287,6 @@ const CUSTOM_TEMPLATE: ProviderTemplate = {
|
||||
};
|
||||
|
||||
const CN_PROVIDER_IDS = new Set([
|
||||
"alibaba",
|
||||
"alibaba-cn",
|
||||
"bailing",
|
||||
"baidu-cloud",
|
||||
@@ -192,10 +298,8 @@ const CN_PROVIDER_IDS = new Set([
|
||||
"infini",
|
||||
"internlm",
|
||||
"kimi-for-coding",
|
||||
"minimax",
|
||||
"minimax-cn",
|
||||
"modelscope",
|
||||
"moonshotai",
|
||||
"moonshotai-cn",
|
||||
"sensenova",
|
||||
"spark",
|
||||
@@ -206,8 +310,6 @@ const CN_PROVIDER_IDS = new Set([
|
||||
"xiaomi",
|
||||
"xirang",
|
||||
"yi",
|
||||
"zai",
|
||||
"zai-coding-plan",
|
||||
"zhipuai",
|
||||
"zhipuai-coding-plan",
|
||||
"zhinao",
|
||||
@@ -259,15 +361,15 @@ const LOCAL_PROVIDER_IDS = new Set([
|
||||
|
||||
const RESOURCE_PROVIDER_API_HOSTS: Record<string, string> = {
|
||||
aihubmix: "https://aihubmix.com",
|
||||
alibaba: "https://dashscope.aliyuncs.com/compatible-mode/v1/",
|
||||
alibaba: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/",
|
||||
"alibaba-cn": "https://dashscope.aliyuncs.com/compatible-mode/v1/",
|
||||
anthropic: "https://api.anthropic.com",
|
||||
deepseek: "https://api.deepseek.com",
|
||||
"kimi-for-coding": "https://api.moonshot.cn/anthropic",
|
||||
"kimi-for-coding": "https://api.kimi.com/coding/",
|
||||
lmstudio: "http://localhost:1234",
|
||||
minimax: "https://api.minimaxi.com/anthropic",
|
||||
minimax: "https://api.minimax.io/anthropic",
|
||||
"minimax-cn": "https://api.minimaxi.com/anthropic",
|
||||
moonshotai: "https://api.moonshot.cn",
|
||||
moonshotai: "https://api.moonshot.ai",
|
||||
"moonshotai-cn": "https://api.moonshot.cn",
|
||||
openai: "https://api.openai.com",
|
||||
openrouter: "https://openrouter.ai/api/v1/",
|
||||
@@ -275,9 +377,18 @@ const RESOURCE_PROVIDER_API_HOSTS: Record<string, string> = {
|
||||
"siliconflow-cn": "https://api.siliconflow.cn",
|
||||
xiaomi: "https://token-plan-cn.xiaomimimo.com/anthropic",
|
||||
"zai-coding-plan": "https://api.z.ai/api/anthropic",
|
||||
"zhipuai-coding-plan": "https://api.z.ai/api/anthropic",
|
||||
"zhipuai-coding-plan": "https://open.bigmodel.cn/api/anthropic",
|
||||
};
|
||||
|
||||
const ANTHROPIC_COMPATIBLE_REGISTRY_PROVIDER_IDS = new Set([
|
||||
"kimi-for-coding",
|
||||
"minimax",
|
||||
"minimax-cn",
|
||||
"xiaomi",
|
||||
"zai-coding-plan",
|
||||
"zhipuai-coding-plan",
|
||||
]);
|
||||
|
||||
function normalizeCatalogProviderType(providerType: string): ProviderType {
|
||||
return isSupportedProviderType(providerType) ? providerType : "openai";
|
||||
}
|
||||
@@ -351,10 +462,13 @@ function buildRegistryTemplates(
|
||||
? `模型目录 · ${apiHost}`
|
||||
: "模型目录供应商,按服务文档补充 API Base URL",
|
||||
category: resolveProviderCategory(providerId),
|
||||
type: providerId === "kimi-for-coding" ? "anthropic-compatible" : "openai",
|
||||
type: ANTHROPIC_COMPATIBLE_REGISTRY_PROVIDER_IDS.has(providerId)
|
||||
? "anthropic-compatible"
|
||||
: "openai",
|
||||
apiHost,
|
||||
defaultModels: [],
|
||||
iconProviderId: providerId,
|
||||
providerResourceId: providerId,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -366,7 +480,9 @@ function dedupeTemplates(templates: ProviderTemplate[]): ProviderTemplate[] {
|
||||
const result: ProviderTemplate[] = [];
|
||||
|
||||
for (const template of templates) {
|
||||
const key = template.systemProviderId ?? template.id;
|
||||
const key = template.providerResourceId
|
||||
? `provider:${template.providerResourceId}:${template.apiHost}`
|
||||
: template.systemProviderId ?? template.id;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -427,6 +543,55 @@ function renderTemplateIcon(template: ProviderTemplate) {
|
||||
);
|
||||
}
|
||||
|
||||
function getRegionLabel(region?: ProviderRegion): string | null {
|
||||
switch (region) {
|
||||
case "cn":
|
||||
return "国内";
|
||||
case "global":
|
||||
return "海外";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getBillingModeLabel(mode?: ProviderBillingMode): string | null {
|
||||
switch (mode) {
|
||||
case "payg":
|
||||
return "按量 API";
|
||||
case "coding_plan":
|
||||
return "Coding Plan";
|
||||
case "subscription":
|
||||
return "订阅套餐";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTemplateBadges(template: ProviderTemplate) {
|
||||
const badges = [
|
||||
template.recommended ? "推荐" : null,
|
||||
getRegionLabel(template.region),
|
||||
getBillingModeLabel(template.billingMode),
|
||||
].filter((item): item is string => Boolean(item));
|
||||
|
||||
if (badges.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex flex-wrap items-center gap-1.5">
|
||||
{badges.map((badge) => (
|
||||
<Badge
|
||||
key={badge}
|
||||
className="border border-slate-200 bg-slate-50 px-2 py-0 text-[11px] text-slate-600 hover:bg-slate-50"
|
||||
>
|
||||
{badge}
|
||||
</Badge>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export const ModelAddPanel: React.FC<ModelAddPanelProps> = ({
|
||||
providers,
|
||||
onAddProvider,
|
||||
@@ -635,14 +800,14 @@ export const ModelAddPanel: React.FC<ModelAddPanelProps> = ({
|
||||
)}
|
||||
data-testid="model-add-catalog"
|
||||
>
|
||||
<div className="mb-4 flex flex-wrap items-center gap-2 rounded-full bg-slate-100 p-1">
|
||||
<div className="mb-4 grid grid-cols-2 gap-1 rounded-[18px] bg-slate-100 p-1 sm:flex sm:flex-wrap sm:items-center sm:gap-2 sm:rounded-full">
|
||||
{CATEGORY_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setCategory(option.value)}
|
||||
className={cn(
|
||||
"min-w-[118px] rounded-full px-4 py-2 text-sm font-semibold transition",
|
||||
"min-w-0 rounded-[14px] px-3 py-2 text-sm font-semibold transition sm:min-w-[118px] sm:rounded-full sm:px-4",
|
||||
category === option.value
|
||||
? "bg-white text-slate-900 shadow-sm shadow-slate-950/5"
|
||||
: "text-slate-500 hover:text-slate-800",
|
||||
@@ -675,15 +840,11 @@ export const ModelAddPanel: React.FC<ModelAddPanelProps> = ({
|
||||
{renderTemplateIcon(template)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="truncate text-sm font-semibold text-slate-900">
|
||||
<span className="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:gap-2">
|
||||
<span className="min-w-0 truncate text-sm font-semibold text-slate-900">
|
||||
{template.name}
|
||||
</span>
|
||||
{template.recommended ? (
|
||||
<Badge className="border border-amber-200 bg-amber-50 px-2 py-0 text-[11px] text-amber-700 hover:bg-amber-50">
|
||||
推荐
|
||||
</Badge>
|
||||
) : null}
|
||||
{renderTemplateBadges(template)}
|
||||
</span>
|
||||
<span className="mt-1 block text-sm leading-5 text-slate-500">
|
||||
{template.description}
|
||||
@@ -748,9 +909,12 @@ export const ModelAddPanel: React.FC<ModelAddPanelProps> = ({
|
||||
{renderTemplateIcon(template)}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate text-lg font-semibold text-slate-900">
|
||||
{template.isCustom ? "自定义供应商" : `配置 ${template.name}`}
|
||||
</h3>
|
||||
<div className="flex min-w-0 flex-col gap-1 sm:flex-row sm:flex-wrap sm:items-center sm:gap-2">
|
||||
<h3 className="min-w-0 truncate text-lg font-semibold text-slate-900">
|
||||
{template.isCustom ? "自定义供应商" : `配置 ${template.name}`}
|
||||
</h3>
|
||||
{renderTemplateBadges(template)}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-slate-500">{template.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ const SPECIAL_PROVIDER_PROTOCOL_HINTS: Partial<Record<ProviderType, string>> = {
|
||||
anthropic:
|
||||
"Anthropic 继续使用原生协议,不会被收敛到普通 OpenAI 兼容请求格式。",
|
||||
"anthropic-compatible":
|
||||
"Anthropic 兼容用于接入实现 Anthropic wire format 的第三方服务,会沿用 Anthropic 请求结构与模型映射。Lime 会自动识别已知官方 Anthropic 兼容端点(如 GLM / Kimi / MiniMax / MiMo);未知端点默认回退为仅显式缓存。",
|
||||
"Anthropic 兼容用于接入实现 Anthropic wire format 的第三方服务,会沿用 Anthropic 请求结构与模型映射。Lime 会自动识别已知官方 Anthropic 兼容端点(如 GLM / Z.AI / Kimi / MiniMax / Alibaba / MiMo);未知端点默认回退为仅显式缓存。",
|
||||
gemini:
|
||||
"Gemini 保留原生协议能力与专属模型映射,不按普通 OpenAI 兼容 Provider 处理。",
|
||||
};
|
||||
|
||||
@@ -124,6 +124,48 @@ describe("providerTypeMapping", () => {
|
||||
expect(resolved).toBe("minimax");
|
||||
});
|
||||
|
||||
it("Z.AI 海外 Anthropic 兼容 Host 应映射到 Z.AI 目录", () => {
|
||||
const resolved = resolveRegistryProviderId("custom-zai-provider", {
|
||||
providerType: "anthropic-compatible",
|
||||
apiHost: "https://api.z.ai/api/anthropic",
|
||||
catalogAliasMap: null,
|
||||
validRegistryProviders: ["anthropic", "zai", "zhipuai"],
|
||||
});
|
||||
|
||||
expect(resolved).toBe("zai");
|
||||
});
|
||||
|
||||
it("Kimi Code 订阅 Host 应映射到 Kimi For Coding 目录", () => {
|
||||
const resolved = resolveRegistryProviderId("custom-kimi-code-provider", {
|
||||
providerType: "anthropic-compatible",
|
||||
apiHost: "https://api.kimi.com/coding/",
|
||||
catalogAliasMap: null,
|
||||
validRegistryProviders: ["anthropic", "kimi-for-coding", "moonshotai"],
|
||||
});
|
||||
|
||||
expect(resolved).toBe("kimi-for-coding");
|
||||
});
|
||||
|
||||
it("阿里 Coding Plan 国内和海外 Host 应分别映射到对应目录", () => {
|
||||
expect(
|
||||
resolveRegistryProviderId("custom-alibaba-cn-provider", {
|
||||
providerType: "anthropic-compatible",
|
||||
apiHost: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
catalogAliasMap: null,
|
||||
validRegistryProviders: ["anthropic", "alibaba-cn", "alibaba"],
|
||||
}),
|
||||
).toBe("alibaba-cn");
|
||||
|
||||
expect(
|
||||
resolveRegistryProviderId("custom-alibaba-global-provider", {
|
||||
providerType: "anthropic-compatible",
|
||||
apiHost: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
||||
catalogAliasMap: null,
|
||||
validRegistryProviders: ["anthropic", "alibaba-cn", "alibaba"],
|
||||
}),
|
||||
).toBe("alibaba");
|
||||
});
|
||||
|
||||
it("MiMo 的 Anthropic 兼容 Host 应映射到 Xiaomi 模型目录", () => {
|
||||
const resolved = resolveRegistryProviderId("custom-mimo-provider", {
|
||||
providerType: "anthropic-compatible",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
openBrowserConnectorGuideWindow as openBrowserConnectorGuideWindowCommand,
|
||||
type BrowserConnectorGuideMode,
|
||||
} from "@/lib/webview-api";
|
||||
import { hasTauriInvokeCapability } from "@/lib/tauri-runtime";
|
||||
|
||||
export type BrowserConnectorGuideMode = "extension" | "cdp";
|
||||
export type { BrowserConnectorGuideMode };
|
||||
|
||||
const GUIDE_ROUTE = "/browser-connector-guide";
|
||||
const GUIDE_WINDOW_LABEL = "browser-connector-guide";
|
||||
|
||||
export function buildBrowserConnectorGuideUrl(
|
||||
mode: BrowserConnectorGuideMode,
|
||||
@@ -11,63 +14,21 @@ export function buildBrowserConnectorGuideUrl(
|
||||
return `${GUIDE_ROUTE}?mode=${encodeURIComponent(mode)}`;
|
||||
}
|
||||
|
||||
async function openTauriGuideWindow(
|
||||
mode: BrowserConnectorGuideMode,
|
||||
url: string,
|
||||
): Promise<boolean> {
|
||||
if (!hasTauriInvokeCapability()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
const label = `${GUIDE_WINDOW_LABEL}-${mode}`;
|
||||
const existingWindow = await WebviewWindow.getByLabel(label).catch(
|
||||
() => null,
|
||||
);
|
||||
|
||||
if (existingWindow) {
|
||||
await existingWindow.show().catch(() => undefined);
|
||||
await existingWindow.setFocus().catch(() => undefined);
|
||||
return true;
|
||||
}
|
||||
|
||||
const guideWindow = new WebviewWindow(label, {
|
||||
url,
|
||||
title: mode === "extension" ? "浏览器扩展连接引导" : "浏览器直连配置引导",
|
||||
width: 900,
|
||||
height: 700,
|
||||
minWidth: 760,
|
||||
minHeight: 560,
|
||||
center: true,
|
||||
visible: true,
|
||||
focus: true,
|
||||
resizable: true,
|
||||
decorations: true,
|
||||
});
|
||||
|
||||
await Promise.race([
|
||||
new Promise<void>((resolve) => {
|
||||
void guideWindow.once("tauri://created", () => resolve());
|
||||
}),
|
||||
new Promise<void>((resolve) => window.setTimeout(resolve, 160)),
|
||||
]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("[连接器引导] 打开独立窗口失败,回退到浏览器窗口:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function openBrowserConnectorGuideWindow({
|
||||
mode,
|
||||
}: {
|
||||
mode: BrowserConnectorGuideMode;
|
||||
}): Promise<void> {
|
||||
const url = buildBrowserConnectorGuideUrl(mode);
|
||||
const openedInTauri = await openTauriGuideWindow(mode, url);
|
||||
if (hasTauriInvokeCapability()) {
|
||||
await openBrowserConnectorGuideWindowCommand({ mode });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!openedInTauri && typeof window !== "undefined") {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
if (typeof window !== "undefined") {
|
||||
window.open(
|
||||
buildBrowserConnectorGuideUrl(mode),
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
mockSetBrowserConnectorInstallRoot,
|
||||
mockOpenBrowserExtensionsPage,
|
||||
mockOpenBrowserRemoteDebuggingPage,
|
||||
mockOpenBrowserConnectorGuideWindowCommand,
|
||||
mockHasTauriInvokeCapability,
|
||||
} = vi.hoisted(() => ({
|
||||
mockOpenDialog: vi.fn(),
|
||||
@@ -25,6 +26,7 @@ const {
|
||||
mockSetBrowserConnectorInstallRoot: vi.fn(),
|
||||
mockOpenBrowserExtensionsPage: vi.fn(),
|
||||
mockOpenBrowserRemoteDebuggingPage: vi.fn(),
|
||||
mockOpenBrowserConnectorGuideWindowCommand: vi.fn(),
|
||||
mockHasTauriInvokeCapability: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -53,6 +55,7 @@ vi.mock("@/lib/webview-api", async () => {
|
||||
setBrowserConnectorInstallRoot: mockSetBrowserConnectorInstallRoot,
|
||||
openBrowserExtensionsPage: mockOpenBrowserExtensionsPage,
|
||||
openBrowserRemoteDebuggingPage: mockOpenBrowserRemoteDebuggingPage,
|
||||
openBrowserConnectorGuideWindow: mockOpenBrowserConnectorGuideWindowCommand,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -168,6 +171,7 @@ beforeEach(() => {
|
||||
});
|
||||
mockOpenBrowserExtensionsPage.mockResolvedValue(true);
|
||||
mockOpenBrowserRemoteDebuggingPage.mockResolvedValue(true);
|
||||
mockOpenBrowserConnectorGuideWindowCommand.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -230,6 +234,15 @@ describe("BrowserConnectorGuideWindow", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("独立窗口内容区应可滚动", async () => {
|
||||
const container = renderGuide("/browser-connector-guide?mode=extension");
|
||||
await flushEffects();
|
||||
|
||||
const main = container.querySelector("main");
|
||||
expect(main?.className).toContain("h-screen");
|
||||
expect(main?.className).toContain("overflow-y-auto");
|
||||
});
|
||||
|
||||
it("cdp 模式应展示直连步骤并打开远程调试页", async () => {
|
||||
const container = renderGuide("/browser-connector-guide?mode=cdp");
|
||||
await flushEffects();
|
||||
@@ -271,6 +284,22 @@ describe("BrowserConnectorGuideWindow", () => {
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
expect(mockOpenBrowserConnectorGuideWindowCommand).not.toHaveBeenCalled();
|
||||
mockWindowOpen.mockRestore();
|
||||
});
|
||||
|
||||
it("Tauri 环境打开引导时应走 Rust 开窗命令", async () => {
|
||||
const mockWindowOpen = vi
|
||||
.spyOn(window, "open")
|
||||
.mockImplementation(() => null);
|
||||
mockHasTauriInvokeCapability.mockReturnValue(true);
|
||||
|
||||
await openBrowserConnectorGuideWindow({ mode: "cdp" });
|
||||
|
||||
expect(mockOpenBrowserConnectorGuideWindowCommand).toHaveBeenCalledWith({
|
||||
mode: "cdp",
|
||||
});
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled();
|
||||
mockWindowOpen.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -334,8 +334,8 @@ function BrowserConnectorGuideContent() {
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="lime-settings-theme-scope min-h-screen overflow-y-auto bg-[color:var(--lime-stage-surface,hsl(var(--background)))] text-[color:var(--lime-text,hsl(var(--foreground)))]">
|
||||
<div className="mx-auto max-w-[900px] px-7 py-12 sm:px-10">
|
||||
<main className="lime-settings-theme-scope h-screen overflow-y-auto bg-[color:var(--lime-stage-surface,hsl(var(--background)))] text-[color:var(--lime-text,hsl(var(--foreground)))]">
|
||||
<div className="mx-auto max-w-[900px] px-7 py-12 pb-20 sm:px-10">
|
||||
<header className="flex flex-col gap-5 sm:flex-row sm:items-start">
|
||||
<div className="flex h-[60px] w-[60px] shrink-0 items-center justify-center rounded-[18px] border border-emerald-200 bg-emerald-50 text-emerald-700">
|
||||
<Sparkles className="h-7 w-7" />
|
||||
|
||||
@@ -246,16 +246,46 @@ describe("buildConfiguredProviders", () => {
|
||||
name: "GLM Anthropic",
|
||||
apiHost: "https://open.bigmodel.cn/api/anthropic",
|
||||
},
|
||||
{
|
||||
id: "zai-anthropic",
|
||||
name: "Z.AI Anthropic",
|
||||
apiHost: "https://api.z.ai/api/anthropic",
|
||||
},
|
||||
{
|
||||
id: "kimi-anthropic",
|
||||
name: "Kimi Anthropic",
|
||||
apiHost: "https://api.moonshot.cn/anthropic",
|
||||
},
|
||||
{
|
||||
id: "kimi-global-anthropic",
|
||||
name: "Kimi Global Anthropic",
|
||||
apiHost: "https://api.moonshot.ai/anthropic",
|
||||
},
|
||||
{
|
||||
id: "kimi-code-subscription",
|
||||
name: "Kimi Code Subscription",
|
||||
apiHost: "https://api.kimi.com/coding/",
|
||||
},
|
||||
{
|
||||
id: "minimax-anthropic",
|
||||
name: "MiniMax Anthropic",
|
||||
apiHost: "https://api.minimaxi.com/anthropic",
|
||||
},
|
||||
{
|
||||
id: "minimax-global-anthropic",
|
||||
name: "MiniMax Global Anthropic",
|
||||
apiHost: "https://api.minimax.io/anthropic",
|
||||
},
|
||||
{
|
||||
id: "alibaba-coding-anthropic",
|
||||
name: "Alibaba Coding Anthropic",
|
||||
apiHost: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
},
|
||||
{
|
||||
id: "alibaba-global-coding-anthropic",
|
||||
name: "Alibaba Global Coding Anthropic",
|
||||
apiHost: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
||||
},
|
||||
{
|
||||
id: "mimo-anthropic",
|
||||
name: "MiMo Anthropic",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getClientOrder,
|
||||
getClientBootstrap,
|
||||
getClientCreditsDashboard,
|
||||
getPublicAuthCatalog,
|
||||
getClientProviderOffer,
|
||||
getClientReferralDashboard,
|
||||
listPublicOAuthProviders,
|
||||
@@ -174,6 +175,11 @@ describe("oemCloudControlPlane desktop auth", () => {
|
||||
loginHint: "使用 Google 登录",
|
||||
},
|
||||
],
|
||||
authPolicy: {
|
||||
required: true,
|
||||
startupTrigger: "oauth",
|
||||
primaryProvider: "google",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
@@ -203,6 +209,63 @@ describe("oemCloudControlPlane desktop auth", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("应读取公开登录目录与启动策略", async () => {
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
provider: "google",
|
||||
displayName: "Google",
|
||||
scopes: ["openid", "email"],
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
authPolicy: {
|
||||
required: true,
|
||||
startupTrigger: "oauth",
|
||||
primaryProvider: "google",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const catalog = await getPublicAuthCatalog("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(catalog).toEqual({
|
||||
providers: [
|
||||
{
|
||||
provider: "google",
|
||||
displayName: "Google",
|
||||
authorizeUrl: undefined,
|
||||
redirectUri: undefined,
|
||||
scopes: ["openid", "email"],
|
||||
enabled: true,
|
||||
loginHint: undefined,
|
||||
},
|
||||
],
|
||||
authPolicy: {
|
||||
required: true,
|
||||
startupTrigger: "oauth",
|
||||
primaryProvider: "google",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("应解析 bootstrap 中缓存的邀请开关与分享事实源", async () => {
|
||||
window.__LIME_SESSION_TOKEN__ = "session-token-001";
|
||||
|
||||
|
||||
@@ -57,6 +57,19 @@ export interface OemCloudPublicOAuthProvider {
|
||||
loginHint?: string;
|
||||
}
|
||||
|
||||
export type OemCloudAuthStartupTrigger = "none" | "oauth";
|
||||
|
||||
export interface OemCloudAuthPolicy {
|
||||
required: boolean;
|
||||
startupTrigger: OemCloudAuthStartupTrigger;
|
||||
primaryProvider?: string;
|
||||
}
|
||||
|
||||
export interface OemCloudPublicAuthCatalog {
|
||||
providers: OemCloudPublicOAuthProvider[];
|
||||
authPolicy: OemCloudAuthPolicy;
|
||||
}
|
||||
|
||||
export interface OemCloudCurrentSession extends Omit<
|
||||
OemCloudCurrentSessionLike,
|
||||
"tenant" | "user" | "session"
|
||||
@@ -181,6 +194,7 @@ export interface OemCloudBootstrapResponse {
|
||||
status: string;
|
||||
distributionChannels: string[];
|
||||
};
|
||||
authPolicy: OemCloudAuthPolicy;
|
||||
providerOffersSummary: OemCloudProviderOfferSummary[];
|
||||
providerPreference: OemCloudProviderPreference;
|
||||
skillCatalog?: unknown;
|
||||
@@ -1128,6 +1142,27 @@ function parsePublicOAuthProvider(value: unknown): OemCloudPublicOAuthProvider {
|
||||
};
|
||||
}
|
||||
|
||||
function parseAuthPolicy(value: unknown): OemCloudAuthPolicy {
|
||||
const record = isRecord(value) ? value : {};
|
||||
const startupTrigger = normalizeText(record.startupTrigger);
|
||||
|
||||
return {
|
||||
required: normalizeBoolean(record.required),
|
||||
startupTrigger: startupTrigger === "oauth" ? "oauth" : "none",
|
||||
primaryProvider: normalizeText(record.primaryProvider) ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function parsePublicAuthCatalog(value: unknown): OemCloudPublicAuthCatalog {
|
||||
const record = isRecord(value) ? value : {};
|
||||
return {
|
||||
providers: Array.isArray(record.items)
|
||||
? record.items.map(parsePublicOAuthProvider)
|
||||
: [],
|
||||
authPolicy: parseAuthPolicy(record.authPolicy),
|
||||
};
|
||||
}
|
||||
|
||||
function parseProviderOfferSummary(
|
||||
value: unknown,
|
||||
): OemCloudProviderOfferSummary {
|
||||
@@ -1486,6 +1521,7 @@ function parseBootstrap(value: unknown): OemCloudBootstrapResponse {
|
||||
value.app && isRecord(value.app) ? value.app.distributionChannels : [],
|
||||
),
|
||||
},
|
||||
authPolicy: parseAuthPolicy(value.authPolicy),
|
||||
providerOffersSummary: Array.isArray(value.providerOffersSummary)
|
||||
? value.providerOffersSummary.map(parseProviderOfferSummary)
|
||||
: [],
|
||||
@@ -2437,13 +2473,17 @@ 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 (await getPublicAuthCatalog(tenantId)).providers;
|
||||
}
|
||||
|
||||
return Array.isArray(response.items)
|
||||
? response.items.map(parsePublicOAuthProvider)
|
||||
: [];
|
||||
export async function getPublicAuthCatalog(
|
||||
tenantId: string,
|
||||
): Promise<OemCloudPublicAuthCatalog> {
|
||||
return parsePublicAuthCatalog(
|
||||
await requestControlPlane<unknown>(
|
||||
`/v1/public/tenants/${encodeURIComponent(tenantId)}/oauth/providers`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function verifyClientAuthEmailCode(
|
||||
|
||||
@@ -69,6 +69,7 @@ const mockPriorityCommands = new Set<string>([
|
||||
"install_browser_connector_extension_cmd",
|
||||
"open_browser_extensions_page_cmd",
|
||||
"open_browser_remote_debugging_page_cmd",
|
||||
"open_browser_connector_guide_window",
|
||||
"disconnect_browser_connector_session",
|
||||
"launch_browser_session",
|
||||
"launch_browser_profile_runtime_assist_cmd",
|
||||
|
||||
@@ -71,6 +71,19 @@
|
||||
"companion_launch_pet",
|
||||
"companion_send_pet_command"
|
||||
],
|
||||
"browserConnectorCommands": [
|
||||
"disconnect_browser_connector_session",
|
||||
"get_browser_connector_settings_cmd",
|
||||
"set_browser_connector_install_root_cmd",
|
||||
"set_browser_connector_enabled_cmd",
|
||||
"set_system_connector_enabled_cmd",
|
||||
"set_browser_action_capability_enabled_cmd",
|
||||
"get_browser_connector_install_status_cmd",
|
||||
"install_browser_connector_extension_cmd",
|
||||
"open_browser_extensions_page_cmd",
|
||||
"open_browser_remote_debugging_page_cmd",
|
||||
"open_browser_connector_guide_window"
|
||||
],
|
||||
"memoryRuntimeCommands": [
|
||||
"memory_runtime_get_stats",
|
||||
"memory_runtime_get_overview",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"contains": "bigmodel.cn/api/coding/paas/v4"
|
||||
},
|
||||
{
|
||||
"provider": "zhipuai",
|
||||
"provider": "zai",
|
||||
"contains": "api.z.ai/api/anthropic"
|
||||
},
|
||||
{
|
||||
@@ -20,6 +20,10 @@
|
||||
"provider": "moonshotai",
|
||||
"contains": "moonshot.ai/anthropic"
|
||||
},
|
||||
{
|
||||
"provider": "kimi-for-coding",
|
||||
"contains": "api.kimi.com/coding"
|
||||
},
|
||||
{
|
||||
"provider": "minimax",
|
||||
"contains": "minimaxi.com/anthropic"
|
||||
@@ -28,6 +32,14 @@
|
||||
"provider": "minimax",
|
||||
"contains": "minimax.io/anthropic"
|
||||
},
|
||||
{
|
||||
"provider": "alibaba-cn",
|
||||
"contains": "coding.dashscope.aliyuncs.com/apps/anthropic"
|
||||
},
|
||||
{
|
||||
"provider": "alibaba",
|
||||
"contains": "coding-intl.dashscope.aliyuncs.com/apps/anthropic"
|
||||
},
|
||||
{
|
||||
"provider": "xiaomi",
|
||||
"contains": "token-plan-cn.xiaomimimo.com/anthropic"
|
||||
|
||||
@@ -43,8 +43,14 @@ describe("providerPromptCacheSupport", () => {
|
||||
|
||||
it.each([
|
||||
"https://open.bigmodel.cn/api/anthropic",
|
||||
"https://api.z.ai/api/anthropic",
|
||||
"https://api.moonshot.cn/anthropic",
|
||||
"https://api.moonshot.ai/anthropic",
|
||||
"https://api.kimi.com/coding/",
|
||||
"https://api.minimaxi.com/anthropic",
|
||||
"https://api.minimax.io/anthropic",
|
||||
"https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
||||
"https://token-plan-cn.xiaomimimo.com/anthropic",
|
||||
])("已知官方 Anthropic 兼容 Host %s 不应误报显式缓存提示", (apiHost) => {
|
||||
expect(isKnownAutomaticAnthropicCompatibleHost(apiHost)).toBe(true);
|
||||
@@ -121,6 +127,29 @@ describe("providerPromptCacheSupport", () => {
|
||||
"https://open.bigmodel.cn/api/anthropic",
|
||||
),
|
||||
).toBe("zhipuai");
|
||||
expect(
|
||||
resolveKnownAnthropicCompatibleProvider(
|
||||
"https://api.z.ai/api/anthropic",
|
||||
),
|
||||
).toBe("zai");
|
||||
expect(
|
||||
getRegistryIdFromType(
|
||||
"anthropic-compatible",
|
||||
"https://api.kimi.com/coding/",
|
||||
),
|
||||
).toBe("kimi-for-coding");
|
||||
expect(
|
||||
getRegistryIdFromType(
|
||||
"anthropic-compatible",
|
||||
"https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
),
|
||||
).toBe("alibaba-cn");
|
||||
expect(
|
||||
getRegistryIdFromType(
|
||||
"anthropic-compatible",
|
||||
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
||||
),
|
||||
).toBe("alibaba");
|
||||
expect(
|
||||
resolveKnownAnthropicCompatibleProvider(
|
||||
"https://token-plan-cn.xiaomimimo.com/anthropic",
|
||||
|
||||
@@ -2,12 +2,10 @@ 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(),
|
||||
}),
|
||||
);
|
||||
const { mockGetPublicAuthCatalog, mockStartOemCloudLogin } = vi.hoisted(() => ({
|
||||
mockGetPublicAuthCatalog: vi.fn(),
|
||||
mockStartOemCloudLogin: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/oemCloudControlPlane", async (importOriginal) => {
|
||||
const actual =
|
||||
@@ -15,7 +13,7 @@ vi.mock("@/lib/api/oemCloudControlPlane", async (importOriginal) => {
|
||||
|
||||
return {
|
||||
...actual,
|
||||
listPublicOAuthProviders: mockListPublicOAuthProviders,
|
||||
getPublicAuthCatalog: mockGetPublicAuthCatalog,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -41,14 +39,21 @@ describe("oemCloudStartupLogin", () => {
|
||||
delete window.__LIME_OEM_CLOUD__;
|
||||
delete window.__LIME_SESSION_TOKEN__;
|
||||
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
mockListPublicOAuthProviders.mockResolvedValue([
|
||||
{
|
||||
provider: "google",
|
||||
displayName: "Google",
|
||||
enabled: true,
|
||||
scopes: ["openid", "email"],
|
||||
mockGetPublicAuthCatalog.mockResolvedValue({
|
||||
providers: [
|
||||
{
|
||||
provider: "google",
|
||||
displayName: "Google",
|
||||
enabled: true,
|
||||
scopes: ["openid", "email"],
|
||||
},
|
||||
],
|
||||
authPolicy: {
|
||||
required: true,
|
||||
startupTrigger: "oauth",
|
||||
primaryProvider: "google",
|
||||
},
|
||||
]);
|
||||
});
|
||||
mockStartOemCloudLogin.mockResolvedValue({
|
||||
mode: "desktop_auth",
|
||||
openedUrl: "https://user.limeai.run/oauth/desktop/authorize",
|
||||
@@ -70,7 +75,7 @@ describe("oemCloudStartupLogin", () => {
|
||||
const result = await startOemCloudStartupLoginIfRequired();
|
||||
|
||||
expect(result.status).toBe("started");
|
||||
expect(mockListPublicOAuthProviders).toHaveBeenCalledWith("tenant-0001");
|
||||
expect(mockGetPublicAuthCatalog).toHaveBeenCalledWith("tenant-0001");
|
||||
expect(mockStartOemCloudLogin).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseUrl: "https://user.limeai.run",
|
||||
@@ -101,20 +106,27 @@ describe("oemCloudStartupLogin", () => {
|
||||
const result = await startOemCloudStartupLoginIfRequired();
|
||||
|
||||
expect(result.status).toBe("has_session");
|
||||
expect(mockListPublicOAuthProviders).not.toHaveBeenCalled();
|
||||
expect(mockGetPublicAuthCatalog).not.toHaveBeenCalled();
|
||||
expect(mockStartOemCloudLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("后端未下发 Google Provider 时保持开源默认使用", async () => {
|
||||
configureRuntime();
|
||||
mockListPublicOAuthProviders.mockResolvedValue([
|
||||
{
|
||||
provider: "github",
|
||||
displayName: "GitHub",
|
||||
enabled: true,
|
||||
scopes: [],
|
||||
mockGetPublicAuthCatalog.mockResolvedValue({
|
||||
providers: [
|
||||
{
|
||||
provider: "github",
|
||||
displayName: "GitHub",
|
||||
enabled: true,
|
||||
scopes: [],
|
||||
},
|
||||
],
|
||||
authPolicy: {
|
||||
required: true,
|
||||
startupTrigger: "oauth",
|
||||
primaryProvider: "google",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const result = await startOemCloudStartupLoginIfRequired();
|
||||
|
||||
@@ -124,7 +136,7 @@ describe("oemCloudStartupLogin", () => {
|
||||
|
||||
it("读取后端登录策略失败时不应阻塞主应用启动", async () => {
|
||||
configureRuntime();
|
||||
mockListPublicOAuthProviders.mockRejectedValue(new Error("network down"));
|
||||
mockGetPublicAuthCatalog.mockRejectedValue(new Error("network down"));
|
||||
|
||||
const result = await startOemCloudStartupLoginIfRequired();
|
||||
|
||||
@@ -134,4 +146,51 @@ describe("oemCloudStartupLogin", () => {
|
||||
});
|
||||
expect(mockStartOemCloudLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("后端未要求启动登录时应保持开源默认使用", async () => {
|
||||
configureRuntime();
|
||||
mockGetPublicAuthCatalog.mockResolvedValue({
|
||||
providers: [
|
||||
{
|
||||
provider: "google",
|
||||
displayName: "Google",
|
||||
enabled: true,
|
||||
scopes: ["openid", "email"],
|
||||
},
|
||||
],
|
||||
authPolicy: {
|
||||
required: false,
|
||||
startupTrigger: "none",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await startOemCloudStartupLoginIfRequired();
|
||||
|
||||
expect(result.status).toBe("not_required");
|
||||
expect(mockStartOemCloudLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("后端要求的启动策略不是 Google OAuth 时不应猜测兜底", async () => {
|
||||
configureRuntime();
|
||||
mockGetPublicAuthCatalog.mockResolvedValue({
|
||||
providers: [
|
||||
{
|
||||
provider: "github",
|
||||
displayName: "GitHub",
|
||||
enabled: true,
|
||||
scopes: [],
|
||||
},
|
||||
],
|
||||
authPolicy: {
|
||||
required: true,
|
||||
startupTrigger: "oauth",
|
||||
primaryProvider: "github",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await startOemCloudStartupLoginIfRequired();
|
||||
|
||||
expect(result.status).toBe("unsupported_policy");
|
||||
expect(mockStartOemCloudLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
listPublicOAuthProviders,
|
||||
getPublicAuthCatalog,
|
||||
type OemCloudPublicOAuthProvider,
|
||||
} from "@/lib/api/oemCloudControlPlane";
|
||||
import {
|
||||
@@ -15,7 +15,9 @@ export type OemCloudStartupLoginStatus =
|
||||
| "not_configured"
|
||||
| "has_session"
|
||||
| "already_attempted"
|
||||
| "not_required"
|
||||
| "no_google_provider"
|
||||
| "unsupported_policy"
|
||||
| "started"
|
||||
| "failed";
|
||||
|
||||
@@ -38,11 +40,32 @@ function hasGoogleOAuthProvider(
|
||||
);
|
||||
}
|
||||
|
||||
function shouldStartGoogleOauth(
|
||||
catalog: Awaited<ReturnType<typeof getPublicAuthCatalog>>,
|
||||
): OemCloudStartupLoginResult | null {
|
||||
if (!catalog.authPolicy.required) {
|
||||
return { status: "not_required" };
|
||||
}
|
||||
|
||||
if (
|
||||
catalog.authPolicy.startupTrigger !== "oauth" ||
|
||||
normalizeProvider(catalog.authPolicy.primaryProvider) !== "google"
|
||||
) {
|
||||
return { status: "unsupported_policy" };
|
||||
}
|
||||
|
||||
if (!hasGoogleOAuthProvider(catalog.providers)) {
|
||||
return { status: "no_google_provider" };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasCurrentTenantSession(runtime: OemCloudRuntimeContext): boolean {
|
||||
const storedSession = getStoredOemCloudSessionState();
|
||||
return Boolean(
|
||||
storedSession?.token &&
|
||||
storedSession.session.tenant.id === runtime.tenantId,
|
||||
storedSession.session.tenant.id === runtime.tenantId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,9 +106,11 @@ export async function startOemCloudStartupLoginIfRequired(
|
||||
return { status: "already_attempted" };
|
||||
}
|
||||
|
||||
let providers: OemCloudPublicOAuthProvider[];
|
||||
let startupDecision: OemCloudStartupLoginResult | null;
|
||||
try {
|
||||
providers = await listPublicOAuthProviders(runtime.tenantId);
|
||||
startupDecision = shouldStartGoogleOauth(
|
||||
await getPublicAuthCatalog(runtime.tenantId),
|
||||
);
|
||||
} catch (error) {
|
||||
const reason =
|
||||
error instanceof Error && error.message.trim()
|
||||
@@ -95,8 +120,8 @@ export async function startOemCloudStartupLoginIfRequired(
|
||||
return { status: "failed", reason };
|
||||
}
|
||||
|
||||
if (!hasGoogleOAuthProvider(providers)) {
|
||||
return { status: "no_google_provider" };
|
||||
if (startupDecision) {
|
||||
return startupDecision;
|
||||
}
|
||||
|
||||
markStartupAttempt(runtime);
|
||||
|
||||
@@ -4845,6 +4845,7 @@ const defaultMocks: Record<string, any> = {
|
||||
},
|
||||
open_browser_extensions_page_cmd: () => true,
|
||||
open_browser_remote_debugging_page_cmd: () => true,
|
||||
open_browser_connector_guide_window: () => ({ success: true }),
|
||||
chrome_bridge_execute_command: (args: any) => ({
|
||||
success: true,
|
||||
request_id: `mock-${Date.now()}`,
|
||||
|
||||
@@ -349,6 +349,8 @@ export interface BrowserConnectorInstallResult {
|
||||
auto_config_path: string;
|
||||
}
|
||||
|
||||
export type BrowserConnectorGuideMode = "extension" | "cdp";
|
||||
|
||||
export type BrowserBackendType =
|
||||
| "aster_compat"
|
||||
| "lime_extension_bridge"
|
||||
@@ -999,6 +1001,14 @@ export async function openBrowserRemoteDebuggingPage(): Promise<boolean> {
|
||||
return safeInvoke<boolean>("open_browser_remote_debugging_page_cmd");
|
||||
}
|
||||
|
||||
export async function openBrowserConnectorGuideWindow(request: {
|
||||
mode: BrowserConnectorGuideMode;
|
||||
}): Promise<void> {
|
||||
return safeInvoke<void>("open_browser_connector_guide_window", {
|
||||
request,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 ChromeBridge 发送测试命令
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user