param( [string]$Root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path ) $ErrorActionPreference = 'Stop' $catalogPath = Join-Path $Root 'src\YMhut.Box.Core\Tools\ToolCatalog.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 = """(?.*?)""";', [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') } } foreach ($match in [regex]::Matches((Get-Content -LiteralPath $nexPath -Raw -Encoding UTF8), 'Tool\("(?[^"]+)"')) { $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 = 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 = '关键词、URL、域名或 IP' } $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 = 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') { $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 ($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 = 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() } Write-Host "Generated $($ids.Count) tool UI definitions in $outputRoot"