merge: 合并远端 dhk-1 最新改动

This commit is contained in:
董宏凯 committed 2026-07-27 19:22:59 +08:00
commit 1e21e559fb
45 files changed
+2629 -314

No files matched your search

+4 -10
View File
@@ -1,3 +1,7 @@
import { resolveEmbedThemeMode } from "./theme-mode.mjs";
export { resolveEmbedThemeMode } from "./theme-mode.mjs";
/**
* 「外部系统 iframe 套用深链」相关的**按标签页**会话开关。承载三个相互独立的能力,多由 URL
* 路由参数开启(独立会话 `isolate` 另可由「外观」设置里的开关在运行时切换,见 `setIsolatedSession`),
@@ -92,16 +96,6 @@ export function readIsolateParam(search: string): FlagParam {
return readFlagParam(search, "isolate");
}
/**
* 把外部传入的主题色映射成离散明/暗。与 `resolveThemeFromQuery` 同源约定(dark-blue → 深色),
* 另外宽松接受 `dark` / 中文「深色/深蓝」直传;其余(含空)一律浅色。
*/
export function resolveEmbedThemeMode(raw: string | null | undefined): EmbedTheme {
const v = (raw ?? "").trim().toLowerCase();
if (v === "dark" || v === "dark-blue" || v === "深色" || v === "深蓝") return "dark";
return "light";
}
/** 取 URL query 里的 `theme` 值(embed 主题来源)。 */
export function readEmbedThemeParam(search: string): string {
return (toSearchParams(search).get("theme") ?? "").trim();
@@ -0,0 +1,19 @@
/**
* @typedef {"light" | "dark"} EmbedThemeMode
*/
/**
* Map external theme names to the two themes supported by iframe surfaces.
*
* @param {string | null | undefined} rawTheme
* @returns {EmbedThemeMode}
*/
export function resolveEmbedThemeMode(rawTheme) {
const theme = (rawTheme ?? "").trim().toLowerCase();
return theme === "dark" ||
theme === "dark-blue" ||
theme === "深色" ||
theme === "深蓝"
? "dark"
: "light";
}
@@ -2,8 +2,11 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
buildChatsIframeRedirectPath,
buildChatsIframeThreadPath,
isExplicitBrainyIframeMode,
isChatsIframeWorkspacePath,
resolvePostLoginRedirectPath,
resolveForcedThemeForPath,
resolveStandardChatFallback,
} from "./chats-iframe-route.ts";
@@ -69,3 +72,105 @@ test("redirects accidental chats_iframe routes to standard full chat", () => {
null,
);
});
test("honors the dark-blue theme query on chats_iframe routes", () => {
assert.equal(
resolveForcedThemeForPath(
"/page/workspace/chats_iframe/new",
"?from=iframe&theme=dark-blue",
),
"dark",
);
assert.equal(
resolveForcedThemeForPath(
"/page/workspace/chats_iframe/new",
"?from=iframe&theme=red",
),
"light",
);
});
test("forces the iframe theme on the login route without changing normal login", () => {
assert.equal(
resolveForcedThemeForPath(
"/login",
"?from=iframe&theme=dark-blue",
),
"dark",
);
assert.equal(
resolveForcedThemeForPath(
"/login/guest",
"?from=iframe&theme=dark-blue",
),
"dark",
);
assert.equal(
resolveForcedThemeForPath("/login", "?theme=dark-blue"),
undefined,
);
assert.equal(
resolveForcedThemeForPath(
"/page/workspace/chats/new",
"?from=iframe&theme=dark-blue",
),
undefined,
);
});
test("preserves the iframe theme in the post-login redirect", () => {
assert.equal(
buildChatsIframeRedirectPath("?from=iframe&theme=dark-blue"),
"/page/workspace/chats_iframe/new?from=iframe&theme=dark-blue",
);
assert.equal(
buildChatsIframeRedirectPath("?from=iframe"),
"/page/workspace/chats_iframe/new?from=iframe",
);
assert.equal(
buildChatsIframeRedirectPath(
"?from=iframe&theme=dark-blue&ticket=secret&authToken=secret",
),
"/page/workspace/chats_iframe/new?from=iframe&theme=dark-blue",
);
});
test("keeps the iframe redirect ahead of login-method fallbacks", () => {
assert.equal(
resolvePostLoginRedirectPath(
"?from=iframe&theme=dark-blue&yUserId=user-1",
"/page/workspace/chats/new",
),
"/page/workspace/chats_iframe/new?from=iframe&theme=dark-blue",
);
assert.equal(
resolvePostLoginRedirectPath("?yUserId=user-1", "/page/workspace/chats/new"),
"/page/workspace/chats/new",
);
});
test("preserves iframe theme when a new chat becomes a thread", () => {
assert.equal(
buildChatsIframeThreadPath(
"/page/workspace",
"thread-123",
"?from=iframe&theme=dark-blue&ticket=secret",
),
"/page/workspace/chats_iframe/thread-123?from=iframe&theme=dark-blue",
);
});
test("preserves explicit Brainy iframe mode and theme across redirects", () => {
assert.equal(
buildChatsIframeRedirectPath("?chatMode=iframe&theme=dark-blue&ticket=secret"),
"/page/workspace/chats_iframe/new?chatMode=iframe&theme=dark-blue",
);
assert.equal(
buildChatsIframeThreadPath(
"/page/workspace",
"thread-123",
"?chatMode=iframe&theme=dark-blue",
),
"/page/workspace/chats_iframe/thread-123?chatMode=iframe&theme=dark-blue",
);
});
@@ -1,6 +1,64 @@
import { resolveEmbedThemeMode } from "../embed/theme-mode.mjs";
const CHATS_IFRAME_PATH = "/page/workspace/chats_iframe/new";
const IFRAME_LOGIN_SOURCE = "iframe";
function toSearchParams(search: string): URLSearchParams {
return new URLSearchParams(search.startsWith("?") ? search : `?${search}`);
}
export function isChatsIframeLogin(search: string): boolean {
return toSearchParams(search).get("from") === IFRAME_LOGIN_SOURCE;
}
/**
* Preserve only the presentation parameters consumed by the iframe workspace.
* Login credentials must never be copied into the post-login address bar.
*/
export function buildChatsIframeRedirectPath(search: string): string {
const source = toSearchParams(search);
const target = new URLSearchParams();
if (isChatsIframeLogin(search)) {
target.set("from", IFRAME_LOGIN_SOURCE);
}
if (isExplicitBrainyIframeMode(search)) {
target.set("chatMode", "iframe");
}
const theme = source.get("theme")?.trim();
if (theme) {
target.set("theme", theme);
}
const query = target.toString();
return query ? `${CHATS_IFRAME_PATH}?${query}` : CHATS_IFRAME_PATH;
}
export function buildChatsIframeThreadPath(
routeBase: string,
threadId: string,
search: string,
): string {
const base = routeBase.endsWith("/") ? routeBase.slice(0, -1) : routeBase;
const query = buildChatsIframeRedirectPath(search).split("?")[1];
const path = `${base}/chats_iframe/${encodeURIComponent(threadId)}`;
return query ? `${path}?${query}` : path;
}
export function resolvePostLoginRedirectPath(
search: string,
fallbackPath: string,
): string {
return isChatsIframeLogin(search)
? buildChatsIframeRedirectPath(search)
: fallbackPath;
}
export function isChatsIframeWorkspacePath(pathname: string): boolean {
return (
pathname === "/page/workspace/chats_iframe/new" ||
pathname === CHATS_IFRAME_PATH ||
pathname.startsWith("/page/workspace/chats_iframe/")
);
}
@@ -54,10 +112,20 @@ export function resolveStandardChatFallback(
export function resolveForcedThemeForPath(
pathname: string,
): "light" | undefined {
if (pathname === "/landing" || isChatsIframeWorkspacePath(pathname)) {
search = "",
): "light" | "dark" | undefined {
if (pathname === "/landing") {
return "light";
}
const isLoginPath = pathname === "/login" || pathname.startsWith("/login/");
if (
isChatsIframeWorkspacePath(pathname) ||
(isLoginPath && isChatsIframeLogin(search))
) {
const rawTheme = toSearchParams(search).get("theme")?.trim();
return rawTheme ? resolveEmbedThemeMode(rawTheme) : "light";
}
return undefined;
}
@@ -187,4 +187,4 @@ test("unmapped incoming url resets auto-selected agent back to default when ther
forceDefaultAssistant: true,
},
);
});
});
@@ -504,12 +504,16 @@ export interface ModelConcurrencySettings {
enabled: boolean;
default_limit: number;
model_limits: Record<string, number>;
user_default_limit: number;
user_model_limits: Record<string, number>;
}
export interface ModelConcurrencyUpdate {
enabled?: boolean;
default_limit?: number;
model_limits?: Record<string, number>;
user_default_limit?: number;
user_model_limits?: Record<string, number>;
}
export async function loadSkillCompressionSettings(): Promise<SkillCompressionSettings> {
+43 -13
View File
@@ -322,11 +322,24 @@ function isModelConcurrencyLimitError(message: string): boolean {
return (
/\b429\b/.test(message) ||
/并发已达到上限/.test(message) ||
/并发调用已达到上限/.test(message) ||
/使用人数较多/.test(message) ||
/too many requests/i.test(message)
);
}
function friendlySendLimitMessage(message: string): string | null {
if (isActiveRunConflictError(message)) {
return "当前对话还在生成回答,请等待本轮回复结束,或先点“停止”再发送新的问题。";
}
if (isModelConcurrencyLimitError(message)) {
return /当前账号对该模型/.test(message)
? "当前账号对该模型的并发调用已达到上限,请等待已有回答结束后再试。"
: "当前使用人数较多,请稍等一下再试。";
}
return null;
}
function buildCurrentUserLabel(): string {
const auth = getStoredAuth();
if (!auth) {
@@ -684,14 +697,11 @@ export function useThreadStream({
}
return;
}
// 上一轮还没结束又发了新问题(后端 409):给一句明确的中文提示,而不是
// 把原始的英文/HTTP 错误抛给用户。
if (isActiveRunConflictError(message)) {
toast.info("当前对话还在生成回答,请等待本轮回复结束,或先点「停止」再发送新的问题。");
return;
}
if (isModelConcurrencyLimitError(message)) {
toast.info("当前使用人数较多,请稍等一下再试。");
// 上一轮还没结束又发了新问题(409)或并发达到上限(429):
// 给一句明确的中文提示,而不是把原始 HTTP 错误抛给用户。
const friendly = friendlySendLimitMessage(message);
if (friendly) {
toast.info(friendly);
return;
}
toast.error(message);
@@ -1197,11 +1207,31 @@ export function useThreadStream({
},
);
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
} catch (error) {
setOptimisticMessages([]);
setIsUploading(false);
throw error;
} finally {
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
const friendly = friendlySendLimitMessage(message);
if (friendly) {
setOptimisticMessages((messages) => {
const human = messages.find((item) => item.type === "human");
const kept = human ? [human] : [];
return [
...kept,
{
type: "ai",
id: `opt-ai-limit-${Date.now()}`,
content: friendly,
additional_kwargs: { element: "task" },
} as Message,
];
});
setIsUploading(false);
return;
}
setOptimisticMessages([]);
setIsUploading(false);
throw error;
} finally {
sendInFlightRef.current = false;
}
},