从 12 个案例提炼 WeKnora 贡献模式
结论先行
通读 15 个 merged PR(首批 5 案 + Phase G 新增 10 案)后,好的 WeKnora 贡献有以下共性:
- 改动范围极度收敛:单 PR 改 1–3 个文件是常态,11 文件已是上限;文件越少合入越快
- PR 描述自带根因链:不只写”修了什么”,而是”为什么出问题 → 怎么修 → 为什么这样修而不是别的方式”
- 测试行数 ≥ 代码行数:过滤类修复的测试覆盖所有已知变体,校验类修复的测试覆盖正常路径 + 全部拒绝分支
- 只加不改:新功能封装到新文件,老文件只加调用入口(通常 ≤10 行),回滚成本极低
- 有 fallback 安全网:转换失败不恶化现状,缓存失效最坏只多发一次请求,删除保护只阻止危险操作不影响正常路径
- 解决噪声在最上游:管线修复遵循”垃圾进垃圾出”原则,在 parser 层过滤比在 chunker/Rerank 层打补丁更可靠
- 命名统一、职责单一:跨 repository 的同类方法保持同名(如
CountByModelID),聚合逻辑单独建文件而非散落在 service 层
1. Issue 选择模式
「够小能交付」的判断标准
从首批 5 案的实际改动规模看:
| 案例 | 文件数 | 净增行数 | 从 Issue 到合入的周期 |
|---|---|---|---|
| case-1785 | 2 | +111/-1 | ~2 天 |
| case-1633 | 预估 1–2 | 预估 ≤10 | 未提交 |
| case-1725 | 3 | +149/-0 | ~1 天 |
| case-1761 | 1 | +3/-0 | ~1 天 |
| case-1774 | 11 | +437/-2 | ~1 天 |
经验值:初次贡献选 1–3 文件、+50 到 +150 行的 issue。case-1774 虽有 11 文件 +437 行,但其中 67% 是测试代码,实际业务逻辑新增约 140 行,仍属可控范围。
各栈的「安全改动区」
- Python docreader(如 case-1785):
docreader/parser/下的各*_parser.py,每个 parser 相对独立,改一个不影响其他。安全区 = 在parse_into_text()的遍历循环中加过滤条件 + 对应单测 - Go docparser(如 case-1725):
internal/infrastructure/docparser/下的 converter 文件。安全区 = 新建 normalizer 文件 + 在 converter 中加一行调用 - Go API(如 case-1774):handler → service → repository 三层。安全区 = 在 repository 加查询方法 + service 层加校验分支 + handler 传错误码
- Vue 前端(如 case-1761、case-1633):
frontend/src/stores/和frontend/src/views/。安全区 = store 层加缓存失效 / 字段归一化,不碰路由和全局状态
反面论证:暂不选作教材的 PR
从 case-index 中被排除的 10 个 PR 可以看到不适合新手的特征:
- 改动 > 8 文件 或 > 500 行:如 #1787(32 文件 +2585/-1507)、#1769(31 文件 +2317)——范围过大,review 成本高,容易因一处问题整个 PR 被 block
- 跨前后端联动:如 #1789(前后端同时改 8 文件)——涉及两个技术栈的维护者同时 review,协调成本翻倍
- 功能极细分或距目标太远:如 #1747(Markdown 渲染正则)、#1784(SearXNG 错误消息)——即使改动小,缺乏可复用性和教学价值
2. 定位模式
从现象到文件的固定步骤(综合首批 5 案提炼):
- 读 Issue 描述 + 截图:确认能复现、记录”什么操作 → 什么现象”
- 画数据流:从用户操作到最终表现,经过哪些层(UI → Store → API → Service → Repository → DB;或 Upload → docreader → docparser → chunker → 向量存储 → 检索 → LLM)
- 定位管线入口:噪声/错误”最早从哪一层进入”(向上游追);或”在哪一层丢失/被拦截”(向下游追)
- grep 关键词:用 Issue 中出现的字符串(如
DISPIMG、undefined、organizationsLoadedAt)在代码仓库全局搜索 - 找到目标文件后读上下文:确认函数签名、调用链、已有的 guard 条件
- 写最小复现脚本/手动复现:确认修改前问题确实存在
- 设计修复 + 验证修复后不回归
- 检查相关文件是否有类似遗漏(如 case-1774 需要同时查 KB 和 Agent 两个表)
各栈调试入口
前端(Vue 3 + Pinia):
- 本地 dev server:
cd frontend && pnpm dev - Vue DevTools 查看 Pinia store 的实时状态(缓存时间戳、列表数据)
- 浏览器 Network 面板确认”请求有没有发出去”——区分缓存拦截 vs API 返回错误
- case-1761 的定位路径:观察到列表不刷新 → Network 面板发现 fetch 没发请求 → 追踪到 TTL 守卫 → 确认创建后时间戳未重置
Go 后端(case-1774 的测试方式):
- 单测直接测 service 方法:
go test ./internal/application/service/ -run TestDeleteModel -v - mock 接口补齐:新增 repository 方法后,所有 mock 实现都需要补上签名(否则编译不过)
- 用
t.Run子测试覆盖多种场景(未引用 / 被 KB 引用 / 被 Agent 引用 / 内置模型)
Python docreader(case-1785 的测试方式):
- 进入 docreader 目录:
cd docreader - 运行单测:
python -m pytest tests/test_excel_parser.py -v - 构造最小输入验证正则:在 REPL 中用
re.match(pattern, test_string)快速验证各变体 - 不需要启动完整服务,parser 层可独立测试
3. 实现模式
PR 描述四段式
| 段落 | 内容 | 真实案例 |
|---|---|---|
| What | 一句话描述用户看到的现象 | case-1785:”RAG 问答时 AI 回复中出现 DISPIMG("ID_xxx",1) 函数字符串” |
| Why | 根因链(从现象到代码路径) | case-1761:”fetchOrganizations() 的 TTL 缓存守卫在创建成功后 10 秒内拦截了请求” |
| How | 修复方案 + 为什么选这个方案而非其他 | case-1725:”在 docparser 层插入 HTML 表格归一化器,能转 Markdown 的转,不能转的剥离样式” |
| Test | 怎么验证修复有效 | case-1774:”新增 model_delete_test.go 覆盖删除保护的所有分支” |
改动粒度统计
| 文件数 | 案例 | 合入速度 |
|---|---|---|
| 1 | case-1761 | 当天 |
| 2 | case-1785 | 次日 |
| 3 | case-1725 | 当天 |
| 11 | case-1774 | 当天(维护者自己提交) |
结论:外部贡献者的 PR 控制在 1–3 文件 是最安全的区间。case-1774 之所以 11 文件也能当天合入,是因为作者本身是主维护者。
命名、错误处理、测试的共性规范
命名:
- 方法名跨文件保持一致:
CountByModelID在 knowledgebase.go 和 custom_agent.go 中签名相同 - 新文件命名表达职责:
html_table_normalizer.go(做什么)、model_usage.go(管什么)、model_delete_test.go(测什么) - 正则变量加注释说明意图:
_IMAGE_FUNC_RE旁注释覆盖哪些变体
错误处理:
- service 层返回
AppError{Code, Message},handler 层翻译成 HTTP 状态码——业务逻辑不耦合 HTTP 语义 - 错误消息包含定量信息(”被 3 个知识库和 1 个 Agent 引用”),帮助用户判断影响面
- 转换类逻辑有 fallback:转换失败退回降级策略,不恶化现状
测试:
- 每个已知变体一个测试用例(case-1785 的 4 种 IMAGE 函数变体)
- 正常路径 + 全部拒绝分支(case-1774 的”未引用可删 / 被 KB 引用拒绝 / 被 Agent 引用拒绝 / 内置模型拒绝”)
- 测试文件与源文件同目录(Go 惯例)或 tests/ 子目录(Python 惯例)
4. 栈与目录对照(2026-06 现状)
| 想改什么 | 目录 | 参考案例 |
|---|---|---|
| Excel/Word/PDF 解析噪声过滤 | docreader/parser/ |
case-1785 |
| OCR 输出格式归一化(HTML→Markdown) | internal/infrastructure/docparser/ |
case-1725 |
| PDF 扫描件强制 OCR / ingest 策略 | docreader/ + knowledge_process/ |
case-1808 |
| MHTML 跨语言管线 | docreader/ + docparser/ |
case-1743 |
| Chunk 策略 / heading breadcrumb | internal/infrastructure/chunker/ |
case-1700 |
| 向量库复制 / ID 映射 | tencentvectordb/repository.go |
case-1668 |
| 向量库 replica / env-store | tencentvectordb/ + types/vectorstore.go |
case-1759 |
| 检索 API(hybrid-search POST) | internal/handler/ + MCP |
case-1754 |
| Web-search 可行动错误 | internal/.../websearch/ |
case-1784 |
| Agent QA 入口 / 权限 guard | internal/application/session/ |
case-1772 |
| Go API 新增校验 / 删除保护 | internal/application/repository/ + service/ + handler/ |
case-1774 |
| 前端 Store 缓存失效 / 状态同步 | frontend/src/stores/ |
case-1761 |
| 前端组件字段映射 / null guard | frontend/src/views/ + frontend/src/stores/ |
case-1633 |
| docreader 单测 | docreader/tests/ |
case-1785 |
| Go embed / 出站安全 | internal/models/embedding/ |
case-1828 |
| Agent thinking / RAG 时间线 | internal/types/ + RagPipelineProgress.vue |
case-1773 |
| Go 后端单测 | 与源文件同目录 *_test.go |
case-1774 · case-1700 · case-1828 |
5. 反模式
从被排除的 PR 提炼
反模式一:改动范围不聚焦
典型代表:#1787(32 文件 +2585/-1507)、#1769(31 文件 +2317/-147)。这类 PR 需要多个维护者同时 review,任意一个文件的问题都可能 block 整个 PR。对新贡献者来说,一个被 block 两周的 PR 比没提交 PR 更打击积极性。
反模式二:跨栈联动无分拆
典型代表:#1789(前后端联动 8 文件)、#1775(IM 集成全栈 19 文件)。正确做法是拆成后端 PR + 前端 PR 分别提交,后端先合入再开前端 PR。
反模式三:缺少测试
case-1633 虽然根因已被社区确认,但迟迟没有正式 PR 的原因之一可能是——仅修改字段映射而不加 normalize 函数的单测,维护者可能觉得不够完整。从首批 5 案数据看,所有当天合入的 PR 都有对应测试。
反模式四:只写 “fix #xxxx” 不解释根因
5 个案例中,所有快速合入的 PR 都有清晰的根因描述。维护者审核带宽有限,一个 self-explanatory 的 PR 描述能把 review 时间从 30 分钟压缩到 5 分钟。
Open Design PR #3356 的经验
这是一个已合入但属于不同仓库(Open Design)的 PR。它的改动规模偏大且跨多个模块,虽然最终合入但经历了较长的 review 周期。教训是:即使你对代码有信心,大范围改动本身就会拉长合入时间——维护者需要更多精力理解影响面。如果能拆成多个小 PR 分批提交,每个 PR 的合入速度和质量都会更好。
6. Phase G 新增模式(新增案例补充)
| 模式 | 代表案例 | 要点 |
|---|---|---|
| sub-chunk metadata 继承 | case-1700 | 二次切分后按 offset 解析最深 breadcrumb,不是整 section 共用 |
| 分页复制 ID 映射 | case-1668 | 向量库 CopyIndices 逐页 query 时必须 remap chunk ID |
| GET→POST API 演进 | case-1754 | 复杂检索 body 需要 POST;同步 MCP tool + OpenAPI |
| ingest 策略路由 | case-1808 | 无 text layer 的 PDF 走强制 OCR,UI 可配置 |
| 死字段误拦清理 | case-1772 | 删未使用的 guard 比加逻辑更安全(-109 行) |
| 跨语言管线 E2E | case-1743 | Python 解析 + Go chunk 需端到端验证图片/链接 |
| 横切安全 transport | case-1828 | 多 provider 抽 transport.go;构造期 SSRF;标杆案 |
| Agent thinking / 时间线 | case-1773 | nil→explicit default;后端 log + 前端时间线同 PR |
| env-store 双端配置 | case-1759 | repository + UI 元数据;QA replica=0 |
完整候选与下一批:case-catalog.md Tier B(一次只写 1 案,见 case-writing-standard)。
7. 思考点
题 1:你看到一个 Issue 描述”上传 PDF 后,某些页面的表格内容丢失”。已知 WeKnora 的 PDF 解析走 docreader/parser/pdf_parser.py。你会用哪条模式定位?第一步做什么?
题 2:你想修复一个前端 bug——”删除知识库后列表没有刷新”。根据 case-1761 的模式,你会去哪个 store 文件找 TTL 缓存守卫?修复方案是什么?
题 3:一个 Issue 要求”当用户删除 Agent 时,如果该 Agent 正在被某个工作流引用,应拒绝删除”。根据 case-1774 的模式,你需要改哪几层的哪些文件?改动的顺序应该是什么?
题 4:你发现 PaddleOCR-VL 解析某些 DOCX 文件时,输出中混入了 \uf0b7(Unicode bullet)字符,影响分块质量。参考 case-1785 和 case-1725 的模式,你会在哪一层做过滤?用什么样的正则?测试用例怎么设计?
题 5:一个 PR 改了 15 个文件(前端 8 个 + 后端 7 个),reviewer 已经一周没回复。根据反模式分析,你应该怎么做才能加速合入?
8. 下一章预告
ch-bridge-1248 将把本章提炼的模式直接映射到 Issue #1248(知识库问答-点赞点踩)的实施方案上:
- 删除保护模式 → feedback 表的关联校验(”这条 message 存在吗?这个 chunk 属于该 message 吗?”)
- TTL 缓存失效模式 → 点赞/点踩后的 UI 即时更新
- 管线归一化模式 → feedback 数据从 message 穿越到 chunk 的归因链路
- null guard 模式 → 对话气泡中 feedback 状态的防御性渲染
- PR 描述四段式 → 提交 #1248 PR 时的描述模板