
1. QTextEdit 当运行日志输出区内存涨到多少会崩1.1 症状append 一时爽内存火葬场QTextEdit 当运行日志输出区时间一长内存狂涨最后崩掉我决定让 Codex 来改上限删行逻辑。Codex 的模型通道我接的是 TaoToken在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentqtextedit_codex 创建 Key 后就能用。接下来按原始需求把 blockCount() 超限判断和 QTextCursor 删除逻辑交给 Codex 检查。本文记录完整排障过程。最早遇到这个问题是在一个 Qt 桌面工具里把 QTextEdit 当作调试输出窗。为了不影响交互控件设置了setReadOnly(true)然后所有日志都走同一个追加函数。一开始很爽textEdit-append()每次自动换行时间戳拼在行首滚动输出跟终端一模一样。但程序连续跑三四个小时后任务管理器里看到进程内存从几十 MB 一路涨到 1.2GB 甚至更多最后 Qt 直接弹出崩溃对话框。拆开看原因并不复杂。QTextEdit内部用QTextDocument按 block 组织文本每次append都会新增一个 block。只要程序不主动清理block 数量就只增不减对应的文本、格式、布局缓存全部驻留内存。日志输出越频繁内存涨得越快最终把堆内存耗尽。1.2 为什么简单的clear()不解决问题有人说那就定时clear()清空好了但这会把正在排查的历史日志全部丢掉用户刚想回看前面的报错就没有了。我们要的是“超过最大行数只删除最旧的行”保留最近几千行。这就需要精确控制QTextCursor的选区选中文档开头到“应删除的最后一行”之间的所有文本然后调用removeSelectedText()。判断行数靠document-blockCount()这是 QTextDocument 给你现成的行数计数器。设一个maxRowNumber当blockCount()超过它就删除前面的rowCount - maxRowNumber行。逻辑听起来简单真正写起来却不顺手如果你用cursor.select(QTextCursor::LineUnderCursor)或BlockUnderCursor去选会发现每次只能删掉第一行删完第一行还在留下一个空段落程序还照样继续涨。这个坑让我决定把代码交给 Codex 审一遍而不是自己继续按直觉试。要让 Codex 干活得先解决模型通道问题。2. 让 Codex 有模型可用TaoToken 的 Key 和 Base URL 这样配2.1 在官网注册并创建 API KeyCodex 是一个靠模型驱动的排障助手但模型接口的额度、可用性经常让人头疼。我这边接的是 TaoToken 作为统一 API 通道先打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentqtextedit_codex 注册账号进入控制台创建 API Key。创建成功后Key 会显示一次复制下来保存为YOUR_API_KEY。这个 Key 等会儿要填到 Codex 的配置里不要截图、不要贴到公开代码仓库。还需要记一个重要的地址https://taotoken.net/api。注意它末尾没有/v1很多 AI 工具默认会在 Base URL 后面补/v1但这里不需要你手动加按原样填就行。官网落地页和处理模型调用的接口地址是两回事前者是给人看的控制台和模型广场用来创建 Key、看用量后者是给代码填的 API 地址不能混在一起。后面排障时如果遇到 404先回来检查是不是把页面地址https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentqtextedit_codex当成接口地址填了。2.2 Codex 的 config.toml 接入配置Codex 支持通过配置文件指定模型供应商。我这边用的是本地~/.codex/config.toml在原来内容基础上新增一个 provider 块再把默认模型指到 TaoToken 的模型 ID 上。示例如下model MODEL_ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat其中MODEL_ID不要自己编以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentqtextedit_codex 的模型广场当时列出的 ID 为准。不同时间上架的模型可能不同直接复制列表里的标识符最稳。配置文件写好后在终端里导出环境变量export TAOTOKEN_API_KEYYOUR_API_KEY再启动 Codex就能看到它通过 TaoToken 通道请求模型了。如果不想用环境变量也可以把 Key 直接写进env_key对应的字段但那样 Key 会以明文存在配置文件里安全性差一些。我习惯用环境变量切换 Key 时不用改文件。3. 把“超限删行”这个需求精确说给 Codex3.1 我给 Codex 的问题描述模型通道通完接下来才是正事。我打开 Codex 对话把原始需求拆成几个要点贴给它没有让它自由发挥。问题描述是这样写的“我的 Qt 程序用 QTextEdit 作为日志输出区控件是只读的。每次追加一行日志调用 append()长时间运行后 document 的 blockCount() 超过十万行内存到 1.2GB 会崩溃。我想在追加函数里设置上限当 blockCount() 大于 maxRowNumber 时删除最前面的 rowCount - maxRowNumber 行。之前我尝试用 QTextCursor 从文档开头向下移动并选中要删的行但发现使用 select(QTextCursor::LineUnderCursor) 或者 BlockUnderCursor 只能删除第一行第一行删除后还是一个空行。请按照这个思路检查代码在 append 之后用 QTextCursor(document) 定位到 Start然后循环执行 movePosition(QTextCursor::Down, QTextCursor::KeepAnchor)次数等于 rowCount - maxRowNumber最后 removeSelectedText()。帮我确认这样做是否正确并给出完整的函数实现。”把问题描述得越具体Codex 越不容易跑偏。它不需要你告诉它什么是 block什么是 QTextEdit你只需要把现象、已有思路、卡住的点说清楚。3.2 Codex 给出的关键修正Codex 看完后没有直接甩代码而是先指出一个关键点不能在append()之前用旧的textCursor()去删行因为此时光标可能已经被滚动条和用户操作带到了末尾选区起点不在文档开头。应该基于QTextDocument新建一个独立的光标对象这样删除动作不受界面上焦点位置影响。然后它给出了修正后的代码核心结构是先追加日志再取document-blockCount()如果超限创建QTextCursor(document)移动到Start用Down KeepAnchor逐行向下选中需要删除的行最后removeSelectedText()。这个思路和原始方案一致但放弃了select(LineUnderCursor)的写法因为那一类选择方式不包含换行符。下面是 Codex 生成的完整追加函数void MainWindow::textEditAppend(const QString text) { // 1. 追加日志光标先移到末尾 QTextCursor cursor ui-textEdit-textCursor(); cursor.movePosition(QTextCursor::End); ui-textEdit-setTextCursor(cursor); ui-textEdit-append(QDateTime::currentDateTime() .toString(yyyy/MM/dd hh:mm:ss ) text); // 2. 获取文档并检查行数 QTextDocument *document ui-textEdit-document(); const int rowCount document-blockCount(); const int maxRowNumber 5000; // 3. 超过上限则删除最早的行 if (rowCount maxRowNumber) { QTextCursor deleteCursor(document); deleteCursor.movePosition(QTextCursor::Start); // 向下选中要删除的行数KeepAnchor 使得选区覆盖换行符 for (int i 0; i rowCount - maxRowNumber; i) { deleteCursor.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor); } deleteCursor.removeSelectedText(); } }这段代码跟原始思路的最大区别是删行光标是从document直接构造的而不是从ui-textEdit-textCursor()复制来的。QTextCursor(document)默认位置在文档开头省掉了movePosition(Start)这一步语义也更清楚。后面那个 for 循环的次数是rowCount - maxRowNumber这意味着只删超出上限的部分保留最近maxRowNumber行。4. 为什么不能只用select(LineUnderCursor)删行4.1 换行符才是删不干净的真凶Codex 在代码里特意用了Down KeepAnchor而不是select(QTextCursor::LineUnderCursor)原因隐藏在 QTextCursor 的选择规则里。LineUnderCursor和BlockUnderCursor只把光标所在块的文本选中不会把块尾的换行符纳入选区。于是第一次删除时第一行的文字被拿掉但段落的结束符还在QTextEdit 里就留下一个空段落。下一次再执行同样的逻辑空段落又变成“第一行”继续删它后面的内容始终一动不动。如果用Down KeepAnchor情况就不一样。每次向下移动一行时KeepAnchor会把移动过程中经过的换行符一并纳入选中范围。比如要删 3 行选区就会从文档开头覆盖到第三行尾部包含行尾的换行符这样removeSelectedText()才能真正把这三行以及它们之间的分隔符全部移除。这就是原始文章里提到的注意事项对删除“整行”来说单纯按照“行”去选择文本是不行的必须把换行符也选进来。Codex 理解了这个需求后没有选择更复杂的正则匹配而是用最原始的movePosition循环反而稳定。4.2 边界情况最后一行没有换行符会怎样还有一个容易被忽略的点如果日志最后一行后面没有换行符blockCount()统计行数时会怎样QTextDocument 的 block 是以换行符为分隔的文本末尾没有换行符时最后一段也算一个 block。这意味着你看到 5000 行日志blockCount()可能是 5001 甚至更多因为初始空文档也有一个空 block。所以maxRowNumber的具体数值要留一点余量。我在程序里把maxRowNumber设为 5000实际运行时 blockCount 会稳定在 5000 多一点不再无限上涨。Codex 也提醒过如果担心最后保留的行数比预期少一行可以在删除后手动清理文档开头可能残留的空 block方法是把光标移到Start用Down KeepAnchor选中一个空行再删除一次。但对于日志窗口来说少一行多一行无所谓我就没做这一步。5. 验证与排障从跑满内存到稳定运行5.1 验证步骤先压测再回归代码改完之后我没有直接让它跑三小时验证。先用一个临时小循环模拟高频日志把maxRowNumber临时改成 200方便观察效果。写一个定时器每 20 毫秒调用一次textEditAppend()内容随意总共调用 2000 次。每 200 次之后记录ui-textEdit-document()-blockCount()和进程内存。如果blockCount()稳定在 201 左右内存没有持续增长说明删除逻辑生效。再把maxRowNumber改回 5000接上真实日志源跑一晚上。关键看一个指标blockCount()是否被限制在上限附近。如果它一直往上走说明if (rowCount maxRowNumber)这个分支根本没进去或者是删行代码写错了地方。我在测试时故意把removeSelectedText()注释掉结果内存照涨验证了删除动作是必须的。5.2 排障Codex 调用不通时先查这三处在让 Codex 跑代码之前模型通道本身也可能出问题。我这里整理了几个实际遇到过的坑环境变量没生效在终端里执行echo $TAOTOKEN_API_KEY如果输出为空说明export没有执行或者你新开了一个终端导致变量丢失。Codex 会报 401 认证失败。Base URL 末尾被加了/v1Codex 有些版本会自动拼/v1而 TaoToken 的 Base URL 是https://taotoken.net/api末尾没有/v1。如果报 404先去掉配置文件里的/v1再试。模型 ID 写错Codex 报 model not found 时去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentqtextedit_codex 的模型广场复制最新 ID不要凭记忆输入。还有一次我遇到的是端口代理冲突但那属于本机网络环境问题和 TaoToken 无关。换了一个干净的无线网络后就正常了。总之要先确认 Key 能用、Base URL 能连通再让 Codex 去改 QTextEdit 的代码不要同时排查两个问题。6. 收尾把这次改动固化成长期方案日志窗口的 QTextEdit 内存崩溃问题本质是“只增加不删除”造成的。加上行数上限和 QTextCursor 删除逻辑后程序连续跑了一周多内存占用稳定在几十 MB再没出现过日志区把进程拖垮的情况。Codex 在这个过程里帮我确认了Down KeepAnchor的选法比我自己记着的LineUnderCursor靠谱很多。如果你也准备让 Codex 用这种思路改代码建议先去 TaoToken 模型对话 发一条测试消息确认这把 Key 能正常返回结果日常写代码量大的话可以看一眼 Coding Plan 和 控制台 API Keys 的用量记录。Key 创建后从控制台复制模型 ID 始终以模型广场为准不要用网上文章里的旧 ID。这样排查代码时你只需要关心 QTextCursor 选中的是不是真的“整行”而不是在接口报错上浪费时间。