feat(library): Wiki 页面级差异与页面展示
build / build (push) Successful in 1m24s

为什么:撤销文档后 Wiki 变化此前只有"快照保存"证据;页面级差异能给出增/改/撤,
但必须写明它不等于实体、概念、边与引用的语义重算。

- overview 差异新增 wiki 字段:读两份快照的 wiki_pages 行数组比较,缺部件时 unknown。
- 发布管理"本次变更"显示 Wiki 新增/更新/撤销页数与页面级声明。
- 真实归档验证:对内 vs 对外 = 新增 31 / 撤销 1,反向镜像一致。
This commit is contained in:
姚章浩
2026-10-28 15:15:33 +08:00
parent 7b8cc0384b
commit 2038462dbb
6 changed files with 113 additions and 1 deletions
+5
View File
@@ -14,6 +14,11 @@
- worker `library.sync` 接执行编排:先闸口、再授权,写入阶段落库;生产目标适配器未配置时明确失败。
- 页面"生产保护与恢复"显示发布授权状态与有效期;系统设置→存储管理显示容量门禁、受保护版本与清理预览。
### Added(Wiki 页面级差异)
- `GET /library/overview` 的差异结果新增 `wiki`:两份快照 `wiki_pages` 的新增/更新/撤销页数与样例,
明确标注页面级、不代表实体/边/引用重算;缺部件时如实 unknown。
### Changed
- 启动同步的缺项包含"没有与目标快照匹配的有效发布授权"与容量预算;作业只有在编排返回成功时才标成功。
+2 -1
View File
@@ -19,7 +19,7 @@ A06(内容、问答、引用图片效果、性能报告)已被用户取消
| --- | --- | --- | --- | --- |
| V01 | 只有对内/对外两个目标,身份来自配置映射 | `internal/httpapi/library_*.go`(scope 只接受 internal/external);`web/src/ReleaseTargets.tsx` | 已实现·已验证 | overview 对非法 scope 返回 400;真实归档里 internal/external 各自解析到唯一 KB(0c84a098… / 对外 KB) |
| V02 | 全部解析/索引/Wiki 就绪,验收绑定本版 | `internal/releaseadapter/v071/library_source_settlement.go`;页面准备清单 | 已实现·未验证 | 结清检查(settlement)在封存前执行且缺项阻断;但对"实时测试库此刻就绪"仍需一次真实封存复核;页面已注明依据的是哪次快照 |
| V03 | 删除文档后 Wiki 共享正文/实体/边/引用真实变化 | — | 未实现 | 现在只能证明 Wiki 完整快照被保存;删除贡献的语义重算与证据未实现,界面不声称已满足 |
| V03 | 删除文档后 Wiki 共享正文/实体/边/引用真实变化 | `wikiDiffOf`(页面级差异) | 部分实现 | 真实归档验证:对内 vs 对外 = 新增 31 页 / 撤销 1 页,反向镜像一致。**页面级不等于**实体/概念/边/引用的语义重算,后者仍需 WeKnora 内部证据,未实现也不声称 |
| V04 | 完整固定版本:文档集合+处理结果+资源+索引+完整 Wiki | `internal/librarysnapshot/*`(10 表 + 索引 + 资源 + Wiki 部件);`artifacts` 清单 | 已实现·已验证 | 真实快照 f992d64e…:4 文档/38 分块/122 索引/21 资源/31 Wiki 页/55,309,044 字节;部件摘要逐项校验 |
| V05 | 导出中源变更则拒绝或隔离 | `internal/libraryfreeze/*`(持久 journal、源锁、暂停/恢复);`ProjectController` | 已实现·未验证 | 冻结窗口内会重新比对兼容契约;跨"项目外写入者"的证明仍缺,需一次带并发写入的隔离演练 |
| V06 | 只同步知识数据,不含账号/会话/凭据 | 导出白名单(10 张知识表 + 知识域索引 + 资源 + Wiki) | 已实现·已验证 | 部件清单里没有 users/sessions/api_keys;真实归档部件名可逐项核对 |
@@ -54,6 +54,7 @@ A06(内容、问答、引用图片效果、性能报告)已被用户取消
比较两份归档快照的 `knowledges` 行数组,输出新增/更新/撤销。真实归档上验证:
对内快照 4 篇对空集为"新增 4",反向为"撤销 4",缺失快照返回原因而不是空差异。
生产权威版本建立后即可直接给出生产→测试差异;在生产版本未知时页面保持"未接入"。
Wiki 页面级差异单独返回(`diff.wiki`),明确标注"不代表实体/边/引用重算"。
## 未通过项如何阻断
+80
View File
@@ -54,6 +54,85 @@ type libraryDiffView struct {
// 避免把某次历史差异当成"当前拟发布"。
BaseSnapshot string `json:"base_snapshot,omitempty"`
TargetSnapshot string `json:"target_snapshot,omitempty"`
// Wiki 是**页面级**差异:只能证明页面被增/改/撤,不能证明共享正文、实体、
// 概念或边被重新计算——后者需要 WeKnora 内部语义,本平台不做也不声称。
Wiki *libraryWikiDiffView `json:"wiki,omitempty"`
}
type libraryWikiDiffView struct {
State string `json:"state"` // computed | unknown
Reason string `json:"reason,omitempty"`
Added int `json:"added"`
Updated int `json:"updated"`
Removed int `json:"removed"`
// Sample 只列前若干页,避免把整库 Wiki 列表塞进接口。
Sample []string `json:"sample"`
Note string `json:"note"`
}
// wikiDiffOf 比较两份快照的 wiki_pages 行数组(页面级)。
// 它不能替代"删除文档后共享正文/实体/边/引用如何变化"的语义证据。
func wikiDiffOf(archive *librarysnapshot.Archive, baseID, targetID string) *libraryWikiDiffView {
unknown := func(reason string) *libraryWikiDiffView {
return &libraryWikiDiffView{State: "unknown", Reason: reason, Sample: []string{},
Note: "页面级比对缺失时不能声称 Wiki 已同步"}
}
rows := func(id string) ([]map[string]any, string) {
manifest, err := archive.Load(id)
if err != nil {
return nil, "快照不可读"
}
for _, part := range manifest.Parts {
if part.Kind != "wiki" || part.Name != "wiki_pages" {
continue
}
reader, err := archive.OpenPart(part)
if err != nil {
return nil, "Wiki 正文不可读"
}
defer reader.Close()
parsed, err := librarydiff.ReadDocuments(reader)
if err != nil {
return nil, "Wiki 行格式不符"
}
return parsed, ""
}
return nil, "快照里没有 wiki_pages 部件"
}
base, reason := rows(baseID)
if reason != "" {
return unknown("生产版本:" + reason)
}
target, reason := rows(targetID)
if reason != "" {
return unknown("测试版本:" + reason)
}
result, err := librarydiff.Compare(base, target)
if err != nil {
return unknown("Wiki 页面比对失败:" + err.Error())
}
view := &libraryWikiDiffView{State: "computed", Added: len(result.Added), Updated: len(result.Updated),
Removed: len(result.Removed), Sample: []string{},
Note: "页面级差异:只说明页面被增/改/撤,不代表实体、概念、边或引用已重算"}
for _, item := range result.Added {
if len(view.Sample) >= 20 {
break
}
view.Sample = append(view.Sample, "新增 "+item.Title)
}
for _, item := range result.Updated {
if len(view.Sample) >= 20 {
break
}
view.Sample = append(view.Sample, "更新 "+item.Title)
}
for _, item := range result.Removed {
if len(view.Sample) >= 20 {
break
}
view.Sample = append(view.Sample, "撤销 "+item.Title)
}
return view
}
// computeLibraryDiff 用归档里的 knowledges 行数组比较两个快照:
@@ -115,6 +194,7 @@ func computeLibraryDiff(root, baseID, targetID string) (libraryDiffView, bool) {
}
view := libraryDiffView{State: "computed", Added: []any{}, Updated: []any{}, Removed: []any{},
BaseSnapshot: baseID, TargetSnapshot: targetID}
view.Wiki = wikiDiffOf(archive, baseID, targetID)
for _, item := range result.Added {
view.Added = append(view.Added, item)
}
+10
View File
@@ -209,6 +209,14 @@ describe('ReleaseTargetsView(确认稿布局)', () => {
removed: [{ id: 'doc-del-0003', title: '撤销的说明', digest: 'c'.repeat(64) }],
base_snapshot: 'prod00000000',
target_snapshot: 'test00000000',
wiki: {
state: 'computed',
added: 2,
updated: 1,
removed: 3,
sample: [],
note: '页面级差异',
},
},
},
})
@@ -218,6 +226,8 @@ describe('ReleaseTargetsView(确认稿布局)', () => {
expect(html).toContain('撤销的说明 · doc-del-')
expect(html).toContain('生产 prod00000000 → 测试 test00000000')
expect(html).not.toContain('变更清单来自生产与测试版本对比')
expect(html).toContain('Wiki 页面:新增 2 · 更新 1 · 撤销 3')
expect(html).toContain('不代表实体/边/引用重算')
})
it('准备信息注明快照时间与范围,不声称实时测试库已就绪', () => {
+6
View File
@@ -311,6 +311,12 @@ function ChangeTable({ diff }: { diff: LibraryOverview['diff'] }) {
)}
</tbody>
</table>
{diff.wiki && diff.wiki.state === 'computed' ? (
<p className="rel-scope-note">
Wiki 页面:新增 {diff.wiki.added} · 更新 {diff.wiki.updated} · 撤销 {diff.wiki.removed}
(页面级,不代表实体/边/引用重算)
</p>
) : null}
</div>
)
}
+10
View File
@@ -250,6 +250,16 @@ export interface LibraryDiff {
/** 这次比对用的两个整库版本(生产 → 测试),避免把历史差异当成当前。 */
base_snapshot?: string
target_snapshot?: string
/** 页面级 Wiki 差异;不代表实体、概念、边或引用被重算。 */
wiki?: {
state: 'computed' | 'unknown'
reason?: string
added: number
updated: number
removed: number
sample: string[]
note: string
}
}
export interface LibraryStageRow {