628 lines
28 KiB
PowerShell
628 lines
28 KiB
PowerShell
param(
|
|
[string]$ApiBaseUrl = "http://192.168.3.90:8001/api/v1",
|
|
[string]$ManifestPath = "",
|
|
[string]$ArtifactRoot = "",
|
|
[string]$AdminEmail = "admin@example.com",
|
|
[string]$AdminPassword = "",
|
|
[string]$AdminPasswordFile = "",
|
|
[string]$KnowledgeBaseName = "AI合同平台法条全文库(Phase14)",
|
|
[string]$KnowledgeBaseId = "",
|
|
[switch]$ForceReupload,
|
|
[switch]$SkipKnowledgeBasePublish
|
|
)
|
|
|
|
$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($ManifestPath)) {
|
|
$ManifestPath = Join-Path $repoRoot "project-docs\phase14-legal-fulltext-library\legal-article-manifest.json"
|
|
}
|
|
if (-not [System.IO.Path]::IsPathRooted($ManifestPath)) {
|
|
$ManifestPath = Join-Path $repoRoot $ManifestPath
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
|
|
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase14-legal-fulltext-import"
|
|
}
|
|
if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) {
|
|
$ArtifactRoot = Join-Path $repoRoot $ArtifactRoot
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($AdminPasswordFile)) {
|
|
$AdminPasswordFile = Join-Path $HOME ".ai-contract-platform\production-admin-password.txt"
|
|
}
|
|
|
|
$artifactDir = Join-Path $ArtifactRoot $timestamp
|
|
$downloadDir = Join-Path $artifactDir "official-downloads"
|
|
New-Item -ItemType Directory -Force -Path $downloadDir | Out-Null
|
|
|
|
$summaryJson = Join-Path $artifactDir "phase14-legal-fulltext-import-summary.json"
|
|
$summaryMd = Join-Path $artifactDir "phase14-legal-fulltext-import-summary.md"
|
|
$ApiBaseUrl = $ApiBaseUrl.TrimEnd("/")
|
|
|
|
$checks = New-Object System.Collections.Generic.List[object]
|
|
$imports = New-Object System.Collections.Generic.List[object]
|
|
$verification = New-Object System.Collections.Generic.List[object]
|
|
$articlePayloads = New-Object System.Collections.Generic.List[object]
|
|
$articlePrefix = [string][char]0x7B2C
|
|
$articleSuffix = [string][char]0x6761
|
|
|
|
function Add-Check {
|
|
param([string]$Key, [string]$Label, [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 Add-Import {
|
|
param([string]$SourceId, [string]$ArticleNo, [string]$Action, [string]$Status, [string]$TargetId, [string]$Detail, [hashtable]$Meta = @{})
|
|
$imports.Add([ordered]@{ source_id = $SourceId; article_no = $ArticleNo; action = $Action; status = $Status; target_id = $TargetId; detail = $Detail; meta = $Meta }) | Out-Null
|
|
}
|
|
|
|
function Add-Verification {
|
|
param([string]$Key, [string]$Status, [string]$Detail, [hashtable]$Meta = @{})
|
|
$verification.Add([ordered]@{ key = $Key; status = $Status; detail = $Detail; meta = $Meta }) | Out-Null
|
|
}
|
|
|
|
function Save-Json {
|
|
param([string]$Path, [object]$Value)
|
|
$Value | ConvertTo-Json -Depth 60 | Set-Content -Encoding UTF8 -Path $Path
|
|
}
|
|
|
|
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 60)
|
|
}
|
|
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 request failed: $Method $Url, status=$([int]$response.StatusCode), response=$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 = 200
|
|
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-LegalArticlesBySourceIds {
|
|
param(
|
|
[string[]]$SourceIds,
|
|
[hashtable]$Headers,
|
|
[string]$PublishedStatus = "",
|
|
[string]$IndexStatus = ""
|
|
)
|
|
$items = New-Object System.Collections.Generic.List[object]
|
|
foreach ($sourceId in @($SourceIds | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)) {
|
|
$query = "source_id=$([uri]::EscapeDataString($sourceId))"
|
|
if (-not [string]::IsNullOrWhiteSpace($PublishedStatus)) {
|
|
$query += "&published_status=$([uri]::EscapeDataString($PublishedStatus))"
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace($IndexStatus)) {
|
|
$query += "&index_status=$([uri]::EscapeDataString($IndexStatus))"
|
|
}
|
|
$sourceItems = Get-PagedApiItems -Url "$ApiBaseUrl/legal-articles?$query" -Headers $Headers
|
|
foreach ($item in @($sourceItems)) {
|
|
$items.Add($item) | Out-Null
|
|
}
|
|
}
|
|
return $items.ToArray()
|
|
}
|
|
|
|
function Get-AccessToken {
|
|
$password = $AdminPassword
|
|
if ([string]::IsNullOrWhiteSpace($password) -and -not [string]::IsNullOrWhiteSpace($env:AICP_ADMIN_PASSWORD)) {
|
|
$password = $env:AICP_ADMIN_PASSWORD
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($password) -and (Test-Path -LiteralPath $AdminPasswordFile -PathType Leaf)) {
|
|
$password = (Get-Content -LiteralPath $AdminPasswordFile -Raw).Trim()
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($password)) {
|
|
throw "Admin password is required. Use -AdminPassword, AICP_ADMIN_PASSWORD, or $AdminPasswordFile."
|
|
}
|
|
|
|
$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 "Login response has no access_token."
|
|
}
|
|
return [string]$data.access_token
|
|
}
|
|
|
|
function Get-AuthHeaders {
|
|
param([string]$Token)
|
|
return @{ Authorization = "Bearer $Token" }
|
|
}
|
|
|
|
function Get-TextChecksum {
|
|
param([string]$Text)
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Text)
|
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
return ([System.BitConverter]::ToString($sha.ComputeHash($bytes))).Replace("-", "").ToLowerInvariant()
|
|
} finally {
|
|
$sha.Dispose()
|
|
}
|
|
}
|
|
|
|
function Get-Base64Url {
|
|
param([string]$Value)
|
|
return [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Value))
|
|
}
|
|
|
|
function Get-OfficialDetailUrl {
|
|
param([string]$Bbbs)
|
|
return "https://flk.npc.gov.cn/detail2.html?$(Get-Base64Url -Value $Bbbs)"
|
|
}
|
|
|
|
function Invoke-OfficialJson {
|
|
param([string]$Url)
|
|
$client = [System.Net.Http.HttpClient]::new()
|
|
try {
|
|
$client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0")
|
|
$client.DefaultRequestHeaders.Add("Referer", "https://flk.npc.gov.cn/")
|
|
$bytes = $client.GetByteArrayAsync($Url).GetAwaiter().GetResult()
|
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
|
return $text | ConvertFrom-Json
|
|
} finally {
|
|
$client.Dispose()
|
|
}
|
|
}
|
|
|
|
function Get-OfficialLawDetail {
|
|
param([string]$Bbbs)
|
|
$encoded = [uri]::EscapeDataString($Bbbs)
|
|
$response = Invoke-OfficialJson -Url "https://flk.npc.gov.cn/law-search/search/flfgDetails?bbbs=$encoded"
|
|
if ($response.code -ne 200 -or $null -eq $response.data) {
|
|
throw "Official detail request failed for $Bbbs."
|
|
}
|
|
return $response.data
|
|
}
|
|
|
|
function Get-OfficialDocxUrl {
|
|
param([string]$Bbbs)
|
|
$encoded = [uri]::EscapeDataString($Bbbs)
|
|
$response = Invoke-OfficialJson -Url "https://flk.npc.gov.cn/law-search/download/pc?format=docx&bbbs=$encoded&fileId="
|
|
if ($response.code -ne 200 -or $null -eq $response.data -or [string]::IsNullOrWhiteSpace([string]$response.data.url)) {
|
|
throw "Official docx download URL request failed for $Bbbs."
|
|
}
|
|
return [string]$response.data.url
|
|
}
|
|
|
|
function Save-OfficialDocx {
|
|
param([string]$Url, [string]$Path)
|
|
Invoke-WebRequest -Uri $Url -OutFile $Path -UseBasicParsing -Headers @{ "User-Agent" = "Mozilla/5.0"; "Referer" = "https://flk.npc.gov.cn/" } -ErrorAction Stop
|
|
}
|
|
|
|
function Convert-DocxToParagraphs {
|
|
param([string]$DocxPath, [string]$WorkDir)
|
|
$zipPath = Join-Path $WorkDir "document.zip"
|
|
$unzipDir = Join-Path $WorkDir "document"
|
|
Copy-Item -LiteralPath $DocxPath -Destination $zipPath -Force
|
|
Expand-Archive -LiteralPath $zipPath -DestinationPath $unzipDir -Force
|
|
$documentXmlPath = Join-Path $unzipDir "word\document.xml"
|
|
if (-not (Test-Path -LiteralPath $documentXmlPath -PathType Leaf)) {
|
|
throw "word/document.xml not found in official docx."
|
|
}
|
|
$xml = Get-Content -LiteralPath $documentXmlPath -Raw -Encoding UTF8
|
|
$paragraphs = New-Object System.Collections.Generic.List[string]
|
|
foreach ($paragraphMatch in [regex]::Matches($xml, "<w:p[\s\S]*?</w:p>")) {
|
|
$parts = New-Object System.Collections.Generic.List[string]
|
|
foreach ($textMatch in [regex]::Matches($paragraphMatch.Value, "<w:t[^>]*>([\s\S]*?)</w:t>")) {
|
|
$parts.Add([System.Net.WebUtility]::HtmlDecode($textMatch.Groups[1].Value)) | Out-Null
|
|
}
|
|
$text = ($parts.ToArray() -join "").Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($text)) {
|
|
$paragraphs.Add($text) | Out-Null
|
|
}
|
|
}
|
|
return $paragraphs.ToArray()
|
|
}
|
|
|
|
function Test-ArticleHeader {
|
|
param([string]$Text)
|
|
if ([string]::IsNullOrWhiteSpace($Text)) { return $false }
|
|
if (-not $Text.StartsWith($articlePrefix)) { return $false }
|
|
$suffixIndex = $Text.IndexOf($articleSuffix)
|
|
return $suffixIndex -gt 0 -and $suffixIndex -le 16
|
|
}
|
|
|
|
function Get-ArticleText {
|
|
param([string[]]$Paragraphs, [string]$ArticleNo)
|
|
$start = -1
|
|
for ($i = 0; $i -lt $Paragraphs.Count; $i++) {
|
|
if ($Paragraphs[$i].StartsWith($ArticleNo)) {
|
|
$start = $i
|
|
break
|
|
}
|
|
}
|
|
if ($start -lt 0) {
|
|
throw "Article not found: $ArticleNo"
|
|
}
|
|
$selected = New-Object System.Collections.Generic.List[string]
|
|
for ($i = $start; $i -lt $Paragraphs.Count; $i++) {
|
|
if ($i -gt $start -and (Test-ArticleHeader -Text $Paragraphs[$i])) {
|
|
break
|
|
}
|
|
$selected.Add($Paragraphs[$i]) | Out-Null
|
|
}
|
|
return ($selected.ToArray() -join "`n").Trim()
|
|
}
|
|
|
|
function ConvertTo-TagsJson {
|
|
param([object[]]$Values)
|
|
$tags = New-Object System.Collections.Generic.List[string]
|
|
foreach ($value in $Values) {
|
|
$text = ([string]$value).Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($text) -and -not $tags.Contains($text)) {
|
|
$tags.Add($text) | Out-Null
|
|
}
|
|
}
|
|
if ($tags.Count -eq 0) { return $null }
|
|
return ($tags.ToArray() | ConvertTo-Json -Compress)
|
|
}
|
|
|
|
function Ensure-KnowledgeBase {
|
|
param([hashtable]$Headers)
|
|
$items = Get-PagedApiItems -Url "$ApiBaseUrl/knowledge-bases" -Headers $Headers
|
|
if (-not [string]::IsNullOrWhiteSpace($KnowledgeBaseId)) {
|
|
$byId = @($items | Where-Object { $_.id -eq $KnowledgeBaseId }) | Select-Object -First 1
|
|
if ($byId) { return $byId }
|
|
}
|
|
$existing = @(
|
|
$items |
|
|
Where-Object { $_.name -eq $KnowledgeBaseName } |
|
|
Sort-Object `
|
|
@{ Expression = { [int]($_.document_count -as [int]) }; Descending = $true }, `
|
|
@{ Expression = { if ($_.updated_at) { [datetime]$_.updated_at } else { [datetime]::MinValue } }; Descending = $true } |
|
|
Select-Object -First 1
|
|
) | Select-Object -First 1
|
|
if ($existing) { return $existing }
|
|
$response = Invoke-FormRequest -Method "POST" -Url "$ApiBaseUrl/knowledge-bases" -Headers $Headers -Fields @{
|
|
name = $KnowledgeBaseName
|
|
description = "Phase14 legal article fulltext library imported from official law sources."
|
|
}
|
|
return Get-ResponseData -Response $response
|
|
}
|
|
|
|
function New-ArticleMarkdown {
|
|
param([object]$Source, [object]$Article, [string]$ArticleText, [string]$OfficialUrl)
|
|
return @"
|
|
# $($Source.title) $($Article.article_no)
|
|
|
|
## Source metadata
|
|
|
|
- source_id: $($Source.source_id)
|
|
- source_title: $($Source.title)
|
|
- source_type: $($Source.source_type)
|
|
- authority: $($Source.authority)
|
|
- published_date: $($Source.published_date)
|
|
- effective_date: $($Source.effective_date)
|
|
- validity_status: $($Source.validity_status)
|
|
- region: $($Source.region)
|
|
- official_url: $OfficialUrl
|
|
- chapter_title: $($Article.chapter_title)
|
|
- section_title: $($Article.section_title)
|
|
|
|
## Article full text
|
|
|
|
$ArticleText
|
|
"@
|
|
}
|
|
|
|
function Import-ArticleDocument {
|
|
param(
|
|
[hashtable]$Headers,
|
|
[string]$KnowledgeBaseId,
|
|
[object]$Source,
|
|
[object]$Article,
|
|
[string]$ArticleText,
|
|
[string]$OfficialUrl,
|
|
[object[]]$ExistingDocuments
|
|
)
|
|
$fileArticleNo = (Get-TextChecksum -Text $Article.article_no).Substring(0, 8)
|
|
$fileName = "$($Source.source_id)-$fileArticleNo.md"
|
|
$existing = @($ExistingDocuments | Where-Object { $_.file_name -eq $fileName }) | Select-Object -First 1
|
|
$tagsJson = ConvertTo-TagsJson -Values @($Source.source_id, $Source.knowledge_domain, "phase14", "legal-fulltext", $Article.tags)
|
|
$fields = @{
|
|
note = "Phase14 legal article fulltext import."
|
|
source_type = $Source.source_type
|
|
authority = $Source.authority
|
|
effective_date = $Source.effective_date
|
|
region = $Source.region
|
|
}
|
|
if ($tagsJson) { $fields["tags_json"] = $tagsJson }
|
|
$content = New-ArticleMarkdown -Source $Source -Article $Article -ArticleText $ArticleText -OfficialUrl $OfficialUrl
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($content)
|
|
|
|
if ($existing -and -not $ForceReupload) {
|
|
Add-Import -SourceId $Source.source_id -ArticleNo $Article.article_no -Action "skip_existing" -Status "skipped" -TargetId $existing.id -Detail "Existing knowledge document reused." -Meta @{ file_name = $fileName; status = $existing.status }
|
|
return $existing
|
|
}
|
|
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
|
|
Add-Import -SourceId $Source.source_id -ArticleNo $Article.article_no -Action "retry_existing" -Status $data.status -TargetId $data.id -Detail $data.status_detail -Meta @{ file_name = $fileName }
|
|
return $data
|
|
}
|
|
|
|
$response = Invoke-FormRequest -Method "POST" -Url "$ApiBaseUrl/knowledge-bases/$KnowledgeBaseId/documents" -Headers $Headers -Fields $fields -FileField "file" -FileName $fileName -FileContent $bytes
|
|
$created = Get-ResponseData -Response $response
|
|
Add-Import -SourceId $Source.source_id -ArticleNo $Article.article_no -Action "upload" -Status $created.status -TargetId $created.id -Detail $created.status_detail -Meta @{ file_name = $fileName }
|
|
return $created
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $ManifestPath -PathType Leaf)) {
|
|
Add-Check -Key "manifest_exists" -Label "Manifest exists" -Status "fail" -Detail "Manifest missing: $ManifestPath"
|
|
} else {
|
|
Add-Check -Key "manifest_exists" -Label "Manifest exists" -Status "pass" -Detail $ManifestPath
|
|
}
|
|
|
|
$manifest = $null
|
|
if ((@($checks | Where-Object { $_.required -and $_.status -eq "fail" })).Count -eq 0) {
|
|
$manifest = Get-Content -LiteralPath $ManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
$sourceCount = @($manifest.sources).Count
|
|
$articleCount = 0
|
|
foreach ($source in @($manifest.sources)) { $articleCount += @($source.articles).Count }
|
|
if ($sourceCount -gt 0 -and $articleCount -gt 0) {
|
|
Add-Check -Key "manifest_shape" -Label "Manifest shape" -Status "pass" -Detail "sources=$sourceCount, articles=$articleCount" -Meta @{ sources = $sourceCount; articles = $articleCount }
|
|
} else {
|
|
Add-Check -Key "manifest_shape" -Label "Manifest shape" -Status "fail" -Detail "Manifest has no sources or articles." -Meta @{ sources = $sourceCount; articles = $articleCount }
|
|
}
|
|
}
|
|
|
|
$token = ""
|
|
$headers = @{}
|
|
$targetKnowledgeBase = $null
|
|
if ((@($checks | Where-Object { $_.required -and $_.status -eq "fail" })).Count -eq 0) {
|
|
$token = Get-AccessToken
|
|
$headers = Get-AuthHeaders -Token $token
|
|
Add-Check -Key "api_login" -Label "API login" -Status "pass" -Detail "Admin API login succeeded."
|
|
$targetKnowledgeBase = Ensure-KnowledgeBase -Headers $headers
|
|
Add-Check -Key "target_knowledge_base" -Label "Target knowledge base" -Status "pass" -Detail $targetKnowledgeBase.name -Meta @{ knowledge_base_id = $targetKnowledgeBase.id }
|
|
}
|
|
|
|
if ((@($checks | Where-Object { $_.required -and $_.status -eq "fail" })).Count -eq 0) {
|
|
$existingDocs = Get-PagedApiItems -Url "$ApiBaseUrl/knowledge-bases/$($targetKnowledgeBase.id)/documents" -Headers $headers
|
|
foreach ($source in @($manifest.sources)) {
|
|
try {
|
|
$officialDetail = Get-OfficialLawDetail -Bbbs $source.bbbs
|
|
$officialUrl = Get-OfficialDetailUrl -Bbbs $source.bbbs
|
|
if ([string]$officialDetail.title -ne [string]$source.title) {
|
|
Add-Check -Key "official_title_$($source.source_id)" -Label "Official title check $($source.source_id)" -Status "fail" -Detail "Expected=$($source.title), actual=$($officialDetail.title)"
|
|
continue
|
|
}
|
|
$docxUrl = Get-OfficialDocxUrl -Bbbs $source.bbbs
|
|
$sourceDir = Join-Path $downloadDir $source.source_id
|
|
New-Item -ItemType Directory -Force -Path $sourceDir | Out-Null
|
|
$docxPath = Join-Path $sourceDir "official.docx"
|
|
Save-OfficialDocx -Url $docxUrl -Path $docxPath
|
|
$docxBytes = [System.IO.File]::ReadAllBytes($docxPath)
|
|
$sourceChecksum = Get-TextChecksum -Text ([Convert]::ToBase64String($docxBytes))
|
|
$paragraphs = Convert-DocxToParagraphs -DocxPath $docxPath -WorkDir $sourceDir
|
|
Add-Check -Key "official_download_$($source.source_id)" -Label "Official download $($source.source_id)" -Status "pass" -Detail "paragraphs=$($paragraphs.Count)" -Meta @{ title = $source.title; bbbs = $source.bbbs; checksum = $sourceChecksum }
|
|
|
|
foreach ($article in @($source.articles)) {
|
|
try {
|
|
$articleText = Get-ArticleText -Paragraphs $paragraphs -ArticleNo $article.article_no
|
|
$knowledgeDocument = Import-ArticleDocument -Headers $headers -KnowledgeBaseId $targetKnowledgeBase.id -Source $source -Article $article -ArticleText $articleText -OfficialUrl $officialUrl -ExistingDocuments $existingDocs
|
|
$articlePayloads.Add([ordered]@{
|
|
source_id = $source.source_id
|
|
source_title = $source.title
|
|
source_type = $source.source_type
|
|
authority = $source.authority
|
|
effective_date = $source.effective_date
|
|
validity_status = $source.validity_status
|
|
region = $source.region
|
|
official_url = $officialUrl
|
|
article_no = $article.article_no
|
|
chapter_title = $article.chapter_title
|
|
section_title = $article.section_title
|
|
article_text = $articleText
|
|
tags = @($source.source_id, $source.knowledge_domain, "phase14", "legal-fulltext") + @($article.tags)
|
|
source_checksum = $sourceChecksum
|
|
text_checksum = Get-TextChecksum -Text $articleText
|
|
knowledge_base_id = $targetKnowledgeBase.id
|
|
knowledge_document_id = $knowledgeDocument.id
|
|
index_status = if ($knowledgeDocument.status -eq "indexed") { "indexed" } else { [string]$knowledgeDocument.status }
|
|
published_status = "published"
|
|
published_by = "CODEX_PHASE14_IMPORT"
|
|
}) | Out-Null
|
|
} catch {
|
|
Add-Import -SourceId $source.source_id -ArticleNo $article.article_no -Action "extract_upload" -Status "failed" -TargetId "" -Detail $_.Exception.Message
|
|
}
|
|
}
|
|
} catch {
|
|
Add-Check -Key "official_download_$($source.source_id)" -Label "Official download $($source.source_id)" -Status "fail" -Detail $_.Exception.Message
|
|
}
|
|
}
|
|
}
|
|
|
|
$importFailures = @($imports | Where-Object { $_.status -eq "failed" }).Count
|
|
if ((@($checks | Where-Object { $_.required -and $_.status -eq "fail" })).Count -eq 0 -and $importFailures -eq 0) {
|
|
$bulkResponse = Invoke-ApiJson -Method "POST" -Url "$ApiBaseUrl/legal-articles/bulk-upsert" -Headers $headers -Body @{ articles = $articlePayloads.ToArray() }
|
|
$bulkData = Get-ResponseData -Response $bulkResponse
|
|
Add-Check -Key "legal_articles_bulk_upsert" -Label "Legal articles bulk upsert" -Status "pass" -Detail "total=$($bulkData.total), created=$($bulkData.created_count), updated=$($bulkData.updated_count)" -Meta @{ total = $bulkData.total; created = $bulkData.created_count; updated = $bulkData.updated_count }
|
|
|
|
if ($SkipKnowledgeBasePublish) {
|
|
Add-Import -SourceId "knowledge_base" -ArticleNo "" -Action "publish" -Status "skipped" -TargetId $targetKnowledgeBase.id -Detail "Skipped by parameter."
|
|
} else {
|
|
$publishResponse = Invoke-ApiJson -Method "POST" -Url "$ApiBaseUrl/knowledge-bases/$($targetKnowledgeBase.id)/publish" -Headers $headers
|
|
$publishedKb = Get-ResponseData -Response $publishResponse
|
|
Add-Import -SourceId "knowledge_base" -ArticleNo "" -Action "publish" -Status "published" -TargetId $publishedKb.id -Detail "Knowledge base published." -Meta @{ current_version_label = $publishedKb.current_version_label; published_at = $publishedKb.published_at }
|
|
}
|
|
}
|
|
|
|
if ((@($checks | Where-Object { $_.required -and $_.status -eq "fail" })).Count -eq 0 -and $importFailures -eq 0) {
|
|
$expectedArticleCount = $articlePayloads.Count
|
|
$documentsAfter = Get-PagedApiItems -Url "$ApiBaseUrl/knowledge-bases/$($targetKnowledgeBase.id)/documents" -Headers $headers
|
|
$documentIds = @($articlePayloads | ForEach-Object { $_.knowledge_document_id })
|
|
$targetDocs = @($documentsAfter | Where-Object { $documentIds -contains $_.id })
|
|
$indexedDocs = @($targetDocs | Where-Object { $_.status -eq "indexed" })
|
|
if ($targetDocs.Count -eq $expectedArticleCount -and $indexedDocs.Count -eq $expectedArticleCount) {
|
|
Add-Verification -Key "knowledge_documents_indexed" -Status "pass" -Detail "All legal article documents are indexed." -Meta @{ expected = $expectedArticleCount; target_documents = $targetDocs.Count; indexed = $indexedDocs.Count }
|
|
} else {
|
|
Add-Verification -Key "knowledge_documents_indexed" -Status "fail" -Detail "Legal article document indexing is incomplete." -Meta @{ expected = $expectedArticleCount; target_documents = $targetDocs.Count; indexed = $indexedDocs.Count }
|
|
}
|
|
|
|
$batchSourceIds = @($articlePayloads | ForEach-Object { [string]$_.source_id } | Select-Object -Unique)
|
|
$apiArticles = Get-LegalArticlesBySourceIds -SourceIds $batchSourceIds -Headers $headers -PublishedStatus "published" -IndexStatus "indexed"
|
|
$expectedChecksums = @($articlePayloads | ForEach-Object { $_.text_checksum })
|
|
$matchedArticles = @($apiArticles | Where-Object { $expectedChecksums -contains $_.text_checksum })
|
|
if ($matchedArticles.Count -eq $expectedArticleCount) {
|
|
Add-Verification -Key "legal_articles_runtime_table" -Status "pass" -Detail "All legal articles are in runtime table." -Meta @{ expected = $expectedArticleCount; matched = $matchedArticles.Count }
|
|
} else {
|
|
Add-Verification -Key "legal_articles_runtime_table" -Status "fail" -Detail "Runtime legal article table is incomplete." -Meta @{ expected = $expectedArticleCount; matched = $matchedArticles.Count }
|
|
}
|
|
}
|
|
|
|
$failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" }).Count
|
|
$failedVerification = @($verification | Where-Object { $_.status -eq "fail" }).Count
|
|
$warnings = @($checks | Where-Object { $_.status -eq "warn" }).Count + @($verification | Where-Object { $_.status -eq "warn" }).Count
|
|
$overallStatus = if ($failedRequired -gt 0 -or $failedVerification -gt 0 -or $importFailures -gt 0) { "FAIL" } elseif ($warnings -gt 0) { "WARN" } else { "PASS" }
|
|
|
|
$summary = [ordered]@{
|
|
name = "phase14-legal-fulltext-import"
|
|
status = $overallStatus
|
|
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
|
api_base_url = $ApiBaseUrl
|
|
manifest_path = $ManifestPath
|
|
artifact_dir = $artifactDir
|
|
knowledge_base_name = $KnowledgeBaseName
|
|
knowledge_base_id = if ($targetKnowledgeBase) { $targetKnowledgeBase.id } else { "" }
|
|
checks = $checks.ToArray()
|
|
imports = $imports.ToArray()
|
|
verification = $verification.ToArray()
|
|
totals = [ordered]@{
|
|
sources = if ($manifest) { @($manifest.sources).Count } else { 0 }
|
|
articles_extracted = $articlePayloads.Count
|
|
import_failures = $importFailures
|
|
failed_required_checks = $failedRequired
|
|
failed_verifications = $failedVerification
|
|
}
|
|
}
|
|
Save-Json -Path $summaryJson -Value $summary
|
|
|
|
$md = New-Object System.Collections.Generic.List[string]
|
|
$md.Add("# Phase14 legal fulltext import report") | Out-Null
|
|
$md.Add("") | Out-Null
|
|
$md.Add("- status: $overallStatus") | Out-Null
|
|
$md.Add("- generated_at: $($summary.generated_at)") | Out-Null
|
|
$md.Add("- api_base_url: $ApiBaseUrl") | Out-Null
|
|
$md.Add("- manifest_path: $ManifestPath") | Out-Null
|
|
$md.Add("- knowledge_base_name: $KnowledgeBaseName") | Out-Null
|
|
$md.Add("- knowledge_base_id: $($summary.knowledge_base_id)") | Out-Null
|
|
$md.Add("") | Out-Null
|
|
$md.Add("## Totals") | Out-Null
|
|
$md.Add("") | Out-Null
|
|
$md.Add("| item | value |") | Out-Null
|
|
$md.Add("|---|---:|") | Out-Null
|
|
$md.Add("| sources | $($summary.totals.sources) |") | Out-Null
|
|
$md.Add("| articles_extracted | $($summary.totals.articles_extracted) |") | Out-Null
|
|
$md.Add("| import_failures | $($summary.totals.import_failures) |") | Out-Null
|
|
$md.Add("| failed_required_checks | $($summary.totals.failed_required_checks) |") | Out-Null
|
|
$md.Add("| failed_verifications | $($summary.totals.failed_verifications) |") | Out-Null
|
|
$md.Add("") | Out-Null
|
|
$md.Add("## Checks") | Out-Null
|
|
$md.Add("") | Out-Null
|
|
$md.Add("| key | required | status | detail |") | Out-Null
|
|
$md.Add("|---|---:|---|---|") | Out-Null
|
|
foreach ($check in $checks) {
|
|
$detail = ([string]$check.detail).Replace("|", "/")
|
|
$requiredText = if ($check.required) { "yes" } else { "no" }
|
|
$md.Add("| $($check.key) | $requiredText | $($check.status) | $detail |") | Out-Null
|
|
}
|
|
$md.Add("") | Out-Null
|
|
$md.Add("## Imports") | Out-Null
|
|
$md.Add("") | Out-Null
|
|
$md.Add("| source_id | article_no | action | status | target_id | detail |") | Out-Null
|
|
$md.Add("|---|---|---|---|---|---|") | Out-Null
|
|
foreach ($item in $imports) {
|
|
$detail = ([string]$item.detail).Replace("|", "/")
|
|
$md.Add("| $($item.source_id) | $($item.article_no) | $($item.action) | $($item.status) | $($item.target_id) | $detail |") | Out-Null
|
|
}
|
|
$md.Add("") | Out-Null
|
|
$md.Add("## Verification") | Out-Null
|
|
$md.Add("") | Out-Null
|
|
$md.Add("| key | status | detail |") | Out-Null
|
|
$md.Add("|---|---|---|") | Out-Null
|
|
foreach ($item in $verification) {
|
|
$detail = ([string]$item.detail).Replace("|", "/")
|
|
$md.Add("| $($item.key) | $($item.status) | $detail |") | Out-Null
|
|
}
|
|
$md | Set-Content -Encoding UTF8 -Path $summaryMd
|
|
|
|
Write-Host "PHASE14_LEGAL_FULLTEXT_IMPORT_$overallStatus $artifactDir"
|
|
if ($overallStatus -eq "FAIL") {
|
|
throw "Phase14 legal fulltext import failed. See: $summaryMd"
|
|
}
|