Merge branch 'feat/2.6.0' into feat/2.6.0-cofco

This commit is contained in:
dolphin
2026-08-03 22:13:29 +08:00
8 changed files with 52 additions and 5 deletions
@@ -52,11 +52,15 @@ const KNOWN_TYPES = new Set([
// they get the calm neutral ServiceBusyNotice (+ optional retry) rather than the red
// failure card — a rate limit is the model vendor's availability blip, not a fault.
// Mirrors the classifier's RETRYABLE bucket.
//
// `file_parse_busy` is deliberately NOT here despite being recoverable: the turn is
// already lost (the attachment never made it into the prompt), so a Retry button
// would re-run against the same throttled service. It keeps its own "busy" wording
// but renders as the red card, and the user re-sends from the input box.
const TRANSIENT_TYPES = new Set([
'rate_limit',
'network_timeout',
'service_unavailable',
'file_parse_busy',
]);
/** Would this failure render as the calm busy notice rather than the red card?
+34 -1
View File
@@ -11,6 +11,7 @@ import { QueryKeys, dataService } from "~/types/chat";
import { addConversation, updateConvoFields } from "~/utils";
import store from "~/store";
import { useLocalize } from "~/hooks";
import { useToastContext } from "~/Providers";
import type { ChatMessage } from "~/api/chatApi";
import { getAgentMessages } from "~/api/chatApi";
import useAiChatSSE, { type SSESubmission } from "~/hooks/useAiChatSSE";
@@ -21,8 +22,22 @@ import { SopStatus, taskModeSkillsState } from "~/store/linsight";
const NO_PARENT = "00000000-0000-0000-0000-000000000000";
/** The fields of an input-box attachment that decide whether it can be sent.
Backends disagree on the path key (filepath / file_path / file_url), so all
three count as "the upload landed somewhere we can point at". */
interface OutgoingAttachment {
filepath?: string;
file_path?: string;
file_url?: string;
valid?: boolean;
name?: string;
filename?: string;
file_name?: string;
}
export default function useAiChat(initialConversationId: string = "new", isLingsi: boolean = false, shareToken: string = "") {
const localize = useLocalize();
const { showToast } = useToastContext();
// --- Local state ---
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [conversationId, setConversationId] = useState(initialConversationId);
@@ -165,6 +180,24 @@ export default function useAiChat(initialConversationId: string = "new", isLings
if (!text.trim() || isStreaming) return;
const taskMode = !!opts?.taskMode;
// An attachment whose upload never landed carries no storage path. It
// used to be sent anyway: the backend stored it as an attachment with
// no object name, the model never saw it, and on render the bubble
// showed "图片已失效" — so the user got an answer that quietly ignored
// one of their files. Block the send and name the offender instead.
const stranded = ((files ?? []) as OutgoingAttachment[]).filter(
(f) => f && (f.valid === false || !(f.filepath || f.file_path || f.file_url)),
);
if (stranded.length) {
showToast({
message: localize("com_error_file_upload_incomplete", {
0: stranded.map((f) => f.name || f.filename || f.file_name || "").join("、"),
}),
status: "error",
});
return;
}
// Drop client-only fields (e.g. the local `previewUrl` blob string used
// for input-box image previews) before they reach the message state or
// the SSE payload — the backend only cares about the file ids/paths.
@@ -620,7 +653,7 @@ export default function useAiChat(initialConversationId: string = "new", isLings
setIsStreaming(true);
setSseSubmission(submission);
},
[conversationId, isStreaming, chatModel, selectedOrgKbs, searchType, selectedAgentTools, dailyTaskSkills, isLingsi, createLinsight, updateLinsight, localize, queryClient]
[conversationId, isStreaming, chatModel, selectedOrgKbs, searchType, selectedAgentTools, dailyTaskSkills, isLingsi, createLinsight, updateLinsight, localize, showToast, queryClient]
);
// --- Stop generating ---
@@ -314,6 +314,7 @@
"com_endpoint_top_p": "Top P",
"com_endpoint_use_active_assistant": "Use Active Assistant",
"com_error_expired_user_key": "Provided key for {{0}} expired at {{1}}. Please provide a new key and try again.",
"com_error_file_upload_incomplete": "\"{{0}}\" hasn't finished uploading. Remove it and upload again before sending.",
"com_error_files_dupe": "Duplicate file detected.",
"com_error_files_empty": "Empty files are not allowed.",
"com_error_files_process": "An error occurred while processing the file.",
@@ -300,6 +300,7 @@
"com_endpoint_top_p": "Top P",
"com_endpoint_use_active_assistant": "アクティブなアシスタントを使用",
"com_error_expired_user_key": "提供された {{0}} キーは {{1}} に有効期限が切れました。新しいキーを提供して再試行してください。",
"com_error_file_upload_incomplete": "「{{0}}」のアップロードが完了していません。削除して再アップロードしてから送信してください。",
"com_error_files_dupe": "重複ファイルを検出しました",
"com_error_files_empty": "空のファイルのアップロードは許可されていません",
"com_error_files_process": "ファイル処理中にエラーが発生しました",
@@ -182,7 +182,7 @@
"12044": "使用的应用未上线",
"12045": "部门同时在线会话数已达上限,请稍后再试",
"12046": "当前使用人数较多,请稍后再试。",
"12047": "文件解析失败,请稍后重试",
"12047": "文件解析失败,并发过大,请稍后重试",
"12060": "消息不存在或已删除,请刷新会话",
"12061": "单次最多选中 200 条消息",
"12062": "消息不存在或无访问权",
@@ -303,6 +303,7 @@
"com_endpoint_top_p": "Top P",
"com_endpoint_use_active_assistant": "使用激活的助手",
"com_error_expired_user_key": "您提供的 {{0}} 密钥已于 {{1}} 过期。请提供新的密钥并重试。",
"com_error_file_upload_incomplete": "「{{0}}」尚未上传完成,请移除后重新上传再发送",
"com_error_files_dupe": "检测到重复文件",
"com_error_files_empty": "不允许上传空文件",
"com_error_files_process": "处理文件时发生错误",
@@ -160,7 +160,14 @@ export function AddSourceDropdown({
const isInsideOtherDialog = (node: Node | null) => {
if (!(node instanceof Element)) return false;
const targetDialog = node.closest('[role="dialog"], [role="alertdialog"]');
return targetDialog != null && targetDialog !== rootDialog;
if (targetDialog) return targetDialog !== rootDialog;
// A modal's full-screen mask is a SIBLING of its [role=alertdialog]
// node inside the same portal layer, so `closest` can never see it.
// Without this, clicking the mask of the stacked "link unrecognized"
// confirm read as "clicked outside" and collapsed this panel — which
// looked like the click had passed through the mask.
const layerDialog = node.parentElement?.querySelector('[role="dialog"], [role="alertdialog"]');
return layerDialog != null && layerDialog !== rootDialog;
};
const closePanel = () => {
@@ -182,7 +182,7 @@
"12044": "使用的应用未上线",
"12045": "部门同时在线会话数已达上限,请稍后再试",
"12046": "当前使用人数较多,请稍后再试。",
"12047": "文件解析失败,请稍后重试",
"12047": "文件解析失败,并发过大,请稍后重试",
"12060": "消息不存在或已删除,请刷新会话",
"12061": "单次最多选中 200 条消息",
"12062": "消息不存在或无访问权",