Commit Graph

75 Commits

Author SHA1 Message Date
姚章浩 87f97a2f9b feat(qaops): 完成详情下拉与每周自动保存草稿
build / build (push) Successful in 1m7s
详情使用共享主表字典,稀疏修改所选字段并保留原始问答及旧复核审计。
月周统计提供文本和下拉双入口;每租户每周一份持久草稿,按技能六节双语模板真实预填。
加入幂等自动保存、冲突保留、未知响应重试、切周及离开保护和schema58非破坏降级保护。

验证:完整make verify、隔离PostgreSQL全mirror竞态回归、真实恢复库浏览器验收通过。
浏览器实测字段精确读回、月周双输入、模板预填、MD/CSV、自动保存、冲突、失败重试与API重启恢复。
未连接或改写DobotAI生产源库,未运行付费模型;提交时尚未部署。
2026-10-20 05:44:02 +08:00
姚章浩 154b0b62ad feat(qaops): 完成区域统计与部门绑定六项联动
build / build (push) Successful in 1m20s
新增北京时间自然周校验与缓存隔离,默认展开到业务二级部门。
选中部门在统计页就地绑定;区域名称列表原位增删改,双版本锁、自动预览、独立读回及删除确认。
筛选和两张区域看板使用同一已保存目录,删除失效筛选回全部。

验证:完整 make verify、全 mirror 隔离 PostgreSQL race、自然周边界与缓存键负例通过;真实恢复库327部门逐行对拍及六项浏览器验收通过。
schema57无迁移,不改真实公司区域绑定或原始问答,不连接DobotAI生产源库;PNG导出本轮不冒充已验证。
2026-10-19 23:41:01 +08:00
姚章浩 9470a127f7 feat(qaops): 完成流水表六项体验与AI执行修正
build / build (push) Successful in 1m11s
统一AI分析入口及双任务安全确认,采用全量载入的可见行渲染,保留完整原文。
右侧非模态详情固定五项质量字段,清理冗余展示并对齐查询和同步按钮。
默认4并发由数据库准入与逐任务租约保护,保留共享限流冷却、截断归因和有界重试。
兼容真实引用标题字段,不伪造证据或准确率,不修改源生产问答与公司区域规则。

验证:完整make verify退出0,710项Go通过、124项已声明可选跳过且必需0跳过;
另全量mirror隔离PostgreSQL race退出0,AI单元race退出0。
真实本地浏览器通过六项交互、390px内滚动及显式AI部分失败/零缺口故障夹具。
治理原生备份已真实恢复,schema57、72表计数与29张QA内容哈希一致。
候选镜像、精确提交CI与正式上线验收待本次提交后执行,不宣称已部署。
2026-10-19 22:09:32 +08:00
姚章浩 4a2b2b7d84 feat(qaops): 增加部门树区域统计与父子联选
build / build (push) Successful in 1m5s
区域配置入口改为区域统计,五列表按当前通讯录整支人员去重;调岗依当前归属、零分母未知、最近真实提问时间和父行收起不重算。仅读治理正式总表,后台单并发/请求合并/60秒缓存,目录与配置版本隔离,客户端失败不伪零。

保留区域自定义目录与预览/双版本锁保存,父部门勾选联选全后代,取消子支清除祖先整支选择,避免隐式覆盖未选子级;允许删除无引用区域,已有引用不静默孤立。统计页与设置共享选择,统计支持累计与北京时间自然月;不新增迁移,不改DobotAI生产、代理、原问答、历史趋势或已有AI任务。

验证已执行:完整make verify退出0,705 Go/340前端通过、120项声明的可选集成跳过且必需0跳过;镜像全量隔离PG race退出0;新增去重/调岗/空分母、后台缓存/并发、HTTP授权/严格请求、真PG目录与配置失效、树选择/收起、五列与删除保护均先红后绿。旧导航测试按新名称更新后全量通过。上线前治理库备份与真实恢复72表/29张QA内容哈希一致,匹配独立主密钥受限备份。

当前尚待精确SHA CI、同镜像真实数据浏览器与正式读回,不把源码验证称为已上线。公司真实区域映射不在本轮擅改;只在隔离库验证保存。
2026-10-19 19:01:36 +08:00
姚章浩 a9870beade feat(qaops): 补充独立历史覆盖率与正确率趋势
build / build (push) Successful in 1m10s
用户提供的历史百分比独立持久化,按ISO自然周和截止范围读取,不伪造分子分母、记录数或冻结时间。现有两张趋势图与PNG共用来源分色/分段及两位小数;缺失周留空,关键词筛选不借用无明细历史,当前指标与原冻结证据不变。

验证:完整make verify退出0;镜像全量隔离PG race退出0,新增来源/精度/关键词/租户/截止/不可变和安全down回归先失败后通过。前端输入/来源及缺失周回归先红后绿。治理库上线前原生备份与真实恢复已完成,71表行数和28张QA表内容哈希一致,独立主密钥同时受限备份。

当前仅完成开发验证,真实数据候选浏览器验收、正式25周导入与应用上线需继续完成。不修改源生产数据库、原问答、已有模型自动开关或隧道;用户原始报表和图像只保存于私有证据目录,不入Git。
2026-10-19 17:55:15 +08:00
姚章浩 533db535f1 feat(qaops): 接入真实模型补缺任务与总表看板
build / build (push) Successful in 1m12s
按已确认需求提供独立安全模型配置、真实探测、正式字典字段评估与原问题译中,持久批次支持只补空白、暂停恢复、有限重试、幂等和逐字段依据。覆盖固定规则、本地语言、组织关联与主表有效看板共用治理侧读模型,保留源生产和冻结历史。

验证:完整 make verify 退出0,702项Go与329项前端通过;默认118项外部/隔离集成明确跳过,另跑完整镜像及组织隔离PG竞态。12并发、输入漂移、过期租约、暂停/恢复及有限重试通过。用户授权的百炼 deepseek-v4.1-flash 完成真实后台与浏览器评估/翻译;552行、16列、11块看板及完整PNG、独立配置页测试/保存、重开恢复和零JS异常通过。仅非业务样本外发,自动处理关闭,原payload不变。

上线前治理库已做导出快照原生备份并真实恢复,60表行数与17个QA表内容哈希一致;不修改DobotAI生产或隧道。本提交时尚未部署,线上验收须独立完成。
2026-10-19 14:29:49 +08:00
姚章浩 f5e0f8d20f feat(qa-ops): 建立独立正式主表并保真导入历史问答
build / build (push) Successful in 1m7s
将可维护总表与可替换2.0镜像分离,稳定来源关联和原始事件键保留;
源端更新/删除不覆盖主表或人工复核。首次收录保留原评估审计,
主表输入/复核独立持久化;历史原标注不冒充新AI或冻结证据。

历史导入绑定源哈希、逐行批准清单与真实恢复备份;三字段精确去重,
批次及原字段留档,重复执行不覆盖后续修改。仅操作本地治理库。

验证:完整make verify退出0;镜像与区域两组隔离PG race退出0;
真实恢复数据隔离导入12522、跳过792、暂缓1,总表15050;
重放保留模拟修改,结构down拒绝丢历史;真实浏览器第41周551条完整,
历史原标注和独立主表复核读回通过,移动布局及JS异常0。
2026-10-19 08:49:18 +08:00
姚章浩 6e67034fbd feat(qa-ops): 交付十一项统计看板与完整图片导出
build / build (push) Successful in 1m10s
以治理库单一只读事务分开聚合累计、自然周、截止月及当前通讯录使用分母;浏览器不再读取全量长答案来构建看板。

新增迁移52与普通worker周质量冻结,保留真实冻结时点、规则和标量贡献;缺少当时快照留空。提交后冻结头与贡献不可修改、删除或追加;只读接口保留管理员、固定租户、严格JSON与输入冲突保护。

已验证:make verify退出0,686个Go用例和305个前端用例通过,必需跳过0;镜像与组织隔离PostgreSQL竞态回归退出0。真实治理副本浏览器核对第41周累计1885与新增471、十一项和完整PNG下载,390px无外溢、JS异常0;明确模拟503后不伪零且重试恢复。没有修改DobotAI原始问答、真实区域规则或生产发布门禁。
2026-10-18 23:26:55 +08:00
姚章浩 76671e0784 feat(qa-ops): 支持自定义区域与分级部门批量归属
build / build (push) Successful in 1m8s
2026-10-18 21:50:29 +08:00
姚章浩 3e7fcce0b1 feat(qa-ops): 持久本地问答副本与安全答案预览
build / build (push) Successful in 1m11s
新增迁移50与每5分钟/手动持久同步,API只读本地;同步失败保留、权限阻断、租约与质量失效原子激活。
保留全量稳定分页/固定列,已存质量批量读取,元数据不反复解压长答案;详情安全Markdown与原文。

已运行整仓 make verify、源与副本隔离PG -race、真实只读源及隔离浏览器。
实际同步2525条;固定窗口640条本地中位1.46秒,对照直接源33.10秒。
停worker、缺源凭据、重启API后仍可查询;未更新8088,未修改生产原问答。
2026-10-18 19:10:47 +08:00
姚章浩 74ba737b1e fix(version): 按完整提交区分同号历史开发说明
build / build (push) Successful in 1m4s
历史分支中存在两条开发号 0.1.1;归档选择按 SHA 匹配,保留原编号与冲突标签。
增加同号前置说明遮蔽及错 SHA 不复用回归,标签计划使用相同精确选择方式。
已运行:定向 Go 测试、快照工具临时仓正反例及整仓 make verify;未部署。
2026-10-18 18:18:43 +08:00
姚章浩 013ce4a24e fix(version): 补全开发快照记录与同步交付闭环
build / build (push) Successful in 1m3s
收录 main、注释开发快照和完整运行 SHA;开发说明绑定原提交,补记 0.1.1–0.1.25。
进行中同步按仓库合并,完成后重新执行;启动入队,部署脚本读回版本证据。
开发快照独立于正式标签与 Release,既有冲突标签不移动。

已运行:整仓 make verify,Go 673 通过、前端 274 通过,92 个已声明可选跳过,必需跳过 0。
隔离 PostgreSQL 重复同步队列用例及 -race 通过;未更新现有 8088、未修改生产问答。
2026-10-18 18:11:49 +08:00
姚章浩 2ea2111420 fix(web): 保留代理端口以修复同源质量查询误拒绝
build / build (push) Successful in 1m1s
Nginx 使用原始 Host 和端口转发,不修改后端跨站防护。补充无 Fetch Metadata 的 LAN/Tailscale 同源和异源、异端口、转发头伪造回归。

验证:配置回归先因丢失端口失败后通过;HTTP竞态、真实隔离Nginx正反例及完整make verify均通过。部署前真实入口无Metadata为403,同源Metadata对照可过;源库及人工复核无写入。
2026-10-18 15:45:09 +08:00
姚章浩 828c2c5fa0 feat(qa-ops): 按确认口径补齐质量比例与分母明细
build / build (push) Successful in 1m6s
使用完整筛选范围的当前有效结论计算覆盖率和正确率,人工优先,失效与对应待确认/未评估/不计入排除;零分母返回null,界面展示原始分子分母和排除构成,前端拒绝不守恒或比例不一致的响应。

验证:make verify退出码0,Go658及前端257通过,91项声明可选集成跳过;隔离PG、三包竞态及合成/真实只读源浏览器验证通过。无新增迁移,不接AI模型,不改绩效,不写源库;本批未推送或部署,8088运行版保持0.1.21。
2026-10-18 02:00:31 +08:00
姚章浩 4b3aaff321 feat(qa-ops): 完成复检留档与人工复核闭环
治理侧保存输入观测版本、自动复检及人工复核历史,输入变化失效、版本冲突和幂等重放不覆盖人工记录。批量质量数量使用完整筛选元数据,未采集原文不伪造快照,延迟旧扫描不回退新输入。

验证:全仓 make verify 退出码0,Go656及前端253通过;五包竞态、源/组织/质量隔离PG、真实只读源和隔离网页读回复核通过。默认91个可选集成跳过不计通过。未接AI模型,不写DobotAI源库,未推送、未合并、未部署。
2026-10-17 22:37:26 +08:00
姚章浩 9556db1ef0 feat(qaops): 增加个人自然月用量与零记录人员明细 2026-10-17 19:59:33 +08:00
姚章浩 56b8cabed9 feat(qaops): 接入问答组织补全与区域使用统计 2026-10-17 19:02:13 +08:00
姚章浩 cc4906b6a9 feat(qaops): 接通网页通讯录持久化同步与后台接管 2026-10-17 17:36:07 +08:00
姚章浩 5874134e04 feat(qaops): 接入真实通讯录采集并补全根部门 2026-10-17 16:11:29 +08:00
姚章浩 dab74b706b feat(qaops): 增加版本化部门区域归属配置与预览 2026-10-17 11:39:38 +08:00
姚章浩 fd04bf7886 feat(qaops): 接入独立只读问答查询与治理覆盖复检 2026-10-17 09:04:19 +08:00
姚章浩 9a8a3e7795 fix(dingtalk): 登记钉钉知识库 409 space_registration_conflict —— 归档行占名 + 错误被吞
build / build (push) Successful in 57s
现象:点「公司级技术支持知识库」登记,返回 HTTP 409:space_registration_conflict。

根因(真数据取证):
- source_connections.name 有唯一约束。这 3 个钉钉来源是**早期路径**登记的
  (base_url 为空,id 与 dws 现在推导的确定性 id 不同):
  sc-18d10ca0… 公司级技术支持知识库 / sc-eaef0ec1… ATOM知识库 / sc-dc7cea1c… 技术诊断部知识沉淀。
- 上一步「去掉来源」只是归档下架,**名字仍占着唯一槽位** ⇒ 重新登记时 INSERT 撞
  UNIQUE(name)。
- 处理器把**所有**存储层错误都翻译成同一个 409 space_registration_conflict(真实原因被吞)。

修复:
- store:同名的另一行若**已归档** ⇒ 改名腾位(`…(已下架 · <短ID>)`,归档行不显示,
  数据与审计一行不动);若**仍在使用** ⇒ ErrDingtalkSpaceNameTaken(绝不悄悄改名)。
  同一个登记键解析到别的 root 时:在用中的行报 ErrDingtalkSpaceIdentityConflict;
  归档过的行允许就地改写("去掉后再用 dws 重新获取"的语义)。
- httpapi:分类映射 —— space_name_conflict / space_identity_conflict(409 + 可行动 hint),
  其余内部故障改回 500 space_registration_failed,不再伪装成冲突。
- web:读一次响应体同时取 error 与 hint,**hint 优先**;补两个新码的中文说明。

验证:真 postgres 集成测试复刻线上形态(旧行占名 + 归档 ⇒ 登记成功、旧行改名保持归档、
内容不丢;同名在用 ⇒ 拒绝;同键换 root 在用 ⇒ 拒绝、归档 ⇒ 就地改写);handler 分类测试
(409/409/500,且不回显内部细节);前端 hint 优先用例;make verify 15/15。
2026-10-14 12:59:52 +08:00
姚章浩 2c7265efc4 feat(sources): 数据源配置收敛成 dws 导入 + PP 导入;「去掉来源」= 归档下架(不删数据)
build / build (push) Successful in 58s
主人要求:去掉入口,统一成 dws 导入与 PP 导入;其他来源连数据一起下架,之后用 dws 重新获取。

界面(web):
- 「数据源配置」只剩两块:**dws 导入(钉钉)**(按当前钉钉账号列出可访问的知识库与钉盘空间)
  与 **PP 导入**(PP 资料下载中心)。上一步加的「显示 AList/钉钉知识库来源」折叠开关删除。
- 来源卡片新增管理员可见的「去掉来源」(带确认弹窗,说明是归档下架、数据保留可恢复)。
- 侧栏文案改成"只留两个入口";发现面板标题改为「dws 导入(钉钉)」。

后端(归档语义,可逆):
- 迁移 000047:source_connections 增 archived_at / archived_by + 只覆盖未归档行的部分索引。
- store:ArchiveSourceConnection / RestoreSourceConnection(幂等;归档同时停用并清空
  next_refresh_at,避免被手工改回后立刻带着旧调度跑)。列表、目录、计划刷新(claimDue)、
  手动扫描(triggerBlock/inspect)四类查询一律排除归档行。
- 注册路径(dws 重新获取):RegisterDingtalkSpace 的 ON CONFLICT 清掉归档标记 ——
  重新登记同一个空间**原地复活同一行**,不产生重复,历史快照仍属于同一行。
- HTTP:POST /api/v1/source-connections/{id}/archive|restore(管理员专属,写意图头
  source-archive / source-restore + Idempotency-Key;404 区分"不存在"与"已是目标状态");
  openapi 同步两条契约。

验证:
- 集成测试(真 postgres):归档从列表消失但 source_items 一行不少、重复归档不漂移归档时间与人、
  未知 ID 报 ErrSourceConnectionNotFound、归档来源不能再被扫描、恢复后回到列表但保持停用;
  dws 重新登记同一空间复活同一行。
- httpapi:写意图/幂等键门禁、200/403/404/500、错误体不回显内部细节、classifyRole 管理员专属。
- 迁移护栏:000047 结构 + 四类 SQL 必须含 archived_at is null(漏一条就是入口没去掉)。
- make verify 15/15。
2026-10-14 12:36:32 +08:00
姚章浩 b7f7e4f121 fix(deploy): 上线前校验 .env 键值形状(两行粘成一行的坏值不再能上线)
build / build (push) Successful in 1m1s
事故复盘(2026-10-14,测试实例 p1 整体不可用约 6 分钟):
- 我用 `rstrip("\n") + 新行` 往 `.env` 追加 `VERSION_HISTORY_LINK_ORIGIN`,而 `.env` 最后一行
  当时没有结尾换行 ⇒ 两行**粘成一行**:
  `VERSION_HISTORY_GITEA_BASE_URL=http://192.168.50.223:3000VERSION_HISTORY_LINK_ORIGIN=https://gitea.zheromiku.top`
- 配置校验按设计拒绝这个非法 origin ⇒ `cmd/api`/`cmd/worker` 启动即退出 ⇒ 容器 crash-loop
  (fail-closed 是对的,但代价是平台起不来)。

修复:
- `scripts/deploy-api-web.sh` 新增 **1a 步护栏**:上线前解析 `.env`,只要某行的值里又出现 `KEY=`
  (两行粘连)或键名不合法 ⇒ 打印 `ENV_VALUE_LOOKS_MERGED` 并**拒绝部署**(不动线上);
- `internal/appconfig` 在校验失败时补线索:值里含 `=` ⇒ 提示"`.env` 里两行可能粘成了一行";
- 回归用例 `TestLoadRejectsMergedVersionHistoryEnvValue`(粘行必拒 + 线索文案 + 合法值仍放行)。

验证:护栏对"粘行样例/键名非法"退出码 1、对"正常样例/当前真实 .env"退出码 0;Go 用例通过;
make verify 15/15。线上已修 `.env` 并重启 api+worker(healthy),重新同步后页面链接已全部走公网 origin。
2026-10-14 11:17:35 +08:00
姚章浩 4cb5110845 fix(version-history): 页面链接改走公网 origin(抓取仍走内网)+ tag 补链接
build / build (push) Successful in 1m1s
现象(主人反馈):点「查看提交」打开是空白页。原因:链接用的是**抓取用的内网地址**
(`http://192.168.50.223:3000/...`),内网外的浏览器到不了;而公网地址
`https://gitea.zheromiku.top/...` 实测 200。另外 tag 证据里**根本没有 url**,
页面渲染出空 `href`,点下去像"没反应"。

修复:

- 新增 `VERSION_HISTORY_LINK_ORIGIN`(页面链接 origin,公网;留空 = 与抓取 origin 相同):
  抓取仍匿名走内网白名单地址,**给人点的链接**用公网 origin 拼 —— 两者本就不该共用一个地址;
  校验必须是干净的 http(s) 绝对地址。
- tag 证据补 `url`(`<link origin>/<repo>/src/tag/<name>`),并在前端**没有链接就不渲染**该按钮。
- 测试:客户端(LinkOrigin 生效 / 未配时退回 / 非法拒绝)、规则(tag 链接写入证据 / 无基址不编链接)、
  前端(有链接才渲染「查看 tag」)。

验证:make verify 15/15;真实链路重同步后库里 commit_url = `https://gitea.zheromiku.top/...`(200)、
release_tags 带 url。
2026-10-14 11:06:13 +08:00
姚章浩 4d43a9386a fix(version-history): tag 之前的提交不再被编号 + 部署脚本注入构建身份
build / build (push) Successful in 1m5s
真实数据发现(本机 p1 实例第一次同步后核对):
- **缺陷**:tag `v0.1.0` 之前的 20 个提交,提交内 CHANGELOG 顶部**也已经**是 0.1.0(旧分支遗留条目),
  而它们的祖先集合被那个**更晚才打**的 tag 全覆盖 ⇒ 按提交数算得 0 ⇒ 被错标成「0.1.0 开发版」。
  它们的正确判定是「历史提交」:那个 tag 是后来才打的,当时还没有发布基线。
- **修复**:基线 tag 必须是该提交的**祖先(或就是该提交)**,否则不给号;新增原因码
  `release_tag_created_later`(页面文案:'该版本是在更晚的提交上发布的 —— 这个提交当时还没有 tag')。
  回归用例两条:`TestDeriveVersionsDoesNotNumberCommitsThatPredateTheTag`、
  `TestDeriveVersionsSideBranchWithoutTagStaysHistorical`(不含 tag 的侧分支同样不给号)。
- **部署脚本注入构建身份**:`scripts/deploy-api-web.sh` 原先不传 `-ldflags -X`,产物 `CommitSHA=unknown`
  ⇒ 页面只能显示"当前运行版本未知"(契约上诚实,但没用)。现在按 `scripts/version.py --identity`
  (唯一来源,与 Makefile/CI 同规则)注入 VERSION/RELEASE_VERSION/BUILD_KIND/COMMITS_SINCE_RELEASE/
  COMMIT_SHA/BUILD_TIME;取不到身份**拒绝继续**(宁可不上线,也不上线一个认不出自己的产物),
  并在收尾核对注入的 SHA 真的进了产物。
- compose.yaml:把 VERSION_HISTORY_* 五项接进应用环境(api/worker 共享锚点),留空即未接线。

验证:真实仓库复验(114 条对账一致)⇒ 正式 1 / 开发 4 / 历史 109(原来错的 19 条现在都是
`release_tag_created_later`);p1 实例重新同步后当前快照分布一致,带号的记录只剩 1 正式 + 4 开发
(含真实存在的「同号不同 SHA」:950c1b4 与 3a1d32d 都是 0.1.1,各占一行);
make verify 15/15 success;前端 18 用例 + typecheck 通过。
2026-10-14 10:00:27 +08:00
姚章浩 0974d8dfa2 feat(version-history): 「版本记录」页 —— 追踪远端 main 全部提交(只读 + 管理员手动同步)
build / build (push) Successful in 57s
按已确认的契约实现(口径见 docs/VERSION_HISTORY.md):

- **版本号口径(只给有证据的)**:注释 tag + tag 指向该提交 + 该提交内 CHANGELOG 顶部三者一致 ⇒ 正式版本;
  tag 之后的提交按 `<主>.<次>.<自该 tag 的提交数>` 显示开发号(与 scripts/version.py **同规则**);
  证据不足 ⇒ 「历史提交 · 短SHA」+ 原因码,**不补造版本号**。完整 SHA 是唯一身份、版本号不是唯一键
  (tag 之后两条并行分支各自的第 1 个提交都算 0.1.1 ⇒ 两条都保留,用短 SHA 区分)。
- **迁移 000046**:version_history_commits(提交记录,主键 repo_key+commit_sha)+ version_history_sync_runs
  (同步状态)。快照 = 一次同步:抓完并与 X-Total-Count 对账通过才切换;失败不切换快照、不清空历史、
  last_success_at 不动(页面显示"上次同步失败(原因)· 仍展示上次成功数据");「同步成功」≠「历史完整」。
- **只读客户端(匿名,不使用任何凭据)**:白名单 origin + 固定仓库、拒绝任意 URL 与跨主机重定向、
  响应上限 5MB / 超时 15s、仅 5xx/429 退避重试(最多 3 次);逐页抓到短页为止;每次全量扫 tag
  ⇒ 旧提交后补的 tag 会更新既有记录(不新增第二行)。
- **接口**(三个都登记进 api/openapi.yaml):GET /api/v1/build-info(viewer)、
  GET /api/v1/version-history(viewer;游标绑定快照,过期返回 409 cursor_expired;不返回伪造 total)、
  POST /api/v1/version-history/sync(admin;并发同步复用同一作业,靠 IdempotencyKey)。
- **「当前运行」只认精确匹配**:产物注入的完整 SHA 命中某条记录才标该条;开发构建显式标注"开发构建";
  没有可用标识 ⇒ "当前运行版本未知";**不拿最新 main / 最新 tag 顶替**,CI 成功不等于已部署。
- **前端**:一级菜单第 5 项(版本记录)+ 时间线页面(全部/正式/开发/历史四段筛选、正文展开、
  加载更多、760px 单列无横向溢出、提交正文与变更记录按纯文本渲染);无打 tag / 发布 / 部署入口。

新增配置(留空即未接线,页面降级):VERSION_HISTORY_GITEA_BASE_URL / _REPO / _ALLOWED_ORIGINS /
_HTTP_TIMEOUT / _MAX_PAGES。

验证:`make verify` 15/15 success(tests-go 578 通过 / 0 失败;tests-web 209 通过 / 0 失败);
新增 Go 用例 30(版本口径与图遍历 12、客户端守卫 6、同步编排 5、接口 7);前端新增 18(组件 10 + 客户端 8);
真实仓库匿名快照拉取(`VERSION_HISTORY_LIVE_BASE_URL=… go test -run TestLiveSyncAgainstRealRepository`):
抓到 113 条与 X-Total-Count 一致、v0.1.0 是注释 tag 且目标与 `git rev-parse v0.1.0^{commit}` 一致、
开发号逐个等于 `git rev-list --count v0.1.0..<sha>`(正式 1 / 开发 22 / 历史 90)。
2026-10-13 19:40:19 +08:00
t 6de5d52363 feat(version): 开发号随提交自动递增 + 首个正式版本 0.1.0
build / build (push) Successful in 51s
按使用者要求定的版本规则(自动升的是**开发标识**,发布版本与 tag 仍由人批准):

- **开发号自动递增**:display_version(development) = `<正式版本的主.次>.<自 v<正式版本> 以来的提交数>`
  (正式 0.1.0 之后第 1 个提交 0.1.1、第 12 个 0.1.12;恰在发布提交上 = 正式号)。
  它**不写进 CHANGELOG**(发布记录只在发版时动),由 git 算出来 ⇒ 每个提交都有自己的号、数字只增。
- **浅克隆不猜号**:`--depth` 会把条数算少 ⇒ 浅仓库里拒绝算号,退回 `<版本>-dev+g<短SHA>`;
  检出步骤在仓库有 tag 时 `--unshallow` 补全历史。
- 接线三处齐全:`version.py --identity` → 门禁 `-X …CommitsSinceRelease=<n>`;Makefile 的
  `COMMITS_SINCE_RELEASE`;Dockerfile 同名 build-arg;Go 侧新增 `CommitsSinceRelease` 与
  DisplayVersion() 新规则;契约测试**交叉核对**"注入的提交数 == git 算出的"(防注入错号)。
- **首个正式版本 0.1.0**:CHANGELOG 的 `[Unreleased]` 提成 `## [0.1.0] - 2026-10-13`;
  头部补"版本怎么走"两行;RELEASE_RUNBOOK 补递增规则与"下一次发布把次版本加一"。
- 发版预检新增:打印**当前开发号**,并挡住"候选正式版本 <= 当前开发号"(会把开发号当正式号;建议次版本加一)。

验证:version.py --self-test(含 tag 后第 2 个提交 = 0.2.2、恰在 tag 上 = 0.2.0、无 tag 不猜号);
release_preflight --self-test(21 分支,含开发号两条);go test 本地三态 + 临时仓库 tag+2 提交实测
(Python 与 Go 都得出 0.2.2);反向夹具 85/0;make verify 15/15 success。
2026-10-13 16:45:56 +08:00
姚章浩 7a9d51cc70 fix(version): 正式构建核证据 + 预检分"建议/就绪" + CI 就绪按必需 context
按要求补三项(本轮不推、不打真实 tag、不部署;全部用临时仓库与假对象验):

1) 正式构建身份不能只认 --release:
   - 新增 release_evidence():核①完整 40 位 SHA ②**指向该提交的注释 tag** v<版本号>
     ③该提交里 CHANGELOG 顶部版本一致。缺任一条 ⇒ rc=2,身份退回 development 并把
     release_blocked 的原因写进 JSON(给机器读,也给人看)。
   - version.py --self-test:六条负例(有版本无 tag / unknown SHA / 短 SHA / tag 指错提交 /
     轻量 tag / 版本不一致)+ 证据齐正例,**tag 全部在 mktemp 临时仓库里创建**。
   - Go 侧新增 TestReleaseBuildRequiresEvidence:release 构建必须 CommitSHA 完整且
     v<ReleaseVersion> 是**注释 tag** 并指向该提交(独立于 Python 的二次复核)。

2) 发布预检区分"准备建议"与"发布就绪":
   - 新增纯函数 readiness(facts) → (是否就绪, 阻塞项, 建议);输出分两段。
   - **就绪要求候选版本已写进目标提交**(version_in_commit:查 git show <sha>:CHANGELOG.md,
     工作区写了没提交不算);首发按**显式指定版本**处理,**不把"记录里有这个版本"当已发布**
     (已发布的证据是 tag,不是记录条目)。
   - 预检**不执行、也不在输出里推荐自动发布路径**(正式发版由人按 docs/RELEASE_RUNBOOK.md)。

3) CI 就绪判据:必需 context 集合来自 .gitea/required-contexts.txt(缺失/为空 ⇒ 没有策略 ⇒
   阻断,不用观察到的状态顶替);**全部 success 才通过**,失败/缺项/pending/查询异常一律阻断。
   Release 查询加 release_state():**只有 HTTP 404 才算不存在**,403/5xx/超时算查询异常 ⇒ 阻断。

验证:version.py --self-test 绿;release_preflight --self-test 绿(就绪判定 19 个分支 +
临时仓库验证 git show 语义 + 五个 HTTP 码语义);go test 三态(未注入 SKIP / 开发 PASS /
自称正式 RED);反向夹具 83/0(含临时仓库 go test 六例、预检"记录未进提交"两态);
make verify 15/15 success。
2026-10-13 15:26:49 +08:00
姚章浩 05144309c3 feat(version): 构建身份(release_version/build_kind/display_version),无基线也放行
解决"开发构建沿用已发布版本号,分不清开发版与正式版":

- 新增 `scripts/version.py --identity`:release_version(无基线即空)/ build_kind /
  commit_sha(完整 40 位)/ display_version / built_at。显示规则:发布 `X.Y.Z`、
  开发 `X.Y.Z-dev+g<短SHA>`、无基线 `dev+g<短SHA>`、什么都拿不到 `dev`。
- `build_kind=release` 只认**显式发布上下文**(`BUILD_KIND=release` / `--release`);
  声明发布构建却没有已发布版本 ⇒ 拒绝(rc=2)。普通 main 构建永远是 development,
  **不许只显示上一正式版本号、不许自称正式版**。
- Go 侧 `internal/version` 加 ReleaseVersion/BuildKind/DisplayVersion()/IsRelease()/Info(),
  原 Number/CommitSHA/BuildTime 语义不变(向后兼容);契约测试改成:发布构建必须等于
  CHANGELOG 顶部、开发构建必须是 dev 形状;`make verify` 的 contracts 项新增
  **负例**「自称正式版 ⇒ 必须红」(只有正例的契约会被静默掏空)。
- 构建接线:Makefile/Dockerfile/ci_checks `--internal-build` 统一走 identity;
  Makefile 的 `make version` 打印身份 JSON;Dockerfile 新增 RELEASE_VERSION/BUILD_KIND。
- `tests-go` 的"按设计跳过"声明补上 `TestDevBuildDoesNotClaimRelease`(它的强制点在 contracts)。

验证:go test 三态实测(未注入 SKIP、开发身份 PASS、自称正式版 FAIL);
--internal-build 产出并打印 dev+g<sha>;--internal-identity 契约项 rc=0;
make verify 15/15 success;反向夹具 80/0。
2026-10-13 14:59:36 +08:00
姚章浩 c04abd6530 style(go): 清掉存量 gofmt(6 个文件的字段对齐与文件末尾换行)
接入格式门禁前先量了一次存量:gofmt -l 在 295 个被跟踪的 .go 文件里报 6 个,
全是字段对齐与缺文件末尾换行这类机械问题。先一次性清干净,门禁接上后 Go 侧一条都不欠。

为什么独立一个提交:格式清理混进功能改动会让 diff 无法 review(正本要求存量清理独立提交)。
验证:gofmt -l 对被跟踪的 .go 文件输出为空;go vet 与 go test 全过。
2026-10-12 00:10:31 +08:00
姚章浩 5f3a71cade feat(version): 版本号唯一来源 = CHANGELOG.md,并要求写清改了什么与怎么验收
build / build (push) Successful in 26s
背景:本仓此前没有任何版本/构建元信息 —— 没有 ldflags 注入、也没有产物可读的
版本号,"线上跑的是哪个提交"答不出来;"这次改了什么、怎么验收"也没有载体。

落地:
- CHANGELOG.md:**版本号的唯一来源**(正本 §6.1:版本号只准有一处)。刻意不放
  VERSION 文件、也不靠 git tag —— CI 是 `git fetch --depth 50`,取不到 tag,
  `git describe` 会静默退化。每个已发布版本必须有两节:### 改了什么 与 ### 怎么验收。
- scripts/version.py:从 CHANGELOG 顶部解析版本号;解析不到即非 0 退出(构建据此失败)。
- scripts/check_changelog.py:结构自检(两节齐、带日期、版本号不重复且严格递减、
  小节不为空),带 --self-test 反向自检。
- internal/version:三个构建期注入变量 + 契约测试(注入的号必须等于 CHANGELOG 顶部)。
- Makefile:build 带 -ldflags 注入;verify 增加 changelog-check 与 version-contract。
- Dockerfile:ARG VERSION/COMMIT_SHA/BUILD_TIME + ldflags(未透传时落 dev —— 故意显眼,
  好过看起来正常的假版本号)。
- CI:新增「版本与变更记录契约」步,并显式断言契约测试真的 PASS(被跳过的断言等于没有断言)。
- docs/提交规范.md(ci_gate.py 的报错文案一直在指它)、docs/agents/traps.md(7 条实测陷阱)。

验证(都真跑过):
- make version → 0.1.0;make changelog-check → 合规
- make version-contract → PASS;注入 9.9.9(CHANGELOG 里没有)→ FAIL(反向自检)
- 删掉真实 CHANGELOG 的「怎么验收」小节 → 报红(对真实文件做,不只是夹具)
- go build ./... 与 go vet ./internal/version/... 通过

未做(会与在审 PR #2 冲突,待其合并后在 main 上补):
- GET /version 端点(internal/httpapi/server.go)与 cmd/api 的 -version 开关
- compose.yaml 透传 build args(当前由 compose 构建的镜像会报 dev)

⚠️ 未跑 make verify 全量(含 go test ./... 与前端),CI 是本次的测试证据。
2026-10-10 18:13:24 +08:00
姚章浩 1283f65a95 feat(catalog): 首次扫描中的来源也出现在「来源与目录」,并显示实时进度
用户反馈:加了钉盘来源后「来源与目录」里看不到。根因不是丢数据,而是列表只列"已经有快照"
的来源 —— 首次扫描要几分钟到几十分钟(钉盘空间 2900+ 个文件夹),这段时间页面上什么都没有。

- 新 Store 方法 ListCatalogScanProgress:已启用 + 还没有 catalog head + 有在跑/排队的
  source.catalog.refresh(三条件同时成立)。进度直接取 worker 写在任务载荷里的同一份计数
  (progress/message/scanned_directories/discovered_directories/scanned_files),不另立口径。
- /api/v1/source-catalog/sources 在既有列表后追加这些行(snapshot_status="scanning" +
  scan_progress/scan_message)。走窄接口按需接线:未接线时安静跳过,不会把旧列表弄坏。
- 前端:左列显示「首次扫描中 44%」,选中后是进度面板(百分比 + 进度条 + 当前文案 +
  已发现文件数),并说明"扫完自动出现目录";有来源在扫描时每 3 秒自动刷新列表,
  扫完那一行自动变成「本地只读快照」。扫描中不拉目录节点、也不显示标注/格式入口。
- 解析器保持严格:snapshot_status 只接受 ready/scanning,scanning 必须带 scan_progress,
  否则整包拒绝 —— 不给"假装 0%"的机会。

测试:postgres 集成测试(真库)覆盖四种不该出现的来源(停用、已有快照、任务类型不符、
历史上已完成的那次任务);前端 parser + 渲染各一条;Go 全绿、前端 192 条 + tsc 干净。

面板实测:左列「公司级营销资料管理 · 首次扫描中 44%」,主区「首次扫描中 · 44% /
正在扫描目录:已完成 1540 个文件夹 / 已发现约 4934 份文件」。
2026-10-10 17:04:59 +08:00
姚章浩 e00cab2f70 feat(settings): 数据源配置改成「列出来、勾选、变成来源」,并去掉文件格式筛选
用户对 系统设置 → 数据源配置 的四条要求:
① 这里不要做文件格式筛选(格式归目录级标注决定);
② 列出我账号下能看到的知识库;③ 列出钉盘能看到哪些;④ 勾选那些作为来源。

① 来源卡片不再渲染 FilenameFilterEditor(目录级筛选仍在「来源与目录」里,按目录就近覆盖)。
②③ 新端点 GET /api/v1/dingtalk-spaces/available(管理员,只读):用 dws 列举
   - 知识库:wiki +space-list --type orgWikiSpace/myWikiSpace(实测 17 + 1 个)
   - 钉盘:wiki space list --type mySpace/orgSpace(实测 我的文件 + 6 个组织空间)
   实测约束写进命令形态:--limit 上限是 50;翻页必须 --page-all(--max-items 兜底),
   否则超过一页的知识库会被静默截断(被截断时页面显示"可能不全"而不是假装列全)。
   列不出来 ≠ 空列表:dws 未登录/权限不足 → 503 + 原因;某一类失败只丢那一类。
④ 面板勾选后走既有登记路径(/api/v1/dingtalk-spaces),首次目录扫描自动排队;
   已登记的按空间名标「已是来源」并禁用勾选;扫码会话失效时直接弹二维码,扫完自动重试。

基础设施:api 与 worker 共享同一份 dws 状态(bind mount 同一份,refresh token 是轮换制,
不能各留一份)。子进程 HOME 必须指向 DWS_HOME —— 这条是实测踩出来的:漏了会报"未登录",
已有回归测试钉住。dws 子进程沿用白名单环境,库口令等变量不进子进程。

测试:新包 internal/dingtalkdiscovery(fixture 用真实 dws 输出形状):paging 约束、
截断提示、一类失败不连带、全部失败→ErrUnavailable、子进程环境;httpapi 侧 401/501/503/500
与不泄漏内部错误;OpenAPI 契约(400/503 组件)通过。前端 189 条 + tsc 干净。

面板实测:知识库(18)/钉盘(7)都列出来,已有三条钉钉来源自动标「已是来源」;
勾选「视觉文档」→ 加入为来源 → 新来源 sc-8a4be5b8293d1c9a048c3c7e9b194155(dingtalk,enabled)
+ source.catalog.refresh 已完成。
2026-10-10 16:46:39 +08:00
姚章浩 f8faed0b21 feat(release): 发布到生产一键完成(建批+锁定+授权合成一个动作)
用户的反馈是对的:上一轮只砍掉了"冻结/锁定"两个按钮,但一条文档从"已批准"到"发出"实际仍要
~9 次交互(找条目→勾选→填名→建批→滚到清单→找批次→授权→输语句→提交),因为"批次仪式"本身
还留在路径上。

- 新端点 POST /api/v1/release-batches/publish-now:在同一操作里先建批(SealImmediately,冻结
  即锁定)再授权生产发布,返回 {batch, execution}。
- 不放松任何约束:请求门禁(403)、备份恢复验证门禁(412)、管理员身份(401)、固定确认语句(400)、
  发布前快照硬门禁(409)、建批门禁(422) 全部沿用既有实现,无旁路;名称可省(服务端固定回退名,
  保证重放指纹一致)。
- 界面:审核行上直接「发布到生产」(管理员 + 门禁开启时显示),工具栏支持选中多条一次发布;
  复用既有确认弹窗(固定语句)。批次名不再是必经步骤。
- 实测(面板):点行级按钮 → 输 `publish-to-production` → 提交 → 批次 rb-bee80550…(sealed)
  + 执行 rx-3d831129…(completed, 项级 verified) → 页面回滚 rx-43835fac…(rolled_back, absent)。

测试:publish_now_test.go(四道关卡 + 组合调用 + 名称回退)与 reviewRelease.test.ts(请求体/回传/错误原因);
Go 26 包、前端 183 条全过。
2026-10-10 15:48:30 +08:00
姚章浩 70131c6161 feat(release): 批量审核 + 建批即锁定 —— 发布管理单文档交互降到 3~4 次
P0-3:
- 批量审核:新增 POST /api/v1/review-candidates/batch-decision,逐条复用单条审核的写入
  路径(同一套合法性校验与"最新验收轮次"绑定),逐条回报 decided/skipped/skipped_ids;
  不可审核的条目只计入 skipped,不打断整批,其他错误整批回滚(不留半成品)。
  界面新增「全选可批准」「批量批准选中」「批量驳回选中」。
- 建批即锁定:建批请求加 seal 字段,服务端在创建后立即执行既有的密封(仍逐项复核门禁,
  失败整批回滚),草稿态不再单独占一步;界面按钮改为「创建并锁定清单」,流程说明从 8 步改 7 步。
- 判据修正:「可批准」原先把已驳回的算进去(review_state !== 'approved'),改为只含 pending + 门禁通过。

语义边界(写进测试):批量批准按人工身份记录,人工可推翻自己先前的驳回;system:auto 发起的
批量批准对人工驳回过的那条必须跳过(Decided=0/Skipped=1)——机器永远不覆盖人工。

测试:review_batch_integration_test.go(批量决定/跳报/重复 id 拒绝/机器不覆盖人工、
建批即锁定)、review_batch_test.go(幂等键、400/409/500/501 映射、服务端盖操作人)、
reviewRelease.test.ts(批量请求体与逐条回报、建批带 seal)。前端 181 条通过。
2026-10-10 15:29:31 +08:00
姚章浩 acf053faf3 feat(release): 建批时冻结 + 验收通过自动批准 —— 把"每份文档 8 步"砍掉两步并去掉两次卡死
背景:发布管理把"逐条核验"的仪式强加给每一份文档。实测一份文档要 9~11 次交互,
且有两处顺序陷阱(冻结后批准 → 建批 422;生成即固化 → 批准被永久钉住)。

P0-1 建批时冻结:
- CreateReleaseBatch 在未指定版本时,自己把当前实时计划冻结成一份**已确认**版本并绑定
  (同一事务;同摘要复用最近版本,planned 或 confirmed 都可复用,因此同一份计划不堆版本)。
- 语义不变:批一旦建立,清单/摘要/验收轮次/目标库绑定全部不可变、回滚按它走;变的只是
  "谁在什么时候按固化键"——从操作员的手动两步变成建批这一下。
- 效果:去掉「生成新版本」「确认冻结」两步;消灭"批准晚于冻结 → 422"这类顺序陷阱;
  前端不再传版本 id,也不再有"所选知识不在冻结清单里"的阻断。
- 计划→清单项的映射抽成 ChangeSetItemsFromPlan 供两处共用,否则两处算出的摘要不同,
  "同一计划复用同一版本"永远不命中。

P0-2 验收通过自动批准:
- 验收三项结论达发布门槛(knowledge_exists/chunks_present=passed,retrieval_hit 与门槛同
  口径)时,由 worker 写入 reviewer_ref='system:auto' 的批准记录,替代"每份文档点一次批准"。
- 策略 TEST_AUTO_REVIEW_POLICY = off | internal(默认,仅对内)| all;值不认识拒绝启动。
  只影响审核这一步:生产授权、双闸与发布前快照门禁不受影响。界面上只读展示当前策略。
- 安全边界(写死在审核 SQL 里):系统只允许写"第一条"审核——人工一旦表过态,机器不得用一条
  更晚的 approved 把人工结论翻回去;人工随时可再审。集成测试去掉该约束即 FAIL(已实测)。
- 自动批准自身的任何失败都不影响验收结论,只上报。

顺带(P1-1 前半,已被 P0-1 取代的部分已删除):
- 写操作失败把服务端 {"error": ...} 的原因带出来,并对已知原因给出下一步(不再只给 HTTP 状态码)。
- 当前的"不在已冻结清单里"标注与就地修复按钮随 P0-1 一起移除(该失败模式已不存在)。

测试:bind_time_freeze_integration_test.go(绑定已确认版本/同计划复用/冻结后不可变/
动作不符仍 NotEligible)、auto_review_integration_test.go(系统只写第一条、人工可再审)、
validate_test.go(对内自动/对外不自动/策略关闭/验收未过不批/自身失败不影响验收)、
auto_review_test.go、config_test 及前端能力解析测试。
实测耗时:本机 Go 全量 26 包通过、前端 178 条通过。
2026-10-10 15:05:20 +08:00
姚章浩 a1b46d2b7c feat(monitor): 需下架车道可逐条移除测试库知识 —— 补齐最后一个没有页面入口的流程
背景:来源侧被判「禁止导入」后,已建好的测试库知识不会自动消失;此前只有变更集
删除路径会入队 test.remove,页面既列不出条目也没有按钮,运维无法闭环(车道只有按
库计数)。

新增:
- GET /api/v1/test-publications?removal=required:条目级列表,判据与车道计数同源
  (superseded_at IS NULL / 有 knowledge_id / completed / retrieval_state NOT IN
  (disabling,disabled) / policy_state='blocked'),过滤是叠加而不是替换。
- POST /api/v1/test-publications/{id}/removal:engineer 级写路由,要求 Idempotency-Key,
  按条目幂等(operator-remove:<id>);置 disabling 并交既有 test.remove 工作流去删除
  远端知识并读回校验。错误映射 404/409(已移除)/409(不可移除)/501(未接线)。
- 界面:需下架车道逐条显示条目 + 「下架(从测试库移除)」,页内二次确认,成功提示,
  完成后计数归零。

修掉过程中踩到的两个真问题:
1. 二次确认的确认态原先经容器 props 传给视图,一次容器改动把三个 props 丢了 ——
   视图永远渲染原按钮、确认步骤静默消失,而所有静态单元测试仍然通过。改为把确认态
   放在视图内部;并把「取消」放在原按钮位置(真实指针连点两下只会落到取消)。
2. jobs 上有部分唯一索引 UNIQUE (business_change_set_id, business_version_id,
   business_target_kb_id, business_action) WHERE business_change_set_id IS NOT NULL。
   本路径给这三列写空串(非 NULL),('','','','remove') 对所有条目是同一个键 ——
   第一条之后每次 INSERT 都撞唯一索引、被 ON CONFLICT DO NOTHING 静默吞掉,留下
   「状态已 disabling、没有任务」的永久卡死条目,而车道恰好隐藏 disabling,运维也
   看不到。改为写 NULL(语义:无变更集身份)+ 冲突目标收敛到 idempotency_key +
   校验 RowsAffected()(0 行回查既有任务;查不到就报错,不提交无任务的 disabling)。
   卡死状态在重试时可自愈。

测试:internal/platform/postgres/test_publication_removal_integration_test.go(两条发布
各自入队、business_* 为 NULL、卡死自愈;把修复退回旧写法该测试即 FAIL,已实测)、
internal/httpapi/test_publication_removal_test.go(权限/幂等键/错误映射/未接线 501/列表
过滤)、前端 testPublications.test.ts + WeKnoraMonitor.test.tsx。

验收(纯界面,两条都走完):解析监控 → 需下架(2)→ 逐条「下架」+ 二次确认 →
retrieval_state=disabled(PDF 03:57:03Z、md 04:08:55Z),jobs completed(attempt 2,
首次 test_remove_pending=远端仍在处理,重试通过;工作流以 GetKnowledge 读回 404 判
成功)→ 车道需下架(0)。核验「确认前不执行」:首次点击后仍 unpublished、移除任务数仍 1。
2026-10-10 12:10:46 +08:00
姚章浩 e9a643cc33 fix(release): 禁止导入未被算进期望策略 —— 生产知识下不了架(计划与建批两处同源缺陷)
界面验收实测(2026-10-10):把已发布到生产的 PDF 改为「禁止导入」后,发布通道既给不出
delete、也建不了 delete 批次。

根因:禁止导入只写 source_items.policy_state='blocked',而"期望策略"的解析只看
source_item_scope_overrides / directory_scope_rules / source_items.visibility——
两处实现都不看 policy_state:
  1) release_plan.go 的 source_truth:被禁止且生产侧存在的条目被算成 unchanged/present、
     can_release=false → 永远不会出现 delete 动作;
  2) release_batches.go 的 releaseDeleteReadyCTEs.source_truth:同样口径 → 计划即使给了
     delete,创建封批仍被拒 422(实测原文「创建封批失败(HTTP 422)」)。

修复:两处都把 policy_state='blocked' 算进期望策略(各一行,注释写明成因)。
新增集成测试 release_plan_policy_state_integration_test.go:用 policy_state(真实路由的写法)
造"生产存在 + 已禁止"的条目,断言①计划给出 action=delete/can_release、②delete 批次可创建;
分别回退任一处修复,该测试即 FAIL(已实测两种回退都 FAIL)。

实测闭环:重新发布使生产存在(enable_status=enabled)→ 禁止导入 → 计划 delete(present/
can_release=true)→ v8 delete_count=1 → 批次 rb-8c1ad1c9…(sealed, action=delete) → 留档
20261010T033100Z + 门禁 PASS → 执行 rx-6c19232a… completed(项级 verified/delete、
production_revision=absent)→ 目标库该行 enable_status=disabled(行保留、检索停用、可恢复)。

附带:从未导入却被标为禁止的 82 条"等待测试"噪音不再占发布计划(195 → 113 项)。
证据与说明补进 docs/USER_FLOWS_ACCEPTANCE.md(C2b + F.3h)。
2026-10-10 11:36:08 +08:00
姚章浩 d0a45d563f fix(monitor): 需下架车道恒为 0(判据用了没人写的状态)+ 隔离会改线上设置的集成测试
界面验收第三遍(禁止导入 → 需下架)暴露两处真问题:

1) 需下架车道死掉:判据 `retrieval_state='enabled'`,而 retrieval_state 在这套状态机里
   描述「是否发布到生产」(unpublished/disabling/disabled/failed),全仓没有任何代码写过
   'enabled',线上 5/5 发布单元都是 unpublished → 车道恒 0,界面「仍在知识库里的文件会
   出现在这里」的承诺不成立。改为与设计注释一致的口径:仍在测试库存活(completed + 有
   knowledge_id + 未 disabling/disabled)+ 来源项 policy_state='blocked'。
   线上对照:旧判据 0 → 新判据 1,命中被禁止导入的 DH-AG95夹爪适配-指令协议.md;
   修后页面显示「需下架(1)· 对内知识库 · 1 份仍在检索中」。新增集成测试覆盖
   存活/未禁止/移除中/已移除/未上传五种情形(真实 Postgres + 独立 schema)。

2) 集成测试改到线上设置(直接踩到并已恢复):ui_settings_integration_test.go 只判断
   TEST_DATABASE_URL 是否存在、无 schema 隔离,而它「创建候选并激活」——跑在共享库上会把
   真实 active 设置挤成 retired。已先备份(backups/ui-settings/…pre-restore.jsonl),再用
   单事务(先 retire 再 activate,uq_ui_setting_active 只允许一条 active)恢复
   uis-d40ea1fc…(15/30/1800/7200 + https://weknora-test.dobot.cn/),并复核线上值。
   根因修复:给该用例加 per-test schema 隔离,复跑后线上设置不再被改动。

同时把两处与事实不符的文案改实话(WeKnoraMonitor.tsx 需下架说明、SourceCatalogBrowser.tsx
禁止导入说明):移除走 test.remove 工作流(删除测试库知识条目,可再入库),入口当前由
变更集删除路径触发,页面尚无独立按钮——条目级列表与下架按钮列为待排期。
2026-10-10 10:55:03 +08:00
姚章浩 eabe55b432 fix(release): wiki 校验与写入的归属口径不一致导致跨环境发布必然失败
端到端验收实测(2026-10-10):文件型文档(docx)跨环境发布时,写入侧按设计保留了
目标库外来源 wiki 页(日志 in_scope=0 foreign=8 untouched=1 degraded=true),
但校验侧 VerifyWiki 仍要求"生产页 slug 集合与包内快照完全相等",必然误判,项级
报 release_verification_failed,批次随后被判失败单项失败。

- verifier.go: 归属判定与写入侧 replaceWiki 共用 packageWikiOwners;
  校验改为"快照内页必须都存在 + 多出来的页只有在不属于本包时才放行",
  保留原有牙齿:本包拥有却缺失/残留的页仍判失败;撤权仍禁止任何页引用被撤权文档
- wiki_channel_test.go: 用例改为 外来源多余页放行 / 本包拥有残留页拒绝 / 缺页拒绝
- 实测:同一批次重排后完成(done=1/1 item=verified 生产可用),回滚 rolled_back
  这也是 wiki 归属门禁上线后第一次真实跨环境发布
2026-10-10 10:22:06 +08:00
姚章浩 d29e04e647 fix(artifact): 白名单环境漏传 dws 变量导致钉钉取件整体失败
端到端验收实测(2026-10-10):标注后取件以 artifact_fetch_rejected: credentials 终态失败。
根因:artifactfetch runner 给子进程的是白名单环境(防凭据外泄),但没有透传 dws 取件器
必需的 ARTIFACT_DINGTALK_FETCHER/DWS_BIN/DWS_CONFIG_DIR/DWS_HOME/DWS_DISABLE_KEYCHAIN
→ dws 找不到配置目录与文件型 keychain($HOME/.local/share/dws-cli 落空)→ 报未登录 →
映射成 credentials 终态(不可重试)→ 且不会回落老路径。脚本单跑正常,因为绕过了白名单。

- handler.go: childEnv() 显式透传 dws 变量(仅非空值);失败时把 stderr 尾部脱敏后带进错误,
  否则只能看到裸 code 无法定位(本次排查就卡在这)
- 新增 3 个回归测试:环境透传 / 空值不透传 / stderr 上报与敏感内容抑制
- 实测:修复后重新入队 → artifact.fetch completed,产物 sn-753f….docx 4799B(adoc→docx 导出),
  content_objects 记为 docx/verified
2026-10-10 09:57:41 +08:00
姚章浩 e95a50ca3a fix(backup): 保存后即时反映主机状态;最近一次备份不再重复同一时间戳
- 后端 handleSaveBackupSchedule 改为先下发、再组装投影:此前页面保存成功那几行仍是
  下发前的主机计划,看起来像没生效(需等下一次刷新)
- 前端 runMoment:清单里的 created_at 就是目录戳,同值不再重复显示(原为
  '20261009T060736Z · 20261009T060736Z')
- 前端补 runMoment 用例(171 个 vitest 全绿)

面板实测:保存后立即显示 每天 03:30 / 保留 7 天 / 已一致,cron 条目 30 03 * * *
2026-10-09 14:12:10 +08:00
姚章浩 343a005db1 test(backup): 迁移计数更新到 45 并补 44/45 内容断言;新增 backup_schedule 真实库往返集成测试
- migrations_test.go 之前硬编码 44:新增 000045 后该包测试失败(全量套件里才暴露)
- 现在断言 migration 44 = production_kb_bindings、45 = backup_schedule
- 新增 TestBackupScheduleRoundTripIntegration:默认值 → 保存(upsert 单行) → 读回(含 updated_by/at)
  → 记录下发结果(不改计划);实测在 governance 库隔离 schema 上 PASS(0.95s)
2026-10-09 13:55:35 +08:00
姚章浩 3f603aa293 feat(backup): 系统设置里的存储与备份——定时时间与储存天数可配、下发到目标主机 cron
需求:整库逻辑备份做定时,但要在「系统设置 → 存储与备份」里设置定时时间与储存天数。

后端
- migration 000045 backup_schedule(单行:enabled/schedule_time/retention_days/retention_months/
  last_applied_*),store + 领域校验(HH:MM、1~365 天、0~24 月,非法值 400 且不落库不下发)
- internal/backuphost:通过 ssh 在目标主机上读回/下发计划并立即备份(未接线时 Available=false,
  页面降级提示;主机侧错误带输出回传)。最近一次只认有 MANIFEST.json 的完整目录,跳 *.incomplete
- 接口 GET/PUT /api/v1/backup-schedule、GET …/host、POST …/apply、POST …/run;
  写操作经 classifyRole 限管理员;run 为异步 202 + 在途单飞(http 写超时 30s,备份约 35s)
- openapi.yaml 补齐 5 条路由(契约测试要求每条都写 400/500)

主机侧
- scripts/install_backup_schedule.sh(幂等):写 /etc/knowledge-governance/backup-schedule.env 与
  /etc/cron.d/kb-logical-backup,参数校验,--show/--disable,最后一行 SCHEDULE_APPLIED 供平台核对
- logical-backup.sh 从该文件读保留策略;失败或被中断把目录改名 *.incomplete,不留'像成功'的半成品
- install --show 只回显计划行(原先把 SHELL=/PATH= 当计划显示)

前端
- web/src/backupSchedule.ts(严格解析:缺计划字段即报错,不把默认值伪装成设置)
- BackupScheduleSection:容器 + 纯视图(管理员表单/只读、已一致/待下发、在途轮询)

运行前提(已落实):运行镜像加 openssh-client,且容器 uid 1000 在 /etc/passwd 有名字
(否则 ssh 报 No user exists for uid 1000);凭据只读挂载 secrets/backup-host(git 忽略)

实测:主机 cron 每天 03:30 / 保留 7 天 / 每月 3 份;立即备份一次 34 秒 / 742.7 MiB / 失败 0;
Go 与前端测试全绿(170 vitest)。
2026-10-09 13:50:23 +08:00
姚章浩 b32755b35f feat(backup): 备份目录契约 + 导入前留档覆盖 ES 层
- scripts/backup_layout.sh:定义 $KB_BACKUP_ROOT/{target-kb-snapshots,rds-logical,es-snapshots,logs}
  与 /etc/knowledge-governance/backup.env,任意主机跑同一份(测试云主机 → 未来生产云主机)
- scripts/es_kb_export.py:只读 scroll 导出目标库 ES 文档(gzip JSONL,_id/_source,可按 _id 重建)
- scripts/target_kb_snapshot.sh:留档改为 PG 行 + ES 文档两层;PULL_ES 默认 0(692 MB / 家用上行 19m40s,
  留在目标主机,清单记 location=cloud:<host>:<path>);挂载源与落点统一走 KB_BACKUP_ROOT
- scripts/logical-backup.sh:自管整库逻辑备份(pg_dump -Fd -j4 -Z6,按天轮转)
- internal/releasesnapshot:门禁要求 PG 与 ES 两层齐备(ES 计数 >0);云侧留档核声明与哈希、
  声明本地却缺失则 fail-closed;新增 3 条用例(共 10)
- cmd/guardcheck:用真实产物复核门禁判断
- docs:PRODUCTION_BACKUP_RECOVERY.md 重写(契约/RDS 控制台/恢复路径/边界);P6 同步留档语义
2026-10-09 12:19:15 +08:00
姚章浩 1fa35e9870 feat(release): 发布前快照硬门禁 + 云内快照 + ES/OSS 保护
按生产级备份组合落地:

- internal/releasesnapshot:新增门禁包,要求 root 下存在覆盖目标库、未过期
  (默认 24h)、含 wiki_pages.jsonl 与 sha256 的快照;本地文件再校验哈希。
  7 条单测(过期/换库/篡改/缺页/未接线/半成品)。
- 发布器:写目标库之前(Publish/Delete/Rollback 三条路)先过门禁;映射目标
  (源库→目标库,共享真实库)或 policy=always 时必须有快照;门禁未接线
  同样拒绝(fail-closed)。新错误码 release_target_snapshot_missing(不可重试)
  + 页面中文提示。6 条发布器单测。
- worker 配置新增 target_snapshot{root,max_age_hours,policy},校验 policy/
  max_age/绝对路径;compose.release-sim.yaml 只读挂载快照目录。
- scripts/target_kb_snapshot.sh:默认云内执行(与 DB 同 VPC,凭据取自 WeKnora
  容器)+ 默认不导 chunks(44s 出 8 张表);修复 \copy 不能跨行/不能带 WITH、
  ES 计数改用 knowledge_base_id.keyword。
- ES:注册 kb_backup 仓库(path.repo),首个快照 36s/4.7GB/0 分片失败,
  SLM kb-hourly(3d)+kb-daily(7d) 已建并执行成功。
- OSS:目标桶开启版本控制(此前未开启)。
- 文档 P6 增补硬门禁与云内执行;真实快照目录上门禁实测:两个绑定库通过、
  未覆盖库拒绝、未接线拒绝。
2026-10-09 10:59:55 +08:00
姚章浩 e5fa063d7d feat(release): wiki 通道按归属判定替换,共享知识库不再被整库清空
WeKnora 的 wiki 图谱与页面同表(wiki_pages),旧的库级 DELETE + 灌包在共享真实
知识库上会把其他文档的页面与 agent 手写页一起抹掉(2026-10-08 生产演练现场)。

- 新增 internal/releaseadapter/v071/wiki_scope.go:按 source_refs 判定归属
  (全部引用属于本包 = InScope;引用包外知识 = 外来页;无引用 = index/log/
  agent 页永不触碰),目标库存在外来页时降级为只替换包内页集,外来页上仅摘掉
  本包知识引用,包内库级页不写;包内会写入的页与外来页同 slug 时 fail-closed
  拒绝发布且不落任何改动。
- Replace 与 ReplaceWiki(撤权/回滚)共用同一套判定,不再整库清空。
- 作用域以一行日志落盘:release wiki replace scope in_scope=… degraded=…
- 测试:单测 7 条(纯函数判定)+ 真库集成 3 条(共享库保护 / slug 冲突拒绝 /
  镜像库整库替换不变),集成用例只用带随机后缀的临时知识库并自清理。
- 文档:P6 新增「wiki 通道的归属门禁」,P8 补验收点与镜像前提。
2026-10-08 19:06:36 +08:00
姚章浩 86732ca987 fix(api): 执行明细补齐 action 与目标知识库字段
逐项明细 DTO 漏了 action(前端 parseReleaseItemExecution 必需),点
「查看结果」整个明细区报 invalid release item execution。同时把遷移 000044
的 target_knowledge_base_id 读出来透出,页面逐项结果显示「目标库」,
让'目标知识库可选'在界面上可核对。测试:Go 契约用例 + 前端解析回归用例。
2026-10-08 17:26:26 +08:00
姚章浩 280a81c855 fix(api): 执行列表返回批次进度字段,修复页面执行列表解析失败
/safeReleaseExecution 没带 batch_size/current_batch/total_batches,而前端
parseReleaseExecution 要求这三个计数,任何一条执行记录都会让整个列表抛
'invalid release execution',页面因此看不到执行历史与回滚/重排按钮,门禁
状态也一并回落到'已锁定'。补上这三个非敏感进度字段。
2026-10-08 17:15:32 +08:00