继续收敛首页剪贴板 IPC 错误
区分空剪贴板与原生读取失败并显示脱敏正文 补充错误处理文档
This commit is contained in:
@@ -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 正文。
|
||||
|
||||
Reference in New Issue
Block a user