Commit Graph

15 Commits

Author SHA1 Message Date
姚章浩 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
姚章浩 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
姚章浩 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
姚章浩 2de7a43286 feat(release): 生产授权支持页面通道(管理员会话 + 固定确认语句)
页面原本没有任何生产入口,授权/重排/回滚只能带 Bearer 调接口,实际还要 admin 会话。
现在三个端点支持两条通道,门禁语义不变:

- 运维通道:Authorization: Bearer <token>,审计记 PRODUCTION_RELEASE_AUTHORIZER_REF
- 页面通道:已认证 admin 会话 + 固定确认语句,审计记该管理员用户名
- 请求门禁 / 备份恢复门禁 / Release Worker 执行门禁三者不受影响
- 发布管理第 6–8 步加「授权发布 / 重排 / 回滚」按钮,点击需逐字输入确认语句,
  仅 admin 可见;重排沿用原执行授权人,不改写历史
- openapi 与 P6 契约同步;测试:双通道各一条 + 非 admin/错短语拒绝 + 前端按钮与确认门禁
2026-10-08 17:00:49 +08:00
姚章浩 6440e3f679 feat(release): 发布目标知识库可选(源库 → 目标库映射)
- 迁移 000044:production_kb_bindings + release_item_executions.target_knowledge_base_id
- 系统设置新增「生产知识库绑定(对内/对外)」:从生产 WeKnora 实际列表选库,
  保存时真校验(不存在拒收;生产端不可达只提示并如实回报),仅管理员可改
- 执行计划按绑定解析目标库并冻结进 release_item_executions;空目标直接拒收;
  已发布过的条目保留原目标(重试/回滚回到同一处)
- DocumentBundle.RemapKnowledgeBase:knowledges/chunks/knowledge_tags/wiki_pages/
  wiki_folders 的 knowledge_base_id 与 ES 文档字段改写为目标库,重算表/文档摘要并重新封装
- 契约比对改为「声明的映射对」(KnowledgeBaseMapping),其余字段继续 fail-closed
- 契约检查器 / 目标身份 / 验证器全部走映射后的目标库;对象通道无需改写
- Compose 拆分:数据面接线 compose.release-sim.yaml + 两道门禁 compose.release-gates.yaml
- 测试:RemapKnowledgeBase 全通道/无操作/fail-closed、映射契约、绑定接口、RBAC、真实 PG 集成
2026-10-08 15:41:13 +08:00
hermes cddc4c90d1 fix(monitor): 修复取件死信无法重排与列表 NULL 标志导致的 500
- platform/postgres: listTestPublicationsSQL 的 can_start/can_retry 在 LEFT JOIN
  内容对象为空时算出 NULL,Go 侧 bool 扫描失败使整个列表接口 500,前端三个 lane
  全空(表现为“待入库 0 / 全部任务 0”)。改为 COALESCE(..., false)。
- 新增 POST /api/v1/job-failures/{id}/requeue:取件任务的幂等键绑在版本上,
  失败那次已占住它,重新标注同一文件不会再入队;现在按新轮次 (…:r<n>) 重排、
  复位意图取件状态并把死信标记为 requeued(幂等信封 + 审计字段)。
- web: 死信区对 artifact.fetch 显示「重新入队」;系统设置新增「知识库绑定」分区,
  挂上一直没被渲染的 RoutingBindingsSection,导入目标库可在页面上切换。
- 测试:真实 PostgreSQL 集成测试(NULL 折叠 + 重排轮次/幂等/拒绝)、前端死信按钮
  与设置分区渲染测试。
2026-10-08 12:09:34 +08:00
姚章浩 1d6ba6dc2e fix(monitor): keep failing publications visible outside the queue page
- The attention lane was derived from page 1 of the queue (limit 50, newest
  by updated_at), so failures ranked past 50 silently disappeared from
  "needs attention" as the completed backlog grew.
- Fetch attention items with a dedicated request: the API now accepts a
  comma-separated processing_state list (failed,stalled) and the view prefers
  that result set over the paged queue.
- Batch retry/cancel targets are computed from the same failure set instead of
  the visible page, so actions reach every failing item.
- 全部任务 shows 已显示 N / 共 M plus 加载更多 so rows past 50 are reachable.
- Fix pre-existing ui_settings integration test: strict JSON decoding rejects
  the legacy updated_by field (actor comes from the session).
- Tests: view regression (attention item outside the page, load-more
  affordance) + handler multi-state filter + isolated-DB set filter.
2026-09-30 10:54:07 +08:00
姚章浩 5037c05e93 feat(monitor): 取消失败发布并修正危险按钮对比度
- Parse-monitor: the batch-retry button rendered red-on-red because
  weknoraMonitor.css overrode the global .danger style with text color
  only; make .danger a filled red button with proper hover/disabled states.
- Add operator cancel: 批量取消所选 / per-task 不再处理 buttons move
  failed/stalled publications to a new 'cancelled' state in one audited
  batch operation (POST /api/v1/test-publications/cancel-batch, idempotent,
  migration 000041 widens the CHECK constraints). Queued upload jobs are
  cancelled, open dead letters abandoned; source records and spool files
  stay intact so re-marking in the catalog can retry later.
- Queue status filter and labels learn 已取消; OpenAPI contract updated.
2026-09-30 10:54:07 +08:00
姚章浩 b3c9331966 feat(ingest): 源与目录文件名过滤及钉钉文档导出
- filename_filter_rules table (000035) with source-root and per-directory rows
- additive inheritance: source default + every ancestor directory contributes
- filename_is_filtered() SQL predicate applied to catalog browsing, subtree
  counts, change-set diffing, routing fan-out and artifact fetch manifests
- fetch_state 'filtered' (CHECK extended); reconcile cancels/restores fetch
  jobs, retires planned publications and blocks items when rules change
- GET/PUT/DELETE /api/v1/filename-filters (Idempotency-Key, normalized
  extensions) + Settings and Catalog filter editors
- failJobSQL no longer swallows the claimed row when the failure projection
  upserts (fixes spurious 'lease lost' retries)
- DingTalk live fetch: adoc/axls now use the export-job pipeline (HTTP 403
  from file/download fixed), header-signature downloads supported
2026-09-30 10:54:07 +08:00
姚章浩 e60b10256a fix(settings): add DingTalk spaces and render the real login QR
Address two gaps in the first DingTalk-session UI:
- add an explicit admin-only '+ 新增钉钉知识库' action on the data-source
  panel; it accepts an alidocs space URL, resolves the live name/root through
  the authenticated sidecar, writes the inventory root atomically, upserts a
  deterministic source connection, and enqueues its first catalog refresh
- read DingTalk's real QR canvas pixels rather than taking a DOM screenshot;
  select by dark-pixel content, composite onto a white quiet zone, and render
  at 320x320 so the dialog contains a large complete code, not a page thumbnail

The live refresh path resolves registered roots from the connection locator,
while existing name-based DingTalk sources remain compatible.
2026-09-30 10:54:07 +08:00
姚章浩 173b44abca feat(settings): refresh DingTalk scan sessions from the data-source page
DingTalk source scanning depends on a shared browser session whose capture
file had to be replaced by hand whenever it expired (every scan 401s).

- services/dingtalk-login: admin-triggered Playwright sidecar that opens the
  space page in a persistent headless Chromium, publishes the QR through
  session polling, verifies the first authenticated XHR against the box API,
  then writes the capture in place (same inode keeps the worker's ro bind
  mount current); tokens never leave the sidecar
- api: POST/GET/DELETE /api/v1/dingtalk-session authenticated relay; QR is a
  login artifact, so the whole prefix is admin-only in classifyRole; OpenAPI
  contract and proxy/503/hostile-ID tests included
- web: 系统设置 → 数据源配置 钉钉卡片新增扫码刷新会话(仅管理员可见),
  QR 弹窗 + 1.5s 轮询 + 成功/过期/失败状态
- compose: sidecar service owns the ONLY rw mount of secrets/; worker keeps ro
2026-09-30 10:54:07 +08:00
姚章浩 021e91eae4 feat(settings): bind internal/external knowledge bases from the UI
Routing targets move from env-only to a UI-managed binding (migration
000034 routing_kb_bindings, single active row):
- GET/PUT /api/v1/routing-bindings + /options: env defaults act as the
  bootstrap fallback; a UI binding wins and takes effect immediately
  (per-request resolution, no restart), and clearing falls back to env
- PUT validates against the connected WeKnora instance (real knowledge
  base list via the scoped API key) and rejects same-KB bindings;
  saves require the admin role
- route/catalog capabilities now resolve through the binding so both
  annotation paths share one source of truth
- system settings > 服务配置 gains a 知识库绑定 panel: dropdown picker
  (or raw ID when WeKnora is unreachable) showing bound name, origin
  (UI vs env default) and last author
- contract tests: OpenAPI, handler matrix (fallback, validation,
  same-KB rejection, capability resolution), frontend fetch/save layer
2026-09-30 10:54:07 +08:00
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
2026-09-30 10:54:06 +08:00