2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VBAとPowerShellで作るWebView2/CDP駆動のRPAエンジン (1/2)

2
Last updated at Posted at 2026-06-29
  1. はじめに
    職場のPCでも利用できる、Selenium等のOSSではなく、ポータブル環境で構築した汎用RPAエンジンです。(目指しています) 利用価値は低いですが
    (私は、powershellなどは殆ど判らない中でのスタートで、ほぼAIさんの力です(GEMINI) ただ、無料利用での開発はコードが大きくなり限界です。出来たら改修してほしい)

エクセルと、WebView2.Core.dll等のダウンロードは必要です。
(設定powershell、概要VBA、マニュアル)は(2/2)

主コードはVBAで パラメータをJSON形式(VBA-JSON-2.3.1ライブラリ使用)で 操作指示を行います。

  1. 全体アーキテクチャ
    スクリーンショット 2026-06-28 123651.png

・ハイブリッド通信のメリット(DOMAPIの安定性と、CDPによるネイティブイベント発火の使い分け)。
・本エンジンは、Microsoft EdgeのレンダリングコアであるWebView2(Chromiumベース)をスタンドアロンのデスクトップUI(WinForms)に埋め込み、「Native(JSインジェクション)」と「CDP(WebSocket経由のデバッガ制御)」の2つの通信経路を状況に応じて切り替えるハイブリッドアーキテクチャを採用しています。
・多段iframeを透過する再帰探索
・異言語間の「型」完全統一

バージョンは PowerShell5.1
Ps_Engine_Core_v101.ps1 (司令塔・ルーター・共通操作)

# ------------------------------------------------------------------------------
# 自立型 WebView2 RPAエンジン(Core)
# コマンド受信および各モジュールへのルーティング
# advice by AI (2026/05)
# ------------------------------------------------------------------------------

<#
汎用RPAエンジン 命名規則およびコーディングルール
• 実行環境: PowerShell 5.1
【1. 変数およびパラメータの基本命名規則】
• ローカル変数: camelCase(キャメルケース)
   ※ ローカル変数の解釈拡大:
      関数内の変数だけでなく、「モジュール読み込み(ドットソース展開)時の初期化処理で
      のみ使用し、以降の処理で状態として参照・操作しない作業用変数」もローカル変数と
      同義として扱い、camelCase を適用する。
   ※ パラメータ名(PascalCase)をローカルで加工・再利用する際は、必ず camelCase の別名とする。
      [NG]: $XPathEscaped = Normalize-XPath $XPath
      [OK]: $xpathEscaped = Normalize-XPath $XPath

• パラメータおよびグローバル変数: PascalCase(パスカルケース)
• 定数・システム設定値(例外ルール):
   $global:CONFIG のように、定数・設定情報として機能する変数は大文字を許容する。

【2. 時間・タイムアウト系の命名規則(厳格化)】
• 時間や間隔を指定する変数は、必ず「単位(Sec/Ms)」を接尾辞として明記する。
   ※ Timeout(単位不明)等の曖昧な命名は使用禁止。
   - 秒単位の例: TimeoutSec, WaitTimeSec
   - ミリ秒単位の例: TimeoutMs, PollIntervalMs, InitialDelayMs
   ※ これらも「基本命名規則」に従い、パラメータなら $TimeoutSec、ローカルなら $timeoutSec となる。

【3. 異言語間(VBA / PowerShell / JavaScript)連携の命名規則】
• [VBA → PowerShell] 真偽値(Boolean)のコマンドライン引数渡し:
   プロセス間通信において、真偽値を文字列型("true"/"false")で受け取る起動引数(param)には、
   内部で保持する Boolean 変数と区別するため、意図的に Flg という接尾辞を付与する。
   - 例: [string]$IsDebugModeFlg

• [PowerShell → JavaScript] 埋め込み用変数の統一:
   PowerShellからJSのヒアドキュメント(@""@)に展開・埋め込むための加工済み変数は、
   元のパラメータが PascalCase であっても、必ず camelCase に変換して埋め込む。
   - 例: $XPath (パラメータ) → $xpathEscaped (JS埋め込み用ローカル変数)
   - 例: $FrameSelector → $frameSelectorEscaped

• [JavaScript 内部コード] 変数名の独立と統一:
   JSコード内で宣言する変数(var/let/const)は、JavaScriptの標準規約に従い
   camelCase を徹底する。PowerShell側の PascalCase 名をJS内に持ち込まない。
   - [NG]: const XPath = '$xpathEscaped';
   - [OK]: const xpath = '$xpathEscaped';
#>

# ------------------------------------------------------------------------------

# 起動引数の受け取り
param (
    [string]$AppSessionId = "AUTO_$(Get-Date -Format 'yyyyMMddHHmm')",
    [int]$CdpPort = 0,
    [string]$IsDebugModeFlg = "true"
)

# 起動引数のグローバル保持
$global:AppSessionId = $AppSessionId
$global:CdpPort = $CdpPort
try {
    $global:IsDebugMode = [bool]::Parse($IsDebugModeFlg)
} catch {
    $global:IsDebugMode = $true
}

# .NETのWinFormsアセンブリをロード(画面サイズ取得用)
Add-Type -AssemblyName System.Windows.Forms

# プライマリモニターの作業領域(タスクバーを除外した有効領域)を取得
$screenWidth  = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Width
$screenHeight = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Height

# 共通設定値の初期化
$global:CONFIG = @{
    MaxLogGenerations = 5
    DefaultTimeoutSec = 10
    BrowserWidth      = $screenWidth
    BrowserHeight     = $screenHeight
    EnableHighlight   = $true
}

# 通信モードに応じたポーリング間隔の設定
if ($global:CdpPort -gt 0) {
    # 高速ポーリング(CDPモード)
    $global:CONFIG.PollIntervalMs = 50
} else {
    # UIスレッド負荷軽減(ネイティブモード)
    $global:CONFIG.PollIntervalMs = 200
}

# システム用グローバル変数の初期化
$global:BrowserForm = $null
$global:WebViewCtrl = $null

# 多段iframe探索用JS共通ユーティリティの定義
$global:ENGINE_JS_UTILS = @"
function utilFindInFrames(win, checkFn) {
    try {
        var res = checkFn(win);
        if (res !== null && res !== false && res !== undefined) return res;
    } catch(e) {}
    for (var i = 0; i < win.frames.length; i++) {
        try {
            var found = utilFindInFrames(win.frames[i], checkFn);
            if (found !== null && found !== false && found !== undefined) return found;
        } catch(e) {}
    }
    return null;
}
"@

# タブ管理構造体の初期化
$global:Tabs = @{}
$global:ActiveTabId = $null

# ------------------------------------------------------------------------------

# 実行フォルダパスの取得
$global:ScriptDirectory = $PSScriptRoot
if ([string]::IsNullOrEmpty($global:ScriptDirectory)) {
    $global:ScriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Definition
}

# ログフォルダの世代管理
$global:ParentLogDirectory = Join-Path $global:ScriptDirectory "Logs"
if (Test-Path $global:ParentLogDirectory) {
    $oldLogFolders = Get-ChildItem -Path $global:ParentLogDirectory -Directory | Sort-Object CreationTime
    if ($oldLogFolders.Count -ge $global:CONFIG.MaxLogGenerations) {
        $deleteCount = $oldLogFolders.Count - $global:CONFIG.MaxLogGenerations + 1
        $oldLogFolders | Select-Object -First $deleteCount | ForEach-Object {
            try { Remove-Item $_.FullName -Recurse -Force -ErrorAction Stop } catch {}
        }
    }
}

# 本セッション用ログフォルダの作成
$global:LogDir = Join-Path $global:ParentLogDirectory $AppSessionId
if (-not (Test-Path $global:LogDir)) { 
    New-Item -ItemType Directory -Path $global:LogDir -Force | Out-Null
}

# コンソール文字コードのShift-JIS強制
try {
    $sjis = [System.Text.Encoding]::GetEncoding("Shift-JIS")
    [Console]::OutputEncoding = $sjis
    [Console]::InputEncoding  = $sjis
} catch {}

# --- ログ出力の実行 --- [// 内部関数 //]
function Write-DebugLog {
    param (
        [Parameter(Mandatory=$true)][string]$Message,
        [ValidateSet('Info', 'Success', 'Warning', 'Error', 'Fatal')][string]$Level = 'Info'
    )

    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $formattedMessage = "<$timestamp> <$Level> $Message"

    # コンソールへの出力
    try { [Console]::WriteLine("[LOG] $formattedMessage") } catch {}

    # ログファイルへの出力
    if ($null -ne $global:LogDir -and (Test-Path $global:LogDir)) {
        $logFilePath = Join-Path $global:LogDir "Engine_SystemLog.txt"
        try {
            $formattedMessage | Out-File -FilePath $logFilePath -Append -Encoding UTF8 -ErrorAction Stop
        } catch {
            # 書き込み失敗時のイベントログへの退避
            try {
                $sourceName = "RPA_WebView2_Engine"
                if (-not [System.Diagnostics.EventLog]::SourceExists($sourceName)) {
                    [System.Diagnostics.EventLog]::CreateEventSource($sourceName, "Application")
                }
                Write-EventLog -LogName Application -Source $sourceName -EntryType Warning -EventId 1000 -Message "ログファイル書き込み失敗: $formattedMessage"
            } catch {}
        }
    }
}

# --- 共通例外エラー生成の実行 --- [// 内部関数 //]
function New-EngineException {
    param(
        [Parameter(Mandatory=$true)][string]$Func,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Timeout","未発見","JSエラー","CDPエラー","ネイティブエラー","初期化エラー","ファイルエラー","UIAエラー","引数エラー","内部エラー")]
        [string]$Type,
        [Parameter(Mandatory=$true)][string]$Message,
        [string]$Details
    )

    # エラーログの多行跨ぎ防止および1行フォーマットへの正規化
    $Message = $Message -replace "`r`n|`n|`r", " "
    
    if ([string]::IsNullOrWhiteSpace($Details)) {
        return "[$Func] [$Type]: $Message"
    } else {
        $Details = $Details -replace "`r`n|`n|`r", " | "
        return "[$Func] [$Type]: $Message ($Details)"
    }
}

# 起動情報ログの出力
Write-DebugLog -Message "[System] 情報: Browser/ Width-Height ($screenWidth) - ($screenHeight)" -Level Info
Write-DebugLog -Message "[System] 情報: 開発モードスイッチ ($global:IsDebugMode)" -Level Info
Write-DebugLog -Message "[System] 情報: 通信モードスイッチ ($global:CdpPort)" -Level Info

# ------------------------------------------------------------------------------

# 常時ロード対象モジュールの定義
$alwaysLoadLibs = @(
    "Lib-WebAction_v101.ps1",
    "Lib-WebXPath_v101.ps1",
    "Lib-DesktopUIA_v101.ps1",
    "Lib-WebDebug_v101.ps1",
    "Lib-WebView2_Init_v101.ps1",
    "Lib-WebView2_Native_v101.ps1"
)

foreach ($libName in $alwaysLoadLibs) {
    $libPath = Join-Path $global:ScriptDirectory $libName
    if (Test-Path $libPath) {
        try {
            . $libPath
            Write-DebugLog -Message "[System] 成功: モジュールをロード ($libName)" -Level Success
        } catch {
            Write-DebugLog -Message "[System] 致命的エラー: $libName のロードに失敗しました ($($_.Exception.Message))" -Level Fatal
            Exit
        }
    } else {
        Write-DebugLog -Message "[System] 致命的エラー: 必須モジュール $libName が見つかりません。起動を中止します。" -Level Fatal
        Exit
    }
}

# CDPモード専用モジュールのロード
if ($global:CdpPort -gt 0) {
    $cdpLibName = "Lib-WebCDP_v101.ps1"
    $cdpLibPath = Join-Path $global:ScriptDirectory $cdpLibName

    if (Test-Path $cdpLibPath) {
        . $cdpLibPath
        Write-DebugLog -Message "[System] 成功: モジュールをロード ($cdpLibName)" -Level Success
        Start-Sleep -Milliseconds 500

        # CDPポート状態の確認
        try {
            $tcpConn = Get-NetTCPConnection -LocalPort $global:CdpPort -ErrorAction Stop | Select-Object -First 1
            Write-DebugLog -Message "[System] 情報: CDPポート ($global:CdpPort) の状態 - $($tcpConn.State) ($($tcpConn.LocalAddress))" -Level Info
        } catch {
            Write-DebugLog -Message "[System] 致命的エラー: $cdpLibName のロードに失敗しました ($($_.Exception.Message))" -Level Fatal
            Exit
        }
    } else {
        Write-DebugLog -Message "[System] 致命的エラー: CDPモジュール $cdpLibName が見つかりません。起動を中止します。" -Level Fatal
        Exit
    }
}

# ------------------------------------------------------------------------------
# タブ管理(WebView2 インスタンス管理)
# ------------------------------------------------------------------------------

# --- アクティブなWebView2インスタンスの取得 --- [// 内部関数 //]
function Get-ActiveWebView {
    # Tabs登録済みWebViewの返却
    if ($null -ne $global:ActiveTabId -and $global:Tabs.ContainsKey($global:ActiveTabId)) {
        return $global:Tabs[$global:ActiveTabId].WebView
    }
    # Tabs未登録時の後方互換対応
    return $global:WebViewCtrl
}

# --- アクティブタブの設定 ---
function Set-ActiveTab {
    param ([string]$TabId)

    $func = $MyInvocation.MyCommand.Name

    if ($global:Tabs.ContainsKey($TabId)) {
        $global:ActiveTabId = $TabId
        $global:Tabs[$TabId].WebView.BringToFront()
        return "ActiveTab = $TabId"
    }
    throw (New-EngineException -Func $func -Type "引数エラー" -Message "指定されたタブIDが見つかりません" -Details $TabId)
}

# --- タブ一覧の取得 ---(開発デバッグ用)
function List-Tabs {
    $result = @()

    foreach ($tabId in $global:Tabs.Keys) {
        $tab = $global:Tabs[$tabId]
        $webview = $tab.WebView

        # Tabs連想配列URLの優先取得(CDP同期用)
        $url = ""
        if ($tab.ContainsKey("Url") -and $tab.Url) {
            $url = $tab.Url
        } else {
            try { $url = $webview.Source.ToString() } catch {}
        }

        # WebView2からのタイトル取得
        $title = ""
        try { $title = $webview.CoreWebView2.DocumentTitle } catch {}

        $result += [PSCustomObject]@{
            TabId    = $tabId
            Url      = $url
            Title    = $title
            IsActive = ($tabId -eq $global:ActiveTabId)
        }
    }

    Write-DebugLog -Message "[List-Tabs] タブ数: $($result.Count)" -Level Info
    # パイプ (|) を使わず、-InputObject に直接配列を渡す
    $resultJson = ConvertTo-Json -InputObject @($result) -Depth 2 -Compress
    Write-DebugLog -Message "$resultJson" -Level Info
    return $resultJson
}

# --- タブの切替 ---
function Switch-Tab {
    param ([Parameter(Mandatory=$true)][string]$TabId)

    $func = $MyInvocation.MyCommand.Name

    if (-not $global:Tabs.ContainsKey($TabId)) {
        throw (New-EngineException -Func $func -Type "引数エラー" -Message "指定されたタブIDが見つかりません" -Details $TabId)
    }

    # アクティブタブの更新
    $global:ActiveTabId = $TabId

    # WebView2の前面表示
    $webview = $global:Tabs[$TabId].WebView
    try {
        $webview.BringToFront()
        $global:WebViewCtrl = $webview
    } catch {}

    if ($global:CdpPort -gt 0) {
        try {
            # Connect-CdpSession -Port $global:CdpPort
        } catch {
            Write-DebugLog -Message "[Switch-Tab] CDP再接続失敗: $($_.Exception.Message)" -Level Warning
        }
    }

    return "Switched to Tab: $TabId"
}

# --- タイトル部分一致によるタブ切替 ---
function Switch-TabByTitle {
    param ([Parameter(Mandatory=$true)][string]$TitleSubstring)
    
    $func = $MyInvocation.MyCommand.Name
    
    foreach ($tabId in $global:Tabs.Keys) {
        $title = ""
        try { 
            # WebView2コントロールからタイトルを取得
            $title = $global:Tabs[$tabId].WebView.CoreWebView2.DocumentTitle
        } catch {}
        
        if ($title -like "*$TitleSubstring*") {
            Write-DebugLog -Message "[$func] 情報: タブ切り替え ($tabId - $title)" -Level Info
            return Switch-Tab -TabId $tabId
        }
    }
    
    # 既存の例外フォーマットに統一
    throw (New-EngineException -Func $func -Type "未発見" -Message "指定されたタイトルを含むタブが見つかりません" -Details $TitleSubstring)
}

# ------------------------------------------------------------------------------
# 汎用ロジックおよび JS 実行ルーター
# ------------------------------------------------------------------------------

# --- タイムアウト付き汎用待機の実行 ---
function Wait-Condition {
    param (
        [Parameter(Mandatory=$true)][scriptblock]$ConditionBlock,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec,
        [int]$PollIntervalMs = $global:CONFIG.PollIntervalMs,
        [string]$TimeoutMessage = "待機処理がタイムアウトしました"
    )

    $func = $MyInvocation.MyCommand.Name

    # UIフリーズを防止した条件成立待機
    $endTime = (Get-Date).AddSeconds($TimeoutSec)
    while ((Get-Date) -lt $endTime) {
        if (& $ConditionBlock) { return $true }
        [System.Windows.Forms.Application]::DoEvents()
        Start-Sleep -Milliseconds $PollIntervalMs
    }

    throw (New-EngineException -Func $func -Type "Timeout" -Message $TimeoutMessage)
}

# --- JavaScript実行のルーティング --- [// 内部関数 //]
function Invoke-WebScript {
    param (
        [Parameter(Mandatory=$true)][string]$Js,
        [int]$Retries = 3
    )

    # アクティブWebViewの取得
    $webview = Get-ActiveWebView

    # CDP通信による最優先実行
    if ($null -ne $global:CdpWebSocket -and $global:CdpWebSocket.State -eq 'Open') {
        try {
            return Invoke-CdpScript -Js $Js
        } catch {
            Write-DebugLog -Message "[Engine] 警告: CDP実行失敗、ネイティブ実行へフォールバック ($($_.Exception.Message))" -Level Warning
        }
    }

    # ネイティブ実行へのフォールバック
    return Invoke-WebView2NativeScript -Js $Js -Retries $Retries
}

# ------------------------------------------------------------------------------

# --- エンジン設定の動的変更 ---
function Set-EngineConfig {
    param ([string]$EnableHighlight)

    $func = $MyInvocation.MyCommand.Name

    # EnableHighlightのboolean変換および更新
    if (-not [string]::IsNullOrEmpty($EnableHighlight)) {
        $global:CONFIG.EnableHighlight = [System.Convert]::ToBoolean($EnableHighlight)
        Write-DebugLog -Message "[$func] 設定変更: EnableHighlight = $($global:CONFIG.EnableHighlight)" -Level Info
    }

    return "Config Updated"
}

# ------------------------------------------------------------------------------
# 通信ループおよびコマンド待機
# ------------------------------------------------------------------------------

# 非同期コマンド受付キューの生成
$cmdQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]::new()

# 非同期入力監視用Runspaceの作成
$runspace = [runspacefactory]::CreateRunspace()
$runspace.Open()

# 標準入力監視スレッドの開始
$psThread = [powershell]::Create().AddScript({
    param ($Queue)

    # 親プロセスからの標準入力の常時監視
    while ($true) {
        $line = [Console]::ReadLine()
        if ([string]::IsNullOrWhiteSpace($line)) { continue }

        # JSONコマンドのキュー投入
        $Queue.Enqueue($line)
        Start-Sleep -Milliseconds 50
    }
}).AddArgument($cmdQueue)

$psThread.Runspace = $runspace
$psThread.BeginInvoke() | Out-Null

# 親プロセスの特定
try {
    $global:ParentPid = (Get-CimInstance Win32_Process -Filter "ProcessId = $PID").ParentProcessId
    $parentName = (Get-Process -Id $global:ParentPid -ErrorAction SilentlyContinue).Name
    Write-DebugLog -Message "[System] 情報: 親プロセス監視開始 (PID: $global:ParentPid, Name: $parentName)" -Level Info
} catch {
    $global:ParentPid = 0
    Write-DebugLog -Message "[System] 警告: 親プロセスの特定失敗" -Level Warning
}

$lastWatchdogTime = Get-Date

# 外部プロセスへのREADY通知
try { [Console]::WriteLine("READY") } catch {}
Write-DebugLog -Message "[System] 成功: エンジン待機状態" -Level Success

# ------------------------------------------------------------------------------
# メインイベントループ
# ------------------------------------------------------------------------------
try {
    while ($global:BrowserForm.Visible) {
        # UIフリーズ防止のためのWinFormsイベント処理
        [System.Windows.Forms.Application]::DoEvents()

        $json = $null

        # ゾンビ化を防止する親プロセスの生存監視
        if ($global:ParentPid -gt 0 -and ((Get-Date) - $lastWatchdogTime).TotalSeconds -gt 2) {
            $lastWatchdogTime = Get-Date

            if (-not (Get-Process -Id $global:ParentPid -ErrorAction SilentlyContinue)) {
                Write-DebugLog -Message "[System] 警告: 親プロセスの消失検知による道連れ終了の実行" -Level Warning
                break
            }
        }

        # JSONコマンドの取り出しおよび実行
        if ($cmdQueue.TryDequeue([ref]$json)) {
            try {
                # JSONからオブジェクトへの変換
                $requestObj = $json | ConvertFrom-Json -ErrorAction Stop
                $method = $requestObj.Command

                # QuitまたはExitコマンドによる即時終了
                if ($method -in @("Quit", "QUIT", "Exit")) { break }

                # パラメータ付きデバッグログの出力
                $paramStr = if ($requestObj.Parameters) { $requestObj.Parameters | ConvertTo-Json -Compress } else { "なし" }
                if ($global:IsDebugMode) {
                    Write-DebugLog -Message "[Engine] 実行: $method | Params: $paramStr" -Level Info
                } else {
                    Write-DebugLog -Message "[Engine] 実行: $method" -Level Info
                }

                # パラメータ辞書の構築
                $parameters = @{}
                if ($null -ne $requestObj.Parameters) {
                    foreach ($prop in $requestObj.Parameters.psobject.Properties) {
                        $parameters[$prop.Name] = $prop.Value
                    }
                }

                # コマンドの動的実行
                $res = & $method @parameters | Out-String

                # 実行結果の出力
                if (-not [string]::IsNullOrWhiteSpace($res)) {
                    [Console]::WriteLine("[RESULT]$($res.Trim())")
                }
                [Console]::WriteLine("[SUCCESS]")

            } catch {
                # エラー結果の出力
                $errMsg = $_.Exception.Message -replace "`r`n", " " -replace "`n", " "
                [Console]::WriteLine("[ERROR]$errMsg")
                Write-DebugLog -Message "[Engine] コマンド実行エラー: $errMsg" -Level Error
            }
        }
        Start-Sleep -Milliseconds 20
    }

} finally {
    # ------------------------------------------------------------------------------
    # 安全な終了処理(クリーンアップ)
    # ------------------------------------------------------------------------------
    Write-DebugLog -Message "[System] 情報: エンジンシャットダウン" -Level Info

    try { $runspace.Close() } catch {}
    try { if ($null -ne $global:BrowserForm) { $global:BrowserForm.Dispose() } } catch {}

    Start-Sleep -Milliseconds 300

    # ゾンビ化防止のための自プロセス強制終了
    Stop-Process -Id $PID -Force
}

以下は、ドットソースで読み込む。
Lib-WebView2_Init_v101.ps1 (ブラウザ画面起動)

# ------------------------------------------------------------------------------
# WebView2 初期化モジュール
# RPA用ブラウザウィンドウの生成とWebView2エンジンの起動
# ------------------------------------------------------------------------------

# ブラウザシステムの初期化開始
Write-DebugLog -Message "[System] 開始: ブラウザシステムの初期化 ..." -Level Info
$libDir = Join-Path $global:ScriptDirectory "Libs"

# 必要なWebView2拡張DLLの動的読み込み
$requiredDlls = @(
    "Microsoft.Web.WebView2.Core.dll",
    "Microsoft.Web.WebView2.WinForms.dll"
)

foreach ($dllName in $requiredDlls) {
    $dllPath = Join-Path $libDir $dllName
    if (Test-Path $dllPath) {
        try {
            $versionInfo = (Get-Item $dllPath).VersionInfo.FileVersion
            Add-Type -Path $dllPath
            Write-DebugLog -Message "[System] 成功: DLLロード ($dllName Version: $versionInfo)" -Level Success
        } catch {
            Write-DebugLog -Message "[System] 致命的エラー: DLLロード失敗 ($dllName - $($_.Exception.Message))" -Level Fatal
            exit 1
        }
    } else {
        Write-DebugLog -Message "[System] 致命的エラー: DLLが存在しません ($dllName)" -Level Fatal
        exit 1
    }
}

# デスクトップUI関連アセンブリのロード
Add-Type -AssemblyName System.Windows.Forms, System.Drawing, UIAutomationClient, UIAutomationTypes

# RPAブラウザ用ウィンドウの生成
$global:BrowserForm = New-Object System.Windows.Forms.Form -Property @{
    Text   = "RPA Browser - $global:AppSessionId"
    Width  = $global:CONFIG.BrowserWidth
    Height = $global:CONFIG.BrowserHeight
    StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
#● WindowState   = [System.Windows.Forms.FormWindowState]::Maximized
}

# WebView2コントロールの配置
$global:WebViewCtrl = New-Object Microsoft.Web.WebView2.WinForms.WebView2
$global:WebViewCtrl.Dock = [System.Windows.Forms.DockStyle]::Fill
$global:BrowserForm.Controls.Add($global:WebViewCtrl)
$global:BrowserForm.Show()

try {
    # ユーザーデータフォルダの準備
    $userDataFolder = Join-Path $global:ScriptDirectory "UserData"
    if (-not (Test-Path $userDataFolder)) {
        New-Item -ItemType Directory -Path $userDataFolder | Out-Null
    }

    # WebView2起動オプションの設定
    $options = [Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions]::new()

    # ディスクキャッシュを禁止・制限する共通引数
    $cacheArgs = " --disable-disk-cache --disk-cache-size=1 --media-cache-size=1"

    if ($global:CdpPort -gt 0) {
        $options.AdditionalBrowserArguments = "--disable-gpu --disable-dev-shm-usage --remote-debugging-port=$global:CdpPort" + $cacheArgs
        Write-DebugLog -Message "[System] 起動モード: CDP有効 (Port: $global:CdpPort)" -Level Info
    } else {
        $options.AdditionalBrowserArguments = "--disable-gpu --disable-dev-shm-usage" + $cacheArgs
        Write-DebugLog -Message "[System] 起動モード: 標準 (CDP無効)" -Level Info
    }

    # WebView2エンジンの非同期起動
    $envTask = [Microsoft.Web.WebView2.Core.CoreWebView2Environment]::CreateAsync(
        $null, $userDataFolder, $options
    )
    while (-not $envTask.IsCompleted) {
        [System.Windows.Forms.Application]::DoEvents()
        Start-Sleep -Milliseconds 10
    }

    # WebView2コントロールの初期化
    $initTask = $global:WebViewCtrl.EnsureCoreWebView2Async($envTask.Result)
    while (-not $initTask.IsCompleted) {
        [System.Windows.Forms.Application]::DoEvents()
        Start-Sleep -Milliseconds 10
    }

    Write-DebugLog -Message "[System] 成功: WebView2エンジン初期化完了" -Level Success

    # 初回タブ(P01)の登録
    $global:Tabs["P01"] = @{
        WebView = $global:WebViewCtrl
        Parent  = $null
        Url     = $global:WebViewCtrl.Source.ToString()
    }
    $global:ActiveTabId = "P01"

    # 初回タブ(P01)のURL更新イベント登録
    $global:WebViewCtrl.CoreWebView2.add_NavigationCompleted({
        param($sender, $e)
        try {
            $global:Tabs["P01"].Url = $sender.Source
            Write-DebugLog -Message "[P01] URL更新: $($sender.Source)" -Level Info
        } catch {}
    })

    # ランタイムバージョンのログ出力
    $runtimeVersion = $global:WebViewCtrl.CoreWebView2.Environment.BrowserVersionString
    Write-DebugLog -Message "[System] 情報: 接続先ランタイム (Version: $runtimeVersion)" -Level Info

    # 新規ウィンドウ要求の仮想タブ捕獲ハンドラ定義
    $global:NewWindowHandler = {
        param($sender, $e)
        Write-DebugLog -Message "[WebView2] 情報: 新規ウィンドウ要求を検知。仮想タブとして捕獲します。" -Level Info

        try {
            $deferral = $e.GetDeferral()

            $newWebView = New-Object Microsoft.Web.WebView2.WinForms.WebView2
            $newWebView.Dock = [System.Windows.Forms.DockStyle]::Fill

            $newWebView.Tag = @{
                Parent   = $global:WebViewCtrl
                Deferral = $deferral
                EventArg = $e
            }

            $dummyHandle = $newWebView.Handle

            $newWebView.add_CoreWebView2InitializationCompleted({
                param($senderInit, $argsInit)

                $state      = $senderInit.Tag
                $parentCtrl = $state.Parent
                $deferral   = $state.Deferral
                $evtArgs    = $state.EventArg

                try {
                    if ($argsInit.IsSuccess) {
                        $tabId = [guid]::NewGuid().ToString()
                        $global:Tabs[$tabId] = @{
                            WebView = $senderInit
                            Parent  = $parentCtrl
                        }
                        $global:ActiveTabId = $tabId

                        try {
                            $global:Tabs[$tabId].TargetId = $evtArgs.WindowFeatures.TargetId
                        } catch {
                            Write-DebugLog -Message "[WebView2] 警告: targetId の取得に失敗" -Level Warning
                        }

                        $global:BrowserForm.Controls.Add($senderInit)
                        $senderInit.BringToFront()

                        $evtArgs.NewWindow = $senderInit.CoreWebView2
                        $evtArgs.Handled   = $true
                        $deferral.Complete()

                        $global:WebViewCtrl = $senderInit

                        $senderInit.CoreWebView2.add_NavigationCompleted({
                            param($navSender, $navArgs)
                            try {
                                $global:Tabs[$tabId].Url = $navSender.Source
                                Write-DebugLog -Message "[$tabId] URL更新: $($navSender.Source)" -Level Info
                            } catch {}
                        })

                        $senderInit.CoreWebView2.add_NewWindowRequested($global:NewWindowHandler)
                        $senderInit.CoreWebView2.add_WindowCloseRequested({
                            param($closeSender, $closeArgs)
                            
                            $mi = [System.Windows.Forms.MethodInvoker]{
                                try {
                                    Write-DebugLog -Message "[WebView2] 情報: ポップアップの終了要求(window.close)を検知。タブ($tabId)を破棄します。" -Level Info

                                    # 透明なゴーストとして残るのを防ぐため、コントロールを削除してメモリ解放
                                    $global:BrowserForm.Controls.Remove($senderInit)
                                    $senderInit.Dispose()
                                    
                                    # 管理リストからの完全削除
                                    if ($global:Tabs.ContainsKey($tabId)) {
                                        $global:Tabs.Remove($tabId)
                                    }

                                    # アクティブタブが閉じられたら、自動的に親画面へ戻る
                                    if ($global:ActiveTabId -eq $tabId) {
                                        # 1. まず、このポップアップを開いた親ウィンドウのTabIdを探す
                                        $nextTabId = $null
                                        foreach ($key in $global:Tabs.Keys) {
                                            if ($global:Tabs[$key].WebView -eq $parentCtrl) {
                                                $nextTabId = $key
                                                break
                                            }
                                        }

                                        # 2. 親が見つかった場合はそこへ確実に戻る
                                        if ($null -ne $nextTabId) {
                                            $global:ActiveTabId = $nextTabId
                                            $global:WebViewCtrl = $parentCtrl
                                            Write-DebugLog -Message "[WebView2] 情報: ゴーストを削除し、親タブ ($nextTabId) へ自動復帰しました。" -Level Info
                                        } else {
                                            # 3. 万が一親が見つからない場合のフォールバック(ご提案のロジックを安全化)
                                            $remainingTabs = @($global:Tabs.Keys)
                                            if ($remainingTabs.Count -gt 0) {
                                                # 順序不定のため警告を出した上で移行
                                                $global:ActiveTabId = $remainingTabs[0]
                                                $global:WebViewCtrl = $global:Tabs[$global:ActiveTabId].WebView
                                                Write-DebugLog -Message "[WebView2] 警告: 親タブ不明のため、残存タブ ($($global:ActiveTabId)) へ強制移行しました。" -Level Warning
                                            } else {
                                                $global:ActiveTabId = $null
                                                $global:WebViewCtrl = $null
                                                Write-DebugLog -Message "[WebView2] 情報: 全タブが閉じられたため、認識をリセットしました。" -Level Info
                                            }
                                        }
                                    }
                                } catch {
                                    Write-DebugLog -Message "[WebView2] 警告: ゴーストタブの破棄中にエラー ($($_.Exception.Message))" -Level Warning
                                }
                            }
                            try { $global:BrowserForm.BeginInvoke($mi) | Out-Null } catch {}
                        }.GetNewClosure())

                    } else {
                        $exMsg = if ($argsInit.InitializationException) {
                            $argsInit.InitializationException.Message
                        } else {
                            "理由不明"
                        }
                        Write-DebugLog -Message "[WebView2] エラー: サブ画面の初期化に失敗 ($exMsg)" -Level Error
                        $deferral.Complete()
                    }
                } catch {
                    Write-DebugLog -Message "[WebView2] エラー: 初期化完了イベント内で例外 ($($_.Exception.Message))" -Level Error
                    try { $deferral.Complete() } catch {}
                }
            })

            $newWebView.EnsureCoreWebView2Async($sender.Environment) | Out-Null
        } catch {
            Write-DebugLog -Message "[WebView2] エラー: 新規ウィンドウ捕獲失敗 ($($_.Exception.Message))" -Level Error
        }
    }

    $global:WebViewCtrl.CoreWebView2.add_NewWindowRequested($global:NewWindowHandler)

} catch {
    Write-DebugLog -Message "[System] 致命的エラー: WebView2 初期化失敗 ($($_.Exception.Message))" -Level Fatal
    exit 1
}

# ウィンドウの最前面表示およびフォーカス確保
try {
    $hWnd = $global:BrowserForm.Handle
    [Win32Api.Win32Utils]::ShowWindow($hWnd, 9) | Out-Null
    [Win32Api.Win32Utils]::SetForegroundWindow($hWnd) | Out-Null

    $global:BrowserForm.Activate()
    $global:WebViewCtrl.Focus()

    $global:BrowserForm.TopMost = $true
    Start-Sleep -Milliseconds 80
    $global:BrowserForm.TopMost = $false

    try {
        $global:WebViewCtrl.CoreWebView2.ExecuteScriptAsync(
            "window.focus(); if(document.activeElement) { document.activeElement.focus(); }"
        ) | Out-Null
    } catch {}

    $global:WebViewCtrl.CoreWebView2.add_NavigationCompleted({
        param ($sender, $e)

        $mi = [System.Windows.Forms.MethodInvoker]{
            try {
                [Win32Api.Win32Utils]::SetForegroundWindow($global:BrowserForm.Handle) | Out-Null
                $global:BrowserForm.Activate()
                $global:WebViewCtrl.Focus()

                try {
                    $global:WebViewCtrl.CoreWebView2.ExecuteScriptAsync(
                        "window.focus(); if(document.activeElement) { document.activeElement.focus(); }"
                    ) | Out-Null
                } catch {}
            } catch {}
        }

        try { $global:BrowserForm.BeginInvoke($mi) | Out-Null } catch {}
    })

    $global:WebViewCtrl.CoreWebView2.add_ProcessFailed({
        param ($sender, $e)

        try {
            $kind = $e.ProcessFailedKind
            $reason = ""
            try { $reason = $e.Reason } catch {}

            $errMsg = "ブラウザプロセスのクラッシュ検知 (Kind: $kind, Reason: $reason)"
            Write-DebugLog -Message "[System] 致命的エラー: $errMsg" -Level Fatal

            try { [Console]::WriteLine("[ERROR] $errMsg") } catch {}

            $mi = [System.Windows.Forms.MethodInvoker]{
                try { $global:BrowserForm.Close() } catch {}
            }
            try { $global:BrowserForm.BeginInvoke($mi) | Out-Null } catch {}
        } catch {}
    })

} catch {
    Write-DebugLog -Message "[System] 警告: フォーカス確保処理で例外発生 ($($_.Exception.Message))" -Level Warning
}

# --- WebView2プロファイルのキャッシュ・Cookie・DOMストレージの削除 ---
function Clear-WebCache {
    param (
        [string]$Mode = "CacheOnly"
    )

    $func = $MyInvocation.MyCommand.Name

    try {
        $webview = Get-ActiveWebView
        if ($null -eq $webview -or $null -eq $webview.CoreWebView2) {
            throw (New-EngineException -Func $func -Type "初期化エラー" -Message "WebView2が初期化されていないか、アクティブタブが存在しません")
        }

        $kinds = [Microsoft.Web.WebView2.Core.CoreWebView2BrowsingDataKinds]::Cache -bor
                 [Microsoft.Web.WebView2.Core.CoreWebView2BrowsingDataKinds]::DiskCache -bor
                 [Microsoft.Web.WebView2.Core.CoreWebView2BrowsingDataKinds]::MemoryCaches

        if ($Mode -eq "All") {
            $kinds = $kinds -bor
                     [Microsoft.Web.WebView2.Core.CoreWebView2BrowsingDataKinds]::Cookies -bor
                     [Microsoft.Web.WebView2.Core.CoreWebView2BrowsingDataKinds]::LocalStorage -bor
                     [Microsoft.Web.WebView2.Core.CoreWebView2BrowsingDataKinds]::AllDomStorage
        }

        $task = $webview.CoreWebView2.Profile.ClearBrowsingDataAsync($kinds)

        $sw = [System.Diagnostics.Stopwatch]::StartNew()
        $timeoutSec = 15

        while (-not $task.IsCompleted) {
            if ($sw.Elapsed.TotalSeconds -gt $timeoutSec) {
                throw (New-EngineException -Func $func -Type "Timeout" -Message "キャッシュクリア処理がタイムアウトしました" -Details "${timeoutSec}秒経過")
            }
            [System.Windows.Forms.Application]::DoEvents()
            Start-Sleep -Milliseconds 20
        }

        return "SUCCESS"

    } catch {
        $errMsg = $_.Exception.Message -replace "`r`n", " " -replace "`n", " "
        throw (New-EngineException -Func $func -Type "内部エラー" -Message "キャッシュクリア処理中に予期せぬエラーが発生しました" -Details $errMsg)
    }
}

Lib-WebView2_Native_v101.ps1 (ネイティブ通信)

# ------------------------------------------------------------------------------
# WebView2 ネイティブ通信モジュール
# WebView2標準APIによるJavaScript実行(CDP非依存)
# ------------------------------------------------------------------------------

# --- WebView2標準機能を利用したJavaScript非同期実行と結果取得 ---
function Invoke-WebView2NativeScript {
    param (
        [Parameter(Mandatory = $true)][string]$Js,
        [int]$Retries = 3,
        [int]$InitialDelayMs = 200
    )

    $func = $MyInvocation.MyCommand.Name
    $jsCode = $Js

    # JSの安全なラップおよびJSON形式での返却
    $wrappedJs = @"
(function() {
    try {
        const result = (function() { $jsCode })();
        // undefinedをnullに置換してJSON消失を防止
        return JSON.stringify({
            status: "success",
            data: result !== undefined ? result : null
        });
    } catch (e) {
        return JSON.stringify({
            status: "error",
            message: e && e.message ? e.message : String(e),
            stack: e && e.stack ? e.stack : "",
            name: e && e.name ? e.name : ""
        });
    }
})();
"@

    $attempt = 0
    $delayMs = $InitialDelayMs

    while ($true) {
        $attempt++

        try {
            # アクティブタブのWebView取得
            $webview = Get-ActiveWebView
            if (-not $webview) {
                throw (New-EngineException -Func $func -Type "初期化エラー" -Message "アクティブなWebViewの取得に失敗しました(タブ未初期化)")
            }

            # JSの非同期実行
            $task = $webview.CoreWebView2.ExecuteScriptAsync($wrappedJs)

            while (-not $task.IsCompleted) {
                [System.Windows.Forms.Application]::DoEvents()
                Start-Sleep -Milliseconds 10
            }

            if ($task.IsFaulted) {
                throw (New-EngineException -Func $func -Type "ネイティブエラー" -Message "WebView2ネイティブAPI(ExecuteScriptAsync)の実行に失敗しました" -Details $task.Exception.InnerException.Message)
            }

            $rawResult = $task.Result

            # undefinedの扱い
            if ($rawResult -eq '"undefined"' -or $rawResult -eq 'undefined') {
                return $null
            }

            # 空またはnullの場合のリトライ処理
            if ([string]::IsNullOrWhiteSpace($rawResult) -or $rawResult -eq "null") {
                if ($attempt -lt $Retries) {
                    Start-Sleep -Milliseconds $delayMs
                    $delayMs = [Math]::Min(2000, $delayMs * 2)
                    continue
                } else {
                    throw (New-EngineException -Func $func -Type "内部エラー" -Message "WebView2ネイティブAPIの戻り値が空またはnullです")
                }
            }

            # 文字列JSONのアンエスケープ
            if ($rawResult -match '^\s*"(.*)"\s*$') {
                $inner = $Matches[1]
                try {
                    $rawResult = [System.Text.RegularExpressions.Regex]::Unescape($inner)
                } catch {
                    $rawResult = $inner
                }
            }

            # JSONのデコード処理
            try {
                $response = $rawResult | ConvertFrom-Json -ErrorAction Stop
            } catch {
                if ($attempt -lt $Retries) {
                    Start-Sleep -Milliseconds $delayMs
                    $delayMs = [Math]::Min(2000, $delayMs * 2)
                    continue
                } else {
                    throw (New-EngineException -Func $func -Type "内部エラー" -Message "WebView2からの応答(JSON)のデコードに失敗しました")
                }
            }

            # JS側のエラー処理
            if ($response.status -ne "success") {
                $stack = $response.stack -replace "`r`n", " | "
                throw (New-EngineException -Func $func -Type "JSエラー" -Message "$($response.name): $($response.message)" -Details $stack)
            }

            return $response.data

        } catch {
            if ($attempt -lt $Retries) {
                Start-Sleep -Milliseconds $delayMs
                $delayMs = [Math]::Min(2000, $delayMs * 2)
                continue
            } else {
                $errMsg = (New-EngineException -Func $func -Type "ネイティブエラー" -Message "スクリプトの実行処理中に予期せぬエラーが発生しました" -Details $_.Exception.Message)
                throw $errMsg
            }
        }
    }
}

Lib-WebCDP_v101.ps1 (WebSocket・CDP高速通信)

# ------------------------------------------------------------------------------
# CDP (Chrome DevTools Protocol) 通信モジュール
# WebSocket経由による高速・低レイテンシのDOM操作の実現
# ------------------------------------------------------------------------------

# グローバル変数の定義
$global:CdpWebSocket = $null
$global:CdpMessageId = 0
$global:CdpEndpoint  = ""

# --- CDPセッションの確立 --- [// 内部関数 //]
function Connect-CdpSession {
    param ([int]$Port = 9222)

    $func = $MyInvocation.MyCommand.Name
    Write-DebugLog -Message "[$func] 開始: CDPセッションの確立 (Port: $Port)" -Level Info

    # 起動直後のCOM例外防止(ActiveTabId未設定時のスキップ)
    if (-not $global:ActiveTabId -or -not $global:Tabs.ContainsKey($global:ActiveTabId)) {
        Write-DebugLog -Message "[$func] ActiveTabId 未設定のため CDP 接続をスキップ" -Level Warning
        return
    }

    # アクティブタブ情報の取得
    $activeTab    = $global:Tabs[$global:ActiveTabId]
    $activeUrl    = $activeTab.Url
    $activeTarget = $activeTab.TargetId

    Write-DebugLog -Message "[$func] アクティブURL = $activeUrl" -Level Info
    if ($activeTarget) {
        Write-DebugLog -Message "[$func] アクティブTargetId = $activeTarget" -Level Info
    } else {
        Write-DebugLog -Message "[$func] TargetId 未設定 → URL マッチにフォールバック" -Level Info
    }

    # CDP/jsonからのpageターゲット探索
    $jsonUrl = "http://127.0.0.1:$Port/json"
    $pageTarget = $null

    for ($i = 1; $i -le 10; $i++) {
        try {
            $pages = Invoke-RestMethod -Uri $jsonUrl -UseBasicParsing -ErrorAction Stop

            if ($activeTarget) {
                # TargetIdの優先
                $pageTarget = $pages | Where-Object {
                    $_.id -eq $activeTarget -and
                    -not [string]::IsNullOrEmpty($_.webSocketDebuggerUrl)
                } | Select-Object -First 1
            } else {
                # URLの柔軟一致(末尾スラッシュやリダイレクト揺れの吸収)
                $normActive = $activeUrl.TrimEnd('/')
                $pageTarget = $pages | Where-Object {
                    $_.type -eq 'page' -and
                    ($_.url.TrimEnd('/') -eq $normActive -or $_.url -like "$normActive*" -or $normActive -like "$($_.url.TrimEnd('/'))*") -and
                    -not [string]::IsNullOrEmpty($_.webSocketDebuggerUrl)
                } | Select-Object -First 1

                # フェイルセーフ(単一Pageターゲットの自動選択)
                if (-not $pageTarget) {
                    $availablePages = $pages | Where-Object { $_.type -eq 'page' -and -not [string]::IsNullOrEmpty($_.webSocketDebuggerUrl) }
                    if ($availablePages.Count -eq 1) {
                        $pageTarget = $availablePages[0]
                        # ▼ フェイルセーフ(自動復旧)機能:「成功」メッセージ
                        Write-DebugLog -Message "[$func] 情報: URL完全一致に失敗しましたが、単一のPageターゲットを自動選択しました" -Level Info
                    }
                }
            }

            if ($pageTarget) { break }
        } catch {}

        Start-Sleep -Milliseconds 500
    }

    if (-not $pageTarget) {
        throw (New-EngineException -Func $func -Type "CDPエラー" -Message "アクティブタブに一致するCDPターゲットが見つかりませんでした")
    }

    # WebSocketDebuggerUrlの採用
    $global:CdpEndpoint = $pageTarget.webSocketDebuggerUrl
    Write-DebugLog -Message "[$func] 情報: WebSocket URL取得 ($($global:CdpEndpoint))" -Level Info

    try {
        # 既存WebSocketの安全破棄
        if ($global:CdpWebSocket -and $global:CdpWebSocket.State -ne 'Closed') {
            try { $global:CdpWebSocket.Abort() } catch {}
            try { $global:CdpWebSocket.Dispose() } catch {}
            Start-Sleep -Milliseconds 100
        }

        # 新規WebSocketの接続
        $global:CdpWebSocket = New-Object System.Net.WebSockets.ClientWebSocket
        $uri = [System.Uri]::new($global:CdpEndpoint)
        $cts = [System.Threading.CancellationTokenSource]::new()

        $task = $global:CdpWebSocket.ConnectAsync($uri, $cts.Token)
        while (-not $task.IsCompleted) {
            [System.Windows.Forms.Application]::DoEvents()
            Start-Sleep -Milliseconds 10
        }

        if ($global:CdpWebSocket.State -ne 'Open') {
            throw (New-EngineException -Func $func -Type "CDPエラー" -Message "WebSocketの接続がOpen状態になりません")
        }

        $global:CdpMessageId = 0
        Write-DebugLog -Message "[$func] 情報: CDP接続完了 (Target: $($pageTarget.title))" -Level Info

    } catch {
        $global:CdpWebSocket = $null
        throw (New-EngineException -Func $func -Type "CDPエラー" -Message "WebSocketの接続に失敗しました" -Details $_.Exception.Message)
    }
}

# --- CDPコマンドの送受信 ---
function Invoke-CdpCommand {
    param (
        [Parameter(Mandatory = $true)][string]$Method,
        [hashtable]$Params = @{},
        [int]$MaxRetries = 3,
        [int]$TimeoutSecPerTry = 4
    )

    $func = $MyInvocation.MyCommand.Name
    $attempt = 0

    while ($attempt -lt $MaxRetries) {
        $attempt++

        # WebSocketの接続状態確認および自動再接続
        if ($null -eq $global:CdpWebSocket -or
            $global:CdpWebSocket.State -ne [System.Net.WebSockets.WebSocketState]::Open) {

            if ($null -eq $global:CdpWebSocket) {
                Write-DebugLog -Message "[$func] 情報: 初回CDPセッション接続要求" -Level Info
            } else {
                Write-DebugLog -Message "[$func] 警告: WebSocket切断検知 → 自動再接続" -Level Warning
            }

            try {
                Connect-CdpSession -Port $global:CdpPort
            } catch {
                throw (New-EngineException -Func $func -Type "CDPエラー" -Message "WebSocketの接続または再接続に失敗しました" -Details $_.Exception.Message)
            }
        }

        # JSON-RPCメッセージの構築
        $msgId = [System.Threading.Interlocked]::Increment([ref]$global:CdpMessageId)
        $payload = @{ id = $msgId; method = $Method; params = $Params }
        $jsonString = $payload | ConvertTo-Json -Depth 5 -Compress

        $sendBytes  = [System.Text.Encoding]::UTF8.GetBytes($jsonString)
        $sendBuffer = [System.ArraySegment[byte]]::new($sendBytes)

        $cts = [System.Threading.CancellationTokenSource]::new()
        $timeoutTime = (Get-Date).AddSeconds($TimeoutSecPerTry)

        try {
            # 送信処理
            $sendTask = $global:CdpWebSocket.SendAsync(
                $sendBuffer,
                [System.Net.WebSockets.WebSocketMessageType]::Text,
                $true,
                $cts.Token
            )

            while (-not $sendTask.IsCompleted) {
                [System.Windows.Forms.Application]::DoEvents()
                Start-Sleep -Milliseconds 5
                if ((Get-Date) -gt $timeoutTime) {
                    throw [System.TimeoutException]::new("CDP_TIMEOUT")
                }
            }

            # 受信処理
            $receiveBuffer  = [byte[]]::new(16384)
            $receiveSegment = [System.ArraySegment[byte]]::new($receiveBuffer)

            while ($true) {
                $responseString = ""

                while ($true) {
                    $receiveTask = $global:CdpWebSocket.ReceiveAsync($receiveSegment, $cts.Token)

                    while (-not $receiveTask.IsCompleted) {
                        [System.Windows.Forms.Application]::DoEvents()
                        Start-Sleep -Milliseconds 5
                        if ((Get-Date) -gt $timeoutTime) {
                            throw [System.TimeoutException]::new("CDP_TIMEOUT")
                        }
                    }

                    if ($receiveTask.IsFaulted -or $receiveTask.IsCanceled) {
                        throw [System.Exception]::new("TASK_ERROR")
                    }

                    $result = $receiveTask.Result

                    if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) {
                        throw [System.Exception]::new("WEBSOCKET_CLOSED")
                    }

                    $responseString += [System.Text.Encoding]::UTF8.GetString(
                        $receiveBuffer, 0, $result.Count
                    )

                    if ($result.EndOfMessage) { break }
                }

                # JSONのパース処理
                try {
                    $responseObj = $responseString | ConvertFrom-Json -ErrorAction Stop
                } catch {
                    continue
                }

                # idが一致した応答の返却
                if ($null -ne $responseObj.id -and $responseObj.id -eq $msgId) {
                    if ($null -ne $responseObj.error) {
                        throw (New-EngineException -Func $func -Type "CDPエラー" -Message $($responseObj.error.message) -Details "Code: $($responseObj.error.code)")
                    }
                    return $responseObj
                }
            }
        }
        catch {
            $errMsg = $_.Exception.Message

            if ($errMsg -eq "CDP_TIMEOUT") {
                $cts.Cancel()

                if ($attempt -lt $MaxRetries) {
                    Write-DebugLog -Message "[$func] 警告: 応答タイムアウト → リトライ ($attempt/$MaxRetries)" -Level Warning
                    continue
                } else {
                    throw (New-EngineException -Func $func -Type "Timeout" -Message "CDPコマンドの応答がタイムアウトしました" -Details "全 $($MaxRetries) 回リトライ失敗")
                }
            } else {
                throw (New-EngineException -Func $func -Type "CDPエラー" -Message "CDPコマンドの送受信中に予期せぬ例外が発生しました" -Details $_.Exception.Message)
            }
        }
    }
}

# --- CDP経由でのJavaScript実行 ---
function Invoke-CdpScript {
    param ([Parameter(Mandatory = $true)][string]$Js)

    $func = $MyInvocation.MyCommand.Name
    $jsCode = $Js

    # JSの安全なラップ
    $wrappedJs = @"
(function() {
    try {
        var result = (function() { $jsCode })();
        return JSON.stringify({
            status: "success",
            data: result !== undefined ? result : null
        });
    } catch (e) {
        return JSON.stringify({
            status: "error",
            message: e && e.message ? e.message : String(e),
            stack: e && e.stack ? e.stack : "",
            name: e && e.name ? e.name : ""
        });
    }
})();
"@

    $params = @{
        expression    = $wrappedJs
        returnByValue = $true
        awaitPromise  = $true
    }

    $res = Invoke-CdpCommand -Method "Runtime.evaluate" -Params $params

    # JS例外の判定
    if ($null -ne $res.result.exceptionDetails) {
        $ex = $res.result.exceptionDetails
        throw (New-EngineException -Func $func -Type "JSエラー" -Message "JavaScript実行時例外が発生しました" -Details $ex.exception.description)
    }

    $rawResult = $res.result.result.value

    if ([string]::IsNullOrWhiteSpace($rawResult) -or $rawResult -eq "null") {
        throw (New-EngineException -Func $func -Type "内部エラー" -Message "CDPスクリプト実行の戻り値が空またはnullです")
    }

    # 文字列JSONのアンエスケープ
    if ($rawResult -match '^\s*"(.*)"\s*$') {
        $inner = $matches[1]
        try {
            $rawResult = [System.Text.RegularExpressions.Regex]::Unescape($inner)
        } catch {
            $rawResult = $inner
        }
    }

    # JSONのデコード処理
    try {
        $response = $rawResult | ConvertFrom-Json -ErrorAction Stop
    } catch {
        throw (New-EngineException -Func $func -Type "内部エラー" -Message "CDPからの応答(JSON)のデコードに失敗しました")
    }

    if ($response.status -ne "success") {
        throw (New-EngineException -Func $func -Type "JSエラー" -Message "$($response.name): $($response.message)" -Details $response.stack)
    }

    return $response.data
}

# --- ネイティブクリックの発火 ---
function Invoke-CdpNativeClick {
    param ([Parameter(Mandatory = $true)][string]$Selector)

    $func = $MyInvocation.MyCommand.Name
    $selectorEscaped = $Selector.Replace("'", "\'")

    # 要素の中央座標の取得
    $js = @"
const el = document.querySelector('$selectorEscaped');
if (!el) throw new Error("Element not found: $selectorEscaped");
el.scrollIntoView({ block: 'center', inline: 'center' });
const rect = el.getBoundingClientRect();
return { x: rect.left + (rect.width / 2), y: rect.top + (rect.height / 2) };
"@

    $res = Invoke-CdpScript -Js $js

    $x = [Math]::Round($res.x)
    $y = [Math]::Round($res.y)

    # マウスダウンおよびマウスアップの発火
    Invoke-CdpCommand -Method "Input.dispatchMouseEvent" -Params @{
        type       = "mousePressed"
        button     = "left"
        clickCount = 1
        x          = $x
        y          = $y
    } | Out-Null

    Invoke-CdpCommand -Method "Input.dispatchMouseEvent" -Params @{
        type       = "mouseReleased"
        button     = "left"
        clickCount = 1
        x          = $x
        y          = $y
    } | Out-Null
}

# --- ネイティブテキストの入力 ---
function Set-CdpNativeTextInput {
    param (
        [Parameter(Mandatory = $true)][string]$Selector,
        [Parameter(Mandatory = $true)][string]$Value
    )

    $func = $MyInvocation.MyCommand.Name
    $selectorEscaped = $Selector.Replace("'", "\'")

    # 対象要素へのフォーカスおよび値のクリア
    $js = @"
var el = document.querySelector('$selectorEscaped');
if (!el) throw new Error('Element not found');
el.focus();
el.value = '';
return true;
"@

    Invoke-CdpScript -Js $js | Out-Null

    # ネイティブ文字入力の実行
    Invoke-CdpCommand -Method "Input.insertText" -Params @{ text = $Value } | Out-Null
}

Lib-WebAction_v101.ps1 (Web標準操作)

# ------------------------------------------------------------------------------
# Web標準操作モジュール
# DOMベースの要素探索、待機、クリック、入力などの共通アクション
# ------------------------------------------------------------------------------

# --- 指定URLへのナビゲーション実行 ---
function Invoke-WebNavigation {
    param ([Parameter(Mandatory=$true)][string]$Url)

    $func = $MyInvocation.MyCommand.Name

    try {
        # アクティブWebViewに対するNavigateの実行
        $activeWv = Get-ActiveWebView
        $activeWv.CoreWebView2.Navigate($Url)
        return "Navigating to $Url"
    } catch {
        throw (New-EngineException -Func $func -Type "ネイティブエラー" -Message "指定されたURLへのナビゲーションに失敗しました" -Details $_.Exception.Message)
    }
}

# --- ページ読み込み完了(iframe含む完全ロード)の待機 ---
function Wait-WebPageLoad {
    param ([int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec)

    $func = $MyInvocation.MyCommand.Name

    # readyState=completeおよび全iframeのDOM完成の再帰チェック
    $condition = {
        try {
            $js = @"
                function isLoaded(win) {
                    try {
                        if (win.document.readyState !== 'complete') return false;

                        // iframe の DOM がまだ構築中のケースに対応
                        let frames = win.frames;
                        for (let i = 0; i < frames.length; i++) {
                            try {
                                if (!isLoaded(frames[i])) return false;
                            } catch(e) {
                                // アクセス不可(CORS)は無視
                            }
                        }
                        return true;
                    } catch(e) {
                        return false;
                    }
                }
                return isLoaded(window);
"@

            # CDPモード時におけるネイティブ実行の強制
            $res = Invoke-WebView2NativeScript -Js $js

            if ($res -eq $true) {
                # DOM安定化のための追加待機
                Start-Sleep -Milliseconds 300
                return $true
            }
        } catch {}
        return $false
    }

    $errMsg = "[$func] タイムアウト: ページまたは iframe のロード未完了"
    Wait-Condition -ConditionBlock $condition -TimeoutSec $TimeoutSec -TimeoutMessage $errMsg | Out-Null

    return $true
}

# --- 画面全体の読み込みステータス(complete)待機 ---
function Wait-WebDocumentReady {
    param ([int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec)

    $func = $MyInvocation.MyCommand.Name

    $js = @"
        $global:ENGINE_JS_UTILS
        var res = utilFindInFrames(window, function(win) {
            try {
                return (win.document.readyState === 'complete');
            } catch(e) {
                return null;
            }
        });
        return res === true;
"@

    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $isReady = $false

    while ($sw.Elapsed.TotalSeconds -lt $TimeoutSec) {
        # 修正後のラッパー構造を介して安全にBoolean($true/$false)を受け取る
        $state = Invoke-WebScript -Js $js -Retries 1
        
        if ($state -eq $true -or $state -eq "true") {
            $isReady = $true
            break
        }
        [System.Windows.Forms.Application]::DoEvents()
        Start-Sleep -Milliseconds 300
    }

    if (-not $isReady) {
        Write-DebugLog -Message "[$func] 警告: Document ReadyState がすべてのフレームで complete になりませんでした" -Level Warning
    }
}

# --- URLの部分一致待機 ---
function Wait-WebUrlContains {
    param (
        [Parameter(Mandatory=$true)][string]$Substring,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name

    # URLへの指定文字列包含待機
    $condition = {
        $webview = Get-ActiveWebView
        if ($webview -and $webview.Source) {
            $url = $webview.Source.ToString()
            if ($url -like "*$Substring*") { return $true }
        }
        return $false
    }

    $errMsg = "[$func] タイムアウト: URL不一致 ($Substring)"
    Wait-Condition -ConditionBlock $condition -TimeoutSec $TimeoutSec -PollIntervalMs 300 -TimeoutMessage $errMsg | Out-Null

    return "URL matched: $(Get-WebUrl)"
}

# --- ページタイトルの部分一致待機 ---
function Wait-WebTitleContains {
    param (
        [Parameter(Mandatory=$true)][string]$Substring,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name

    # タイトルへの指定文字列包含待機
    $condition = {
        $webview = Get-ActiveWebView
        if ($webview) {
            $title = $webview.CoreWebView2.DocumentTitle
            if ($title -and $title.Trim().IndexOf($Substring, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
                return $true
            }
        }
        return $false
    }

    $errMsg = "[$func] タイムアウト: タイトル不一致 ($Substring)"
    Wait-Condition -ConditionBlock $condition -TimeoutSec $TimeoutSec -PollIntervalMs 300 -TimeoutMessage $errMsg | Out-Null

    return "Title matched: $(Get-WebTitle)"
}

# --- 指定要素の出現および可視化待機 ---
function Wait-WebElement {
    param (
        [Parameter(Mandatory=$true)][string]$Selector,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name
    $selectorEscaped = $Selector.Replace("'", "\'")

    # DOM出現およびdisplay/visibility/opacity/rectによる可視判定
    $condition = {
        try {
            $js = @"
                $global:ENGINE_JS_UTILS
                var selector = '$selectorEscaped';
                
                var found = utilFindInFrames(window, function(win) {
                    try {
                        var el = win.document.querySelector(selector);
                        if (el) {
                            var style = win.getComputedStyle(el);
                            var rect = el.getBoundingClientRect();
                            // 要素が存在し、かつ可視状態なら true を返す
                            return (style.display !== 'none' 
                                    && style.visibility !== 'hidden'
                                    && style.opacity !== '0'
                                    && rect.width > 0 && rect.height > 0);
                        }
                    } catch(e) {}
                    return null;
                });
                return found === true;
"@

            $res = Invoke-WebScript -Js $js
            if ($res) { return $true }
            
        } catch {}
        return $false
    }

    $errMsg = "[$func] タイムアウト: 要素の未出現または非表示 ($Selector)"
    Wait-Condition -ConditionBlock $condition -TimeoutSec $TimeoutSec -TimeoutMessage $errMsg | Out-Null

    return $true
}

# --- iframe内要素の出現および可視化待機 ---
function Wait-WebElementInFrame {
    param (
        [Parameter(Mandatory=$true)][string]$FrameSelector,
        [Parameter(Mandatory=$true)][string]$ElementSelector,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name
    $frameEscaped = $FrameSelector.Replace("'", "\'")
    $elementEscaped = $ElementSelector.Replace("'", "\'")

    # iframe.contentDocumentを用いた直接探索
    $condition = {
        try {
            $js = @"
                var frm = document.querySelector('$frameEscaped');
                if (!frm || !frm.contentDocument) return 'frame_not_found';
                var el = frm.contentDocument.querySelector('$elementEscaped');
                if (!el) return 'not_found';
                var style = frm.contentWindow.getComputedStyle(el);
                var rect = el.getBoundingClientRect();
                var isVisible = (style.display !== 'none'
                                 && style.visibility !== 'hidden'
                                 && style.opacity !== '0'
                                 && rect.width > 0 && rect.height > 0);
                return isVisible ? 'visible' : 'hidden';
"@

            $res = Invoke-WebScript -Js $js
            if ($res -eq "visible") { return $true }
        } catch {}
        return $false
    }

    $errMsg = "[$func] タイムアウト: iframe内要素の未出現 ($ElementSelector)"
    Wait-Condition -ConditionBlock $condition -TimeoutSec $TimeoutSec -TimeoutMessage $errMsg | Out-Null

    return $true
}

# --- iframe内要素のクリック実行 ---
function Invoke-WebClickInFrame {
    param (
        [Parameter(Mandatory=$true)][string]$FrameSelector,
        [Parameter(Mandatory=$true)][string]$ElementSelector,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name
    $frameEscaped = $FrameSelector.Replace("'", "\'")
    $elementEscaped = $ElementSelector.Replace("'", "\'")

    # iframe内でのscrollIntoViewおよびclickの実行
    Wait-WebElementInFrame -FrameSelector $FrameSelector -ElementSelector $ElementSelector -TimeoutSec $TimeoutSec | Out-Null

    $js = @"
        var frm = document.querySelector('$frameEscaped');
        if (frm && frm.contentDocument) {
            var el = frm.contentDocument.querySelector('$elementEscaped');
            if (el) {
                el.scrollIntoView({block: 'center', inline: 'center'});
                el.click();
                return true;
            }
        }
        return false;
"@

    $res = Invoke-WebScript -Js $js
    if (-not $res) { throw (New-EngineException -Func $func -Type "未発見" -Message "iframe内でのクリック実行に失敗しました" -Details $ElementSelector) }
}

# --- 指定要素の非表示またはDOM削除待機 ---
function Wait-WebElementInvisible {
    param (
        [Parameter(Mandatory=$true)][string]$Selector,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name
    $selectorEscaped = $Selector.Replace("'", "\'")

    # 多段iframe対応と、display/visibility/opacity/rectによる判定に統一
    $condition = {
        $js = @"
            $global:ENGINE_JS_UTILS
            var selector = '$selectorEscaped';
            var found = utilFindInFrames(window, function(win) {
                try {
                    var el = win.document.querySelector(selector);
                    if (el) {
                        var style = win.getComputedStyle(el);
                        var rect = el.getBoundingClientRect();
                        var isVisible = (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0);
                        if (isVisible) return true; // まだ表示されている
                    }
                } catch(e) {}
                return null;
            });
            // どこにも無いか、あっても非表示なら 'hidden' を返す
            return found === true ? 'visible' : 'hidden';
"@

        $res = Invoke-WebScript -Js $js
        if ($res -eq "hidden") { return $true }
        return $false
    }

    $errMsg = "[$func] タイムアウト: 要素が非表示になりません ($Selector)"
    Wait-Condition -ConditionBlock $condition -TimeoutSec $TimeoutSec -TimeoutMessage $errMsg | Out-Null

    return $true
}

# --- 画面のローディングマスク解除(非表示)待機 ---(汎用)
function Wait-WebScreenUnlock {
    param ([int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec)

    $func = $MyInvocation.MyCommand.Name

    $js = @"
        function checkMask(doc, win) {
            const divs = doc.querySelectorAll('div');
            for (let d of divs) {
                const s = win.getComputedStyle(d);
                // s.zIndexが "auto" や空文字だった場合に "0" としてパースさせる
                const z = parseInt(s.zIndex || "0", 10);
                // 汎用判定:z-indexが100以上で、画面の80%以上を覆っており、透明でない
                if (!isNaN(z) && z >= 100 && (s.position === 'fixed' || s.position === 'absolute') &&
                    d.offsetWidth >= win.innerWidth * 0.8 && d.offsetHeight >= win.innerHeight * 0.8 &&
                    s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0') {
                    return true;
                }
            }
            
            // iframe / frame の中も再帰的に探査する
            const frames = doc.querySelectorAll('iframe, frame');
            for (let f of frames) {
                try {
                    if (f.contentDocument && f.contentWindow) {
                        if (checkMask(f.contentDocument, f.contentWindow)) return true;
                    }
                } catch(e) { 
                    // クロスドメイン(CORS)のセキュリティエラーは安全に無視
                }
            }
            return false;
        }
        return checkMask(document, window);
"@

    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $wasBlocked = $false 

    while ($sw.Elapsed.TotalSeconds -lt $TimeoutSec) {
        try {
            $isBlocked = Invoke-WebScript -Js $js

            if ($isBlocked -eq $true -or $isBlocked -eq "true") {
                $wasBlocked = $true 
            }

            if ($isBlocked -eq $false -or $isBlocked -eq "false") {
                if ($wasBlocked) {
                    Write-DebugLog -Message "[$func] 情報: マスク解除を確認しました" -Level Info
                }
                return "Screen Unlocked"
            }
        } catch {
            # DOMアクセスエラー等は無視してリトライを継続
        }

        [System.Windows.Forms.Application]::DoEvents()
        Start-Sleep -Milliseconds 300
    }

    Write-DebugLog -Message "[$func] 警告: 指定時間(${TimeoutSec}秒)内にマスクが消えませんでした。誤検知またはシステム遅延の可能性があります。" -Level Warning
    return "Timeout"
}

# --- 指定要素のクリック実行 ---
function Invoke-WebClick {
    param (
        [Parameter(Mandatory=$true)][string]$Selector,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name
    $selectorEscaped = $Selector.Replace("'", "\'")

    # 要素出現の待機
    Wait-WebElement -Selector $Selector -TimeoutSec $TimeoutSec | Out-Null

    # 多段iframeの再帰探索
    $js = @"
        $global:ENGINE_JS_UTILS
        var found = utilFindInFrames(window, function(win) {
            try {
                var el = win.document.querySelector('$selectorEscaped');
                if (el) {
                    el.scrollIntoView({block: 'center', inline: 'center'});
                    el.click();
                    return true;
                }
            } catch(e) {}
            return null;
        });
        return found === true;
"@

    $res = Invoke-WebScript -Js $js
    if (-not $res) {
        throw (New-EngineException -Func $func -Type "未発見" -Message "クリック実行に失敗しました" -Details $Selector)
    }
}

# --- テキストボックスへの値入力 ---
function Set-WebTextInput {
    param (
        [Parameter(Mandatory=$true)][string]$Selector,
        [Parameter(Mandatory=$true)][string]$Value,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name
    $selectorEscaped = $Selector.Replace("'", "\'")
    $valueEscaped = $Value.Replace("'", "\'").Replace("\", "\\")

    # 要素出現の待機
    Wait-WebElement -Selector $Selector -TimeoutSec $TimeoutSec | Out-Null

    # value設定およびinput/changeイベントの発火
    $js = @"
        $global:ENGINE_JS_UTILS
        var selector = '$selectorEscaped';
        var val = '$valueEscaped';
        var found = utilFindInFrames(window, function(win) {
            try {
                var el = win.document.querySelector(selector);
                if (el) {
                    el.value = val;
                    el.dispatchEvent(new Event('input', { bubbles: true }));
                    el.dispatchEvent(new Event('change', { bubbles: true }));
                    return true;
                }
            } catch(e) {}
            return null;
        });
        return found === true;
"@

    $res = Invoke-WebScript -Js $js
    if (-not $res) {
        throw (New-EngineException -Func $func -Type "未発見" -Message "入力に失敗しました" -Details $Selector)
    }
}

# --- ドロップダウンリストの指定値選択 ---
function Select-WebDropdown {
    param (
        [Parameter(Mandatory=$true)][string]$Selector,
        [Parameter(Mandatory=$true)][string]$Value,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name
    $selectorEscaped = $Selector.Replace("'", "\'")
    $valueEscaped = $Value.Replace("'", "\'")

    # 要素出現の待機
    Wait-WebElement -Selector $Selector -TimeoutSec $TimeoutSec | Out-Null

    # value設定およびchangeイベントの発火
    $js = @"
        $global:ENGINE_JS_UTILS
        var selector = '$selectorEscaped';
        var val = '$valueEscaped';
        var found = utilFindInFrames(window, function(win) {
            try {
                var el = win.document.querySelector(selector);
                if (el) {
                    el.value = val;
                    el.dispatchEvent(new Event('change', { bubbles: true }));
                    return true;
                }
            } catch(e) {}
            return null;
        });
        return found === true; 
"@

    $res = Invoke-WebScript -Js $js
    if (-not $res) {
        throw (New-EngineException -Func $func -Type "未発見" -Message "ドロップダウンリストの選択に失敗しました" -Details $Selector)
    }
}

# --- チェックボックスの状態設定 ---
function Set-WebCheckbox {
    param (
        [Parameter(Mandatory=$true)][string]$Selector,
        [Parameter(Mandatory=$true)][bool]$State = $true,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name
    $selectorEscaped = $Selector.Replace("'", "\'")
    $stateStr = $State.ToString().ToLower()

    # 要素出現の待機
    Wait-WebElement -Selector $Selector -TimeoutSec $TimeoutSec | Out-Null

    # 状態差分が存在する場合のみのclickおよびchangeイベント発火
    $js = @"
        $global:ENGINE_JS_UTILS
        var selector = '$selectorEscaped';
        var targetState = $stateStr; // true または false (JSのBooleanとして直接評価される)
        var found = utilFindInFrames(window, function(win) {
            try {
                var el = win.document.querySelector(selector);
                if (el) {
                    if (el.checked !== targetState) {
                        el.click();
                        el.checked = targetState;
                        el.dispatchEvent(new Event('change', { bubbles: true }));
                    }
                    return true;
                }
            } catch(e) {}
        });
        return found === true; 
"@

    $res = Invoke-WebScript -Js $js
    if (-not $res) {
        throw (New-EngineException -Func $func -Type "未発見" -Message "チェックボックスの状態変更に失敗しました" -Details $Selector)
    }
}

# --- 指定要素のテキスト取得 ---
function Get-WebText {
    param (
        [Parameter(Mandatory=$true)][string]$Selector,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name
    $selectorEscaped = $Selector.Replace("'", "\'")

    # 要素出現の待機
    Wait-WebElement -Selector $Selector -TimeoutSec $TimeoutSec | Out-Null

    # innerTextまたはvalueの返却
    $js = @"
        $global:ENGINE_JS_UTILS
        var selector = '$selectorEscaped';
        var text = utilFindInFrames(window, function(win) {
            try {
                var el = win.document.querySelector(selector);
                // 要素があれば String、無ければ null を返す
                if (el) { return el.innerText || el.value || ''; }
            } catch(e) {}
            return null;
        });
        return text !== null ? text : '';
"@

    return Invoke-WebScript -Js $js
}

# --- 現在のページURLの取得 ---
function Get-WebUrl {
    $webview = Get-ActiveWebView
    if ($webview -and $webview.Source) {
        return $webview.Source.ToString()
    }
    return $null
}

# --- 現在のページタイトルの取得 ---
function Get-WebTitle {
    try { return Invoke-WebScript -Js "return document.title;" }
    catch { return $null }
}

# --- 指定ディレクトリ・指定ファイル名への完全サイレントダウンロードを有効化 ---
function Enable-SilentDownload {
    param (
        [Parameter(Mandatory = $true)][string]$DownloadDirectory,
        [string]$FileName = "" 
    )

    $func = $MyInvocation.MyCommand.Name

    try {
        if (-not (Test-Path $DownloadDirectory)) {
            New-Item -ItemType Directory -Path $DownloadDirectory -Force | Out-Null
        }

        # 保存先とファイル名をグローバル変数にセット(毎回の書き換えに対応)
        $global:TargetDownloadDirectory = $DownloadDirectory
        $global:TargetDownloadFileName = $FileName
        
        $webview = Get-ActiveWebView
        if ($null -eq $webview -or $null -eq $webview.CoreWebView2) {
            throw "WebView2インスタンスが取得できません"
        }

        if ($global:SilentDownloadRegistered) {
            Write-DebugLog -Message "[$func] ダウンロード先を更新: Dir=$DownloadDirectory, File=$FileName" -Level Info
            return
        }

        # new() を使わず、PowerShellの「型キャスト」を使って安全にイベントを登録
        $handler = [System.EventHandler[Microsoft.Web.WebView2.Core.CoreWebView2DownloadStartingEventArgs]] {
            param($sender, $e)

            # バックグラウンドスレッドを守る
            try {
                $e.Handled = $true

                # VBAからファイル名が指定されていればそれを使用し、無ければ元ファイル名を使用
                if ([string]::IsNullOrEmpty($global:TargetDownloadFileName)) {
                    $nameToSave = [System.IO.Path]::GetFileName($e.ResultFilePath)
                } else {
                    $nameToSave = $global:TargetDownloadFileName
                }

                # 最終的な保存先フルパス
                $e.ResultFilePath = Join-Path $global:TargetDownloadDirectory $nameToSave
            } catch {
                Write-DebugLog -Message "[Download] 致命的エラー: サイレント保存中に例外発生 ($($_.Exception.Message))" -Level Error
            }
        }

        # 作成した安全なハンドラをセット
        $webview.CoreWebView2.add_DownloadStarting($handler)
        $global:SilentDownloadRegistered = $true

        Write-DebugLog -Message "[$func] サイレントダウンロードを有効化しました。" -Level Info
    } catch {
        throw (New-EngineException -Func $func -Type "初期化エラー" -Message "ダウンロード動作の設定に失敗しました" -Details $_.Exception.Message)
    }
}

# --- ダウンロード(ファイル書き込み)の完了を待機 ---
function Wait-FileDownload {
    param (
        [Parameter(Mandatory = $true)][string]$FilePath,
        [int]$TimeoutSec = 30
    )
    
    $func = $MyInvocation.MyCommand.Name

    $waitSw = [System.Diagnostics.Stopwatch]::StartNew()
    while ($waitSw.Elapsed.TotalSeconds -lt $TimeoutSec) {
        
        # WebView2のイベント(ダウンロード開始等)をブロックさせないための息継ぎ処理
        [System.Windows.Forms.Application]::DoEvents()

        # 1. ファイルが作成されているか確認
        if (Test-Path $FilePath) {
            try {
                # 2. Chrome/Edge特有の .crdownload(一時ファイル)が存在しないか確認
                $tempFile = $FilePath + ".crdownload"
                if (-not (Test-Path $tempFile)) {
                    # 3. ファイルを排他モードで開けるか(書き込みロックが解除されたか)テスト
                    $stream = [System.IO.File]::Open($FilePath, 'Open', 'Read', 'None')
                    $stream.Close()
                    
#●                 Write-DebugLog -Message "[$func] 情報: ファイル保存完了 ($FilePath)" -Level Info
                    return $FilePath
                }
            } catch {
                # ロック中のため待機継続
            }
        }
        Start-Sleep -Milliseconds 200
    }
    
    throw (New-EngineException -Func $func -Type "Timeout" -Message "ファイルの保存完了確認がタイムアウトしました" -Details $FilePath)
}

Lib-DesktopUIA_v101.ps1 (デスクトップ操作・UIA)

# ------------------------------------------------------------------------------
# デスクトップ操作モジュール (OSネイティブ/UIA)
# ------------------------------------------------------------------------------

# .NET UIAutomation アセンブリのロード
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes

# 外部ウィンドウ制御用Win32 APIの宣言
$win32Signature = @"
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
"@

if (-not ([System.Management.Automation.PSTypeName]"Win32Api.Win32Utils").Type) {
    Add-Type -MemberDefinition $win32Signature -Name "Win32Utils" -Namespace "Win32Api"
}

# --- 外部アプリケーションウィンドウのアクティブ化 ---
function Switch-AppWindow {
    param ([string]$Name)

    $func = $MyInvocation.MyCommand.Name

    # 完全一致および部分一致によるウィンドウ検索
    $hWnd = [Win32Api.Win32Utils]::FindWindow($null, $Name)
    if ($hWnd -eq [IntPtr]::Zero -or $hWnd -eq $null) {
        $proc = Get-Process | Where-Object { $_.MainWindowTitle -like "*$Name*" } | Select-Object -First 1
        if ($proc) { $hWnd = $proc.MainWindowHandle }
    }

    # ウィンドウの前面化
    if ($hWnd -and $hWnd -ne [IntPtr]::Zero) {
        [Win32Api.Win32Utils]::ShowWindow($hWnd, 9) | Out-Null
        [Win32Api.Win32Utils]::SetForegroundWindow($hWnd) | Out-Null
        return "Focused: $Name"
    }

    throw (New-EngineException -Func $func -Type "未発見" -Message "指定されたウィンドウが見つかりません" -Details $Name)
}

# --- UIAを利用したデスクトップアプリの要素操作 ---
function Invoke-UiaAction {
    param (
        [Parameter(Mandatory=$true)][string]$Action,
        [Parameter(Mandatory=$true)][string]$Name = "",
        [string]$AutomationId = "",
        [string]$Value = "",
        [string]$Mode = "Pattern",
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec,
        [int]$PollIntervalMs = $global:CONFIG.PollIntervalMs
    )

    $func = $MyInvocation.MyCommand.Name

    try {
        # 検索条件の構築(AutomationId優先)
        $condition = if (![string]::IsNullOrEmpty($AutomationId)) {
            New-Object System.Windows.Automation.PropertyCondition(
                [System.Windows.Automation.AutomationElement]::AutomationIdProperty,
                $AutomationId
            )
        } else {
            New-Object System.Windows.Automation.PropertyCondition(
                [System.Windows.Automation.AutomationElement]::NameProperty,
                $Name
            )
        }

        # タイムアウト付きUIA要素探索
        $root = [System.Windows.Automation.AutomationElement]::RootElement
        $element = $null
        $endTime = (Get-Date).AddSeconds($TimeoutSec)

        while ((Get-Date) -lt $endTime) {
            $element = $root.FindFirst([System.Windows.Automation.TreeScope]::Subtree, $condition)
            if ($element) { break }
            Start-Sleep -Milliseconds $PollIntervalMs
        }

        if (-not $element) { throw (New-EngineException -Func $func -Type "未発見" -Message "指定されたUIA要素が見つかりません" -Details "Timeout: ${TimeoutSec}s") }

        # アクションの実行
        switch ($Action) {
            "Click" {
                try {
                    if ($Mode -eq "Safe") {
                        # --- 安全モード(物理クリック) ---
                        $element.SetFocus()
                        Start-Sleep -Milliseconds 100
                        $wshell = New-Object -ComObject WScript.Shell
                        $wshell.SendKeys(" ")
                    } else {
                        # --- パターンモード(バックグラウンドクリック) ---
                        $invokePattern = $null
                        $legacyPattern = $null

                        if ($element.TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$invokePattern)) {
                            $invokePattern.Invoke()
                        } elseif ($element.TryGetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePattern]::Pattern, [ref]$legacyPattern)) {
                            $legacyPattern.DoDefaultAction()
                        } else {
                            throw "InvokePattern および LegacyIAccessiblePattern をサポートしていません"
                        }
                    }
                        Start-Sleep -Milliseconds 300
                } catch {
                    throw (New-EngineException -Func $func -Type "UIAエラー" -Message "対象要素のクリックに失敗しました" -Details $_.Exception.Message)
                }
            }
            "Input" {
                try {
                    if ($Mode -eq "Safe") {
                        # --- 安全モード(物理入力) ---
                        $element.SetFocus()
                        Start-Sleep -Milliseconds 100
                        $wshell = New-Object -ComObject WScript.Shell
                        $wshell.SendKeys($Value)
                    } else {
                        # --- パターンモード(バックグラウンド入力) ---
                        $valuePattern = $null

                        if ($element.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$valuePattern)) {
                            $valuePattern.SetValue($Value)
                        } else {
                            throw "ValuePattern をサポートしていません"
                        }
                    }
                    Start-Sleep -Milliseconds 300
                } catch {
                    throw (New-EngineException -Func $func -Type "UIAエラー" -Message "対象要素への入力に失敗しました" -Details $_.Exception.Message)
                }
            }
            "GetText" {
                return $element.Current.Name
            }
            "GetValue" {
                $valuePattern = $null
                if ($element.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$valuePattern)) {
                    return $valuePattern.Current.Value
                }
                return ""
            }
            default { throw (New-EngineException -Func $func -Type "引数エラー" -Message "未対応のUIAアクションが指定されました" -Details $Action) }
        }

        return "Action Completed: $Action"

    } catch {
        throw (New-EngineException -Func $func -Type "UIAエラー" -Message "UIA操作中に予期せぬエラーが発生しました" -Details $_.Exception.Message)
    }
}

# --- 「名前を付けて保存」ダイアログの捕捉および保存の実行 ---
function Invoke-UiaSafeSaveAs {
    param(
        [Parameter(Mandatory=$true)][string]$FilePath,
        [int]$TimeoutSec = 10
    )

    $func = $MyInvocation.MyCommand.Name

    $dialogNames = @("名前を付けて保存", "Save As", "保存", "Save")
    $hwnd = [IntPtr]::Zero
    $sw = [System.Diagnostics.Stopwatch]::StartNew()

    # --- 1. Win32APIで安全にハンドルを取得 ---
    while ($sw.Elapsed.TotalSeconds -lt $TimeoutSec) {
        foreach ($name in $dialogNames) {
            # [重要] PowerShellの $null はWin32API(C#)に渡る際 "" (空文字) に変換されてしまうため、
            # [NullString]::Value を使用するか、標準ダイアログクラス "#32770" を明示指定する
            
            # パターンA: 標準のダイアログクラス名(#32770)で検索
            $hwnd = [Win32Api.Win32Utils]::FindWindow("#32770", $name)
            
            if ($hwnd -eq [IntPtr]::Zero -or $hwnd -eq $null) {
                # パターンB: クラス名問わず、完全な null ポインタとして検索(特殊定数を使用)
                $hwnd = [Win32Api.Win32Utils]::FindWindow([System.Management.Automation.NullString]::Value, $name)
            }

            if ($hwnd -ne [IntPtr]::Zero -and $hwnd -ne $null) { break }
        }
        if ($hwnd -ne [IntPtr]::Zero -and $hwnd -ne $null) { break }
        Start-Sleep -Milliseconds 200
    }

    if ($hwnd -eq [IntPtr]::Zero -or $hwnd -eq $null) {
        throw (New-EngineException -Func $func -Type "未発見" -Message "保存ダイアログのウィンドウが見つかりませんでした")
    }

    # --- 2. ウィンドウを最前面に強制引き上げ ---
    try {
        [Win32Api.Win32Utils]::ShowWindow($hwnd, 9) | Out-Null
        [Win32Api.Win32Utils]::SetForegroundWindow($hwnd) | Out-Null
        Start-Sleep -Milliseconds 300
    } catch {}

    # --- 3. 取得した安全なハンドルからのみUIA要素を生成 ---
    $dialog = $null
    try {
        $dialog = [System.Windows.Automation.AutomationElement]::FromHandle($hwnd)
    } catch {
        throw (New-EngineException -Func $func -Type "UIAエラー" -Message "ハンドルからUIA要素への変換に失敗しました")
    }

    # --- ファイル名入力欄(AutomationId=1001)の取得 ---
    $cndEdit = New-Object System.Windows.Automation.PropertyCondition(
        [System.Windows.Automation.AutomationElement]::AutomationIdProperty, "1001"
    )
    $editBox = $dialog.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $cndEdit)

    if ($null -eq $editBox) {
        Write-DebugLog -Message "[$func] 警告: 入力欄(1001)が未発見。デフォルトフォーカスを利用します。" -Level Warning
    } else {
        try {
            $editBox.SetFocus()
            Start-Sleep -Milliseconds 200
        } catch {}
    }

    # --- 4. クリップボードと物理キーによる絶対安全な操作 ---
    try {
        Write-DebugLog -Message "[$func] 情報: 物理キー(SendKeys)で保存を実行します" -Level Info

        $wshell = New-Object -ComObject WScript.Shell

        # 既存のテキストを全選択して削除 (Ctrl+A -> Delete)
        $wshell.SendKeys("^a")
        Start-Sleep -Milliseconds 100
        $wshell.SendKeys("{DELETE}")
        Start-Sleep -Milliseconds 100

        # クリップボード経由でパスを貼り付け
        Set-Clipboard -Value $FilePath
        Start-Sleep -Milliseconds 100
        $wshell.SendKeys("^v")
        Start-Sleep -Milliseconds 300

        # Enterキーで保存実行
        $wshell.SendKeys("{ENTER}")

    } catch {
        throw (New-EngineException -Func $func -Type "UIAエラー" -Message "パスの入力または保存の実行に失敗しました" -Details $_.Exception.Message)
    }

    # --- 保存完了待機(ファイルロック解除まで) ---
    $waitSw = [System.Diagnostics.Stopwatch]::StartNew()
    while ($waitSw.Elapsed.TotalSeconds -lt 30) {
        if (Test-Path $FilePath) {
            try {
                $stream = [System.IO.File]::Open($FilePath, 'Open', 'Read', 'None')
                $stream.Close()

                return $FilePath
            } catch {}
        }
        Start-Sleep -Milliseconds 300
    }

    throw (New-EngineException -Func $func -Type "Timeout" -Message "ファイル保存の完了確認ができませんでした" -Details $FilePath)
}

Lib-WebXPath_v101.ps1 (XPathによる特殊要素操作)

# ------------------------------------------------------------------------------
# XPath 専用 Web 自動化操作モジュール
# document.evaluateを利用した動的要素の特定および操作
# ------------------------------------------------------------------------------

# --- XPathの正規化 --- [// 内部関数 //]
function Normalize-XPath {
    param([string]$XPath)

    # normalize-space包含時のスキップ
    if ($XPath -match "normalize-space") {
        return $XPath
    }

    # //*[text()='xxx'] から contains(normalize-space(.), 'xxx') への変換
    $pattern = "\[[^\]]*text\(\)\s*=\s*'([^']+)'\]"
    if ($XPath -match $pattern) {
        $value = $Matches[1]
        return ($XPath -replace $pattern, "[contains(normalize-space(.), '$value')]")
    }

    # //*[.='xxx'] から contains(normalize-space(.), 'xxx') への変換
    $pattern2 = "\[[^\]]*\.\s*=\s*'([^']+)'\]"
    if ($XPath -match $pattern2) {
        $value = $Matches[1]
        return ($XPath -replace $pattern2, "[contains(normalize-space(.), '$value')]")
    }

    return $XPath
}

# --- 指定XPath要素の出現および可視化待機 ---
function Wait-WebXPathElement {
    param (
        [Parameter(Mandatory = $true)][string]$XPath,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name

    # XPathの正規化およびJS用エスケープ
    $xpathEscaped = Normalize-XPath $XPath
    $xpathEscaped = $xpathEscaped.Replace("'", "\'")

    # 条件ブロックの定義
    $condition = {
        try {
            $js = @"
                $global:ENGINE_JS_UTILS
                var xpath = '$xpathEscaped';

                var found = utilFindInFrames(window, function(win) {
                    try {
                        var el = win.document.evaluate(xpath, win.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
                        if (el) {
                            var style = win.getComputedStyle(el);
                            var rect  = el.getBoundingClientRect();
                            var visible = (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0);
                            return visible ? 'visible' : null;
                        }
                    } catch(e) {}
                    return null;
                });
                return found || 'not_found';
"@

            $res = Invoke-WebScript -Js $js

            if ($res -eq "visible") {
                # 任意ハイライト処理の実行
                if ($global:CONFIG.EnableHighlight) {
                    $jsHighlight = @"
                        $global:ENGINE_JS_UTILS
                        var xpath = '$xpathEscaped';

                        var el = utilFindInFrames(window, function(win) {
                            try { return win.document.evaluate(xpath, win.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue || null; }
                            catch(e) { return null; }
                        });

                        if (el) {
                            var oldOutline = el.style.outline;
                            el.style.outline = '2px solid red';
                            setTimeout(() => { el.style.outline = oldOutline; }, 800);
                        }
"@

                    Invoke-WebScript -Js $jsHighlight | Out-Null
                }
                return $true
            }
        } catch {}
        return $false
    }

    $errMsg = "[$func] タイムアウト: XPath要素が見つからない ($XPath)"
    Wait-Condition -ConditionBlock $condition -TimeoutSec $TimeoutSec -TimeoutMessage $errMsg | Out-Null

    return $true
}

# --- 指定XPath要素の消滅または非表示待機 ---
function Wait-WebXPathElementDisappear {
    param (
        [Parameter(Mandatory = $true)][string]$XPath,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name

    $xpathEscaped = Normalize-XPath $XPath
    $xpathEscaped = $xpathEscaped.Replace("'", "\'")

    $js = @"
        $global:ENGINE_JS_UTILS
        var xpath = '$xpathEscaped';
        
        var found = utilFindInFrames(window, function(win) {
            try {
                var el = win.document.evaluate(xpath, win.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
                if (el) {
                    var style = win.getComputedStyle(el);
                    var rect = el.getBoundingClientRect();
                    // display, visibility, opacity のいずれかで非表示になれば false 扱いとする
                    var isVisible = (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0);
                    if (isVisible) return true; // まだ表示されている
                }
            } catch(e) {}
            return null;
        });
        return found === true;
"@

    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $isGone = $false

    while ($sw.Elapsed.TotalSeconds -lt $TimeoutSec) {
        # JS実行による要素存在の真偽値取得
        $exists = Invoke-WebView2NativeScript -Js $js -Retries 1

        # False(未発見・非表示)による消滅判定
        if ($exists -eq $false -or $exists -match "false") {
            $isGone = $true
            break
        }
        Start-Sleep -Milliseconds 500
    }

    if (-not $isGone) {
        throw (New-EngineException -Func $func -Type "Timeout" -Message "指定された時間が経過しても要素が消滅・非表示になりませんでした" -Details "${TimeoutSec}秒経過 ($XPath)")
    }
}

# --- 指定XPath要素のクリック実行 ---
function Invoke-WebXPathClick {
    param (
        [Parameter(Mandatory = $true)][string]$XPath,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name

    $xpathEscaped = Normalize-XPath $XPath
    $xpathEscaped = $xpathEscaped.Replace("'", "\'")

    # 要素出現の待機
    Wait-WebXPathElement -XPath $XPath -TimeoutSec $TimeoutSec | Out-Null

    # クリック処理の実行
    $js = @"
        $global:ENGINE_JS_UTILS
        var xpath = '$xpathEscaped';
        var el = utilFindInFrames(window, function(win) {
            try {
                return win.document.evaluate(xpath, win.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue || null;
            } catch(e) { return null; }
        });

        if (el) {
            el.scrollIntoView({block:'center', inline:'center'});
            el.dispatchEvent(new MouseEvent('mouseover', {bubbles:true}));
            el.dispatchEvent(new MouseEvent('mousemove', {bubbles:true}));
            el.dispatchEvent(new MouseEvent('mousedown', {bubbles:true}));
            el.dispatchEvent(new MouseEvent('mouseup', {bubbles:true}));
            el.focus();
            el.click();
            return true;
        }
        return false;
"@

    $res = Invoke-WebScript -Js $js
    if (-not $res) {
        throw (New-EngineException -Func $func -Type "未発見" -Message "XPath要素のクリック実行に失敗しました" -Details $XPath)
    }
}

# --- 指定XPath要素へのテキスト入力 ---
function Set-WebXPathTextInput {
    param (
        [Parameter(Mandatory = $true)][string]$XPath,
        [Parameter(Mandatory = $true)][string]$Value,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name

    $xpathEscaped = Normalize-XPath $XPath
    $xpathEscaped = $xpathEscaped.Replace("'", "\'")

    # 入力値のエスケープ処理
    $safeValue = $Value.Replace("\", "\\").Replace("'", "\'")

    Wait-WebXPathElement -XPath $XPath -TimeoutSec $TimeoutSec | Out-Null

    $js = @"
        $global:ENGINE_JS_UTILS
        var xpath = '$xpathEscaped';
        var el = utilFindInFrames(window, function(win) {
            try {
                return win.document.evaluate(xpath, win.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue || null;
            } catch(e) { return null; }
        });

        if (el) {
            el.focus();
            el.value = '$safeValue';
            el.dispatchEvent(new Event('input',  { bubbles: true }));
            el.dispatchEvent(new Event('change', { bubbles: true }));
            return true;
        }
        return false;
"@

    $res = Invoke-WebScript -Js $js
    if (-not $res) {
        throw (New-EngineException -Func $func -Type "未発見" -Message "XPath要素へのテキスト入力に失敗しました" -Details $XPath)
    }
}

# --- 指定XPath要素のテキスト取得 ---
function Get-WebXPathText {
    param (
        [Parameter(Mandatory = $true)][string]$XPath,
        [int]$TimeoutSec = $global:CONFIG.DefaultTimeoutSec
    )

    $func = $MyInvocation.MyCommand.Name

    $xpathEscaped = Normalize-XPath $XPath
    $xpathEscaped = $xpathEscaped.Replace("'", "\'")

    Wait-WebXPathElement -XPath $XPath -TimeoutSec $TimeoutSec | Out-Null

    $js = @"
        $global:ENGINE_JS_UTILS
        var xpath = '$xpathEscaped';
        var el = utilFindInFrames(window, function(win) {
            try {
                return win.document.evaluate(xpath, win.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue || null;
            } catch(e) { return null; }
        });

        if (!el) throw new Error("XPath element not found: " + xpath);

        const tag = el.tagName ? el.tagName.toLowerCase() : '';
        if (tag === 'input' || tag === 'textarea' || tag === 'select') {
            return el.value;
        } else {
            return el.innerText || el.textContent;
        }
"@

    return Invoke-WebScript -Js $js
}

Lib-WebDebug_v101.ps1 (HTML/CSV保存・スクショ・デバッグメモ)

# ------------------------------------------------------------------------------
# デバッグおよび証跡ファイルエクスポートモジュール
# 画面状態のスナップショット保存およびDOM/iframe/Window情報の解析
# ------------------------------------------------------------------------------

# /// ブラウザ画面の取得および保存 ///

# --- 画面全体(多段iframe含む)のHTML保存 ---
function Export-WebHtml {
    param ([string]$Prefix = "WebHtml")

    $func = $MyInvocation.MyCommand.Name

    if ($null -eq $global:LogDir -or -not (Test-Path $global:LogDir)) {
        Write-DebugLog -Message "[$func] 警告: ログディレクトリ未発見" -Level Warning
        return
    }

    $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
    $filePath  = Join-Path $global:LogDir "${Prefix}_${timestamp}.html"

    try {
        $js = @"
function dump(win, depth) {
    let url = "UNKNOWN";
    // 【追加】URL取得自体を保護(クロスオリジンブロック対策)
    try { url = win.location.href; } catch(e) { url = "CROSS_ORIGIN_DENIED"; }
    
    let html = "\n";
    try {
        html += win.document.documentElement.outerHTML + "\n";
    } catch(e) {
        html += "\n";
    }
    for (let i = 0; i < win.frames.length; i++) {
        try { html += dump(win.frames[i], depth + 1); } catch(e) {}
    }
    return html;
}
return dump(window, 0);
"@
        $html = Invoke-WebScript -Js $js
        $html | Out-File -FilePath $filePath -Encoding UTF8 -Force

        return $filePath
    } catch {
        throw (New-EngineException -Func $func -Type "ファイルエラー" -Message "HTMLファイルの保存に失敗しました" -Details $_.Exception.Message)
    }
}

# --- 画面スクリーンショット(CDP/WebView2)のPNG保存 ---
function Export-WebScreenshot {
    param ([string]$Prefix = "WebScreenshot")

    $func = $MyInvocation.MyCommand.Name

    if ($null -eq $global:LogDir -or -not (Test-Path $global:LogDir)) {
        Write-DebugLog -Message "[$func] 警告: ログディレクトリ未発見" -Level Warning
        return
    }

    $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
    $guid      = [guid]::NewGuid().ToString()
    $fileName  = "${Prefix}_${timestamp}_${guid}.png"
    $filePath  = Join-Path $global:LogDir $fileName

    try {
        $useNative = $false

        # CDPモードでの撮影試行
        if ($global:CdpPort -gt 0) {
            try {
                # リトライとタイムアウトを短めに設定してCDP撮影を試みる
                $res = Invoke-CdpCommand -Method "Page.captureScreenshot" -Params @{ format = "png" } -MaxRetries 1 -TimeoutSecPerTry 3
                $bytes = [Convert]::FromBase64String($res.result.data)
                [IO.File]::WriteAllBytes($filePath, $bytes)
            } catch {
                Write-DebugLog -Message "[$func] 警告: CDP撮影失敗。ネイティブAPI(CapturePreview)へフォールバックします ($($_.Exception.Message))" -Level Warning
                $useNative = $true
            }
        } else {
            $useNative = $true
        }

        # ネイティブAPIでの撮影(CDP無効時、またはCDP失敗時)
        if ($useNative) {
            $stream = [IO.MemoryStream]::new()
            $webview = Get-ActiveWebView
            
            $task = $webview.CoreWebView2.CapturePreviewAsync(
                [Microsoft.Web.WebView2.Core.CoreWebView2CapturePreviewImageFormat]::Png,
                $stream
            )

            $sw = [System.Diagnostics.Stopwatch]::StartNew()
            while (-not $task.IsCompleted) {
                if ($sw.Elapsed.TotalSeconds -gt 10) {
                    throw (New-EngineException -Func $func -Type "Timeout" -Message "ネイティブスクショ取得がタイムアウトしました")
                }
                [System.Windows.Forms.Application]::DoEvents()
                Start-Sleep -Milliseconds 10
            }

            if ($task.IsFaulted) {
                throw (New-EngineException -Func $func -Type "ネイティブエラー" -Message "CapturePreviewAsyncAPI失敗" -Details $task.Exception.InnerException.Message)
            }

            $fileStream = [IO.File]::Create($filePath)
            $stream.Seek(0, [IO.SeekOrigin]::Begin) | Out-Null
            $stream.CopyTo($fileStream)
            $fileStream.Dispose()
            $stream.Dispose()
        }

        return $filePath

    } catch {
        throw (New-EngineException -Func $func -Type "ファイルエラー" -Message "スクリーンショットの保存処理中に例外が発生しました" -Details $_.Exception.Message)
    }
}

# /// Web要素およびフレームの解析 ///

# --- 指定テーブルのCSV保存およびVBA連携用データの返却 ---
function Export-WebTableToCsv {
    param (
        [Parameter(Mandatory = $true)][string]$Selector,
        [string]$FileName = "WebTable.csv"
    )

    $func = $MyInvocation.MyCommand.Name
    $selectorEscaped = $Selector.Replace("'", "\'")

    if ($null -eq $global:LogDir -or -not (Test-Path $global:LogDir)) {
        Write-DebugLog -Message "[$func] 警告: ログディレクトリ未発見" -Level Warning
        return
    }

    try {
        $js = @"
            $global:ENGINE_JS_UTILS
            try {
                var table = utilFindInFrames(window, function(win) {
                    try { return win.document.querySelector('$selectorEscaped') || null; }
                    catch(e) { return null; }
                });

                if (!table) return 'NOT_FOUND';

                var rows = Array.from(table.rows);
                if (rows.length === 0) return 'EMPTY_TABLE';

                // CSV出力用(人間が読む用)
                var csvData = rows.map(row => {
                    return Array.from(row.cells).map(cell => {
                        var text = (cell.innerText || '').trim().replace(/\r?\n|\r/g, ' ');
                        return '"' + text.replace(/"/g, '""') + '"';
                    }).join(',');
                }).join('\r\n');

                // VBA連携用(データ抽出用)
                var vbaData = rows.map(row => {
                    return Array.from(row.cells).map(cell => {
                        // 1. テキストがあればそれを返す
                        var text = (cell.innerText || '').trim().replace(/\r?\n|\r/g, ' ');
                        if (text !== '') return text;

                        // 2. テキストが空なら画像ファイル名を返す(汎用処理)
                        var img = cell.querySelector('img');
                        if (img && img.src) {
                            var parts = img.src.split('/');
                            return parts[parts.length - 1]; // 例: "IP10B030.png"
                        }
                        return '';
                    }).join('<T>');
                }).join('<R>');

                return { csv: csvData, vba: vbaData };
            } catch(e) {
                return 'JS_EXCEPTION: ' + e.message;
            }
"@

        $result = Invoke-WebScript -Js $js

        if ($result -eq 'NOT_FOUND')    { throw (New-EngineException -Func $func -Type "未発見" -Message "指定されたテーブルが見つかりません" -Details $Selector) }
        if ($result -eq 'EMPTY_TABLE')  { throw (New-EngineException -Func $func -Type "未発見" -Message "テーブル内に行データが存在しません" -Details $Selector) }
        if ($result -match '^JS_EXCEPTION:') { throw (New-EngineException -Func $func -Type "JSエラー" -Message "テーブル解析中にJavaScript例外が発生しました" -Details $result) }

        if ($FileName -ne "") {
            $savePath = Join-Path $global:LogDir $FileName
            $result.csv | Out-File -FilePath $savePath -Encoding UTF8 -Force
        }
        return $result.vba

    } catch {
        throw (New-EngineException -Func $func -Type "ファイルエラー" -Message "テーブルのCSV保存処理中に例外が発生しました" -Details $_.Exception.Message)
    }
}

# --- 操作可能要素(input/button/a等)の抽出およびCSV保存 ---
function Export-WebElementsToCsv {
    param (
        [string]$FileName = "WebElements.csv",
        [int]$Limit = 1000
    )

    $func = $MyInvocation.MyCommand.Name

    if ($null -eq $global:LogDir -or -not (Test-Path $global:LogDir)) {
        Write-DebugLog -Message "[$func] 警告: ログディレクトリ未発見" -Level Warning
        return
    }

    $js = @"
        function dump(win, arr) {
            let els = win.document.querySelectorAll('*');
            for (let el of els) {
                let tag = el.tagName.toLowerCase();
                if (['a','button','input','select','textarea'].includes(tag) || el.id) {
                    let style = win.getComputedStyle(el);
                    arr.push({
                        TagName: tag,
                        Type: el.type || '',
                        Name: el.name || '',
                        ID: el.id || '',
                        Value: (el.value || '').substring(0,50),
                        Text: (el.innerText || '').substring(0,50),
                        OuterHTML: (el.outerHTML || '').substring(0,200),
                        IsDisplayed: (style.display !== 'none' && style.visibility !== 'hidden')
                    });
                }
            }
            for (let i = 0; i < win.frames.length; i++) {
                try { dump(win.frames[i], arr); } catch(e) {}
            }
            return arr;
        }
        return dump(window, []);
"@

    try {
        $elements = Invoke-WebScript -Js $js

        if ($null -eq $elements -or $elements.Count -eq 0) {
            Write-DebugLog -Message "[$func] 情報: 要素なし" -Level Info
            return
        }

        $csv = @()
        $idx = 1

        foreach ($el in $elements) {
            $csv += [PSCustomObject]@{
                No          = $idx
                TagName     = $el.TagName
                Type        = $el.Type
                Name        = $el.Name
                ID          = $el.ID
                Value       = $el.Value
                Text        = $el.Text
                OuterHTML   = $el.OuterHTML
                IsDisplayed = $el.IsDisplayed
            }
            $idx++
            if ($idx -gt $Limit) { break }
        }

        $filePath = Join-Path $global:LogDir $FileName
        $csv | Export-Csv -Path $filePath -Encoding UTF8 -NoTypeInformation -Force

        return $filePath

    } catch {
        throw (New-EngineException -Func $func -Type "ファイルエラー" -Message "Web要素のCSVエクスポートに失敗しました" -Details $_.Exception.Message)
    }
}

# --- iframe/frame階層構造のツリー形式CSV保存 ---
function Export-WebFrameTreeToCsv {
    param ([string]$FileName = "WebFrameTree.csv")

    $func = $MyInvocation.MyCommand.Name

    if ($null -eq $global:LogDir -or -not (Test-Path $global:LogDir)) {
        Write-DebugLog -Message "[$func] 警告: ログディレクトリ未発見" -Level Warning
        return
    }

    $filePath = Join-Path $global:LogDir $FileName

    $js = @"
        function getFrameTree(win, depth, path) {
            var frames = win.document.querySelectorAll('iframe, frame');
            var tree = [];

            for (var i = 0; i < frames.length; i++) {
                var f = frames[i];
                var currentPath = path === '' ? i.toString() : path + '.' + i;

                var info = {
                    Depth: depth,
                    Path: currentPath,
                    Index: i,
                    Tag: f.tagName.toLowerCase(),
                    Id: f.id || '',
                    Name: f.name || '',
                    Src: f.src || ''
                };

                try {
                    var hasAccess = f.contentWindow && f.contentWindow.document;
                    if (hasAccess) {
                        var children = getFrameTree(f.contentWindow, depth + 1, currentPath);
                        tree.push(info);
                        tree = tree.concat(children);
                    } else {
                        info.Note = "Cross-Origin (No Access)";
                        tree.push(info);
                    }
                } catch(e) {
                    info.Note = "Access Denied";
                    tree.push(info);
                }
            }
            return tree;
        }
        return JSON.stringify(getFrameTree(window, 0, ''));
"@

    try {
        $json = Invoke-WebScript -Js $js

        if ([string]::IsNullOrWhiteSpace($json) -or $json -eq "[]") {
            Write-DebugLog -Message "[$func] 情報: iframeなし" -Level Info
            return
        }

        $frames = $json | ConvertFrom-Json

        $results = foreach ($f in $frames) {
            $indent = "  " * $f.Depth
            $prefix = if ($f.Depth -eq 0) { "■" } else { "└─" }

            [PSCustomObject]@{
                VisualHierarchy = "$indent$prefix $($f.Tag)"
                IndexPath       = $f.Path
                Id              = $f.Id
                Name            = $f.Name
                Depth           = $f.Depth
                Tag             = $f.Tag
                Src             = $f.Src
                Note            = $f.Note
            }
        }

        $results | Export-Csv -Path $filePath -NoTypeInformation -Encoding UTF8 -Force

        return $filePath

    } catch {
        throw (New-EngineException -Func $func -Type "ファイルエラー" -Message "フレームツリーの解析またはCSV保存に失敗しました" -Details $_.Exception.Message)
    }
}

# /// ウィンドウおよびOSレベルの取得 ///

# --- RPAブラウザウィンドウ全体(枠含む)のPNG保存 ---
function Export-WindowScreenshot {
    param ([string]$Prefix = "WindowScreenshot")

    $func = $MyInvocation.MyCommand.Name

    if ($null -eq $global:LogDir -or -not (Test-Path $global:LogDir)) {
        Write-DebugLog -Message "[$func] 警告: ログディレクトリ未発見" -Level Warning
        return
    }

    try {
        Add-Type -AssemblyName System.Drawing

        if ($null -eq $global:BrowserForm) {
            throw (New-EngineException -Func $func -Type "未発見" -Message "撮影対象のRPAブラウザウィンドウが存在しません")
        }

        $bounds = $global:BrowserForm.Bounds

        # --- DPIスケーリング(125%など)の補正計算 ---
        $tmpGraphics = [System.Drawing.Graphics]::FromHwnd([IntPtr]::Zero)
        $dpiX = $tmpGraphics.DpiX
        $tmpGraphics.Dispose()
        $scale = $dpiX / 96.0  # 100%時は1.0、125%時は1.25になる

        # 物理ピクセル座標・サイズへの変換
        $phyX = [int][Math]::Round($bounds.X * $scale)
        $phyY = [int][Math]::Round($bounds.Y * $scale)
        $phyW = [int][Math]::Round($bounds.Width * $scale)
        $phyH = [int][Math]::Round($bounds.Height * $scale)

        # 補正後の物理サイズでBitmap作成
        $bmp      = New-Object System.Drawing.Bitmap $phyW, $phyH
        $graphics = [System.Drawing.Graphics]::FromImage($bmp)

        # OSレベルの画面キャプチャ (物理ピクセルで指定)
        $graphics.CopyFromScreen(
            $phyX, $phyY,
            0, 0,
            $bmp.Size,
            [System.Drawing.CopyPixelOperation]::SourceCopy
        )

        $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
        $guid      = [guid]::NewGuid().ToString()
        $fileName  = "${Prefix}_${timestamp}_${guid}.png"

        $filePath = Join-Path $global:LogDir $fileName
        $bmp.Save($filePath, [System.Drawing.Imaging.ImageFormat]::Png)

        $graphics.Dispose()
        $bmp.Dispose()

        return $filePath

    } catch {
        throw (New-EngineException -Func $func -Type "内部エラー" -Message "ウィンドウ全体スクリーンショットの撮影または保存に失敗しました" -Details $_.Exception.Message)
    }
}

# --- 実行中ウィンドウ一覧のCSV保存 ---
function Export-WindowHierarchyToCsv {
    param ([string]$FileName = "WindowHierarchy.csv")

    $func = $MyInvocation.MyCommand.Name

    if ($null -eq $global:LogDir -or -not (Test-Path $global:LogDir)) {
        Write-DebugLog -Message "[$func] 警告: ログディレクトリ未発見" -Level Warning
        return
    }

    try {
        $processes = Get-Process | Where-Object { $_.MainWindowHandle -ne 0 }

        $results = foreach ($p in $processes) {
            [PSCustomObject]@{
                ProcessName = $p.ProcessName
                Handle      = $p.MainWindowHandle
                Title       = $p.MainWindowTitle
                ID          = $p.Id
            }
        }

        $savePath = Join-Path $global:LogDir $FileName
        $results | Export-Csv -Path $savePath -NoTypeInformation -Encoding UTF8 -Force

        return $savePath

    } catch {
        throw (New-EngineException -Func $func -Type "内部エラー" -Message "OSウィンドウ一覧の取得またはCSV保存に失敗しました" -Details $_.Exception.Message)
    }
}

# /// 汎用デバッグ情報の保存 ///

# --- 任意のデバッグ文字列のテキストファイル追記 ---
function Write-DebugTextFile {
    param (
        [Parameter(Mandatory = $true)][string]$Text,
        [string]$FileName = "DebugMemo.txt"
    )

    $func = $MyInvocation.MyCommand.Name

    if ($null -eq $global:LogDir -or -not (Test-Path $global:LogDir)) { return }

    $filePath = Join-Path $global:LogDir $FileName

    try {
        $Text | Out-File -FilePath $filePath -Append -Encoding UTF8 -Force

        return $filePath

    } catch {
        throw (New-EngineException -Func $func -Type "ファイルエラー" -Message "デバッグメモのファイル書き込みに失敗しました" -Details $_.Exception.Message)
    }
}

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?