[Bug][macOS] 输入框重做会重复恢复先前文本,且撤销范围过大

未关闭
#3,744 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
48/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃
技术栈
react, typescript
领域
frontend, testing

调研方向

Start with ChatInput.tsx around the undo handler, send-time clearing, and draft restoration, then trace value synchronization in RichTextInput.tsx and review RichTextInput.test.tsx. The isolated WKWebView reproduction shows old text can return after clear and undo/redo, but real keyboard and IME behavior still needs product-level validation. Done means regression coverage confirms submitted text stays out of new drafts and undo grouping works across typing, IME, images, and draft or session changes.

由索引模型根据 Issue 内容生成。

描述

在 macOS 的聊天输入框中,按一次 ⌘Z 后发现撤销范围过大,再按一次 ⌘⇧Z,输入框出现了大量以前输入过的内容,并有重复拼接。另一个经常遇到的现象是:想撤销刚输入的几个字,结果整行文字被一起撤销。

期望:undo/redo 只影响当前草稿的编辑;先前已提交的文本不应在新草稿中重新出现。撤销粒度应可预期,尤其是中文输入法分次上屏、停顿后继续输入时。

下面附有独立系统 WKWebView 的最小复现、源码定位与候选修复方向。底层历史污染已经复现;真实键盘/中文 IME 的具体事务分组,以及产品内跨会话切换流程仍需维护者验收。

用户实际现象

输入框内先按 ⌘Z,撤销内容多于预期;紧接着按 ⌘⇧Z,当前草稿被大量先前输入的文本污染,包含重复内容。没有通过对话回滚按钮触发。

已运行的独立 WKWebView 最小复现

使用同机系统 WKWebView、空白 contenteditable 与原生 execCommand,不依赖 React 或 OpenBitFun 后端。复现序列:

  1. insertText('OLD')。
  2. editor.textContent = '',模拟提交后清空。
  3. insertText('NEW')。
  4. 一次 undo,一次 redo。
  5. 实际得到 OLDOLDNEW,而不是 NEW。

连续输入旧消息并清空三轮后,重做结果是:

旧消息第一轮旧消息第一轮旧消息第二轮旧消息第二轮旧消息第三轮旧消息第三轮当前草稿

把清空方式改成 replaceChildren() 也能复现;改成非空 textContent 替换后,旧文本也可能混入恢复后的草稿。

以下 JS 应在独立空白 WKWebView 中执行:

(async () => {
  const editor = document.createElement('div');
  editor.contentEditable = 'true';
  document.body.appendChild(editor);
  const pause = () => new Promise(resolve => setTimeout(resolve, 60));
  const insert = async text => {
    editor.focus();
    const range = document.createRange();
    range.selectNodeContents(editor);
    range.collapse(false);
    const selection = getSelection();
    selection.removeAllRanges();
    selection.addRange(range);
    document.execCommand('insertText', false, text);
    await pause();
  };
  for (const text of ['旧消息第一轮', '旧消息第二轮', '旧消息第三轮']) {
    await insert(text);
    editor.textContent = '';
    await pause();
  }
  await insert('当前草稿');
  document.execCommand('undo');
  await pause();
  document.execCommand('redo');
  await pause();
  return editor.textContent;
})();
撤销粒度的关联实验
  • 连续三次 insertText('我刚刚')、insertText('输入了')、insertText('这一整行'),一次 undo 清空三段。
  • 段间停顿约 1.6 秒,第一次 undo 仍同时撤销第二、三段,第二次才撤销第一段。
  • 这些实验验证了 WebKit API 的事务合并,不等同于已经完成 OpenBitFun 真实物理键盘/中文 IME 上屏的 E2E 测试。
与产品实现吻合的源码路径

以下链接固定到 v1.0.8 commit:

  • ChatInput.tsx:5783:无有效图片可撤销时,明确放行浏览器原生文本 undo;没有自管文本 redo/事务边界。
  • ChatInput.tsx:5449:发送时清空 composer 值。
  • RichTextInput.tsx:1573:同步外部值时,清空直接使用 editor.textContent = '';非空替换最终也直接修改 DOM。
  • ChatInput.tsx:1994 恢复会话草稿,而 6579 行 的编辑宿主仍被复用。因此存在跨草稿历史隔离缺口;这里是源码发现,未现场复现产品 A→B 切换。
  • RichTextInput.test.tsx:48:现有 jsdom 测试不能覆盖真实 WebKit 原生 undo 栈。

上游 main 对照固定到 02a38b01aadf162086c9c76703934e397e7a7703 时,输入组件的这些历史/清空逻辑仍在:RichTextInput。

候选修复方向与回归

另一个隔离实验将旧 contenteditable 整个移除并创建新宿主,再输入当前草稿;连续 undo/redo 到栈尽头都没有将旧文本带回新宿主。这支持在提交清空/草稿替换处建立真正的编辑历史边界。仅 key={sessionId} 不覆盖同一会话内连续发送;最小实验也不能替代产品中的焦点、选择、IME、胶囊和异步草稿恢复验收。

长期需要明确编辑事务的所有权和分组规则,统一文本、IME、粘贴和附件的撤销/重做。补充源码发现:当前独立图片 undo 栈会优先于最近输入的文本被消费,且没有对应图片 redo,可一并检查顺序一致性。

建议回归:

  • 同一会话连续发送多轮 → 输入新稿 → undo/redo,旧消息不得复活。
  • 草稿/会话 A→B、外部整稿恢复后的历史隔离。
  • 真实中文 IME 多次上屏、输入停顿、移动光标后的撤销范围。
  • 图片粘贴后继续打字,undo/redo 应按编辑顺序工作。
  • 键盘快捷键与 macOS 编辑菜单两条入口。
Environment
  • OpenBitFun 1.0.8 / cbe1fb8972b22609fcd2f5d7fc1ac703e18b1d2d
  • macOS 27.2 (26B123), arm64
  • System WebKit 22625.2.8.11.9
  • Harness: Swift 6.2.3, macOS SDK 26.2, AppKit + WebKit
  • No model/provider involved; no model request was made.
主要语言
Rust
星标
2.3k
派生
236
平均合并
13 小时 3 分钟
30 天内合并 PR
351

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

GCWing/OpenBitFun 的其他 Issue

查看 GCWing/OpenBitFun 的全部 Issue

相似的 Issue

更多 Rust Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。