6440e3f679
- 迁移 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 集成
109 lines
4.1 KiB
Go
109 lines
4.1 KiB
Go
package v071
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"knowledge-governance/internal/releasebundle"
|
|
releaseworkflow "knowledge-governance/internal/workflows/release"
|
|
)
|
|
|
|
type targetContractDB interface {
|
|
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
|
}
|
|
|
|
type elasticsearchMappingInspector interface {
|
|
MappingSHA256(ctx context.Context) (string, error)
|
|
}
|
|
|
|
type TargetContractInspectorConfig struct {
|
|
DB targetContractDB
|
|
Mapping elasticsearchMappingInspector
|
|
KnowledgeBaseID string
|
|
TenantID uint64
|
|
IndexName string
|
|
}
|
|
|
|
type TargetContractInspector struct {
|
|
config TargetContractInspectorConfig
|
|
}
|
|
|
|
const targetContractSQL = `
|
|
SELECT migration.version,
|
|
migration.dirty,
|
|
knowledge_base.tenant_id,
|
|
model.name,
|
|
COALESCE(NULLIF(model.parameters->'embedding_parameters'->>'dimension', '')::INTEGER, 0),
|
|
COALESCE(vector_store.engine_type, ''),
|
|
COALESCE(vector_store.index_config->>'index_name', '')
|
|
FROM knowledge_bases AS knowledge_base
|
|
JOIN models AS model
|
|
ON model.id = knowledge_base.embedding_model_id
|
|
AND model.deleted_at IS NULL
|
|
JOIN schema_migrations AS migration ON TRUE
|
|
LEFT JOIN vector_stores AS vector_store
|
|
ON vector_store.id = knowledge_base.vector_store_id
|
|
AND vector_store.deleted_at IS NULL
|
|
WHERE knowledge_base.id = $1
|
|
AND knowledge_base.deleted_at IS NULL
|
|
`
|
|
|
|
func NewTargetContractInspector(config TargetContractInspectorConfig) (*TargetContractInspector, error) {
|
|
config.KnowledgeBaseID = strings.TrimSpace(config.KnowledgeBaseID)
|
|
config.IndexName = strings.TrimSpace(config.IndexName)
|
|
if config.DB == nil || config.Mapping == nil || config.TenantID == 0 || !validSearchIdentifier(config.KnowledgeBaseID) || !validSearchIdentifier(config.IndexName) {
|
|
return nil, errors.New("invalid target contract inspector configuration")
|
|
}
|
|
return &TargetContractInspector{config: config}, nil
|
|
}
|
|
|
|
func (inspector *TargetContractInspector) Contract(ctx context.Context, item releaseworkflow.PlanItem) (releasebundle.TargetContract, error) {
|
|
if item.TestKnowledgeBaseID != inspector.config.KnowledgeBaseID {
|
|
return releasebundle.TargetContract{}, errors.New("release plan knowledge base does not match target contract")
|
|
}
|
|
// The route is keyed by the TESTED base (where the content was verified);
|
|
// the contract is read for the base this release actually writes into —
|
|
// the 源库 → 目标库 mapping when the item carries one, else the same base.
|
|
targetKnowledgeBaseID := item.TargetBase()
|
|
if !validSearchIdentifier(targetKnowledgeBaseID) {
|
|
return releasebundle.TargetContract{}, errors.New("invalid release target knowledge base")
|
|
}
|
|
var (
|
|
migrationVersion int
|
|
dirty bool
|
|
tenantID uint64
|
|
embeddingModel string
|
|
dimension int
|
|
engineType string
|
|
indexName string
|
|
)
|
|
if err := inspector.config.DB.QueryRow(ctx, targetContractSQL, targetKnowledgeBaseID).Scan(
|
|
&migrationVersion, &dirty, &tenantID, &embeddingModel, &dimension, &engineType, &indexName,
|
|
); err != nil {
|
|
return releasebundle.TargetContract{}, errors.New("read target WeKnora contract")
|
|
}
|
|
if migrationVersion != releasebundle.WeKnoraMigrationV071 || dirty || tenantID != inspector.config.TenantID ||
|
|
!validSearchIdentifier(embeddingModel) || dimension < 1 ||
|
|
(engineType != "" && engineType != "elasticsearch") ||
|
|
(indexName != "" && indexName != inspector.config.IndexName) {
|
|
return releasebundle.TargetContract{}, errors.New("target WeKnora contract mismatch")
|
|
}
|
|
mappingSHA256, err := inspector.config.Mapping.MappingSHA256(ctx)
|
|
if err != nil || !validLowerSHA256(mappingSHA256) {
|
|
return releasebundle.TargetContract{}, errors.New("read target Elasticsearch mapping contract")
|
|
}
|
|
return releasebundle.TargetContract{
|
|
WeKnoraVersion: releasebundle.WeKnoraVersionV071,
|
|
MigrationVersion: releasebundle.WeKnoraMigrationV071,
|
|
TenantID: tenantID,
|
|
KnowledgeBaseID: targetKnowledgeBaseID,
|
|
RetrieverEngine: "elasticsearch_v8",
|
|
EmbeddingModel: embeddingModel,
|
|
EmbeddingDimension: dimension,
|
|
ElasticsearchMappingSHA256: mappingSHA256,
|
|
}, nil
|
|
}
|