From 691f882740b46c5354fb33404fef6efb370e05dc Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期五, 09 十月 2026 15:52:05 +0800
Subject: [PATCH] 实时行也带上判定出的方向,实时页的工步名称才有值

---
 scripts/j22-introspect-files.ps1 |  259 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 259 insertions(+), 0 deletions(-)

diff --git a/scripts/j22-introspect-files.ps1 b/scripts/j22-introspect-files.ps1
new file mode 100644
index 0000000..8d17537
--- /dev/null
+++ b/scripts/j22-introspect-files.ps1
@@ -0,0 +1,259 @@
+<#
+  22 所上位机文件侧只读探查:工步方案(含可能的工步名称)在不在软件目录的文件里。
+
+  为什么走文件侧:该机 SQL Server 的 5141 个用户库**全部**是 DB_<时间> 测试库、其他库 0 个,
+  所以工步方案不可能在库里。
+
+  方案文件是 `.pcj`(2026-10-09 现场确认):`SysSchedule` 格式的 XML、**没有压缩**,声明 utf-8,
+  每个工步一条 `<Step>`,带 `CtrlTypeName`(控制类型名)、`CtrlValue`(设定值)、`Limit`(截止条件)、
+  `GotoStep`(跳转),方案名在 `<ScheduleName>`。遗留的 `.sto`(gzip 压缩的 XML)也一并看。
+
+  只读纪律:只列目录、只读文件内容;不写、不改厂商目录与文件;报告只写到本脚本所在目录。
+
+  用法(在那台机器上):
+      powershell -ExecutionPolicy Bypass -File .\j22-introspect-files.ps1
+      powershell -ExecutionPolicy Bypass -File .\j22-introspect-files.ps1 -Roots 'D:\22所5v10A测试柜'
+      powershell -ExecutionPolicy Bypass -File .\j22-introspect-files.ps1 -SearchRoot 'D:\' -MaxDepth 4
+#>
+param(
+    # 要探查的目录;留空则自动找(见下)。
+    [string[]]$Roots = @(),
+    [string]$SearchRoot = 'D:\',
+    [int]$MaxDepth = 3,
+    # 最多深入解析几个方案文件(只打印结构,不倒内容)。
+    [int]$MaxFiles = 3,
+    [int]$MaxHeadChars = 400,
+    [int]$MaxNames = 25,
+    # 工步方案里打印前几个与后几个工步(中间省略),以及每个工步最多打印多少字。
+    [int]$StepPreview = 4,
+    [int]$MaxStepChars = 300,
+    [string]$ReportFile = ''
+)
+
+$ErrorActionPreference = 'Stop'
+
+if (-not $ReportFile) {
+    $base = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
+    $ReportFile = Join-Path $base 'j22-introspect-files-report.txt'
+}
+$report = New-Object System.Collections.ArrayList
+function Say([string]$text) {
+    Write-Host $text
+    [void]$report.Add($text)
+}
+
+# 方案文件的判据:.pcj 是这台机器上软件的工步方案(SysSchedule XML,2026-10-09 现场确认),
+# .sto 是遗留方案(gzip 压缩的 XML);名字里带 step/plan/task/recipe/scheme/program/工步/方案的也算。
+# 其余文件(设备配置、单位换算表等)不看——它们存不下工步名称。
+function Test-PlanFile([string]$name) {
+    $extension = [System.IO.Path]::GetExtension($name)
+    if ($extension -ieq '.pcj' -or $extension -ieq '.sto') { return $true }
+    return $name -imatch '(step|plan|task|recipe|scheme|program|工步|方案)'
+}
+
+# 取一个 XML 标签的文本值;没有这个标签就返回空串。
+function Get-TagValue([string]$text, [string]$tag) {
+    return [regex]::Match($text, '<' + $tag + '>([^<]*)</' + $tag + '>').Groups[1].Value
+}
+
+# 读文件文本:gzip(.sto 是 gzip 压缩的 XML)自动解压;只读前若干字符,避免把大文件全读进来。
+# 编码:有 BOM 按 BOM,没有 BOM 按 GBK(Windows 上的国产上位机多半是 GBK),否则中文名称会读成乱码。
+function Read-PlanText([string]$path, [int]$maxChars = 200000) {
+    $stream = [System.IO.File]::OpenRead($path)
+    try {
+        $head = New-Object byte[] 2
+        $read = $stream.Read($head, 0, 2)
+        $stream.Position = 0
+        $maxBytes = [Math]::Max(4096, $maxChars * 3)
+        if ($read -eq 2 -and $head[0] -eq 0x1F -and $head[1] -eq 0x8B) {
+            $gzip = New-Object System.IO.Compression.GZipStream($stream, [System.IO.Compression.CompressionMode]::Decompress)
+            try { $bytes = Read-Bytes $gzip $maxBytes }
+            finally { $gzip.Dispose() }
+        }
+        else { $bytes = Read-Bytes $stream $maxBytes }
+        return Convert-PlanBytes $bytes
+    }
+    finally { $stream.Dispose() }
+}
+
+# 最多读 $maxBytes 个字节;读到多少算多少(文件可能比上限大,也可能很小)。非文本文件也照读,
+# 由调用方按"开头是不是 <"判断它是不是 XML,不在这里猜。
+function Read-Bytes($stream, [int]$maxBytes) {
+    $buffer = New-Object byte[] 8192
+    $memory = New-Object System.IO.MemoryStream
+    try {
+        while ($memory.Length -lt $maxBytes) {
+            $wanted = [Math]::Min($buffer.Length, $maxBytes - $memory.Length)
+            $count = $stream.Read($buffer, 0, $wanted)
+            if ($count -le 0) { break }
+            $memory.Write($buffer, 0, $count)
+        }
+        return $memory.ToArray()
+    }
+    finally { $memory.Dispose() }
+}
+
+# 编码判定:先看 BOM;再看 XML 声明里的 encoding;都没有按 GBK 兜底——国产上位机在中文 Windows 上
+# 多半写 GBK,按 UTF-8 硬解会把中文工步名称读成乱码(.sto 是 gzip,流不可回退,所以必须先读再判)。
+function Convert-PlanBytes([byte[]]$bytes) {
+    if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
+        return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
+    }
+    if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
+        return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
+    }
+    $asciiHead = [System.Text.Encoding]::ASCII.GetString($bytes, 0, [Math]::Min(200, $bytes.Length))
+    if ($asciiHead -match '(?i)encoding\s*=\s*"([\w\-]+)"') {
+        try { return [System.Text.Encoding]::GetEncoding($Matches[1]).GetString($bytes) }
+        catch { }
+    }
+    return [System.Text.Encoding]::GetEncoding(936).GetString($bytes)
+}
+
+# XML 里出现过哪些元素名:只看结构,不打印内容。判断"有没有名称字段"靠这个。
+function Get-ElementNames([string]$text) {
+    $names = New-Object System.Collections.ArrayList
+    foreach ($match in [regex]::Matches($text, '<\s*([A-Za-z_][\w.\-]*)')) {
+        $name = [string]$match.Groups[1].Value
+        if (-not $names.Contains($name)) { [void]$names.Add($name) }
+    }
+    return , $names
+}
+
+# 名称类元素或属性及其取值:这是"方案里到底有没有工步名称"的直接证据。
+# 先取名字再筛关键字——不能把关键字塞进正则的主体里:属性名就叫 Name 时关键字前面没有字符,
+# 那种写法一条也匹配不到(演练里踩过)。
+function Get-NameEvidence([string]$text) {
+    $evidence = New-Object System.Collections.ArrayList
+    $keyword = '(?i)(name|desc|title|remark|说明|名称|备注)'
+    foreach ($match in [regex]::Matches($text, '<\s*([A-Za-z_][\w.\-]*)\s*>([^<]{1,200})</')) {
+        $name = [string]$match.Groups[1].Value
+        if ($name -match $keyword) {
+            [void]$evidence.Add('<' + $name + '> = ' + [string]$match.Groups[2].Value)
+        }
+    }
+    foreach ($match in [regex]::Matches($text, '([A-Za-z_][\w.\-]*)\s*=\s*"([^"]{1,200})"')) {
+        $name = [string]$match.Groups[1].Value
+        if ($name -match $keyword) {
+            [void]$evidence.Add($name + ' = ' + [string]$match.Groups[2].Value)
+        }
+    }
+    return , $evidence
+}
+
+try {
+    # ---------------------------------------------------------------- 1. 要找哪些目录
+    # 要探查的目录放进自己的列表:-Roots 是 [string[]] 定长数组,不能往里 Add。
+    $targets = New-Object System.Collections.ArrayList
+    if (-not $Roots -or $Roots.Count -eq 0) {
+        Say '=== 1. 自动查找目录 ==='
+        $found = New-Object System.Collections.ArrayList
+        # 运行目录的特征文件:VariableMap.xml / TaskModel.xml / config.ini。
+        foreach ($marker in @('VariableMap.xml', 'TaskModel.xml', 'config.ini')) {
+            try {
+                $hits = Get-ChildItem -LiteralPath $SearchRoot -Recurse -Depth $MaxDepth -Filter $marker -File `
+                    -ErrorAction SilentlyContinue | Select-Object -First 5
+            }
+            catch { $hits = @() }
+            foreach ($hit in $hits) {
+                $directory = $hit.DirectoryName
+                if (-not $found.Contains($directory)) { [void]$found.Add($directory) }
+            }
+        }
+        # 现场的遗留方案目录(存在就一起看)。
+        foreach ($legacy in @('D:\22所5v10A测试柜', 'D:\22所5v10A测试柜\')) {
+            if (Test-Path -LiteralPath $legacy) { if (-not $found.Contains($legacy)) { [void]$found.Add($legacy) } }
+        }
+        foreach ($directory in $found) {
+            Say ("  候选目录:" + $directory)
+            [void]$targets.Add($directory)
+        }
+        if ($targets.Count -eq 0) {
+            Say '  没有找到任何候选目录。请手工指定,例如:-Roots ''D:\<软件目录>'''
+        }
+    }
+    else {
+        Say '=== 1. 指定目录 ==='
+        foreach ($root in $Roots) {
+            # 用 -File 从命令行传数组时,多个目录可能被当成一个字符串('D:\a,D:\b'),按逗号再拆一次。
+            foreach ($single in @($root -split ',')) {
+                $trimmed = $single.Trim().Trim('"')
+                if ($trimmed) { Say ("  指定目录:" + $trimmed); [void]$targets.Add($trimmed) }
+            }
+        }
+    }
+
+    # ---------------------------------------------------------------- 2. 方案候选文件
+    $candidates = New-Object System.Collections.ArrayList
+    foreach ($root in $targets) {
+        if (-not (Test-Path -LiteralPath $root)) { Say ("  目录不存在:" + $root); continue }
+        Say ''
+        Say ("=== 2. 方案候选文件:" + $root + " ===")
+        $files = Get-ChildItem -LiteralPath $root -Recurse -Depth 1 -File -ErrorAction SilentlyContinue |
+            Where-Object { Test-PlanFile $_.Name } | Sort-Object LastWriteTime -Descending
+        $shown = 0
+        foreach ($file in $files) {
+            if ($shown -lt 30) { Say ("  " + $file.Name + "  " + [math]::Round($file.Length / 1KB, 1) + " KB  " + $file.LastWriteTime) }
+            $shown++
+            [void]$candidates.Add($file)
+        }
+        if ($shown -eq 0) { Say '  (没有匹配到方案候选文件)' }
+        elseif ($shown -gt 30) { Say ("  …… 共 " + $shown + " 个,只列了前 30 个") }
+    }
+
+    # ---------------------------------------------------------------- 3. 解析最像方案的几个
+    Say ''
+    Say ("=== 3. 结构解析(最多 " + $MaxFiles + " 个,按修改时间倒序) ===")
+    $parsed = 0
+    foreach ($file in ($candidates | Sort-Object LastWriteTime -Descending)) {
+        if ($parsed -ge $MaxFiles) { break }
+        $parsed++
+        Say ''
+        Say ("  " + $file.FullName + "  (" + [math]::Round($file.Length / 1KB, 1) + " KB)")
+        try { $text = Read-PlanText $file.FullName }
+        catch { Say ('    读取失败:' + $_.Exception.Message); continue }
+        if ([string]::IsNullOrWhiteSpace($text)) { Say '    (空文件或解压后为空)'; continue }
+        if (-not $text.TrimStart().StartsWith('<')) { Say '    (不是 XML 文本,可能不是方案文件)' }
+        $names = Get-ElementNames $text
+        $limited = @($names | Select-Object -First $MaxNames)
+        Say ('    元素名(前 ' + $limited.Count + ' / 共 ' + $names.Count + '):' + ($limited -join ', '))
+        $evidence = Get-NameEvidence $text
+        if ($evidence.Count -gt 0) {
+            Say ('    名称类元素或属性(最多 5 条)——**有名称就直接抄下来**:')
+            foreach ($line in @($evidence | Select-Object -First 5)) { Say ('      ' + $line) }
+        }
+        else { Say '    没有发现名称类元素或属性。' }
+        $head = $text.Trim()
+        if ($head.Length -gt $MaxHeadChars) { $head = $head.Substring(0, $MaxHeadChars) + ' …' }
+        Say ('    文本开头:' + $head)
+
+        # 工步方案(.pcj / SysSchedule):逐工步的设定值、截止条件与跳转都在这里。
+        # 工步的"名称"通常就是每个工步的 CtrlTypeName(例如"恒流充电""搁置")——这一节就是要看它。
+        if ($text -match '<Steps>') {
+            $steps = [regex]::Matches($text, '(?s)<Step>.*?</Step>')
+            Say ('    方案名: ' + (Get-TagValue $text 'ScheduleName') + '    任务目录: ' + (Get-TagValue $text 'SchedulePath'))
+            Say ('    方案标题: ' + (Get-TagValue $text 'Caption') + '    说明: ' + (Get-TagValue $text 'Description') + '    最后修改: ' + (Get-TagValue $text 'LastUpdateTime'))
+            Say ('    工步数: ' + $steps.Count)
+            $index = 0
+            foreach ($step in $steps) {
+                $index++
+                # 只打印前几个与后几个:中间的省略,否则一个方案就能刷满屏。
+                if ($index -gt $StepPreview -and $index -le ($steps.Count - $StepPreview)) { continue }
+                $flat = $step.Value -replace '\s+', ' '
+                if ($flat.Length -gt $MaxStepChars) { $flat = $flat.Substring(0, $MaxStepChars) + ' …' }
+                Say ('      工步 ' + $index + ': ' + $flat)
+            }
+        }
+    }
+
+    Say ''
+    if ($parsed -eq 0) { Say '=== 结论:没有可解析的方案候选文件 ===' }
+    else { Say '=== 结论:见上面各文件的"名称类元素或属性"与"元素名"两行 ===' }
+    Say '  如果方案里根本没有名称,就只剩两条路:① 用推导方向当名称(标注"推导");'
+    Say '  ② 在来源配置里声明「工步号 → 名称」,把甲方界面上对得上的名字配上。'
+    Say ('报告同时写在:' + $ReportFile)
+}
+finally {
+    # 中途出错也把已有内容落盘:控制台刷不完,文件可以慢慢复制。
+    $report | Set-Content -LiteralPath $ReportFile -Encoding UTF8
+}

--
Gitblit v1.7.1