news 2026/11/5 19:19:07

Qt环境下bsdiff增量更新:从移植到qlz压缩的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt环境下bsdiff增量更新:从移植到qlz压缩的完整实践

简介:面向需要在 Qt 应用中实现差分升级的开发者,这份资源将开源工具 bsdiff 移植到 Qt 环境,并使用快速压缩算法 qlz 生成 patch 包。其基本思路是比较新旧版本文件,只提取差异部分并压缩输出,特别适合嵌入式、网络带宽受限或需要轻量级增量更新的场景。资源包共 705 个文件,压缩后约 6.22MB;除 675 个 idx 索引文件外,还包含 cpp/h 源码、ui 界面、pro 工程与 makefile 等构建配置,以及 bin 示例文件和可执行程序,覆盖算法封装、界面调用、编译构建、补丁生成与验证的完整链路。已有 496 人学习下载,参考价值明确。借助其中的 diffUpgradeWidget 界面模块和 old.bin、new.bin 等示例数据,可以清晰理解 bsdiff 差异比较与 qlz 压缩在补丁生成、合并流程中的具体配合方式,为自有 Qt 工程引入增量升级能力提供可直接参照的移植范本。

1. Qt 里跑通 bsdiff 增量更新:不只是把 C 代码编进工程

做客户端增量更新的工程师,大概率都听过 bsdiff 这个名字。算法老、差分效果好、生成的 patch 比整包小一个数量级,这是它这么多年没被淘汰的原因。但真把 bsdiff 塞进 Qt 工程,事情就没那么顺了——原版代码是纯 C 风格,依赖 bzip2 做二次压缩,内存分配策略又糙,直接编进 Qt 项目会踩出一串编译和运行时的问题。

这份资源的核心,是把 bsdiff 移植到 Qt 环境下,生成 patch 包,并把二次压缩算法从 bzip2 换成 qlz。qlz 的压缩率略低于 bzip2,但压缩速度快一个量级,解压速度更是快得明显,非常适合客户端做增量包的场景。适合谁?手里有 Qt 跨平台客户端、想自己做差分升级、又不想引入一堆第三方运行时的人。下文从移植过程、接口设计、qlz 接入到 patch 校验,完整过一遍,最后给你看几个掉过坑的排查记录。

2. 移植前先摸清 bsdiff 的底:核心流程与 qlz 选型理由

2.1 bsdiff 为什么快:suffix sorting 与三元组编码

bsdiff 的思路其实不复杂:把旧文件拆成一个个 block,用 suffix sorting 算法建立索引,然后在新文件里找最长的匹配片段,匹配不上的部分记录为插入数据。patch 文件里最终保存的是三类指令——额外数据、差分数据和长度信息,应用 patch 时按这些指令把旧文件重组成新文件。

这一步是整个差分算法的重头戏,原版用的是 divsufsort 做后缀排序,复杂度接近 O(n)。移植到 Qt 时要注意,bsdiff 内部有自己的内存分配宏,默认走 malloc/free,而 Qt 工程通常启用了异常和特定的 new/delete 行为,混着用没问题,但如果全局重载过 new,就要保证 bsdiff 的分配不走 Qt 的内存池,否则后续排查内存问题时非常难受。

另外,bsdiff 的输出是二进制格式,没有做任何容错设计。patch 文件头里写的是 Ctrl 块长度、Diff 块长度、New 文件大小三个 64 位整数。移植时最好保留这个格式不动,因为应用端要严格按这个格式读,改动格式会导致旧版本客户端无法升级。

2.2 qlz 替换 bzip2 的三个理由

原版 bsdiff 用 bzip2 压缩 diff 数据,压缩率高但慢。qlz(QuickLZ)是压缩速度导向的算法,level 1 压缩速度能到几百 MB/s,解压速度更快,而且解压不需要额外分配大块内存。对于客户端升级场景来说,压缩在服务端做,慢一点无所谓,但解压在用户机器上做,快就是实打实的体验提升。

还有一层考虑是依赖体积。bzip2 库本身不大,但要为每个平台交叉编译一遍,而 qlz 是单文件实现,核心就一个 .c 和一个 .h,往 Qt 工程里加非常省事。对于纯 Qt 项目,少一个外部依赖就少一组配置问题,这是选 qlz 很现实的理由。

2.3 移植时改了什么:从 C 到 C++/Qt 的适配点

bsdiff 原版的 main 函数是面向命令行的,参数解析、文件读写全是标准 C 库。移植到 Qt 后,需要拆成可复用的库接口,IO 部分换成 QFile 或自定义回调,这样 patch 的生成逻辑可以嵌入到服务端工具里,也可以做成 GUI 程序的一个按钮动作。

我一般会把 bsdiff 拆成两个层次:核心 diff 引擎保持纯 C,不依赖 Qt;外层包一层 C++ 类,处理文件路径、进度回调、错误码映射。这样核心算法可以单独单元测试,外层逻辑方便 Qt 的信号槽集成。

核心适配点如下:

  • 替换 malloc/free 为可配置的分配回调(可选,默认保持原样)
  • 把文件读写改为回调函数,方便接入 QFile 或网络流
  • 移除全局状态,避免多次调用 bsdiff 时静态变量互相干扰
  • 增加错误码,替代原版的直接 exit
// bspatch_core.h 核心接口设计(节选) typedef struct { void* (*malloc_fn)(size_t size); void (*free_fn)(void* ptr); } bsdiff_allocator_t; typedef struct { int (*read_old)(void* userdata, const unsigned char* buf, int64_t len); int (*write_patch)(void* userdata, const unsigned char* buf, int64_t len); } bsdiff_io_t; int bsdiff_core(const char* old_file, const char* new_file, const char* patch_file);

上面这段接口定义了内存分配器和 IO 回调。bsdiff_core 是对外的主入口,内部会创建 suffix sort 索引、做匹配、写 patch 文件。注意 read_old 的返回值为 0 表示成功,非 0 表示读取失败,这个约定要和 Qt 的 QFile 错误处理对齐,避免错误码语义混淆。

2.4 Qt 工程内的目录结构与 .pro 配置

把 bsdiff 源码放进 Qt 工程时,推荐单独建一个 ThirdParty 目录,不跟业务代码混在一起。目录结构大致是这样:

ThirdParty/ bsdiff/ include/ bsdiff.h bspatch.h qlz.h src/ bsdiff.c bspatch.c qlz.c bsdiff.pri

.pri 文件里要处理的事情有几件:头文件路径、源文件列表、还有针对 C 文件的编译选项。bsdiff.c 和 bspatch.c 是 C 文件,如果工程用 C++ 编译,要么改文件后缀为 .cpp,要么在 .pro 里用 QMAKE_CFLAGS 单独指定。我建议保留 .c 后缀,在 .pri 里单独加配置,这样以后升级源码版本时可以直接替换文件,不用动代码。

# bsdiff.pri INCLUDEPATH += $$PWD/include SOURCES += \ $$PWD/src/bsdiff.c \ $$PWD/src/bspatch.c \ $$PWD/src/qlz.c # C 文件按 C99 标准编译,防止旧语法告警 QMAKE_CFLAGS += -std=c99 -w

注意,如果工程启用了 C++11 或更高标准,不要把这个 CFLAGS 加到全局,否则会影响其他 C++ 文件的编译。写在 .pri 里只对当前模块生效,这是最稳妥的做法。另外,-w 参数是关闭所有告警,如果你的团队要求零告警,可以去掉 -w 然后逐个处理告警,但 bsdiff 的源码比较老,可能会有几百条风格类告警,酌情处理。

3. 把 bsdiff 封装成 Qt 接口:生成 patch 与进度回报

3.1 PatchGenerator 类设计:信号槽、取消机制、错误码

封装 bsdiff 成 Qt 类,主要是为了三件事:把耗时的差分计算放到后台线程、把进度反馈给界面、允许用户在计算过程中取消。这三件事在纯 C 的 bsdiff 里都没有,需要自己补。

// patchgenerator.h class PatchGenerator : public QObject { Q_OBJECT public: explicit PatchGenerator(QObject* parent = nullptr); void setFilePair(const QString& oldFile, const QString& newFile); void setOutputPath(const QString& patchFile); void setCompressLevel(int level); // qlz level: 1~3 public slots: void start(); void cancel(); signals: void progressUpdated(int percent); void finished(const QString& patchPath, qint64 patchSize); void failed(int errCode, const QString& errMsg); private: QString m_oldFile; QString m_newFile; QString m_patchFile; int m_compressLevel; bool m_cancelled; };

这个类把差分计算封装成一个任务。start() 里会 new 一个线程跑 bsdiff_core,跑完通过信号把结果传回主线程。cancel() 是给用户取消用的,实际实现里不是立即中断计算,而是设置一个 flag,让核心代码在每处理完一块数据后检查一次,这样能保证退出时不会留下一半的 patch 文件。

信号设计上,finished 带 patch 路径和大小,方便上层直接展示给用户;failed 带错误码和描述,错误码从 bsdiff 内部的错误枚举映射过来。这样界面层不用关心 bsdiff 内部逻辑,拿到错误码直接查表显示即可。

3.2 多线程执行与进度计算:避免 UI 卡死的实现细节

bsdiff 的耗时集中在 suffix sorting 阶段,文件越大耗时越长,几百 MB 的文件跑几十秒很正常。如果不放后台线程,界面直接假死。进度计算也不是简单的按字节算,因为大部分时间花在排序和匹配上,这两阶段的时间占比在文件内容不同时差异很大。

void PatchGenerator::start() { QtConcurrent::run([this]() { int ret = bsdiff_core(m_oldFile.toUtf8().constData(), m_newFile.toUtf8().constData(), m_patchFile.toUtf8().constData()); if (ret == 0) { QFile f(m_patchFile); emit finished(m_patchFile, f.size()); } else { emit failed(ret, QStringLiteral("bsdiff failed with code %1").arg(ret)); } }); }

QtConcurrent::run 是这里最省事的方案,不用手动管理 QThread 生命周期。要注意的是,m_oldFile 这些成员变量在这个 lambda 里被读取,如果用户在任务执行期间再次调用 setFilePair,会引发数据竞争。一个简单的做法是在 start() 内部做一次拷贝,后续逻辑只操作局部变量,这样即使主线程改了成员变量,也不会影响正在执行的任务。

进度计算方面,bsdiff 原版没有回调接口,我把 bsdiff_core 内部加了一个可选的 progress_callback,在排序阶段每处理完 10% 的数据回调一次,匹配阶段按已扫描的新文件字节数回调。两层加起来做个加权平均,exhibit 出来大约是个平滑增长的进度条,不会出现卡在 99% 的情况。

3.3 通过测试用例验证封装正确性

封装完成后,第一件事不是接入业务,而是写几个测试用例验证正确性。测试方案很直接:用随机文件、文本文件、二进制文件各做一组,生成 patch 后用 bspatch 还原,比对还原文件和原文件是否逐字节一致。

void TestPatchGenerator::testRoundTrip() { PatchGenerator gen; gen.setFilePair("/tmp/old.bin", "/tmp/new.bin"); gen.setOutputPath("/tmp/patch.bin"); QSignalSpy spy(&gen, &PatchGenerator::finished); gen.start(); QVERIFY(spy.wait(30000)); Patcher patcher; patcher.setPatchFile("/tmp/patch.bin"); patcher.apply("/tmp/old.bin", "/tmp/restored.bin"); QByteArray original = readFile("/tmp/new.bin"); QByteArray restored = readFile("/tmp/restored.bin"); QCOMPARE(original, restored); }

这是一个 Qt Test 的典型用例。QSignalSpy 用来等待 finished 信号触发,避免用 QThread::sleep 这种不靠谱的同步方式。还原出来后直接 QCOMPARE 两个 QByteArray,能过就说明生成和应用两端都正常。这个用例跑通之后,才建议继续接 UI 层。

4. 接入 qlz 压缩层:patch 体积再降 40% 的关键

4.1 qlz 的 API 与内存要求

qlz 的 API 极简,核心就两个函数:qlz_compress 和 qlz_decompress。压缩前需要准备一个 state 缓冲区,大小由 qlz_state_size 函数返回,level 越高 state 越大。注意一点,qlz 的压缩和解压需要各自的 state,不能共用。

// qlz 基本用法 void* state = malloc(qlz_state_size(QLZ_COMPRESSION_LEVEL)); size_t compressed_size = qlz_compress(state, src, dst, src_len); free(state); void* state_d = malloc(qlz_state_size(QLZ_COMPRESSION_LEVEL)); size_t restored_len = qlz_decompress(state_d, src, dst, src_len); free(state_d);

qlz_compress 的返回值是压缩后的字节数,这个值必须原样写入 patch 文件,解压时才能知道读多少字节。qlz_decompress 的返回值是解压后的字节数,正常情况下应该等于原始长度。移植时把这个逻辑封装一下,保证压缩和解压使用相同的 level,否则会解压失败。

4.2 压缩与差分编码的配合方式

bsdiff 生成 patch 的过程中,diff 数据和 extra 数据是分别压进 patch 文件的。这两个数据块的压缩是独立的,qlz 可以分别处理。在 patch 文件里,Ctrl 块记录了每个 diff 段的长度、extra 段的长度和在新文件中的偏移,压缩后这些长度仍然有效,跟压缩无关。

// bsdiff.c 中压缩段落的替换逻辑(示意) unsigned char* diff_buf = NULL; size_t diff_len = ...; // 差分数据 size_t diff_comp_len = 0; void* cstate = malloc(qlz_state_size(level)); if (cstate) { unsigned char* comp_buf = malloc(diff_len + 1024); diff_comp_len = qlz_compress(cstate, diff_buf, comp_buf, diff_len); // 写入 patch 的 diff 段 write_patch(comp_buf, diff_comp_len); free(comp_buf); free(cstate); }

注意这里给压缩输出多分配了 1024 字节。这是 qlz 的一个特性——最坏情况下,压缩输出可能比输入略大,因为要写入头部信息和状态标记。如果你的 patch 比较小,比如只有几 KB,压缩后可能反而变大了,这时可以做一个判断,压缩后如果比原始数据还大,就直接写原始数据,不压。

4.3 patch 文件格式调整与兼容性处理

接入 qlz 后,patch 文件的格式从 bsdiff 标准格式变成了自定义格式。主要改动有两处:头部增加压缩算法标识字段,diff 和 extra 数据段增加压缩前原始长度字段。应用端读 patch 时,先读标识,再决定走 bzip2 还是 qlz 路径。

patch 文件布局(接入 qlz 后): Header: magic "BSDIFF40" + 压缩算法标识(4字节) + ctrl_len(8字节) + diff_len(8字节) + new_size(8字节) Ctrl 数据区: 压缩后存储 Diff 数据区: 压缩后存储 Extra 数据区: 压缩后存储

这里有一个不太容易发现的坑:bsdiff 标准格式里,Ctrl 段是一组 3×8 字节的结构体数组,应用端需要流式读取。如果你改动格式,要保证 Ctrl 段内部的结构不破坏,否则应用端无法解析。我实际做的时候,只加了算法标识和额外的长度字段,Ctrl 段内部保持原样,这样应用端的解析逻辑改动最小。

4.4 兼容旧客户端:双格式 patch 还是强制升级

一旦接入 qlz,生成的 patch 包只有新版本客户端能解。如果你的用户群里有还没升级到新客户端的存量用户,服务端必须能区分下发哪种 patch。

两种常见做法:一是服务端同时存两套 patch——一套旧格式(bzip2),一套新格式(qlz),按客户端版本下发;二是只生成新格式 patch,并在服务端做版本控制,新 patch 只推送给已升级到兼容版本的客户端。前者需要服务器多存一倍数据,后者需要客户端升级过程不能断档。

从工程实践来看,我一般建议用第一种。理由很简单:客户端升级是不可控的,你没法强制用户第二天就全部升级。保留旧格式 patch 的成本就是磁盘多占一点,但这能保证新老版本都能正常升级,尤其是做强制升级版本的时候,新的安装包本身就必须能被旧版本下载。

5. 避坑与排查:移植 bsdiff 过程中常见的五个坑

5.1 坑一:大文件差分时内存飙升到无法接受

现象:对 500MB 的旧文件跑 bsdiff,程序在排序阶段占用了超过 2GB 内存,直接 OOM。

原因:bsdiff 的 suffix sorting 需要为每个字节建立一个后缀数组索引,这个数组本身就是 N×4 字节(int 型),加上辅助数组,内存占用大约是文件大小的 6~8 倍。500MB 文件对应 3~4GB 内存,这在 32 位环境下直接崩,64 位环境下也不好看。

解决:一是尽量用 64 位构建;二是把内存分配器换成可监控的版本,实时观察内存占用;三是调整业务策略,对超过 200MB 的大文件改用分块差分,把文件切成多个段,分别生成 patch。分块方案会降低压缩率,但对于超大文件,可用性是第一位的。

5.2 坑二:Qt 的 QString 到 C 字符串转换导致路径错误

现象:在 Windows 环境下,生成的 patch 文件路径总是不对,或者根本找不到旧文件。

原因:QString::toLocal8Bit 和 toUtf8 在处理含中文或空格路径时,转换结果在 C 函数里可能被截断或乱码。更隐蔽的是,bsdiff_core 内部用 fopen 打开文件,而 Windows 的 fopen 默认按 ANSI 编码解释路径,中文路径必挂。

解决:b sdiff 的 IO 层改用宽字符接口,或者在调用前把路径转为 8.3 短路径。Qt 里更省事的方式是,不传路径给 bsdiff,而是通过 QFile 打开文件句柄,再把句柄传进 bsdiff 的 IO 回调。这样绕过了路径编码问题,最可靠。

5.3 坑三:qlz 压缩级别不一致导致解压失败

现象:服务端生成 patch 时 compression level 设为 2,客户端解压时写成 1,解压出来的数据全是乱码。

原因:qlz 的压缩数据头里带有 level 信息,但 qlz_decompress 的 state 必须用相同 level 初始化。如果用 level 1 的 state 去解 level 2 的数据,内部表的大小不对,解压结果就是错的。

解决:在 patch 文件头的算法标识字段里写入 level 值,解压前读取并校验。如果不匹配,直接报错而不是继续解。同时建议在代码里做一层断言,压制这类问题的发生。

5.4 坑四:生成 patch 过程中取消操作留下半截文件

现象:用户点击取消后,输出目录里留了一个只有几十 KB 的 patch 文件,下次生成时工具提示文件已存在或直接覆盖,导致旧文件被误删。

原因:bsdiff 原版的输出流程是边计算边写入,取消时没有清理逻辑。

解决:生成时先写到一个临时文件,全部完成后再 rename 成最终文件名。取消或异常退出时删掉临时文件。这个改动很小,但能避免很多后续的认知混乱。

5.5 坑五:Qt 4 与 Qt 5 的 QString::toUtf8 行为差异

现象:同一段代码,在 Qt 4 下生成的 patch 正常,在 Qt 5 下生成的 patch 应用后客户端崩溃。

原因:Qt 5 的 QString 底层从 UTF-16 改为 UTF-8 存储后,toUtf8 在含有非法 UTF-16 字符时行为变化,导致路径或文件内容读取时字节不一致。

解决:统一强制用 QFile::encodeName 做路径转换,不要手写 toUtf8。对于文件名中的特殊字符,QFile::encodeName 在 Windows 下会自动处理为 8.3 格式,这是最省心的方案。

6. patch 校验与回滚预案:上线前必须做的一套验证流程

patch 生成和接入 qlz 都完成之后,不要急着上线。增量更新最怕的不是 patch 生成慢,而是 patch 在部分用户机器上应用失败,又没有回滚机制,用户就卡在旧版本上了。我从项目初期开始做增量更新,血的教训告诉我,上生产前至少要做三轮验证。

6.1 校验一:字节级一致性验证

用真实的旧版本安装包和新版本安装包生成 patch,应用后逐字节比对还原文件与原始新文件。这个校验用脚本自动化,不能靠人眼比对。

# verify_patch.sh 核心逻辑 ./bspatch_tool old.bin new.bin patch.bin ./bspatch_tool apply old.bin restored.bin patch.bin cmp new.bin restored.bin && echo "PASS" || echo "FAIL"

cmp 返回 0 才输出 PASS。这里要注意,cmp 对大文件的性能较差,但校验阶段文件通常只有几十 MB,可以接受。如果文件特别大,可以用 sha256sum 替代 cmp,速度更快。

6.2 校验二:版本目录的增量历史链验证

客户端升级不是只有一版,用户可能从任意旧版本升上来。所以服务端要维护的 patch 不只是“最新版对上一版”,而是最新版对所有仍受支持的历史版本。校验时随机抽几个历史版本做升级链测试。

常见做法是:每个历史版本都生成对应的 patch,然后模拟升级路径——旧版本 → patch 应用到中间版本 → 再应用下一个 patch → 直到最新。这条链上任何一个环节失败,都要排查是差分问题还是 patch 格式问题。这里有一个经验:不要把多个 patch 链式应用,因为每个 patch 应用后文件的校验和会不同,出错时很难定位是哪一个 patch 造成的。最好是每个版本直接生成对最新版的 patch,不做中间过渡。

6.3 校验三:异常输入与容错测试

patch 文件损坏、旧文件被篡改、磁盘空间不足、版本号不匹配,这些都属于异常输入。客户端应用 patch 时要把这些情况都考虑进去。

int Patcher::apply(const QString& oldFile, const QString& patchFile, const QString& newFile) { // 前置检查:patch 文件头部魔数与版本号 QFile patch(patchFile); if (!patch.open(QIODevice::ReadOnly)) return ERR_PATCH_OPEN_FAILED; QDataStream in(&patch); quint32 magic; in >> magic; if (magic != 0x42534449) return ERR_BAD_MAGIC; // "BSDI" // 检查补丁版本是否与当前客户端兼容 quint16 version; in >> version; if (version != PATCH_FORMAT_VERSION) return ERR_VERSION_MISMATCH; // 解析并应用 return applyInternal(oldFile, patchFile, newFile); }

这一段代码展示了 patch 应用前的两个关键检查:魔数校验和版本号校验。魔数不匹配说明文件根本不是 patch,报错即可;版本号不匹配说明 patch 是旧格式或未来格式,需要提示用户升级客户端而不是盲目解析。这个逻辑看起来简单,但很多团队为了省事直接跳过,等到线上出问题才补回来。

从那以后,我每次生成 patch 都强制走一遍校验脚本,不再靠“测了一次没问题”就上线。这套流程看起来繁琐,但它省掉的是线上用户反馈“升级失败”之后漫长的排查时间。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 19:18:56

Codex 生成前端组件实战:从提示词到工程化对齐的秒级开发

1. 从"等组件"到"秒级出活":前端开发节奏正在被什么改变前端开发这行有个很微妙的痛点:真正写业务逻辑的时间,往往不到一半。剩下的一半,全耗在"搭组件"上——一个带搜索、分页、空状态、加载骨架的…

作者头像 李华
网站建设 2026/10/23 9:55:39

记录 Zabbix 6.4升级7.0.13

环境是Ubuntu 22.04 mysql nginx(系统不同升级命令不同,具体命令详见官方文档)1、更新APT源wget https://repo.zabbix.com/zabbix/7.0/ubuntu/pool/main/z/zabbix-release/zabbix-release_latestubuntu22.04_all.deb2、安装zabbix源dpkg -i…

作者头像 李华
网站建设 2026/11/2 22:23:07

操作系统第八版习题答案:从静态文档到可运行代码的解题框架

简介:这份文档面向正在学习《操作系统精髓与设计原理》第八版的高校学生与考研备考者,针对课后习题缺少参考答案、进程与并发概念难以自检的痛点,提供系统化的解题参考。压缩包内仅含1个docx文件,约904KB,按章节组织答…

作者头像 李华
网站建设 2026/10/23 15:12:26

激光焊缝跟踪系统:开启机器人具身智能之路

激光焊缝跟踪系统它不仅仅是一个提高焊接精度和效率的工业工具,更是赋予工业机器人初步的、与物理环境进行实时动态交互能力的关键技术。它实现了基础的“感知-行动”闭环,解决了机器人适应环境不确定性的核心难题,完美体现了具身智能的核心思…

作者头像 李华
网站建设 2026/10/23 23:03:37

大模型应用后端设计:上下文管理、缓存、降级策略的工程方案总结

上一篇文章讨论了 Agent 的核心组件——RAG、向量库、Function Calling 与 Skill 体系。但把这些组件拼起来之后,更棘手的问题浮出水面:多轮对话怎么管?重复请求怎么省?上游挂了怎么办? 这三个问题对应的正是大模型后端…

作者头像 李华
网站建设 2026/10/26 19:19:47

幽默代码注释与终端Banner:程序员整活的工程边界与实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华