Files
YMhut-box-C-/scripts/generate-tool-ui-dat.ps1

267 lines
15 KiB
PowerShell

param(
[string]$Root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
)
$ErrorActionPreference = 'Stop'
$catalogPath = Join-Path $Root 'src\YMhut.Box.Core\Tools\ToolCatalog.cs'
$pearCatalogPath = Join-Path $Root 'src\YMhut.Box.Core\Api\PearApiCatalog.cs'
$nexPath = Join-Path $Root 'src\YMhut.Box.Core\Tools\NexNativeTools.cs'
$outputRoot = Join-Path $Root 'assets\data\tool-ui'
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
$catalogText = Get-Content -LiteralPath $catalogPath -Raw -Encoding UTF8
$rawMatch = [regex]::Match($catalogText, 'private const string RawToolData = """(?<data>.*?)""";', [Text.RegularExpressions.RegexOptions]::Singleline)
if (-not $rawMatch.Success) { throw 'RawToolData was not found.' }
$ids = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
$metadata = @{}
foreach ($line in ($rawMatch.Groups['data'].Value -split "`r?`n")) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$parts = $line -split "`t"
if ($parts.Count -lt 4) { continue }
$id = $parts[0].Trim()
if ([string]::IsNullOrWhiteSpace($id)) { continue }
[void]$ids.Add($id)
$metadata[$id] = [ordered]@{
zh = $parts[1].Trim()
en = $parts[1].Trim()
descriptionZh = $parts[2].Trim()
descriptionEn = $parts[2].Trim()
category = $parts[3].Trim()
offline = ($parts.Count -gt 4 -and $parts[4].Trim() -eq 'true')
}
}
$pearPattern = 'D\("(?<id>[^"]+)",\s*(?<apiId>\d+),\s*"(?<name>[^"]+)",\s*"(?<description>[^"]*)",\s*"(?<category>[^"]+)",\s*"(?<path>[^"]+)",\s*PearApiRequestMethod\.(?<method>\w+),\s*PearApiPrivacyClass\.(?<privacy>\w+),\s*PearApiResultKind\.(?<result>\w+)'
foreach ($line in (Get-Content -LiteralPath $pearCatalogPath -Encoding UTF8)) {
$match = [regex]::Match($line, $pearPattern)
if (-not $match.Success) { continue }
$id = $match.Groups['id'].Value
[void]$ids.Add($id)
$metadata[$id] = [ordered]@{
zh = $match.Groups['name'].Value
en = $match.Groups['name'].Value
descriptionZh = $match.Groups['description'].Value
descriptionEn = $match.Groups['description'].Value
category = 'network'
offline = $false
pear = $true
pearCategory = $match.Groups['category'].Value
privacy = $match.Groups['privacy'].Value
result = $match.Groups['result'].Value
apiId = $match.Groups['apiId'].Value
}
}
foreach ($alias in @{ baidu_hot = '百度'; bili_hot = '哔哩哔哩'; zhihu_hot = '知乎' }.GetEnumerator()) {
if (-not $metadata.ContainsKey($alias.Key)) { continue }
$metadata[$alias.Key].pear = $true
$metadata[$alias.Key].pearCategory = '网络'
$metadata[$alias.Key].privacy = 'Public'
$metadata[$alias.Key].result = 'RankedList'
$metadata[$alias.Key].apiId = '141'
}
foreach ($match in [regex]::Matches((Get-Content -LiteralPath $nexPath -Raw -Encoding UTF8), 'Tool\("(?<id>[^"]+)"')) {
$id = $match.Groups['id'].Value
[void]$ids.Add($id)
if (-not $metadata.ContainsKey($id)) {
$metadata[$id] = [ordered]@{ zh = $id; en = $id; descriptionZh = ''; descriptionEn = ''; category = 'system'; offline = $true }
}
}
[void]$ids.Add('dev_environment_config')
if (-not $metadata.ContainsKey('dev_environment_config')) {
$metadata['dev_environment_config'] = [ordered]@{ zh = 'Development environment'; en = 'Development environment'; descriptionZh = 'Inspect and configure the local development environment.'; descriptionEn = 'Inspect and configure the local development environment.'; category = 'dev'; offline = $false }
}
[void]$ids.Add('plugin-host')
$metadata['plugin-host'] = [ordered]@{ zh = '插件安全宿主'; en = 'Plugin security host'; descriptionZh = '隔离第三方 WebView 内容、权限和运行状态。'; descriptionEn = 'Isolates third-party WebView content, permissions, and runtime status.'; category = 'plugin'; offline = $false }
[void]$ids.Add('external-tool-host')
$metadata['external-tool-host'] = [ordered]@{ zh = '外部工具宿主'; en = 'External tool host'; descriptionZh = '显示外部程序来源、风险、启动选项和运行结果。'; descriptionEn = 'Shows source, risk, launch options, and results for external programs.'; category = 'external'; offline = $true }
$sha = [Security.Cryptography.SHA256]::Create()
try { $keyBytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes('YMhut.Box.ToolUiDefinition.v1')) } finally { $sha.Dispose() }
$aes = [Security.Cryptography.Aes]::Create()
try {
$aes.Key = $keyBytes
$aes.Mode = [Security.Cryptography.CipherMode]::CBC
$aes.Padding = [Security.Cryptography.PaddingMode]::PKCS7
foreach ($id in ($ids | Sort-Object)) {
$meta = $metadata[$id]
$category = [string]$meta.category
$risk = if ($category -in @('security', 'system', 'plugin', 'external')) { 'high' } else { 'normal' }
$primaryKind = if ($meta.pear) {
switch ([string]$meta.privacy) {
'Public' { 'Output'; break }
'FileUpload' { 'FilePicker'; break }
default { 'Query' }
}
} else { switch ($category) {
'image' { 'FilePicker'; break }
'calculator' { 'Number'; break }
'network' { 'Query'; break }
'security' { 'MultilineText'; break }
'text' { 'MultilineText'; break }
'dev' { 'MultilineText'; break }
'plugin' { 'WebView'; break }
'external' { 'ExternalSurface'; break }
default { 'Text' }
} }
$maxLength = 4096
if ($category -eq 'text' -or $category -eq 'dev' -or $category -eq 'security') { $maxLength = 200000 }
$placeholder = '输入此工具所需的值'
if ($category -eq 'network') { $placeholder = '按此接口页面的字段填写查询条件' }
$primaryControls = [Collections.Generic.List[object]]::new()
$primaryControls.Add([ordered]@{
id = 'primary'
kind = $primaryKind
label = [ordered]@{ zh = '主要输入'; en = 'Primary input' }
placeholder = $placeholder
defaultValue = ''
required = $false
isReadOnly = $false
validation = [ordered]@{ maxLength = $maxLength }
})
$sections = [Collections.Generic.List[object]]::new()
$primaryTitle = if ($meta.pear) {
[ordered]@{ zh = "PearAPI · $([string]$meta.pearCategory)"; en = "PearAPI · $([string]$meta.pearCategory)" }
} else { switch ($category) {
'calculator' { [ordered]@{ zh = '计算参数'; en = 'Calculation parameters' } }
'network' { [ordered]@{ zh = '查询条件'; en = 'Query criteria' } }
'image' { [ordered]@{ zh = '文件与媒体'; en = 'Files and media' } }
'security' { [ordered]@{ zh = '安全输入'; en = 'Security input' } }
'dev' { [ordered]@{ zh = '代码与数据'; en = 'Code and data' } }
'text' { [ordered]@{ zh = '文本工作区'; en = 'Text workspace' } }
'system' { [ordered]@{ zh = '系统入口'; en = 'System entry' } }
'plugin' { [ordered]@{ zh = '插件安全容器'; en = 'Plugin security container' } }
'external' { [ordered]@{ zh = '外部程序运行'; en = 'External program launch' } }
default { [ordered]@{ zh = '主要操作'; en = 'Primary operation' } }
} }
if ($category -eq 'calculator') {
$primaryControls.Clear()
foreach ($numberId in @('number-primary', 'number-secondary', 'number-third', 'number-fourth')) {
$primaryControls.Add([ordered]@{ id = $numberId; kind = 'Number'; label = [ordered]@{ zh = '数值参数'; en = 'Numeric parameter' }; defaultValue = '1'; required = $true; isReadOnly = $false; validation = [ordered]@{ minimum = -100000000; maximum = 100000000 } })
}
} elseif ($category -eq 'network' -and -not $meta.pear) {
$primaryControls.Add([ordered]@{ id = 'parameter'; kind = 'ComboBox'; label = [ordered]@{ zh = '来源或分类'; en = 'Source or category' }; defaultValue = ''; required = $false; isReadOnly = $false })
} elseif ($category -eq 'image') {
$primaryControls.Add([ordered]@{ id = 'file'; kind = 'FilePicker'; label = [ordered]@{ zh = '文件'; en = 'File' }; placeholder = '选择本地文件'; defaultValue = ''; required = $false; isReadOnly = $false })
}
$sections.Add([ordered]@{
id = 'primary'
title = $primaryTitle
description = [ordered]@{ zh = [string]$meta.descriptionZh; en = [string]$meta.descriptionEn }
controls = @($primaryControls)
isAdvanced = $false
layout = $(if ($category -eq 'calculator') { 'grid' } else { 'stack' })
})
if (-not $meta.pear -and $category -in @('text', 'dev', 'security', 'network', 'design')) {
$advancedControls = [Collections.Generic.List[object]]::new()
if ($category -in @('text', 'dev', 'security')) {
$advancedControls.Add([ordered]@{ id = 'rules'; kind = 'ComboBox'; label = [ordered]@{ zh = '处理规则'; en = 'Processing rules' }; defaultValue = ''; required = $false; isReadOnly = $false })
$advancedControls.Add([ordered]@{ id = 'auxiliary'; kind = 'Text'; label = [ordered]@{ zh = '附加参数'; en = 'Additional parameter' }; defaultValue = ''; required = $false; isReadOnly = $false })
} elseif ($category -eq 'network') {
$advancedControls.Add([ordered]@{ id = 'rules'; kind = 'ComboBox'; label = [ordered]@{ zh = '筛选与显示'; en = 'Filtering and display' }; defaultValue = ''; required = $false; isReadOnly = $false })
} else {
$advancedControls.Add([ordered]@{ id = 'rules'; kind = 'ComboBox'; label = [ordered]@{ zh = '预览设置'; en = 'Preview settings' }; defaultValue = ''; required = $false; isReadOnly = $false })
}
$advancedControls.Add([ordered]@{ id = 'advanced-toggle'; kind = 'Toggle'; label = [ordered]@{ zh = '启用高级选项'; en = 'Enable advanced options' }; defaultValue = 'false'; required = $false; isReadOnly = $false })
$sections.Add([ordered]@{
id = 'advanced'
title = [ordered]@{ zh = '高级选项'; en = 'Advanced options' }
description = [ordered]@{ zh = '按需展开,不改变默认安全行为。'; en = 'Expand when needed without changing the safe defaults.' }
controls = @($advancedControls)
isAdvanced = $true
layout = 'stack'
})
}
$resultKind = if ($meta.pear) {
"pear-$(([string]$meta.result).ToLowerInvariant())"
} else { switch ($category) {
'calculator' { 'calculator-table' }
'image' { 'image-preview' }
'data' { 'ranked-list' }
'network' { 'link-cards' }
'dev' { if ($id -like '*json*') { 'json-tree' } else { 'code-preview' } }
'security' { 'status-list' }
'system' { 'system-cards' }
'plugin' { 'status-list' }
'external' { 'status-list' }
default { 'text' }
} }
$actions = [Collections.Generic.List[object]]::new()
$actionLabel = switch ($category) {
'calculator' { [ordered]@{ zh = '计算'; en = 'Calculate' } }
'network' { [ordered]@{ zh = '查询'; en = 'Query' } }
'image' { [ordered]@{ zh = '处理'; en = 'Process' } }
'security' { [ordered]@{ zh = '分析'; en = 'Analyze' } }
'dev' { [ordered]@{ zh = '运行'; en = 'Run' } }
default { [ordered]@{ zh = '执行'; en = 'Run' } }
}
if ($category -eq 'plugin') {
$actions.Add([ordered]@{ id = 'reload'; label = [ordered]@{ zh = '安全重新加载'; en = 'Safe reload' }; requiresConfirmation = $false; isEnabled = $true })
$actions.Add([ordered]@{ id = 'cancel'; label = [ordered]@{ zh = '停用'; en = 'Disable' }; requiresConfirmation = $true; isEnabled = $true })
} elseif ($category -eq 'external') {
$actions.Add([ordered]@{ id = 'run'; label = [ordered]@{ zh = '启动'; en = 'Launch' }; requiresConfirmation = $true; isEnabled = $true })
} else {
$actions.Add([ordered]@{ id = 'run'; label = $actionLabel; requiresConfirmation = ($risk -eq 'high'); isEnabled = $true })
if ($category -eq 'image') { $actions.Add([ordered]@{ id = 'choose-file'; label = [ordered]@{ zh = '选择文件'; en = 'Choose file' }; requiresConfirmation = $false; isEnabled = $true }) }
$actions.Add([ordered]@{ id = 'clear'; label = [ordered]@{ zh = '清空'; en = 'Clear' }; requiresConfirmation = $false; isEnabled = $true })
$actions.Add([ordered]@{ id = 'copy-result'; label = [ordered]@{ zh = '复制结果'; en = 'Copy result' }; requiresConfirmation = $false; isEnabled = $true })
}
$definition = [ordered]@{
toolId = $id
schemaVersion = 1
title = [ordered]@{ zh = [string]$meta.zh; en = [string]$meta.en }
description = [ordered]@{ zh = [string]$meta.descriptionZh; en = [string]$meta.descriptionEn }
riskLevel = $risk
capabilities = @($(if ($meta.offline) { 'offline' } else { 'network' }))
sections = @($sections)
actions = @($actions)
result = [ordered]@{ kind = $resultKind; title = [ordered]@{ zh = '结果'; en = 'Result' }; isScrollable = $true; isCopyable = $true }
isFallback = $false
diagnostic = $null
responsiveHint = [ordered]@{ zh = '窄屏自动单列,宽屏采用输入与结果双栏。'; en = 'Narrow layouts stack controls; wide layouts use input and result columns.' }
advancedExpandedByDefault = $false
}
$json = [Text.Encoding]::UTF8.GetBytes(($definition | ConvertTo-Json -Depth 10 -Compress))
$toolIdBytes = [Text.Encoding]::UTF8.GetBytes($id)
if ($toolIdBytes.Length -eq 0 -or $toolIdBytes.Length -gt 512) { throw "Tool ID length is invalid: $id" }
$digest = [Security.Cryptography.SHA256]::Create()
try { $digestBytes = $digest.ComputeHash($json) } finally { $digest.Dispose() }
$aes.GenerateIV()
$encryptor = $aes.CreateEncryptor()
try { $cipher = $encryptor.TransformFinalBlock($json, 0, $json.Length) } finally { $encryptor.Dispose() }
$magic = [Text.Encoding]::ASCII.GetBytes('YMHUTUI1')
$output = [byte[]]::new($magic.Length + 1 + 2 + $toolIdBytes.Length + $digestBytes.Length + $aes.IV.Length + $cipher.Length)
[Array]::Copy($magic, 0, $output, 0, $magic.Length)
$output[$magic.Length] = 2
$lengthBytes = [BitConverter]::GetBytes([uint16]$toolIdBytes.Length)
[Array]::Copy($lengthBytes, 0, $output, $magic.Length + 1, 2)
$offset = $magic.Length + 3
[Array]::Copy($toolIdBytes, 0, $output, $offset, $toolIdBytes.Length)
$offset += $toolIdBytes.Length
[Array]::Copy($digestBytes, 0, $output, $offset, $digestBytes.Length)
$offset += $digestBytes.Length
[Array]::Copy($aes.IV, 0, $output, $offset, $aes.IV.Length)
$offset += $aes.IV.Length
[Array]::Copy($cipher, 0, $output, $offset, $cipher.Length)
[IO.File]::WriteAllBytes((Join-Path $outputRoot "$id.dat"), $output)
}
} finally {
$aes.Dispose()
}
foreach ($stale in Get-ChildItem -LiteralPath $outputRoot -Filter '*.dat' -File -ErrorAction SilentlyContinue) {
if (-not $ids.Contains($stale.BaseName)) {
Remove-Item -LiteralPath $stale.FullName -Force
}
}
Write-Host "Generated $($ids.Count) tool UI definitions in $outputRoot"