- はじめに
職場のPCでも利用できる、Selenium等のOSSではなく、ポータブル環境で構築した汎用RPAエンジンです。(目指しています) 利用価値は低いですが
(私は、powershellなどは殆ど判らない中でのスタートで、ほぼAIさんの力です(GEMINI) ただ、無料利用での開発はコードが大きくなり限界です。出来たら改修してほしい)
エクセルと、WebView2.Core.dll等のダウンロードは必要です。
(設定powershell、概要VBA、マニュアル)は(2/2)
主コードはVBAで パラメータをJSON形式(VBA-JSON-2.3.1ライブラリ使用)で 操作指示を行います。
・ハイブリッド通信のメリット(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)
}
}
