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" }