继续收敛首页剪贴板 IPC 错误

区分空剪贴板与原生读取失败并显示脱敏正文

补充错误处理文档
This commit is contained in:
kdletters
2026-10-05 22:04:29 +08:00
parent 8e9415c5d2
commit 93248f6c15
4 changed files with 52 additions and 22 deletions
@@ -16,6 +16,7 @@ import { Upload } from 'lucide-react';
import React, { useEffect, useRef } from 'react';
import RichTextInput from '../../../../components/RichTextInput';
import { visibleClientErrorMessage } from '../../../../services/errorReporting';
import type { Draft, HomeAttachmentDraft } from '../../useHomeDraftStore';
import { $createAttachmentNode, AttachmentNode } from './attachmentNode';
@@ -24,6 +25,7 @@ type RichInputAreaProps = {
placeholder: string;
onChange: (value: Draft) => void;
onEnter: () => void;
onError?: (message: string) => void;
children?: React.ReactNode;
};
const INSERT_ATTACHMENTS_COMMAND: LexicalCommand<HomeAttachmentDraft[]> =
@@ -40,7 +42,10 @@ function createAttachmentDrafts(files: readonly File[]) {
}));
}
async function readNativeClipboardImageFile() {
async function readNativeClipboardImageFile(): Promise<{
file: File | null;
error: unknown | null;
}> {
// this used Tauri native api, unavailable in browser env.
let image;
try {
@@ -54,7 +59,7 @@ async function readNativeClipboardImageFile() {
canvas.height = height;
const context = canvas.getContext('2d');
if (!context) {
return null;
return { file: null, error: null };
}
context.putImageData(
new ImageData(new Uint8ClampedArray(rgba), width, height),
@@ -65,23 +70,29 @@ async function readNativeClipboardImageFile() {
canvas.toBlob(resolve, 'image/png'),
);
if (!blob) {
return null;
return { file: null, error: null };
}
return new File([blob], `pasted-image-${Date.now()}.png`, {
type: 'image/png',
});
} catch {
return null;
return {
file: new File([blob], `pasted-image-${Date.now()}.png`, {
type: 'image/png',
}),
error: null,
};
} catch (error) {
return { file: null, error };
} finally {
await image?.close();
}
}
async function readNativeClipboardText() {
async function readNativeClipboardText(): Promise<{
text: string;
error: unknown | null;
}> {
try {
return await readText();
} catch {
return '';
return { text: await readText(), error: null };
} catch (error) {
return { text: '', error };
}
}
@@ -97,7 +108,7 @@ function selectEditableEndWhenNeeded() {
root.selectEnd();
}
function EditorPlugins() {
function EditorPlugins({ onError }: { onError?: (message: string) => void }) {
const [editor] = useLexicalComposerContext();
useEffect(
@@ -145,23 +156,30 @@ function EditorPlugins() {
PASTE_COMMAND,
(event) => {
event.preventDefault();
void readNativeClipboardImageFile().then(async (file) => {
if (!file) {
const text = await readNativeClipboardText();
if (text.length === 0) {
void readNativeClipboardImageFile().then(async (imageResult) => {
if (!imageResult.file) {
const textResult = await readNativeClipboardText();
if (textResult.text.length === 0) {
const error = textResult.error ?? imageResult.error;
if (error) {
onError?.(visibleClientErrorMessage(error));
}
return;
}
editor.dispatchCommand(INSERT_CLIPBOARD_TEXT_COMMAND, text);
editor.dispatchCommand(
INSERT_CLIPBOARD_TEXT_COMMAND,
textResult.text,
);
return;
}
const nextAttachments = createAttachmentDrafts([file]);
const nextAttachments = createAttachmentDrafts([imageResult.file]);
editor.dispatchCommand(INSERT_ATTACHMENTS_COMMAND, nextAttachments);
});
return true;
},
COMMAND_PRIORITY_HIGH,
),
[editor],
[editor, onError],
);
return null;
@@ -218,7 +236,7 @@ export default function RichInputArea(props: RichInputAreaProps) {
</span>
}
>
<EditorPlugins />
<EditorPlugins onError={props.onError} />
{props.children}
</RichTextInput>
);
@@ -153,6 +153,7 @@ export default function HomeView({
(state) => state.setRichText,
);
const [homeCreationBusy, setHomeCreationBusy] = useState(false);
const [homeInputError, setHomeInputError] = useState('');
const [planningCompletionEnabled, setPlanningCompletionEnabled] =
useState(false);
const homeCreationBusyRef = useRef(false);
@@ -271,7 +272,11 @@ export default function HomeView({
<RichInputArea
value={homeRichText}
placeholder={activeCreationType.placeholder}
onChange={setHomeRichText}
onChange={(value) => {
setHomeInputError('');
setHomeRichText(value);
}}
onError={setHomeInputError}
onEnter={() => {
void createFromHome();
}}
@@ -318,6 +323,11 @@ export default function HomeView({
</button>
</div>
</div>
{homeInputError ? (
<p className="m-0 text-xs text-red-600" role="alert">
{homeInputError}
</p>
) : null}
</RichInputArea>
</form>
{recoverableCreatedProjectPath && !creationBusy ? (
@@ -142,6 +142,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
- 资源画布历史任务账本读取属于用户可见的恢复路径:异常正文必须进入提示条;只有响应形状错误这类没有底层异常的情况才使用明确的契约提示。
- 异步业务 facade 不得把底层异常折成“空结果”来逃避错误处理;只有真实的空回包、未配置或浏览器桥不可用等无正文事实允许返回空值,Tauri/网络/IPC 异常必须上抛到统一脱敏出口。
- Direct 活动回合事件桥注册失败即使仍有一次性快照,也必须标明事件更新通道已失联并展示安全正文,不能只显示旧快照而让用户误以为活动状态持续可靠。
- 首页富文本剪贴板读取的“无内容”与“原生 IPC 失败”必须区分;后者展示安全正文,不能静默当作空剪贴板。
## 后果与边界
@@ -63,3 +63,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
- 资源画布历史生成任务账本读取失败原来统一显示“暂时无法恢复历史任务”;现在保留 Tauri/文件读取正文,非数组响应仍单独保留契约错误提示。
- 发现默认 `requestChatPromptPolish` 还会把 Tauri 润色命令异常吞成 `null`,导致共享 hook 无法看到平台正文;现仅对空回包/桥不可用保留 `null`,原生异常继续上抛并统一脱敏。
- Direct 活动回合事件桥注册失败原来只退回启动快照,面板没有说明后续更新已失联;现保留事件桥 IPC 正文并在标题栏/活动项目面板显示。
- 首页富文本输入的原生剪贴板图片/文本读取失败原来直接按空剪贴板处理;现通过输入区错误提示保留脱敏后的剪贴板 IPC 正文。