760 lines
29 KiB
PowerShell
760 lines
29 KiB
PowerShell
param(
|
||
[ValidateSet("Validate", "Review", "Production")]
|
||
[string]$Mode = "Validate",
|
||
[string]$ApiBaseUrl = "http://127.0.0.1:8000/api/v1",
|
||
[string]$LibraryRoot = "",
|
||
[string]$ArtifactRoot = "",
|
||
[string]$AdminEmail = "admin@example.com",
|
||
[string]$AdminPassword = "",
|
||
[string]$KnowledgeBaseName = "",
|
||
[switch]$ForceReupload,
|
||
[switch]$SkipTemplates,
|
||
[switch]$SkipReviewRules
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
Add-Type -AssemblyName System.Net.Http
|
||
|
||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff"
|
||
|
||
if ([string]::IsNullOrWhiteSpace($LibraryRoot)) {
|
||
$LibraryRoot = Join-Path $repoRoot "project-docs\phase13-legal-knowledge-template-library"
|
||
}
|
||
if (-not [System.IO.Path]::IsPathRooted($LibraryRoot)) {
|
||
$LibraryRoot = Join-Path $repoRoot $LibraryRoot
|
||
}
|
||
|
||
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
|
||
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase13-import"
|
||
}
|
||
if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) {
|
||
$ArtifactRoot = Join-Path $repoRoot $ArtifactRoot
|
||
}
|
||
|
||
$artifactDir = Join-Path $ArtifactRoot $timestamp
|
||
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
|
||
|
||
$summaryJson = Join-Path $artifactDir "phase13-import-summary.json"
|
||
$summaryMd = Join-Path $artifactDir "phase13-import-summary.md"
|
||
|
||
$ApiBaseUrl = $ApiBaseUrl.TrimEnd("/")
|
||
|
||
if ([string]::IsNullOrWhiteSpace($KnowledgeBaseName)) {
|
||
if ($Mode -eq "Production") {
|
||
$KnowledgeBaseName = "AI合同平台法律知识库(生产)"
|
||
} else {
|
||
$KnowledgeBaseName = "阶段13法律知识库(待法务复核)"
|
||
}
|
||
}
|
||
|
||
$checks = New-Object System.Collections.Generic.List[object]
|
||
$assetReport = New-Object System.Collections.Generic.List[object]
|
||
$importReport = New-Object System.Collections.Generic.List[object]
|
||
|
||
function Add-Check {
|
||
param(
|
||
[string]$Key,
|
||
[string]$Label,
|
||
[ValidateSet("pass", "fail", "warn", "skip", "blocked")]
|
||
[string]$Status,
|
||
[string]$Detail,
|
||
[bool]$Required = $true,
|
||
[hashtable]$Meta = @{}
|
||
)
|
||
|
||
$checks.Add([ordered]@{
|
||
key = $Key
|
||
label = $Label
|
||
status = $Status
|
||
required = $Required
|
||
detail = $Detail
|
||
meta = $Meta
|
||
}) | Out-Null
|
||
}
|
||
|
||
function Save-Json {
|
||
param([string]$Path, [object]$Value)
|
||
$Value | ConvertTo-Json -Depth 30 | Set-Content -Encoding UTF8 -Path $Path
|
||
}
|
||
|
||
function Import-CsvRows {
|
||
param([string]$Path)
|
||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||
return @()
|
||
}
|
||
return @(Import-Csv -LiteralPath $Path)
|
||
}
|
||
|
||
function Test-RequiredFlag {
|
||
param([string]$Value)
|
||
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||
return $false
|
||
}
|
||
return @("true", "yes", "1", "required") -contains $Value.ToLowerInvariant()
|
||
}
|
||
|
||
function Test-PendingReviewer {
|
||
param([string]$Value)
|
||
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||
return $true
|
||
}
|
||
return @("pending", "待法务复核", "待业务复核", "待复核") -contains $Value.Trim()
|
||
}
|
||
|
||
function Test-ProductionReadyAsset {
|
||
param([object]$Row)
|
||
$statusAllowed = @("APPROVED", "PUBLISHED") -contains $Row.status
|
||
$ready = Test-RequiredFlag -Value $Row.production_ready
|
||
$reviewed = -not (Test-PendingReviewer -Value $Row.legal_reviewer)
|
||
return ($statusAllowed -and $ready -and $reviewed)
|
||
}
|
||
|
||
function Test-ReviewImportEligibleAsset {
|
||
param([object]$Row)
|
||
return @("LEGAL_REVIEW", "BUSINESS_REVIEW", "APPROVED", "PUBLISHED") -contains $Row.status
|
||
}
|
||
|
||
function Resolve-LibraryPath {
|
||
param([string]$RelativePath)
|
||
return Join-Path $LibraryRoot ($RelativePath -replace "/", "\")
|
||
}
|
||
|
||
function Read-LibraryTextIfExists {
|
||
param([string]$RelativePath)
|
||
if ([string]::IsNullOrWhiteSpace($RelativePath)) {
|
||
return ""
|
||
}
|
||
$absolutePath = Resolve-LibraryPath -RelativePath $RelativePath
|
||
if (-not (Test-Path -LiteralPath $absolutePath -PathType Leaf)) {
|
||
return ""
|
||
}
|
||
return Get-Content -LiteralPath $absolutePath -Raw -Encoding UTF8
|
||
}
|
||
|
||
function Get-ResponseData {
|
||
param([object]$Response)
|
||
if ($null -eq $Response) {
|
||
return $null
|
||
}
|
||
if ($Response.PSObject.Properties.Name -contains "data") {
|
||
return $Response.data
|
||
}
|
||
return $Response
|
||
}
|
||
|
||
function Invoke-ApiJson {
|
||
param(
|
||
[string]$Method,
|
||
[string]$Url,
|
||
[hashtable]$Headers = @{},
|
||
[object]$Body = $null
|
||
)
|
||
|
||
$args = @{
|
||
Method = $Method
|
||
Uri = $Url
|
||
Headers = $Headers
|
||
ErrorAction = "Stop"
|
||
}
|
||
if ($null -ne $Body) {
|
||
$args["ContentType"] = "application/json; charset=utf-8"
|
||
$args["Body"] = ($Body | ConvertTo-Json -Depth 20)
|
||
}
|
||
return Invoke-RestMethod @args
|
||
}
|
||
|
||
function Invoke-FormRequest {
|
||
param(
|
||
[string]$Method,
|
||
[string]$Url,
|
||
[hashtable]$Headers = @{},
|
||
[hashtable]$Fields = @{},
|
||
[string]$FileField = "",
|
||
[string]$FileName = "",
|
||
[byte[]]$FileContent = $null,
|
||
[string]$FileContentType = "text/markdown; charset=utf-8"
|
||
)
|
||
|
||
$client = [System.Net.Http.HttpClient]::new()
|
||
try {
|
||
foreach ($headerName in $Headers.Keys) {
|
||
$client.DefaultRequestHeaders.Remove($headerName) | Out-Null
|
||
$client.DefaultRequestHeaders.Add($headerName, [string]$Headers[$headerName])
|
||
}
|
||
|
||
$form = [System.Net.Http.MultipartFormDataContent]::new()
|
||
try {
|
||
foreach ($fieldName in $Fields.Keys) {
|
||
$value = [string]$Fields[$fieldName]
|
||
$stringContent = [System.Net.Http.StringContent]::new($value, [System.Text.Encoding]::UTF8)
|
||
$form.Add($stringContent, $fieldName)
|
||
}
|
||
|
||
if ($FileContent -ne $null -and -not [string]::IsNullOrWhiteSpace($FileField)) {
|
||
$byteContent = [System.Net.Http.ByteArrayContent]::new($FileContent)
|
||
$byteContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse($FileContentType)
|
||
$form.Add($byteContent, $FileField, $FileName)
|
||
}
|
||
|
||
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($Method), $Url)
|
||
$request.Content = $form
|
||
$response = $client.SendAsync($request).GetAwaiter().GetResult()
|
||
$body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
||
if (-not $response.IsSuccessStatusCode) {
|
||
throw "API 请求失败:$Method $Url,状态码=$([int]$response.StatusCode),响应=$body"
|
||
}
|
||
if ([string]::IsNullOrWhiteSpace($body)) {
|
||
return $null
|
||
}
|
||
return $body | ConvertFrom-Json
|
||
} finally {
|
||
if ($null -ne $form) {
|
||
$form.Dispose()
|
||
}
|
||
}
|
||
} finally {
|
||
$client.Dispose()
|
||
}
|
||
}
|
||
|
||
function Get-PagedApiItems {
|
||
param(
|
||
[string]$Url,
|
||
[hashtable]$Headers
|
||
)
|
||
|
||
$items = New-Object System.Collections.Generic.List[object]
|
||
$page = 1
|
||
$pageSize = 100
|
||
while ($true) {
|
||
$separator = if ($Url.Contains("?")) { "&" } else { "?" }
|
||
$response = Invoke-ApiJson -Method "GET" -Url "$Url${separator}page=$page&page_size=$pageSize" -Headers $Headers
|
||
$data = Get-ResponseData -Response $response
|
||
if ($null -eq $data -or $null -eq $data.items) {
|
||
break
|
||
}
|
||
foreach ($item in @($data.items)) {
|
||
$items.Add($item) | Out-Null
|
||
}
|
||
if ($items.Count -ge [int]$data.total -or @($data.items).Count -eq 0) {
|
||
break
|
||
}
|
||
$page += 1
|
||
}
|
||
return $items.ToArray()
|
||
}
|
||
|
||
function Get-AuthHeaders {
|
||
param([string]$Token)
|
||
return @{ Authorization = "Bearer $Token" }
|
||
}
|
||
|
||
function Get-AccessToken {
|
||
if ($Mode -eq "Validate") {
|
||
return ""
|
||
}
|
||
|
||
$password = $AdminPassword
|
||
if ([string]::IsNullOrWhiteSpace($password)) {
|
||
$password = $env:AICP_ADMIN_PASSWORD
|
||
}
|
||
if ([string]::IsNullOrWhiteSpace($password)) {
|
||
throw "非 Validate 模式需要通过 -AdminPassword 或环境变量 AICP_ADMIN_PASSWORD 提供管理员密码。"
|
||
}
|
||
|
||
$response = Invoke-ApiJson -Method "POST" -Url "$ApiBaseUrl/auth/login" -Body @{
|
||
account = $AdminEmail
|
||
password = $password
|
||
}
|
||
$data = Get-ResponseData -Response $response
|
||
if ($null -eq $data -or [string]::IsNullOrWhiteSpace($data.access_token)) {
|
||
throw "登录成功响应中缺少 access_token。"
|
||
}
|
||
return [string]$data.access_token
|
||
}
|
||
|
||
function Ensure-KnowledgeBase {
|
||
param([hashtable]$Headers)
|
||
|
||
$items = Get-PagedApiItems -Url "$ApiBaseUrl/knowledge-bases" -Headers $Headers
|
||
$existing = @($items | Where-Object { $_.name -eq $KnowledgeBaseName }) | Select-Object -First 1
|
||
if ($existing) {
|
||
return $existing
|
||
}
|
||
|
||
$response = Invoke-FormRequest -Method "POST" -Url "$ApiBaseUrl/knowledge-bases" -Headers $Headers -Fields @{
|
||
name = $KnowledgeBaseName
|
||
description = "阶段 13 法律知识卡导入知识库;资料未经法务复核前仅用于内部复核和试点验证。"
|
||
}
|
||
return Get-ResponseData -Response $response
|
||
}
|
||
|
||
function Get-KnowledgeBaseDocuments {
|
||
param([hashtable]$Headers, [string]$KnowledgeBaseId)
|
||
return Get-PagedApiItems -Url "$ApiBaseUrl/knowledge-bases/$KnowledgeBaseId/documents" -Headers $Headers
|
||
}
|
||
|
||
function Import-KnowledgeCard {
|
||
param(
|
||
[hashtable]$Headers,
|
||
[string]$KnowledgeBaseId,
|
||
[object]$Card,
|
||
[string]$AbsolutePath,
|
||
[object[]]$ExistingDocuments,
|
||
[hashtable]$SourceMetadataLookup
|
||
)
|
||
|
||
$fileName = "$($Card.knowledge_card_id).md"
|
||
$existing = @($ExistingDocuments | Where-Object { $_.file_name -eq $fileName }) | Select-Object -First 1
|
||
$bytes = [System.IO.File]::ReadAllBytes($AbsolutePath)
|
||
$note = "阶段13知识卡导入;编号=$($Card.knowledge_card_id);状态=$($Card.status);法源=$($Card.source_ids)。"
|
||
$fields = @{ note = $note }
|
||
$metadataFields = Get-KnowledgeCardMetadataFields -Card $Card -SourceMetadataLookup $SourceMetadataLookup
|
||
foreach ($fieldName in $metadataFields.Keys) {
|
||
$fields[$fieldName] = $metadataFields[$fieldName]
|
||
}
|
||
|
||
if ($existing -and -not $ForceReupload) {
|
||
return [ordered]@{
|
||
asset_type = "knowledge_card"
|
||
asset_id = $Card.knowledge_card_id
|
||
action = "skip_existing"
|
||
status = "skipped"
|
||
target_id = $existing.id
|
||
detail = "同名知识卡文档已存在,未重复上传。"
|
||
}
|
||
}
|
||
|
||
if ($existing -and $ForceReupload) {
|
||
$response = Invoke-FormRequest -Method "POST" -Url "$ApiBaseUrl/knowledge-bases/$KnowledgeBaseId/documents/$($existing.id)/retry" -Headers $Headers -Fields $fields -FileField "file" -FileName $fileName -FileContent $bytes
|
||
$data = Get-ResponseData -Response $response
|
||
return [ordered]@{
|
||
asset_type = "knowledge_card"
|
||
asset_id = $Card.knowledge_card_id
|
||
action = "retry_existing"
|
||
status = $data.status
|
||
target_id = $data.id
|
||
detail = $data.status_detail
|
||
}
|
||
}
|
||
|
||
$response = Invoke-FormRequest -Method "POST" -Url "$ApiBaseUrl/knowledge-bases/$KnowledgeBaseId/documents" -Headers $Headers -Fields $fields -FileField "file" -FileName $fileName -FileContent $bytes
|
||
$data = Get-ResponseData -Response $response
|
||
return [ordered]@{
|
||
asset_type = "knowledge_card"
|
||
asset_id = $Card.knowledge_card_id
|
||
action = "upload"
|
||
status = $data.status
|
||
target_id = $data.id
|
||
detail = $data.status_detail
|
||
}
|
||
}
|
||
|
||
function Get-UniqueValues {
|
||
param([object[]]$Values)
|
||
return @($Values | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | ForEach-Object { ([string]$_).Trim() } | Select-Object -Unique)
|
||
}
|
||
|
||
function Get-KnowledgeCardMetadataFields {
|
||
param(
|
||
[object]$Card,
|
||
[hashtable]$SourceMetadataLookup
|
||
)
|
||
|
||
$sourceIds = @($Card.source_ids -split ";" | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||
$sources = @()
|
||
foreach ($sourceId in $sourceIds) {
|
||
if ($SourceMetadataLookup.ContainsKey($sourceId)) {
|
||
$sources += $SourceMetadataLookup[$sourceId]
|
||
}
|
||
}
|
||
|
||
$sourceTypes = Get-UniqueValues -Values @($sources | ForEach-Object { $_.source_type })
|
||
$authorities = Get-UniqueValues -Values @($sources | ForEach-Object { $_.issuer })
|
||
$effectiveDates = Get-UniqueValues -Values @($sources | ForEach-Object { $_.effective_date } | Where-Object { $_ -and $_ -ne "VERIFY_REQUIRED" })
|
||
$domains = Get-UniqueValues -Values @($sources | ForEach-Object { $_.knowledge_domain })
|
||
|
||
$tags = New-Object System.Collections.Generic.List[string]
|
||
foreach ($value in @($sourceIds + $domains)) {
|
||
if (-not [string]::IsNullOrWhiteSpace($value) -and -not $tags.Contains($value)) {
|
||
$tags.Add($value) | Out-Null
|
||
}
|
||
}
|
||
|
||
$fields = @{}
|
||
if ($sourceTypes.Count -gt 0) {
|
||
$fields["source_type"] = if ($sourceTypes.Count -eq 1) { $sourceTypes[0] } else { ($sourceTypes -join ";") }
|
||
}
|
||
if ($authorities.Count -gt 0) {
|
||
$fields["authority"] = if ($authorities.Count -eq 1) { $authorities[0] } else { ($authorities -join ";") }
|
||
}
|
||
if ($effectiveDates.Count -gt 0) {
|
||
$fields["effective_date"] = if ($effectiveDates.Count -eq 1) { $effectiveDates[0] } else { ($effectiveDates -join ";") }
|
||
}
|
||
$fields["region"] = "CN"
|
||
if ($tags.Count -gt 0) {
|
||
$fields["tags_json"] = ($tags.ToArray() | ConvertTo-Json -Compress)
|
||
}
|
||
return $fields
|
||
}
|
||
|
||
function Get-TemplateVariablesJson {
|
||
param([string]$VariableSchemaPath)
|
||
if ([string]::IsNullOrWhiteSpace($VariableSchemaPath)) {
|
||
return $null
|
||
}
|
||
$absolutePath = Resolve-LibraryPath -RelativePath $VariableSchemaPath
|
||
if (-not (Test-Path -LiteralPath $absolutePath -PathType Leaf)) {
|
||
return $null
|
||
}
|
||
return Get-Content -LiteralPath $absolutePath -Raw -Encoding UTF8
|
||
}
|
||
|
||
function Import-TemplateAsset {
|
||
param([hashtable]$Headers, [object]$Template, [object[]]$ExistingTemplates)
|
||
$templatePath = Resolve-LibraryPath -RelativePath $Template.template_path
|
||
$content = Get-Content -LiteralPath $templatePath -Raw -Encoding UTF8
|
||
$variablesJson = Get-TemplateVariablesJson -VariableSchemaPath $Template.variable_schema_path
|
||
$fields = @{
|
||
name = $Template.template_name
|
||
contract_type = $Template.contract_type
|
||
description = $Template.notes
|
||
content = $content
|
||
enabled = "true"
|
||
}
|
||
if (-not [string]::IsNullOrWhiteSpace($variablesJson)) {
|
||
$fields["variables_json"] = $variablesJson
|
||
}
|
||
|
||
$existing = @($ExistingTemplates | Where-Object { $_.name -eq $Template.template_name }) | Select-Object -First 1
|
||
if ($existing) {
|
||
$response = Invoke-FormRequest -Method "PUT" -Url "$ApiBaseUrl/templates/$($existing.id)" -Headers $Headers -Fields $fields
|
||
$data = Get-ResponseData -Response $response
|
||
return [ordered]@{ asset_type = "template"; asset_id = $Template.template_id; action = "update"; status = "upserted"; target_id = $data.id; detail = "模板已更新。" }
|
||
}
|
||
|
||
$response = Invoke-FormRequest -Method "POST" -Url "$ApiBaseUrl/templates" -Headers $Headers -Fields $fields
|
||
$created = Get-ResponseData -Response $response
|
||
return [ordered]@{ asset_type = "template"; asset_id = $Template.template_id; action = "create"; status = "created"; target_id = $created.id; detail = "模板已创建。" }
|
||
}
|
||
|
||
function Import-ReviewRuleAsset {
|
||
param([hashtable]$Headers, [object]$Rule, [object[]]$ExistingRules)
|
||
$ruleBody = Read-LibraryTextIfExists -RelativePath $Rule.modification_suggestion_path
|
||
$description = $Rule.review_output_template
|
||
if (-not [string]::IsNullOrWhiteSpace($ruleBody)) {
|
||
$description = "$description`n`n$ruleBody"
|
||
}
|
||
$suggestion = if (-not [string]::IsNullOrWhiteSpace($ruleBody)) {
|
||
$ruleBody
|
||
} else {
|
||
$Rule.modification_suggestion_path
|
||
}
|
||
$isActive = if ($Mode -eq "Production") {
|
||
Test-ProductionReadyAsset -Row $Rule
|
||
} else {
|
||
Test-ReviewImportEligibleAsset -Row $Rule
|
||
}
|
||
$fields = @{
|
||
name = $Rule.risk_name
|
||
keyword = $Rule.trigger_condition
|
||
contract_type = "all"
|
||
risk_level = $Rule.severity.ToLowerInvariant()
|
||
description = $description
|
||
suggestion = $suggestion
|
||
legal_basis = $Rule.source_ids
|
||
is_active = ([string]$isActive).ToLowerInvariant()
|
||
}
|
||
$existing = @($ExistingRules | Where-Object { $_.name -eq $Rule.risk_name -or $_.keyword -eq $Rule.trigger_condition }) | Select-Object -First 1
|
||
if ($existing) {
|
||
$response = Invoke-FormRequest -Method "PUT" -Url "$ApiBaseUrl/review-rules/$($existing.id)" -Headers $Headers -Fields $fields
|
||
$data = Get-ResponseData -Response $response
|
||
return [ordered]@{ asset_type = "review_rule"; asset_id = $Rule.rule_id; action = "update"; status = "upserted"; target_id = $data.id; detail = "审核规则已更新。" }
|
||
}
|
||
$response = Invoke-FormRequest -Method "POST" -Url "$ApiBaseUrl/review-rules" -Headers $Headers -Fields $fields
|
||
$created = Get-ResponseData -Response $response
|
||
return [ordered]@{ asset_type = "review_rule"; asset_id = $Rule.rule_id; action = "create"; status = "created"; target_id = $created.id; detail = "审核规则已创建。" }
|
||
}
|
||
|
||
if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
|
||
Add-Check -Key "gstack" -Label "gstack 前置检查" -Status "fail" -Detail "gstack 缺失"
|
||
} else {
|
||
Add-Check -Key "gstack" -Label "gstack 前置检查" -Status "pass" -Detail "GSTACK_OK"
|
||
}
|
||
|
||
if (Test-Path -LiteralPath $LibraryRoot -PathType Container) {
|
||
Add-Check -Key "library_root" -Label "阶段 13 资料包根目录" -Status "pass" -Detail "资料包根目录存在" -Meta @{ root = $LibraryRoot }
|
||
} else {
|
||
Add-Check -Key "library_root" -Label "阶段 13 资料包根目录" -Status "fail" -Detail "资料包根目录缺失" -Meta @{ root = $LibraryRoot }
|
||
}
|
||
|
||
$legalSourceInventoryPath = Join-Path $LibraryRoot "inventories\legal-source-inventory.csv"
|
||
$knowledgeCardInventoryPath = Join-Path $LibraryRoot "inventories\legal-knowledge-card-inventory.csv"
|
||
$templateInventoryPath = Join-Path $LibraryRoot "inventories\contract-template-inventory.csv"
|
||
$reviewRuleInventoryPath = Join-Path $LibraryRoot "inventories\review-risk-rule-inventory.csv"
|
||
$clauseInventoryPath = Join-Path $LibraryRoot "inventories\clause-library-inventory.csv"
|
||
$reviewSampleInventoryPath = Join-Path $LibraryRoot "inventories\review-golden-sample-inventory.csv"
|
||
$generationSampleInventoryPath = Join-Path $LibraryRoot "inventories\generation-golden-sample-inventory.csv"
|
||
$kbQaSampleInventoryPath = Join-Path $LibraryRoot "inventories\kb-qa-golden-sample-inventory.csv"
|
||
|
||
$sourceRows = Import-CsvRows -Path $legalSourceInventoryPath
|
||
$sourceIdSet = @{}
|
||
$sourceMetadataLookup = @{}
|
||
foreach ($source in $sourceRows) {
|
||
if (-not [string]::IsNullOrWhiteSpace($source.source_id)) {
|
||
$sourceIdSet[$source.source_id] = $true
|
||
$sourceMetadataLookup[$source.source_id] = $source
|
||
}
|
||
}
|
||
|
||
$knowledgeCards = Import-CsvRows -Path $knowledgeCardInventoryPath
|
||
$templates = Import-CsvRows -Path $templateInventoryPath
|
||
$reviewRules = Import-CsvRows -Path $reviewRuleInventoryPath
|
||
$clauses = Import-CsvRows -Path $clauseInventoryPath
|
||
$reviewSamples = Import-CsvRows -Path $reviewSampleInventoryPath
|
||
$generationSamples = Import-CsvRows -Path $generationSampleInventoryPath
|
||
$kbQaSamples = Import-CsvRows -Path $kbQaSampleInventoryPath
|
||
|
||
if ($knowledgeCards.Count -gt 0) {
|
||
Add-Check -Key "knowledge_card_inventory" -Label "法律知识卡清单" -Status "pass" -Detail "知识卡数量=$($knowledgeCards.Count)"
|
||
} else {
|
||
Add-Check -Key "knowledge_card_inventory" -Label "法律知识卡清单" -Status "warn" -Required $false -Detail "知识卡清单为空"
|
||
}
|
||
|
||
$validationFailures = 0
|
||
$eligibleKnowledgeCards = New-Object System.Collections.Generic.List[object]
|
||
foreach ($card in $knowledgeCards) {
|
||
$absolutePath = Resolve-LibraryPath -RelativePath $card.summary_path
|
||
$missing = New-Object System.Collections.Generic.List[string]
|
||
foreach ($field in @("knowledge_card_id", "title", "source_ids", "summary_path", "status", "owner", "legal_reviewer")) {
|
||
if ([string]::IsNullOrWhiteSpace($card.$field)) {
|
||
$missing.Add($field) | Out-Null
|
||
}
|
||
}
|
||
|
||
$unknownSources = @()
|
||
foreach ($sourceId in @($card.source_ids -split ";" | ForEach-Object { $_.Trim() } | Where-Object { $_ })) {
|
||
if (-not $sourceIdSet.ContainsKey($sourceId)) {
|
||
$unknownSources += $sourceId
|
||
}
|
||
}
|
||
if ($unknownSources.Count -gt 0) {
|
||
$missing.Add("unknown_source_ids=$($unknownSources -join ';')") | Out-Null
|
||
}
|
||
if (-not (Test-Path -LiteralPath $absolutePath -PathType Leaf)) {
|
||
$missing.Add("summary_path_missing") | Out-Null
|
||
}
|
||
|
||
$productionReady = Test-ProductionReadyAsset -Row $card
|
||
$reviewEligible = @("LEGAL_REVIEW", "BUSINESS_REVIEW", "APPROVED", "PUBLISHED") -contains $card.status
|
||
$eligible = if ($Mode -eq "Production") { $productionReady } elseif ($Mode -eq "Review") { $reviewEligible } else { $reviewEligible -or $productionReady }
|
||
|
||
if ($missing.Count -gt 0) {
|
||
$validationFailures += 1
|
||
} elseif ($eligible -and ($Mode -ne "Production" -or $productionReady)) {
|
||
$eligibleKnowledgeCards.Add($card) | Out-Null
|
||
}
|
||
|
||
$assetReport.Add([ordered]@{
|
||
asset_type = "knowledge_card"
|
||
asset_id = $card.knowledge_card_id
|
||
title = $card.title
|
||
status = $card.status
|
||
production_ready = $card.production_ready
|
||
legal_reviewer = $card.legal_reviewer
|
||
eligible_for_mode = $eligible
|
||
validation_status = if ($missing.Count -eq 0) { "pass" } else { "fail" }
|
||
validation_detail = if ($missing.Count -eq 0) { "字段、法源和文件路径通过" } else { "问题:" + ($missing -join ";") }
|
||
summary_path = $card.summary_path
|
||
}) | Out-Null
|
||
}
|
||
|
||
if ($validationFailures -eq 0) {
|
||
Add-Check -Key "knowledge_card_validation" -Label "知识卡字段与路径校验" -Status "pass" -Detail "知识卡字段、法源编号和摘要文件路径通过"
|
||
} else {
|
||
Add-Check -Key "knowledge_card_validation" -Label "知识卡字段与路径校验" -Status "fail" -Detail "存在 $validationFailures 张知识卡未通过校验"
|
||
}
|
||
|
||
$token = ""
|
||
$headers = @{}
|
||
$targetKnowledgeBase = $null
|
||
if ($Mode -ne "Validate" -and $validationFailures -eq 0) {
|
||
$token = Get-AccessToken
|
||
$headers = Get-AuthHeaders -Token $token
|
||
$targetKnowledgeBase = Ensure-KnowledgeBase -Headers $headers
|
||
Add-Check -Key "api_login" -Label "管理员登录与知识库确认" -Status "pass" -Detail "目标知识库:$($targetKnowledgeBase.name)" -Meta @{ knowledge_base_id = $targetKnowledgeBase.id }
|
||
} elseif ($Mode -eq "Validate") {
|
||
Add-Check -Key "api_login" -Label "管理员登录与知识库确认" -Status "skip" -Required $false -Detail "Validate 模式不调用系统 API"
|
||
}
|
||
|
||
if ($Mode -ne "Validate" -and $validationFailures -eq 0) {
|
||
$existingDocs = Get-KnowledgeBaseDocuments -Headers $headers -KnowledgeBaseId $targetKnowledgeBase.id
|
||
foreach ($card in $eligibleKnowledgeCards) {
|
||
$absolutePath = Resolve-LibraryPath -RelativePath $card.summary_path
|
||
try {
|
||
$result = Import-KnowledgeCard -Headers $headers -KnowledgeBaseId $targetKnowledgeBase.id -Card $card -AbsolutePath $absolutePath -ExistingDocuments $existingDocs -SourceMetadataLookup $sourceMetadataLookup
|
||
$importReport.Add($result) | Out-Null
|
||
} catch {
|
||
$importReport.Add([ordered]@{
|
||
asset_type = "knowledge_card"
|
||
asset_id = $card.knowledge_card_id
|
||
action = "upload"
|
||
status = "failed"
|
||
target_id = ""
|
||
detail = $_.Exception.Message
|
||
}) | Out-Null
|
||
}
|
||
}
|
||
|
||
if (-not $SkipTemplates -and $templates.Count -gt 0) {
|
||
$existingTemplates = Get-PagedApiItems -Url "$ApiBaseUrl/templates" -Headers $headers
|
||
foreach ($template in $templates) {
|
||
$templateEligible = if ($Mode -eq "Production") { Test-ProductionReadyAsset -Row $template } else { Test-ReviewImportEligibleAsset -Row $template }
|
||
if (-not $templateEligible) {
|
||
continue
|
||
}
|
||
try {
|
||
$importReport.Add((Import-TemplateAsset -Headers $headers -Template $template -ExistingTemplates $existingTemplates)) | Out-Null
|
||
} catch {
|
||
$importReport.Add([ordered]@{ asset_type = "template"; asset_id = $template.template_id; action = "upsert"; status = "failed"; target_id = ""; detail = $_.Exception.Message }) | Out-Null
|
||
}
|
||
}
|
||
}
|
||
|
||
if (-not $SkipReviewRules -and $reviewRules.Count -gt 0) {
|
||
$existingRules = Get-PagedApiItems -Url "$ApiBaseUrl/review-rules" -Headers $headers
|
||
foreach ($rule in $reviewRules) {
|
||
$ruleEligible = if ($Mode -eq "Production") { Test-ProductionReadyAsset -Row $rule } else { Test-ReviewImportEligibleAsset -Row $rule }
|
||
if (-not $ruleEligible) {
|
||
continue
|
||
}
|
||
try {
|
||
$importReport.Add((Import-ReviewRuleAsset -Headers $headers -Rule $rule -ExistingRules $existingRules)) | Out-Null
|
||
} catch {
|
||
$importReport.Add([ordered]@{ asset_type = "review_rule"; asset_id = $rule.rule_id; action = "upsert"; status = "failed"; target_id = ""; detail = $_.Exception.Message }) | Out-Null
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
$importFailures = @($importReport | Where-Object { $_.status -eq "failed" }).Count
|
||
$blockedByReview = @($assetReport | Where-Object { $_.asset_type -eq "knowledge_card" -and -not $_.eligible_for_mode }).Count
|
||
$importedOrSkipped = @($importReport | Where-Object { $_.status -ne "failed" }).Count
|
||
|
||
if ($Mode -eq "Validate") {
|
||
Add-Check -Key "import_execution" -Label "导入执行" -Status "skip" -Required $false -Detail "Validate 模式只完成资料校验"
|
||
} elseif ($importFailures -eq 0) {
|
||
Add-Check -Key "import_execution" -Label "导入执行" -Status "pass" -Detail "导入或跳过成功数量=$importedOrSkipped"
|
||
} else {
|
||
Add-Check -Key "import_execution" -Label "导入执行" -Status "fail" -Detail "导入失败数量=$importFailures"
|
||
}
|
||
|
||
$failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" }).Count
|
||
$warnings = @($checks | Where-Object { $_.status -eq "warn" }).Count
|
||
|
||
$overallStatus = if ($failedRequired -gt 0 -or $validationFailures -gt 0 -or $importFailures -gt 0) {
|
||
"FAIL"
|
||
} elseif ($Mode -eq "Production" -and $blockedByReview -gt 0) {
|
||
"PASS_WITH_REVIEW_BLOCKED_ASSETS"
|
||
} elseif ($warnings -gt 0) {
|
||
"WARN"
|
||
} else {
|
||
"PASS"
|
||
}
|
||
|
||
$summary = [ordered]@{
|
||
name = "phase13-library-import"
|
||
mode = $Mode
|
||
status = $overallStatus
|
||
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
||
api_base_url = $ApiBaseUrl
|
||
artifact_dir = $artifactDir
|
||
library_root = $LibraryRoot
|
||
knowledge_base_name = $KnowledgeBaseName
|
||
knowledge_base_id = if ($targetKnowledgeBase) { $targetKnowledgeBase.id } else { "" }
|
||
checks = $checks.ToArray()
|
||
assets = $assetReport.ToArray()
|
||
imports = $importReport.ToArray()
|
||
totals = [ordered]@{
|
||
legal_sources = $sourceRows.Count
|
||
knowledge_cards = $knowledgeCards.Count
|
||
eligible_knowledge_cards = $eligibleKnowledgeCards.Count
|
||
clauses = $clauses.Count
|
||
templates = $templates.Count
|
||
review_rules = $reviewRules.Count
|
||
review_samples = $reviewSamples.Count
|
||
generation_samples = $generationSamples.Count
|
||
kb_qa_samples = $kbQaSamples.Count
|
||
validation_failures = $validationFailures
|
||
import_failures = $importFailures
|
||
blocked_by_review = $blockedByReview
|
||
}
|
||
}
|
||
Save-Json -Path $summaryJson -Value $summary
|
||
|
||
$md = New-Object System.Collections.Generic.List[string]
|
||
$md.Add("# 阶段 13 资料导入报告") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
$md.Add("- 模式:$Mode") | Out-Null
|
||
$md.Add("- 状态:$overallStatus") | Out-Null
|
||
$md.Add("- 生成时间:$($summary.generated_at)") | Out-Null
|
||
$md.Add("- 接口地址:$ApiBaseUrl") | Out-Null
|
||
$md.Add("- 资料包:$LibraryRoot") | Out-Null
|
||
$md.Add("- 目标知识库:$KnowledgeBaseName") | Out-Null
|
||
if ($targetKnowledgeBase) {
|
||
$md.Add("- 目标知识库编号:$($targetKnowledgeBase.id)") | Out-Null
|
||
}
|
||
$md.Add("") | Out-Null
|
||
$md.Add("## 统计") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
$md.Add("| 指标 | 数量 |") | Out-Null
|
||
$md.Add("|---|---:|") | Out-Null
|
||
$md.Add("| 法源 | $($sourceRows.Count) |") | Out-Null
|
||
$md.Add("| 知识卡 | $($knowledgeCards.Count) |") | Out-Null
|
||
$md.Add("| 当前模式可导入知识卡 | $($eligibleKnowledgeCards.Count) |") | Out-Null
|
||
$md.Add("| 标准条款 | $($clauses.Count) |") | Out-Null
|
||
$md.Add("| 合同模板 | $($templates.Count) |") | Out-Null
|
||
$md.Add("| 审核规则 | $($reviewRules.Count) |") | Out-Null
|
||
$md.Add("| 合同审核黄金样本 | $($reviewSamples.Count) |") | Out-Null
|
||
$md.Add("| 合同生成黄金样本 | $($generationSamples.Count) |") | Out-Null
|
||
$md.Add("| 知识库问答黄金样本 | $($kbQaSamples.Count) |") | Out-Null
|
||
$md.Add("| 校验失败 | $validationFailures |") | Out-Null
|
||
$md.Add("| 导入失败 | $importFailures |") | Out-Null
|
||
$md.Add("| 当前模式阻塞资料 | $blockedByReview |") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
$md.Add("## 检查项") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
$md.Add("| 检查项 | 必需 | 状态 | 说明 |") | Out-Null
|
||
$md.Add("|---|---:|---|---|") | Out-Null
|
||
foreach ($check in $checks) {
|
||
$requiredText = if ($check.required) { "是" } else { "否" }
|
||
$detail = ([string]$check.detail).Replace("|", "/")
|
||
$md.Add("| $($check.label) | $requiredText | $($check.status) | $detail |") | Out-Null
|
||
}
|
||
$md.Add("") | Out-Null
|
||
$md.Add("## 导入结果") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
if ($importReport.Count -eq 0) {
|
||
$md.Add("本次未执行 API 导入。") | Out-Null
|
||
} else {
|
||
$md.Add("| 类型 | 编号 | 动作 | 状态 | 说明 |") | Out-Null
|
||
$md.Add("|---|---|---|---|---|") | Out-Null
|
||
foreach ($item in $importReport) {
|
||
$detail = ([string]$item.detail).Replace("|", "/")
|
||
$md.Add("| $($item.asset_type) | $($item.asset_id) | $($item.action) | $($item.status) | $detail |") | Out-Null
|
||
}
|
||
}
|
||
$md.Add("") | Out-Null
|
||
$md.Add("本地产物:") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
$md.Add('- `phase13-import-summary.json`') | Out-Null
|
||
$md.Add('- `phase13-import-summary.md`') | Out-Null
|
||
$md | Set-Content -Encoding UTF8 -Path $summaryMd
|
||
|
||
Write-Host "PHASE13_IMPORT_$overallStatus $artifactDir"
|
||
if ($overallStatus -eq "FAIL") {
|
||
throw "阶段 13 资料导入失败。查看:$summaryMd"
|
||
}
|