PDF取得

**REGION 01 入力フォームと続行判定
# チェックは自己確認のみ。両方ONかつOKで次へ進み、開始は第2画面のOK後だけ。
SET PreparationBrowserReady TO False
SET BaseUrl TO $'''http://127.0.0.1:8765/'''
SET InputButton TO $'''not_submitted'''
SET InputOutcome TO $'''not_submitted'''
SET DefaultIds TO $'''DEMO-001
DEMO-002
DEMO-ERROR'''
SET DefaultWords TO $'''検査報告書
該当なし確認'''
SET ConfirmDummy1 TO $'''false'''
SET ConfirmDummy2 TO $'''false'''
SET WaitMode TO $'''text-stable'''
SET WaitScenario TO $'''staged'''
SET ArrivalText TO $'''検索結果を表示'''
SET DownloadMode TO $'''browser'''
SET WaitHelpText TO $'''[処理中のIDナンバー]の一致 & [目印の文字:%ArrivalText%]を確認 → PDF一覧の名前・URL・件数が0.9秒変わらなくなるまで待ちます。IDは検索結果の属性で照合し、目印とは連結しません。最大90秒。指定時間は3秒。安定・指定時間は全件表示の保証ではありません。'''
SET InputNotice TO $'''開始前の確認2項目にチェックしてください。現在はローカル検証環境です。'''
LABEL ShowPreflightForm
@@statistics_TextBlock: '2'
@@statistics_Input_Toggle: '2'
@@statistics_Action_Submit: '2'
Display.ShowCustomDialog CardTemplateJson: '''{
  "type": "AdaptiveCard",
  "version": "1.4",
  "id": "AdaptiveCard",
  "body": [
    {
      "type": "TextBlock",
      "id": "Instructions",
      "text": "${InputNotice_Text}",
      "wrap": true
    },
    {
      "type": "Input.Toggle",
      "id": "ConfirmDummy1",
      "title": "チェック項目ダミー1",
      "value": "${ConfirmDummy1_Value}",
      "valueOn": "true",
      "valueOff": "false"
    },
    {
      "type": "Input.Toggle",
      "id": "ConfirmDummy2",
      "title": "チェック項目ダミー2",
      "value": "${ConfirmDummy2_Value}",
      "valueOn": "true",
      "valueOff": "false"
    },
    {
      "type": "TextBlock",
      "text": "チェックはダミーの自己確認です。実際の認証状態は検証しません。",
      "wrap": true,
      "isSubtle": true
    }
  ],
  "actions": [
    {
      "type": "Action.Submit",
      "id": "OK",
      "title": "OK"
    },
    {
      "type": "Action.Submit",
      "id": "Cancel",
      "title": "キャンセル",
      "associatedInputs": "none"
    }
  ],
  "FormTitle": "PDF取得|開始前の確認"
}''' CustomFormData=> PreflightFormData ButtonPressed=> InputButton @InputNotice_Text: InputNotice @ConfirmDummy1_Value: ConfirmDummy1 @ConfirmDummy2_Value: ConfirmDummy2
ON ERROR
    GOTO InputFormFailed
END
IF InputButton <> $'''OK''' THEN
    SET InputOutcome TO $'''cancelled'''
    EXIT Code: 0
END
SET ConfirmDummy1 TO PreflightFormData['ConfirmDummy1']
SET ConfirmDummy2 TO PreflightFormData['ConfirmDummy2']
IF ConfirmDummy1 <> $'''true''' OR ConfirmDummy2 <> $'''true''' THEN
    SET InputNotice TO $'''開始していません。「チェック項目ダミー1」「チェック項目ダミー2」を両方チェックしてからOKを押してください。選択は保持しています。'''
    GOTO ShowPreflightForm
END
Scripting.RunVBScript.RunVBScript VBScriptCode: $'''Option Explicit
Dim shell, files, downloads, localData, documents, settingsDir, settingsFile, outputRoot
Dim stream, selected, status, configText, network
Set shell = CreateObject("Shell.Application")
Set files = CreateObject("Scripting.FileSystemObject")
downloads = shell.NameSpace("shell:Downloads").Self.Path
localData = shell.NameSpace(28).Self.Path
documents = shell.NameSpace(5).Self.Path
If Not files.FolderExists(downloads) Then Err.Raise 5, , "Downloads unavailable"
settingsDir = files.BuildPath(localData, "PadPdfLab")
settingsFile = files.BuildPath(settingsDir, "browser-download-folder.txt")
outputRoot = files.BuildPath(documents, "PDF-Results")
selected = downloads
status = "auto"
If files.FileExists(settingsFile) Then
    Set stream = CreateObject("ADODB.Stream")
    stream.Type = 2
    stream.Charset = "utf-8"
    stream.Open
    stream.LoadFromFile settingsFile
    configText = Trim(stream.ReadText)
    stream.Close
    If Len(configText) > 0 Then
        If InStr(configText, vbCr) > 0 Or InStr(configText, vbLf) > 0 Then
            selected = "-"
            status = "invalid"
        ElseIf Not files.FolderExists(configText) Then
            selected = "-"
            status = "invalid"
        ElseIf files.GetAbsolutePathName(configText) <> configText Then
            selected = "-"
            status = "invalid"
        Else
            selected = configText
            status = "override"
        End If
    End If
End If
Set network = CreateObject("WScript.Network")
WScript.Echo downloads & vbCrLf & settingsDir & vbCrLf & outputRoot & vbCrLf & status & vbCrLf & selected & vbCrLf & network.UserDomain & Chr(92) & network.UserName''' ScriptOutput=> FolderResolution
ON ERROR
    GOTO FolderResolutionFailed
END
Text.SplitText.SplitWithDelimiter Text: FolderResolution.Trimmed CustomDelimiter: $'''\r\n|\r|\n''' IsRegEx: True Result=> UserFolderValues
IF UserFolderValues.Count <> 6 THEN
    GOTO FolderResolutionFailed
END
SET KnownDownloadsFolder TO UserFolderValues[0]
SET UserSettingsDirectory TO UserFolderValues[1]
SET OutputRoot TO UserFolderValues[2]
SET FolderSettingsStatus TO UserFolderValues[3]
SET BrowserDownloadFolder TO UserFolderValues[4]
SET ExecutionUser TO UserFolderValues[5]
SET UserSettingsPath TO $'''%UserSettingsDirectory%\\browser-download-folder.txt'''
SET InputNotice TO $'''IDナンバーはExcelの範囲貼り付けができます。タブ・改行・空白で区切ります。検索ワードは1行に1件入力してください。空のIDと重複は取り除きます。'''
IF FolderSettingsStatus = $'''invalid''' THEN
    SET InputNotice TO $'''受け取り先の既存管理者設定が無効です。ブラウザー取得では管理者による設定確認が必要です。PDFの最終保存先とは別の設定です。'''
END
LABEL ShowStartForm
IF PreparationBrowserReady = True THEN
    WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { if (window.__padPreparationPanel) window.__padPreparationPanel.remove(); delete window.__padPreparationPanel; return "cleared"; }''' Result=> PreparationNoticeState
    ON ERROR
        GOTO PreparationRetryCleanup
    END
    GOTO PreparationRetryCleanupDone
    LABEL PreparationRetryCleanup
    ERROR => PreparationNoticeError Reset: True
    LABEL PreparationRetryCleanupDone
END
@@statistics_TextBlock: '4'
@@statistics_Input_Text: '3'
@@statistics_Input_ChoiceSet: '2'
@@statistics_Action_Submit: '2'
Display.ShowCustomDialog CardTemplateJson: '''{
  "type": "AdaptiveCard",
  "version": "1.4",
  "id": "AdaptiveCard",
  "body": [
    {
      "type": "TextBlock",
      "id": "Instructions",
      "text": "${InputNotice_Text}",
      "wrap": true
    },
    {
      "type": "Input.Text",
      "id": "IdNumbers",
      "label": "IDナンバー(Excel範囲貼り付け可・最大20件)",
      "isMultiline": true,
      "maxLength": 2000,
      "value": "${DefaultIds_Value}"
    },
    {
      "type": "Input.Text",
      "id": "SearchWords",
      "label": "検索ワード(1行に1件・最大20件)",
      "isMultiline": true,
      "maxLength": 2000,
      "value": "${DefaultWords_Value}"
    },
    {
      "type": "Input.ChoiceSet",
      "id": "WaitMode",
      "label": "検索結果の待ち方",
      "value": "${WaitMode_Value}",
      "choices": [
        {
          "title": "IDの一致+目印の文字 → PDF一覧の安定(おすすめ)",
          "value": "text-stable"
        },
        {
          "title": "完了の合図を待つ",
          "value": "explicit"
        },
        {
          "title": "一覧が落ち着くまで",
          "value": "stable"
        },
        {
          "title": "指定時間だけ待つ",
          "value": "fixed"
        }
      ]
    },
    {
      "type": "TextBlock",
      "id": "WaitHelp",
      "text": "${WaitHelp_Text}",
      "wrap": true
    },
    {
      "type": "Input.ChoiceSet",
      "id": "DownloadMode",
      "label": "PDFの取得方式",
      "value": "${DownloadMode_Value}",
      "choices": [
        {
          "title": "ブラウザーのログイン状態で取得(おすすめ)",
          "value": "browser"
        },
        {
          "title": "HTTPで直接取得(ログイン状態を引き継がない)",
          "value": "http"
        }
      ]
    },
    {
      "type": "TextBlock",
      "id": "DownloadHelp",
      "text": "通常はおすすめのまま開始できます。HTTP方式はブラウザーの認証を引き継ぎません。受け取り先は既知Downloads(既存の管理者設定があればその設定)で今回分だけ確認します。Edge実設定の自動判定ではありません。",
      "wrap": true
    },
    {
      "type": "Input.Text",
      "id": "OutputRoot",
      "label": "PDFの保存先(親フォルダー)",
      "value": "${OutputRoot_Value}",
      "isRequired": true,
      "errorMessage": "PDFの保存先を入力してください。",
      "maxLength": 1024
    },
    {
      "type": "TextBlock",
      "id": "OutputRootHelp",
      "text": "このフォルダー内に実行ごとのフォルダーを作成し、ID付きPDFと結果ログを保存します。絶対パスの既存フォルダーを指定してください。ブラウザーの受け取り先とは別です。変更は今回だけで、次回の既定値は変えません。",
      "wrap": true
    }
  ],
  "actions": [
    {
      "type": "Action.Submit",
      "id": "OK",
      "title": "開始"
    },
    {
      "type": "Action.Submit",
      "id": "Cancel",
      "title": "キャンセル",
      "associatedInputs": "none"
    }
  ],
  "FormTitle": "PDF取得|入力"
}''' CustomFormData=> InputFormData ButtonPressed=> InputButton @InputNotice_Text: InputNotice @DefaultIds_Value: DefaultIds @DefaultWords_Value: DefaultWords @WaitMode_Value: WaitMode @DownloadMode_Value: DownloadMode @WaitHelp_Text: WaitHelpText @OutputRoot_Value: OutputRoot
ON ERROR
    GOTO InputFormFailed
END
IF InputButton = $'''Cancel''' THEN
    SET InputOutcome TO $'''cancelled'''
    EXIT Code: 0
END
IF InputButton <> $'''OK''' THEN
    SET InputOutcome TO $'''cancelled'''
    EXIT Code: 0
END
IF PreparationBrowserReady = False THEN
    WebAutomation.LaunchEdge.LaunchEdge Url: BaseUrl WindowState: WebAutomation.BrowserWindowState.Normal ClearCache: False ClearCookies: False WaitForPageToLoadTimeout: 60 Timeout: 60 BrowserInstance=> Browser
    ON ERROR
        GOTO PreparationBrowserFailed
    END
    SET PreparationBrowserReady TO True
END
WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { function preparationNotice(stage, data) { const key = "__padPreparationPanel"; if (stage === "clear") { if (window[key]) window[key].remove(); delete window[key]; return "cleared"; } let panel = window[key]; if (!panel || !panel.isConnected) { panel = document.createElement("div"); panel.setAttribute("role", "status"); panel.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#f6f8fc;color:#192c44;display:grid;place-items:center;padding:32px;box-sizing:border-box;font-family:Segoe UI,Meiryo,sans-serif;overflow:auto"; const card = document.createElement("section"); card.style.cssText = "width:min(760px,85vw);background:white;border:1px solid #dbe3ed;border-radius:18px;padding:32px;box-sizing:border-box;box-shadow:0 8px 32px #172b4510"; const heading = document.createElement("h1"); heading.style.cssText = "font-size:26px;margin:0 0 14px"; const message = document.createElement("p"); message.style.cssText = "color:#596b80;font-size:15px;line-height:1.7;margin:0"; const details = document.createElement("div"); details.style.cssText = "max-height:50vh;overflow:auto;margin-top:22px"; const table = document.createElement("table"); table.style.cssText = "border-collapse:collapse;font-size:14px;line-height:1.8;text-align:left"; table.style.width = "100" + String.fromCharCode(37); const header = document.createElement("thead"); const headerRow = document.createElement("tr"); for (const title of ["項目", "件数", "内容"]) { const cell = document.createElement("th"); cell.setAttribute("scope", "col"); cell.textContent = title; cell.style.cssText = "background:#f1f5fa;padding:10px 12px;border-bottom:1px solid #dbe3ed;white-space:nowrap"; headerRow.append(cell); } header.append(headerRow); const body = document.createElement("tbody"); panel.rows = []; for (const title of ["対象ID", "検索ワード"]) { const row = document.createElement("tr"); const label = document.createElement("th"); label.setAttribute("scope", "row"); label.textContent = title; label.style.cssText = "padding:12px;border-bottom:1px solid #e7edf4;white-space:nowrap;vertical-align:top"; const count = document.createElement("td"); count.style.cssText = "padding:12px;border-bottom:1px solid #e7edf4;white-space:nowrap;vertical-align:top"; const values = document.createElement("td"); values.style.cssText = "padding:12px;border-bottom:1px solid #e7edf4;overflow-wrap:anywhere;word-break:break-word;vertical-align:top"; row.append(label, count, values); body.append(row); panel.rows.push({count, values}); } table.append(header, body); details.append(table); card.append(heading, message, details); panel.append(card); panel.details = details; panel.heading = heading; panel.message = message; document.documentElement.append(panel); window[key] = panel; } const validated = stage === "validated"; panel.heading.textContent = validated ? "取得準備中" : "処理準備中"; panel.message.textContent = validated ? "初期設定を行っています。しばらくお待ちください。" : "入力内容・保存先を確認しています。"; panel.details.hidden = !validated; if (validated) { [data.ids, data.words].forEach((items, index) => { panel.rows[index].count.textContent = items.length + "件"; panel.rows[index].values.textContent = items.join("、"); }); } return "shown"; }; return preparationNotice("preparing",null); }''' Result=> PreparationNoticeState
ON ERROR
    GOTO PreparationBrowserFailed
END
SET DefaultIds TO InputFormData['IdNumbers']
SET DefaultWords TO InputFormData['SearchWords']
SET WaitMode TO InputFormData['WaitMode']
SET DownloadMode TO InputFormData['DownloadMode']
SET OutputRoot TO InputFormData['OutputRoot']
SET OutputPathStatus TO $'''unavailable'''
SET OutputPathInputFile TO $''''''
File.GetTempPath TempFile=> OutputPathInputFile
ON ERROR
    GOTO OutputPathCheckFailed
END
Variables.ConvertCustomObjectToJson CustomObject: InputFormData Json=> InputFormJson
ON ERROR
    GOTO OutputPathCheckFailed
END
File.WriteText File: OutputPathInputFile TextToWrite: InputFormJson AppendNewLine: False IfFileExists: File.IfFileExists.Overwrite Encoding: File.FileEncoding.UTF8
ON ERROR
    GOTO OutputPathCheckFailed
END
Scripting.RunVBScript.RunVBScript VBScriptCode: $'''Option Explicit
Dim shell, stream, exitCode
Set shell = CreateObject("WScript.Shell")
shell.Environment("PROCESS")("PAD_INPUT_CARRIER") = "%OutputPathInputFile.FullName%"
shell.Environment("PROCESS")("PAD_INPUT_NO_NOTICE") = "1"
exitCode = shell.Run("powershell.exe -NoLogo -NoProfile -NonInteractive -STA -EncodedCommand cABhAHIAYQBtACgAWwBzAHQAcgBpAG4AZwBdACQAQwBhAHIAcgBpAGUAcgAgAD0AIAAkAGUAbgB2ADoAUABBAEQAXwBJAE4AUABVAFQAXwBDAEEAUgBSAEkARQBSACwAIABbAHMAdwBpAHQAYwBoAF0AJABOAG8ATgBvAHQAaQBjAGUAKQAKACQARQByAHIAbwByAEEAYwB0AGkAbwBuAFAAcgBlAGYAZQByAGUAbgBjAGUAIAA9ACAAJwBTAHQAbwBwACcACgAKAGYAdQBuAGMAdABpAG8AbgAgAEMAbwBuAHYAZQByAHQALQBJAG4AcAB1AHQATABpAHMAdAAoAFsAcwB0AHIAaQBuAGcAXQAkAFQAZQB4AHQALAAgAFsAcwB0AHIAaQBuAGcAXQAkAEQAZQBsAGkAbQBpAHQAZQByACwAIABbAHMAdAByAGkAbgBnAF0AJABQAGEAdAB0AGUAcgBuACkAIAB7AAoAIAAgACAAIAAkAGkAdABlAG0AcwAgAD0AIABbAEMAbwBsAGwAZQBjAHQAaQBvAG4AcwAuAEcAZQBuAGUAcgBpAGMALgBMAGkAcwB0AFsAcwB0AHIAaQBuAGcAXQBdADoAOgBuAGUAdwAoACkACgAgACAAIAAgACQAcwBlAGUAbgAgAD0AIABbAEMAbwBsAGwAZQBjAHQAaQBvAG4AcwAuAEcAZQBuAGUAcgBpAGMALgBIAGEAcwBoAFMAZQB0AFsAcwB0AHIAaQBuAGcAXQBdADoAOgBuAGUAdwAoAFsAUwB0AHIAaQBuAGcAQwBvAG0AcABhAHIAZQByAF0AOgA6AE8AcgBkAGkAbgBhAGwAKQAKACAAIAAgACAAJAB2AGEAbABpAGQAIAA9ACAAJAB0AHIAdQBlAAoAIAAgACAAIABmAG8AcgBlAGEAYwBoACAAKAAkAHAAYQByAHQAIABpAG4AIABbAHIAZQBnAGUAeABdADoAOgBTAHAAbABpAHQAKAAkAFQAZQB4AHQALAAgACQARABlAGwAaQBtAGkAdABlAHIAKQApACAAewAKACAAIAAgACAAIAAgACAAIAAkAHYAYQBsAHUAZQAgAD0AIAAkAHAAYQByAHQALgBUAHIAaQBtACgAKQAKACAAIAAgACAAIAAgACAAIABpAGYAIAAoACQAdgBhAGwAdQBlAC4ATABlAG4AZwB0AGgAIAAtAGUAcQAgADAAKQAgAHsAIABjAG8AbgB0AGkAbgB1AGUAIAB9AAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgALQBuAG8AdAAgAFsAcgBlAGcAZQB4AF0AOgA6AEkAcwBNAGEAdABjAGgAKAAkAHYAYQBsAHUAZQAsACAAJABQAGEAdAB0AGUAcgBuACkAKQAgAHsAIAAkAHYAYQBsAGkAZAAgAD0AIAAkAGYAYQBsAHMAZQA7ACAAYwBvAG4AdABpAG4AdQBlACAAfQAKACAAIAAgACAAIAAgACAAIABpAGYAIAAoACQAcwBlAGUAbgAuAEEAZABkACgAJAB2AGEAbAB1AGUAKQApACAAewAgACQAaQB0AGUAbQBzAC4AQQBkAGQAKAAkAHYAYQBsAHUAZQApACAAfQAKACAAIAAgACAAfQAKACAAIAAgACAAcgBlAHQAdQByAG4AIABAAHsAIABpAHQAZQBtAHMAIAA9ACAAJABpAHQAZQBtAHMALgBUAG8AQQByAHIAYQB5ACgAKQA7ACAAdgBhAGwAaQBkACAAPQAgACQAdgBhAGwAaQBkACAAfQAKAH0ACgAKAGYAdQBuAGMAdABpAG8AbgAgAFQAZQBzAHQALQBPAHUAdABwAHUAdABSAG8AbwB0ACgAWwBzAHQAcgBpAG4AZwBdACQAUgBvAG8AdAApACAAewAKACAAIAAgACAAaQBmACAAKAAkAFIAbwBvAHQALgBMAGUAbgBnAHQAaAAgAC0AZQBxACAAMAApACAAewAgAHIAZQB0AHUAcgBuACAAJwBlAG0AcAB0AHkAJwAgAH0ACgAgACAAIAAgAGkAZgAgACgAWwByAGUAZwBlAHgAXQA6ADoASQBzAE0AYQB0AGMAaAAoACQAUgBvAG8AdAAsACAAJwBbAFwAeAAwADAALQBcAHgAMQBmADwAPgAiAHwAPwAqAF0AJwApACkAIAB7ACAAcgBlAHQAdQByAG4AIAAnAGkAbgB2AGEAbABpAGQAXwBjAGgAYQByAGEAYwB0AGUAcgBzACcAIAB9AAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIABbAHIAZQBnAGUAeABdADoAOgBJAHMATQBhAHQAYwBoACgAJABSAG8AbwB0ACwAIAAnAF4AKAA/ADoAWwBBAC0AWgBhAC0AegBdADoAXABcAHwAXABcAFwAXABbAF4AXABcAC8AOgAqAD8AIgA8AD4AfABdACsAXABcAFsAXgBcAFwALwA6ACoAPwAiADwAPgB8AF0AKwAoAD8AOgBcAFwAfAAkACkAKQAnACkAKQAgAHsAIAByAGUAdAB1AHIAbgAgACcAYQBiAHMAbwBsAHUAdABlAF8AcABhAHQAaABfAHIAZQBxAHUAaQByAGUAZAAnACAAfQAKACAAIAAgACAAaQBmACAAKAAkAFIAbwBvAHQALgBTAHQAYQByAHQAcwBXAGkAdABoACgAJwBcAFwAPwBcACcAKQAgAC0AbwByACAAJABSAG8AbwB0AC4AUwB0AGEAcgB0AHMAVwBpAHQAaAAoACcAXABcAC4AXAAnACkAKQAgAHsAIAByAGUAdAB1AHIAbgAgACcAZABlAHYAaQBjAGUAXwBwAGEAdABoAF8AbgBvAHQAXwBzAHUAcABwAG8AcgB0AGUAZAAnACAAfQAKACAAIAAgACAAJAByAGUAbQBhAGkAbgBpAG4AZwAgAD0AIAAkAFIAbwBvAHQACgAgACAAIAAgAGkAZgAgACgAJABSAG8AbwB0AC4ATABlAG4AZwB0AGgAIAAtAGcAdAAgADEAIAAtAGEAbgBkACAAJABSAG8AbwB0AFsAMQBdACAALQBlAHEAIAAnADoAJwApACAAewAgACQAcgBlAG0AYQBpAG4AaQBuAGcAIAA9ACAAJABSAG8AbwB0AC4AUwB1AGIAcwB0AHIAaQBuAGcAKAAyACkAIAB9AAoAIAAgACAAIABpAGYAIAAoACQAcgBlAG0AYQBpAG4AaQBuAGcALgBDAG8AbgB0AGEAaQBuAHMAKAAnADoAJwApACAALQBvAHIAIAAkAFIAbwBvAHQALgBDAG8AbgB0AGEAaQBuAHMAKAAnAC8AJwApACkAIAB7ACAAcgBlAHQAdQByAG4AIAAnAGkAbgB2AGEAbABpAGQAXwBjAGgAYQByAGEAYwB0AGUAcgBzACcAIAB9AAoAIAAgACAAIABpAGYAIAAoAFsAcgBlAGcAZQB4AF0AOgA6AEkAcwBNAGEAdABjAGgAKAAkAFIAbwBvAHQALAAgACcAKABeAHwAXABcACkAWwAuAF0AewAxACwAMgB9ACgAXABcAHwAJAApAHwAWwAgAC4AXQAoAFwAXAB8ACQAKQAnACkAKQAgAHsAIAByAGUAdAB1AHIAbgAgACcAYQBtAGIAaQBnAHUAbwB1AHMAXwBwAGEAdABoAF8AcwBlAGcAbQBlAG4AdAAnACAAfQAKACAAIAAgACAAaQBmACAAKAAtAG4AbwB0ACAAWwBJAE8ALgBEAGkAcgBlAGMAdABvAHIAeQBdADoAOgBFAHgAaQBzAHQAcwAoACQAUgBvAG8AdAApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACQAZABlAGYAYQB1AGwAdABSAG8AbwB0ACAAPQAgAFsASQBPAC4AUABhAHQAaABdADoAOgBDAG8AbQBiAGkAbgBlACgAWwBFAG4AdgBpAHIAbwBuAG0AZQBuAHQAXQA6ADoARwBlAHQARgBvAGwAZABlAHIAUABhAHQAaAAoACcATQB5AEQAbwBjAHUAbQBlAG4AdABzACcAKQAsACAAJwBQAEQARgAtAFIAZQBzAHUAbAB0AHMAJwApAAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgALQBuAG8AdAAgAFsAUwB0AHIAaQBuAGcAQwBvAG0AcABhAHIAZQByAF0AOgA6AE8AcgBkAGkAbgBhAGwASQBnAG4AbwByAGUAQwBhAHMAZQAuAEUAcQB1AGEAbABzACgAJABSAG8AbwB0ACwAIAAkAGQAZQBmAGEAdQBsAHQAUgBvAG8AdAApACkAIAB7ACAAcgBlAHQAdQByAG4AIAAnAGYAbwBsAGQAZQByAF8AbgBvAHQAXwBmAG8AdQBuAGQAJwAgAH0ACgAgACAAIAAgACAAIAAgACAAdAByAHkAIAB7ACAAWwB2AG8AaQBkAF0AWwBJAE8ALgBEAGkAcgBlAGMAdABvAHIAeQBdADoAOgBDAHIAZQBhAHQAZQBEAGkAcgBlAGMAdABvAHIAeQAoACQAUgBvAG8AdAApACAAfQAgAGMAYQB0AGMAaAAgAHsAIAByAGUAdAB1AHIAbgAgACcAZgBvAGwAZABlAHIAXwB1AG4AYQB2AGEAaQBsAGEAYgBsAGUAJwAgAH0ACgAgACAAIAAgAH0ACgAgACAAIAAgACQAcAByAG8AYgBlAFAAYQB0AGgAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAbwBtAGIAaQBuAGUAKAAkAFIAbwBvAHQALAAgAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABSAGEAbgBkAG8AbQBGAGkAbABlAE4AYQBtAGUAKAApACkACgAgACAAIAAgACQAcAByAG8AYgBlACAAPQAgACQAbgB1AGwAbAAKACAAIAAgACAAJABvAHcAbgBlAGQAIAA9ACAAJABmAGEAbABzAGUACgAgACAAIAAgACQAcwB0AGEAdAB1AHMAIAA9ACAAJwBvAGsAJwAKACAAIAAgACAAdAByAHkAIAB7AAoAIAAgACAAIAAgACAAIAAgACQAcAByAG8AYgBlACAAPQAgAFsASQBPAC4ARgBpAGwAZQBdADoAOgBPAHAAZQBuACgAJABwAHIAbwBiAGUAUABhAHQAaAAsACAAWwBJAE8ALgBGAGkAbABlAE0AbwBkAGUAXQA6ADoAQwByAGUAYQB0AGUATgBlAHcALAAgAFsASQBPAC4ARgBpAGwAZQBBAGMAYwBlAHMAcwBdADoAOgBXAHIAaQB0AGUALAAgAFsASQBPAC4ARgBpAGwAZQBTAGgAYQByAGUAXQA6ADoATgBvAG4AZQApAAoAIAAgACAAIAAgACAAIAAgACQAbwB3AG4AZQBkACAAPQAgACQAdAByAHUAZQAKACAAIAAgACAAIAAgACAAIAAkAGIAeQB0AGUAcwAgAD0AIABbAFQAZQB4AHQALgBFAG4AYwBvAGQAaQBuAGcAXQA6ADoAVQBUAEYAOAAuAEcAZQB0AEIAeQB0AGUAcwAoACcAUABBAEQAIABvAHUAdABwAHUAdAAgAHAAYQB0AGgAIABjAGgAZQBjAGsAJwApAAoAIAAgACAAIAAgACAAIAAgACQAcAByAG8AYgBlAC4AVwByAGkAdABlACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQAKACAAIAAgACAAIAAgACAAIAAkAHAAcgBvAGIAZQAuAEYAbAB1AHMAaAAoACkACgAgACAAIAAgAH0AIABjAGEAdABjAGgAIAB7ACAAJABzAHQAYQB0AHUAcwAgAD0AIAAnAGYAbwBsAGQAZQByAF8AbgBvAHQAXwB3AHIAaQB0AGEAYgBsAGUAJwAgAH0ACgAgACAAIAAgAGYAaQBuAGEAbABsAHkAIAB7AAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABuAHUAbABsACAALQBuAGUAIAAkAHAAcgBvAGIAZQApACAAewAgAHQAcgB5ACAAewAgACQAcAByAG8AYgBlAC4ARABpAHMAcABvAHMAZQAoACkAIAB9ACAAYwBhAHQAYwBoACAAewAgACQAcwB0AGEAdAB1AHMAIAA9ACAAJwB3AHIAaQB0AGUAXwBjAGgAZQBjAGsAXwBmAGEAaQBsAGUAZAAnACAAfQAgAH0ACgAgACAAIAAgACAAIAAgACAAaQBmACAAKAAkAG8AdwBuAGUAZAApACAAewAgAHQAcgB5ACAAewAgAFsASQBPAC4ARgBpAGwAZQBdADoAOgBEAGUAbABlAHQAZQAoACQAcAByAG8AYgBlAFAAYQB0AGgAKQAgAH0AIABjAGEAdABjAGgAIAB7ACAAJABzAHQAYQB0AHUAcwAgAD0AIAAnAHAAcgBvAGIAZQBfAGMAbABlAGEAbgB1AHAAXwBmAGEAaQBsAGUAZAAnACAAfQAgAH0ACgAgACAAIAAgAH0ACgAgACAAIAAgAHIAZQB0AHUAcgBuACAAJABzAHQAYQB0AHUAcwAKAH0ACgAKACQAbgBvAHQAaQBjAGUAIAA9ACAAJABuAHUAbABsAAoAJAByAGUAcwB1AGwAdAAgAD0AIABAAHsAIABzAHQAYQB0AHUAcwAgAD0AIAAnAHUAbgBhAHYAYQBpAGwAYQBiAGwAZQAnADsAIABpAGQAcwAgAD0AIABAACgAKQA7ACAAdwBvAHIAZABzACAAPQAgAEAAKAApADsAIABtAGUAcwBzAGEAZwBlACAAPQAgACcAZVGbUoVRuVuSMLp4jYpnME0wfjBbMJMwAjDWU5dfbzCLlctZVzBmMEQwfjBbMJMwAjAnADsAIABuAG8AdABpAGMAZQBTAGgAbwB3AG4AIAA9ACAAJABmAGEAbABzAGUAIAB9AAoAdAByAHkAIAB7AAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAkAE4AbwBOAG8AdABpAGMAZQAgAC0AYQBuAGQAIAAkAGUAbgB2ADoAUABBAEQAXwBJAE4AUABVAFQAXwBOAE8AXwBOAE8AVABJAEMARQAgAC0AbgBlACAAJwAxACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAdAByAHkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAQQBkAGQALQBUAHkAcABlACAALQBBAHMAcwBlAG0AYgBsAHkATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAbgBvAHQAaQBjAGUAIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFcAaQBuAGQAbwB3AHMALgBGAG8AcgBtAHMALgBGAG8AcgBtAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABuAG8AdABpAGMAZQAuAFQAZQB4AHQAIAA9ACAAJwBQAEQARgDWU5dfXP+LlctZlm6ZUCcACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAG4AbwB0AGkAYwBlAC4AQwBsAGkAZQBuAHQAUwBpAHoAZQAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAARAByAGEAdwBpAG4AZwAuAFMAaQB6AGUAKAA0ADYAMAAsACAAMQAwADUAKQAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAbgBvAHQAaQBjAGUALgBTAHQAYQByAHQAUABvAHMAaQB0AGkAbwBuACAAPQAgACcAQwBlAG4AdABlAHIAUwBjAHIAZQBlAG4AJwAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAbgBvAHQAaQBjAGUALgBGAG8AcgBtAEIAbwByAGQAZQByAFMAdAB5AGwAZQAgAD0AIAAnAEYAaQB4AGUAZABUAG8AbwBsAFcAaQBuAGQAbwB3ACcACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAG4AbwB0AGkAYwBlAC4AQwBvAG4AdAByAG8AbABCAG8AeAAgAD0AIAAkAGYAYQBsAHMAZQAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAbgBvAHQAaQBjAGUALgBUAG8AcABNAG8AcwB0ACAAPQAgACQAdAByAHUAZQAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAbgBvAHQAaQBjAGUALgBTAGgAbwB3AEkAbgBUAGEAcwBrAGIAYQByACAAPQAgACQAZgBhAGwAcwBlAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABsAGEAYgBlAGwAIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFcAaQBuAGQAbwB3AHMALgBGAG8AcgBtAHMALgBMAGEAYgBlAGwACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAGwAYQBiAGUAbAAuAFQAZQB4AHQAIAA9ACAAIgBlUZtShVG5W/sw3U9YW0hRkjC6eI2KVzBmMEQwfjBZMCYgYAByAGAAbgDNZFxPWzBaMAEwXTBuMH4wfjBKMIVfYTBPMGAwVTBEMAIwIgAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAbABhAGIAZQBsAC4ARABvAGMAawAgAD0AIAAnAEYAaQBsAGwAJwAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAbABhAGIAZQBsAC4AVABlAHgAdABBAGwAaQBnAG4AIAA9ACAAJwBNAGkAZABkAGwAZQBDAGUAbgB0AGUAcgAnAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABsAGEAYgBlAGwALgBGAG8AbgB0ACAAPQAgAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABEAHIAYQB3AGkAbgBnAC4ARgBvAG4AdAAoACcAWQB1ACAARwBvAHQAaABpAGMAIABVAEkAJwAsACAAMQAyACkACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAG4AbwB0AGkAYwBlAC4AQwBvAG4AdAByAG8AbABzAC4AQQBkAGQAKAAkAGwAYQBiAGUAbAApAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABuAG8AdABpAGMAZQAuAFMAaABvAHcAKAApAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAWwBXAGkAbgBkAG8AdwBzAC4ARgBvAHIAbQBzAC4AQQBwAHAAbABpAGMAYQB0AGkAbwBuAF0AOgA6AEQAbwBFAHYAZQBuAHQAcwAoACkACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAHIAZQBzAHUAbAB0AC4AbgBvAHQAaQBjAGUAUwBoAG8AdwBuACAAPQAgACQAdAByAHUAZQAKACAAIAAgACAAIAAgACAAIAB9ACAAYwBhAHQAYwBoACAAewAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABuAHUAbABsACAALQBuAGUAIAAkAG4AbwB0AGkAYwBlACkAIAB7ACAAJABuAG8AdABpAGMAZQAuAEQAaQBzAHAAbwBzAGUAKAApADsAIAAkAG4AbwB0AGkAYwBlACAAPQAgACQAbgB1AGwAbAAgAH0ACgAgACAAIAAgACAAIAAgACAAfQAKACAAIAAgACAAfQAKACAAIAAgACAAJABpAG4AcAB1AHQARABhAHQAYQAgAD0AIABbAEkATwAuAEYAaQBsAGUAXQA6ADoAUgBlAGEAZABBAGwAbABUAGUAeAB0ACgAJABDAGEAcgByAGkAZQByACwAIABbAFQAZQB4AHQALgBFAG4AYwBvAGQAaQBuAGcAXQA6ADoAVQBUAEYAOAApACAAfAAgAEMAbwBuAHYAZQByAHQARgByAG8AbQAtAEoAcwBvAG4ACgAgACAAIAAgAGkAZgAgACgAJABpAG4AcAB1AHQARABhAHQAYQAuAEkAZABOAHUAbQBiAGUAcgBzACAALQBpAHMAbgBvAHQAIABbAHMAdAByAGkAbgBnAF0AIAAtAG8AcgAgACQAaQBuAHAAdQB0AEQAYQB0AGEALgBTAGUAYQByAGMAaABXAG8AcgBkAHMAIAAtAGkAcwBuAG8AdAAgAFsAcwB0AHIAaQBuAGcAXQAgAC0AbwByACAAJABpAG4AcAB1AHQARABhAHQAYQAuAE8AdQB0AHAAdQB0AFIAbwBvAHQAIAAtAGkAcwBuAG8AdAAgAFsAcwB0AHIAaQBuAGcAXQApACAAewAgAHQAaAByAG8AdwAgACcAaQBuAHYAYQBsAGkAZABfAGkAbgBwAHUAdABfAHMAYwBoAGUAbQBhACcAIAB9AAoAIAAgACAAIAAkAGkAZABzACAAPQAgAEMAbwBuAHYAZQByAHQALQBJAG4AcAB1AHQATABpAHMAdAAgACQAaQBuAHAAdQB0AEQAYQB0AGEALgBJAGQATgB1AG0AYgBlAHIAcwAgACcAXABzACsAJwAgACcAXgBbAEEALQBaAGEALQB6ADAALQA5AF0AWwBBAC0AWgBhAC0AegAwAC0AOQBfAC0AXQB7ADAALAAzADkAfQAkACcACgAgACAAIAAgACQAdwBvAHIAZABzACAAPQAgAEMAbwBuAHYAZQByAHQALQBJAG4AcAB1AHQATABpAHMAdAAgACQAaQBuAHAAdQB0AEQAYQB0AGEALgBTAGUAYQByAGMAaABXAG8AcgBkAHMAIAAnAFwAcgBcAG4AfABcAHIAfABcAG4AJwAgACcAXgBbAFwAcAB7AEwAfQBcAHAAewBOAH0AIABfACgAKQAuAPsw/DAtAF0AewAxACwAOAAwAH0AJAAnAAoAIAAgACAAIAAkAHIAZQBzAHUAbAB0AC4AaQBkAHMAIAA9ACAAQAAoACQAaQBkAHMALgBpAHQAZQBtAHMAKQAKACAAIAAgACAAJAByAGUAcwB1AGwAdAAuAHcAbwByAGQAcwAgAD0AIABAACgAJAB3AG8AcgBkAHMALgBpAHQAZQBtAHMAKQAKACAAIAAgACAAJAByAGUAcwB1AGwAdAAuAG0AZQBzAHMAYQBnAGUAIAA9ACAAJwAnAAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAkAGkAZABzAC4AdgBhAGwAaQBkACkAIAB7ACAAJAByAGUAcwB1AGwAdAAuAG0AZQBzAHMAYQBnAGUAIAA9ACAAJwA6UwdSijCMX24wSQBEAMow8zDQMPwwbzABMEpT0onxgnBlV1tnMMtZfjCLMDQAMACHZVdb5U6FUW4w8YJwZVdb+zDPMKQw1TDzMPswojDzMMAw/DC5MLMwojBnMGVRm1JXMGYwTzBgMFUwRDACMA1OY2tqMIdlV1tvMOqB1VLuT6lfVzB+MFswkzACMCcAIAB9AAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAkAHcAbwByAGQAcwAuAHYAYQBsAGkAZAApACAAewAgACQAcgBlAHMAdQBsAHQALgBtAGUAcwBzAGEAZwBlACAAPQAgACcAHGkife8w/DDJMG8wOAAwAIdlV1vlToVRbjCHZVdb+zBwZVdb+zB6en12+zBfACgAKQAuAPsw/DAtAGcwZVGbUlcwZjBPMGAwVTBEMAIwFV8odSZ7ATDRMPwwuzDzMMgwATA6UwdSijCHZVdbajBpMG8wf08odWcwTTB+MFswkzACMCcAIAB9AAoAIAAgACAAIABpAGYAIAAoACQAcgBlAHMAdQBsAHQALgBpAGQAcwAuAEMAbwB1AG4AdAAgAC0AZQBxACAAMAAgAC0AbwByACAAJAByAGUAcwB1AGwAdAAuAHcAbwByAGQAcwAuAEMAbwB1AG4AdAAgAC0AZQBxACAAMAAgAC0AbwByACAAJAByAGUAcwB1AGwAdAAuAGkAZABzAC4AQwBvAHUAbgB0ACAALQBnAHQAIAAyADAAIAAtAG8AcgAgACQAcgBlAHMAdQBsAHQALgB3AG8AcgBkAHMALgBDAG8AdQBuAHQAIAAtAGcAdAAgADIAMAApACAAewAKACAAIAAgACAAIAAgACAAIAAkAHIAZQBzAHUAbAB0AC4AbQBlAHMAcwBhAGcAZQAgAD0AIAAnAEkARADKMPMw0DD8MG8wenp9dl6YZzAGUnJSVzABMHp6bjAkUGgwzZEHiZIwZJZEMGYwMQBe/zIAMAD2TmswVzBmME8wYDBVMEQwAjAcaSJ97zD8MMkwbzAxAEyIazAxAPZOATB6ekyIaDDNkQeJkjBklkQwZjAxAF7/MgAwAPZOZzBZMAIw1lOXX28wi5XLWVcwZjBEMH4wWzCTMAIwJwAKACAAIAAgACAAfQAKACAAIAAgACAAaQBmACAAKAAkAG4AdQBsAGwAIAAtAG4AZQAgACQAbgBvAHQAaQBjAGUAKQAgAHsAIABbAFcAaQBuAGQAbwB3AHMALgBGAG8AcgBtAHMALgBBAHAAcABsAGkAYwBhAHQAaQBvAG4AXQA6ADoARABvAEUAdgBlAG4AdABzACgAKQAgAH0ACgAgACAAIAAgACQAcgBlAHMAdQBsAHQALgBzAHQAYQB0AHUAcwAgAD0AIABUAGUAcwB0AC0ATwB1AHQAcAB1AHQAUgBvAG8AdAAgACQAaQBuAHAAdQB0AEQAYQB0AGEALgBPAHUAdABwAHUAdABSAG8AbwB0AAoAfQAgAGMAYQB0AGMAaAAgAHsACgAgACAAIAAgACQAcgBlAHMAdQBsAHQALgBzAHQAYQB0AHUAcwAgAD0AIAAnAHAAcgBlAHAAYQByAGEAdABpAG8AbgBfAGYAYQBpAGwAZQBkACcACgAgACAAIAAgACQAcgBlAHMAdQBsAHQALgBtAGUAcwBzAGEAZwBlACAAPQAgACcAZVGbUoVRuVuSMLp4jYpnME0wfjBbMJMwAjDWU5dfbzCLlctZVzBmMEQwfjBbMJMwAjAnAAoAfQAgAGYAaQBuAGEAbABsAHkAIAB7AAoAIAAgACAAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AbgBlACAAJABuAG8AdABpAGMAZQApACAAewAgACQAbgBvAHQAaQBjAGUALgBDAGwAbwBzAGUAKAApADsAIAAkAG4AbwB0AGkAYwBlAC4ARABpAHMAcABvAHMAZQAoACkAIAB9AAoAfQAKAFsASQBPAC4ARgBpAGwAZQBdADoAOgBXAHIAaQB0AGUAQQBsAGwAVABlAHgAdAAoACQAQwBhAHIAcgBpAGUAcgAsACAAKAAkAHIAZQBzAHUAbAB0ACAAfAAgAEMAbwBuAHYAZQByAHQAVABvAC0ASgBzAG8AbgAgAC0ARABlAHAAdABoACAANQAgAC0AQwBvAG0AcAByAGUAcwBzACkALAAgAFsAVABlAHgAdAAuAFUAVABGADgARQBuAGMAbwBkAGkAbgBnAF0AOgA6AG4AZQB3ACgAJABmAGEAbABzAGUAKQApAAoA", 0, True)
If exitCode <> 0 Then Err.Raise 5, , "Input preparation failed"
Set stream = CreateObject("ADODB.Stream")
stream.Type = 2
stream.Charset = "utf-8"
stream.Open
stream.LoadFromFile "%OutputPathInputFile.FullName%"
WScript.Echo stream.ReadText
stream.Close''' ScriptOutput=> PreparedInputJson
ON ERROR
    GOTO OutputPathCheckFailed
END
Variables.ConvertJsonToCustomObject Json: PreparedInputJson CustomObject=> PreparedInput
ON ERROR
    GOTO OutputPathCheckFailed
END
SET OutputPathStatus TO PreparedInput['status']
GOTO OutputPathCheckCleanup
LABEL OutputPathCheckFailed
ERROR => OutputPathCheckError Reset: True
LABEL OutputPathCheckCleanup
IF IsNotEmpty(OutputPathInputFile) THEN
    File.Delete Files: OutputPathInputFile
    ON ERROR
        GOTO OutputPathCleanupFailed
    END
END
GOTO OutputPathCheckDone
LABEL OutputPathCleanupFailed
ERROR => OutputPathCleanupError Reset: True
SET OutputPathStatus TO $'''temporary_cleanup_failed'''
LABEL OutputPathCheckDone
IF OutputPathStatus.Trimmed <> $'''ok''' THEN
    SET InputNotice TO $'''PDFの保存先を確認できません(%OutputPathStatus.Trimmed%)。書き込み可能な絶対パスの既存フォルダーを指定してください。入力内容は保持しています。取得は開始していません。'''
    GOTO ShowStartForm
END
GOTO InputFormAccepted
LABEL InputFormFailed
ERROR => InputDialogError Reset: True
IF PreparationBrowserReady = True THEN
    WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { if (window.__padPreparationPanel) window.__padPreparationPanel.remove(); delete window.__padPreparationPanel; return "cleared"; }''' Result=> PreparationNoticeState
    ON ERROR
        GOTO PreparationInputCleanup
    END
    GOTO PreparationInputCleanupDone
    LABEL PreparationInputCleanup
    ERROR => PreparationNoticeError Reset: True
    LABEL PreparationInputCleanupDone
END
SET InputOutcome TO $'''form_closed_or_error'''
EXIT Code: 0
LABEL OutputRunFolderFailed
ERROR => OutputRunFolderError Reset: True
SET InputNotice TO $'''実行用フォルダーを作成できません。PDFの保存先の存在・権限を確認してください。入力内容は保持しています。取得は開始していません。'''
GOTO ShowStartForm
LABEL FolderResolutionFailed
ERROR => FolderResolutionError Reset: True
SET ValidationMessage TO $'''Windowsの既知Downloadsまたは既定の保存先情報を取得できません。管理者へフォルダーの存在・権限・VBScript実行可否をご確認ください。取得は開始していません。'''
GOTO ShowValidationForm
LABEL InputFormAccepted
**ENDREGION
**REGION 02 入力の整形と検証
# 前後の空白・空行・重複を除き、安全な文字と件数を検査してから出力先を作る。
SET ValidationMessage TO $''''''
IF DownloadMode <> $'''browser''' AND DownloadMode <> $'''http''' THEN
    SET ValidationMessage TO $'''取得方式が不正です。'''
END
IF DownloadMode = $'''browser''' AND IsEmpty(BrowserDownloadFolder) THEN
    SET ValidationMessage TO $'''ブラウザーの受け取り確認先が未設定です。管理者設定をご確認ください。取得は開始していません。'''
END
IF DownloadMode = $'''browser''' AND FolderSettingsStatus = $'''invalid''' THEN
    SET ValidationMessage TO $'''既存の受け取り先管理者設定が無効です。個人別設定ファイルを管理者に確認してください。設定は自動変更していません。取得は開始していません。'''
END
IF WaitMode <> $'''text-stable''' AND WaitMode <> $'''explicit''' AND WaitMode <> $'''stable''' AND WaitMode <> $'''fixed''' THEN
    SET ValidationMessage TO $'''待機方式が不正です。取得は開始していません。'''
END
IF WaitScenario <> $'''staged''' AND WaitScenario <> $'''pause''' AND WaitScenario <> $'''silent''' AND WaitScenario <> $'''zero''' AND WaitScenario <> $'''empty''' THEN
    SET ValidationMessage TO $'''模擬条件が不正です。取得は開始していません。'''
END
SET IdNumbers TO PreparedInput['ids']
SET SearchWords TO PreparedInput['words']
IF IsNotEmpty(PreparedInput['message']) THEN
    SET ValidationMessage TO PreparedInput['message']
END
LABEL ShowValidationForm
IF IsNotEmpty(ValidationMessage) THEN
    IF PreparationBrowserReady = True THEN
        WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { if (window.__padPreparationPanel) window.__padPreparationPanel.remove(); delete window.__padPreparationPanel; return "cleared"; }''' Result=> PreparationNoticeState
        ON ERROR
            GOTO PreparationValidationCleanup
        END
        GOTO PreparationValidationCleanupDone
        LABEL PreparationValidationCleanup
        ERROR => PreparationNoticeError Reset: True
        LABEL PreparationValidationCleanupDone
    END
    SET InputOutcome TO $'''invalid_input'''
    @@statistics_TextBlock: '1'
    @@statistics_Action_Submit: '1'
    Display.ShowCustomDialog CardTemplateJson: '''{
  "type": "AdaptiveCard",
  "version": "1.4",
  "id": "AdaptiveCard",
  "body": [
    {
      "type": "TextBlock",
      "id": "Validation",
      "text": "${Validation_Text}",
      "wrap": true
    }
  ],
  "actions": [
    {
      "type": "Action.Submit",
      "id": "Close",
      "title": "閉じる"
    }
  ],
  "FormTitle": "入力内容をご確認ください(取得は開始していません)"
}''' CustomFormData=> ValidationFormData ButtonPressed=> ValidationButton @Validation_Text: ValidationMessage
    ON ERROR
        GOTO InputFormFailed
    END
    EXIT Code: 0
END
SET InputOutcome TO $'''accepted'''
WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { function preparationNotice(stage, data) { const key = "__padPreparationPanel"; if (stage === "clear") { if (window[key]) window[key].remove(); delete window[key]; return "cleared"; } let panel = window[key]; if (!panel || !panel.isConnected) { panel = document.createElement("div"); panel.setAttribute("role", "status"); panel.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#f6f8fc;color:#192c44;display:grid;place-items:center;padding:32px;box-sizing:border-box;font-family:Segoe UI,Meiryo,sans-serif;overflow:auto"; const card = document.createElement("section"); card.style.cssText = "width:min(760px,85vw);background:white;border:1px solid #dbe3ed;border-radius:18px;padding:32px;box-sizing:border-box;box-shadow:0 8px 32px #172b4510"; const heading = document.createElement("h1"); heading.style.cssText = "font-size:26px;margin:0 0 14px"; const message = document.createElement("p"); message.style.cssText = "color:#596b80;font-size:15px;line-height:1.7;margin:0"; const details = document.createElement("div"); details.style.cssText = "max-height:50vh;overflow:auto;margin-top:22px"; const table = document.createElement("table"); table.style.cssText = "border-collapse:collapse;font-size:14px;line-height:1.8;text-align:left"; table.style.width = "100" + String.fromCharCode(37); const header = document.createElement("thead"); const headerRow = document.createElement("tr"); for (const title of ["項目", "件数", "内容"]) { const cell = document.createElement("th"); cell.setAttribute("scope", "col"); cell.textContent = title; cell.style.cssText = "background:#f1f5fa;padding:10px 12px;border-bottom:1px solid #dbe3ed;white-space:nowrap"; headerRow.append(cell); } header.append(headerRow); const body = document.createElement("tbody"); panel.rows = []; for (const title of ["対象ID", "検索ワード"]) { const row = document.createElement("tr"); const label = document.createElement("th"); label.setAttribute("scope", "row"); label.textContent = title; label.style.cssText = "padding:12px;border-bottom:1px solid #e7edf4;white-space:nowrap;vertical-align:top"; const count = document.createElement("td"); count.style.cssText = "padding:12px;border-bottom:1px solid #e7edf4;white-space:nowrap;vertical-align:top"; const values = document.createElement("td"); values.style.cssText = "padding:12px;border-bottom:1px solid #e7edf4;overflow-wrap:anywhere;word-break:break-word;vertical-align:top"; row.append(label, count, values); body.append(row); panel.rows.push({count, values}); } table.append(header, body); details.append(table); card.append(heading, message, details); panel.append(card); panel.details = details; panel.heading = heading; panel.message = message; document.documentElement.append(panel); window[key] = panel; } const validated = stage === "validated"; panel.heading.textContent = validated ? "取得準備中" : "処理準備中"; panel.message.textContent = validated ? "初期設定を行っています。しばらくお待ちください。" : "入力内容・保存先を確認しています。"; panel.details.hidden = !validated; if (validated) { [data.ids, data.words].forEach((items, index) => { panel.rows[index].count.textContent = items.length + "件"; panel.rows[index].values.textContent = items.join("、"); }); } return "shown"; }; return preparationNotice("validated",%PreparedInputJson%); }''' Result=> PreparationNoticeState
ON ERROR
    GOTO PreparationBrowserFailed
END
**ENDREGION
**REGION 03 実行準備
Variables.ConvertJsonToCustomObject Json: $'''{"ResponseDelayMilliseconds":150,"SearchInputId":"idNumberInput","SearchButtonId":"idSearchButton","SearchStatusId":"searchState","ReadyState":"ready","EmptyState":"empty","FailedState":"failed","ResultAreaSelector":"#documents","ArrivalAreaSelector":"#resultArrival","AreaLinkSelector":"a[data-pdf-id]","ResultIdAttribute":"data-id-number","ResultRequestAttribute":"data-request","ResultGenerationAttribute":"data-generation","StateAttribute":"data-state"}''' CustomObject=> SiteConfig
# navigateは実submitでトップレベルの新Documentへ遷移。domは旧比較方式。認証やiframeは対象外。
SET SearchTransport TO $'''navigate'''
# 対象サイトで合わせ込む設定。入力欄・ボタンはHTML ID。下の状態属性と一覧セレクターは模擬サイト用。
# 同一DOM用。模擬のID・要求・世代の属性は対象サイトの結果識別手掛かりへ合わせる。iframeは未対応。
SET PollSeconds TO 0.3
SET MaximumWaitSeconds TO 90
SET StableSeconds TO 0.9
SET MinimumWaitSeconds TO 0.9
SET FixedWaitSeconds TO 3
SET ListingPollLimit TO 300
# 対象は一覧抽出時、成功・失敗は保存確認後に加算する。ページ読取失敗と該当なしは別の件数。
SET ExpectedCount TO 0
SET PageFailureCount TO 0
DateTime.GetCurrentDateTime.Local DateTimeFormat: DateTime.DateTimeFormat.DateAndTime CurrentDateTime=> RunStarted
Text.ConvertDateTimeToText.FromCustomDateTime DateTime: RunStarted CustomFormat: $'''yyyyMMdd_HHmmss_fff''' Result=> RunStamp
Text.ConvertDateTimeToText.FromCustomDateTime DateTime: RunStarted CustomFormat: $'''yyyy-MM-dd HH:mm:ss.fff''' Result=> StartedText
Folder.Create FolderPath: OutputRoot FolderName: $'''PAD_Hierarchy_%RunStamp%''' Folder=> RunFolder
ON ERROR
    GOTO OutputRunFolderFailed
END
SET DetailLogPath TO $'''%RunFolder.FullName%\\ダウンロード結果.txt'''
Variables.CreateNewList List=> PlannedKeys
# Downloads全体ではなく、この実行の予定名接頭辞だけを事前確認。既存候補があれば取得しない。
IF DownloadMode = $'''browser''' THEN
    Folder.GetFiles Folder: BrowserDownloadFolder FileFilter: $'''PAD_SESSION_%RunStamp%_*''' IncludeSubfolders: False FailOnAccessDenied: True SortBy1: Folder.SortBy.NoSort SortDescending1: False SortBy2: Folder.SortBy.NoSort SortDescending2: False SortBy3: Folder.SortBy.NoSort SortDescending3: False Files=> DownloadBaseline
    ON ERROR
        GOTO RunProcessingFailed
    END
    IF DownloadBaseline.Count > 0 THEN
        File.WriteText File: DetailLogPath TextToWrite: $'''今回接頭辞の既存候補があるため取得せず終了。移動・削除・改名なし。''' AppendNewLine: True IfFileExists: File.IfFileExists.Append Encoding: File.FileEncoding.UTF8
        GOTO InputFormFailed
    END
END
SET ReportPath TO $'''%RunFolder.FullName%\\ダウンロード結果.txt'''
SET DetailLog TO $''''''
SET IdLogContext TO $'''ProcessStart=%StartedText%  UserName=%ExecutionUser%'''
SET RunLogContext TO $'''保存先:%RunFolder.FullName%
実行ユーザー:%ExecutionUser%
実行開始:%StartedText%'''
Variables.ConvertJsonToCustomObject Json: $'''{}''' CustomObject=> SearchWordsObject
SET SearchWordsObject['words'] TO SearchWords
Variables.ConvertCustomObjectToJson CustomObject: SearchWordsObject Json=> SearchWordsJson
# 模擬サイトの通常フォームでダミーセッションを開始。Cookie値を読まず、コピーせず、ログにも出さない。
WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { function beginPreparationSession() { window.__padPreparationSession = "pending"; fetch("/session-login", {method:"POST", credentials:"same-origin", headers:{Accept:"application/json"}}) .then(response => { if (!response.ok || !response.headers.get("content-type").includes("application/json")) throw new Error("Session start failed"); return response.json(); }) .then(state => { if (state.active !== true) throw new Error("Session start unconfirmed"); return fetch("/session-status", {credentials:"same-origin", cache:"no-store"}); }) .then(response => { if (!response.ok) throw new Error("Session check failed"); return response.json(); }) .then(state => { window.__padPreparationSession = state.active === true ? "ready" : "failed"; }) .catch(() => { window.__padPreparationSession = "failed"; }); return "session_start_requested"; }; return beginPreparationSession(); }''' Result=> SessionStartState
ON ERROR
    GOTO BrowserStartupFailed
END
SET SessionReady TO $'''false'''
LOOP SessionIndex FROM 1 TO 60 STEP 1
    WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { if (window.__padPreparationSession === "failed") throw new Error("Session check failed"); return window.__padPreparationSession === "ready" ? "true" : "false"; }''' Result=> SessionReady
    ON ERROR
        GOTO BrowserStartupFailed
    END
    IF SessionReady = $'''true''' THEN
        EXIT LOOP
    END
    WAIT 0.5
END
IF SessionReady <> $'''true''' THEN
    GOTO BrowserSessionUnavailable
END
Variables.ConvertJsonToCustomObject Json: $'''{"path":""}''' CustomObject=> FolderUriData
SET FolderUriData['path'] TO RunFolder.FullName
Variables.ConvertCustomObjectToJson CustomObject: FolderUriData Json=> FolderPathJson
WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { const panel = window.__padPreparationPanel; if (panel) { panel.heading.textContent = "処理開始"; panel.message.textContent = "対象IDの取得を開始します。"; } const path = (%FolderPathJson%).path.split(String.fromCharCode(92)).join("/"); const parts = path.startsWith("//") ? path.slice(2).split("/") : path.split("/"); return (path.startsWith("//") ? "file://" : "file:///") + parts.map(function(part, index) { return index === 0 ? part : Array.from(part).map(function(character) { return character.codePointAt(0) > 127 ? character : encodeURIComponent(character); }).join(""); }).join("/") + "/"; }''' Result=> RunFolderUri
ON ERROR
    GOTO BrowserStartupFailed
END
**ENDREGION
**REGION 04 IDと検索ワードごとの取得
Variables.CreateNewList List=> AllIdPlans
SET PipelineFailure TO $''''''
WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { function createDownloadPipeline(runStamp, origin, start, readTransfers, clock = () => Date.now(), schedule = callback => setTimeout(callback, 300)) { const records = new Map(); const keys = new Set(); let timerPending = false; let nextStart = 0; let sealed = false; let cancelled = false; let observationGrace = 0; const fail = (job, reason) => {job.state = "failed"; job.reason = reason;}; const identity = job => JSON.stringify([job.id, job.name, job.url, job.searchWord, job.logGroupIndex, job.plannedKey, job.key]); function checked(plan) { if (!plan || plan.runStamp !== runStamp || plan.request !== runStamp + "_" + plan.idNumber || !Array.isArray(plan.jobs) || !Array.isArray(plan.logGroups) || !Array.isArray(plan.noMatchWords) || plan.noMatchCount !== plan.noMatchWords.length) throw new Error("Invalid ID manifest"); const local = new Set(); const jobs = plan.jobs.map((job, index) => { if (!job || ![job.id, job.name, job.url, job.searchWord, job.plannedKey, job.key].every(value => typeof value === "string" && value.length > 0) || !job.name.startsWith(plan.idNumber + "_") || Array.from(job.name).some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 92 || String.fromCharCode(60,62,58,34,47,124,63,42).includes(character)) || !job.name.endsWith(".pdf") || new URL(job.url).origin !== origin || job.plannedKey !== plan.idNumber + " | " + job.searchWord + " | " + job.name || job.key !== "PAD_SESSION_" + runStamp + "_" + plan.idNumber + "_" + index + ".pdf" || !Number.isInteger(job.logGroupIndex) || job.logGroupIndex < 0 || job.logGroupIndex >= plan.logGroups.length || job.state !== "queued" || local.has(job.key.toLowerCase())) throw new Error("Invalid reservation"); local.add(job.key.toLowerCase()); return Object.freeze({id:job.id, name:job.name, url:job.url, searchWord:job.searchWord, logGroupIndex:job.logGroupIndex, plannedKey:job.plannedKey, key:job.key}); }); return Object.freeze(jobs); } function tick() { timerPending = false; if (cancelled) return; const now = clock(); const transfers = readTransfers(); const jobs = [...records.values()].flatMap(record => record.jobs); for (const job of jobs) if (job.state === "queued" && now >= job.launchBy) fail(job, "取得開始の待機上限"); const receiving = Object.values(transfers).filter(transfer => transfer.state === "receiving").length; const job = jobs.find(entry => entry.state === "queued"); if (job && receiving < 5 && now >= nextStart) { nextStart = now + 300; try { if (transfers[job.key]) throw new Error("Duplicate transfer key"); start(job); if (!readTransfers()[job.key]) throw new Error("Start receipt missing"); job.state = "pending"; job.expires = now + 90000; } catch (error) {fail(job, error.message);} } if (jobs.some(entry => entry.state === "queued")) { timerPending = true; schedule(tick); } } function accept(plan, snapshot) { if (sealed || cancelled) throw new Error("Manifest already sealed or cancelled"); const manifest = checked(plan); const signature = JSON.stringify(manifest); const existing = records.get(plan.idNumber); if (existing) { if (existing.signature !== signature || existing.request !== plan.request) throw new Error("ID manifest changed"); return existing.ack; } if (manifest.some(job => keys.has(job.key.toLowerCase()))) throw new Error("Duplicate reservation key"); if (!snapshot || !Array.isArray(snapshot.downloads) || !Array.isArray(snapshot.output)) throw new Error("Disk snapshot missing"); const jobs = manifest.map(job => ({...job, state:"queued", reason:"", bytes:0, stable:0, lastSize:-1, receipt:null, launchBy:clock() + 90000 + manifest.length * 300})); for (const job of jobs) { const candidates = snapshot.downloads.filter(file => file.name.toLowerCase().startsWith(job.key.toLowerCase())); if (candidates.length || snapshot.output.some(file => file.name.toLowerCase() === job.name.toLowerCase())) fail(job, "予定名の既存候補があるため取得しません"); } const ack = JSON.stringify({idNumber:plan.idNumber, request:plan.request, count:jobs.length, keys:jobs.map(job => job.key)}); records.set(plan.idNumber, {manifest, signature, request:plan.request, jobs, ack}); jobs.forEach(job => keys.add(job.key.toLowerCase())); if (!timerPending) tick(); return ack; } function seal(plans) { const received = new Set(); for (const plan of plans) { if (plan.pageLog) continue; const manifest = checked(plan); if (received.has(plan.idNumber)) throw new Error("Duplicate ID manifest"); received.add(plan.idNumber); const record = records.get(plan.idNumber); if (record && (record.signature !== JSON.stringify(manifest) || record.request !== plan.request)) throw new Error("PAD/browser manifest mismatch"); if (!record && !plan.startFailure) throw new Error("ID manifest not acknowledged"); } if ([...records.keys()].some(idNumber => !received.has(idNumber))) throw new Error("Unexpected ID manifest"); if (!sealed) observationGrace = clock() + 30000; sealed = true; return "sealed"; } function poll(snapshot, expire = false) { if (!sealed) throw new Error("Seal all manifests before monitoring"); const now = clock(); const transfers = readTransfers(); const jobs = [...records.values()].flatMap(record => record.jobs); for (const job of jobs) { if (job.state === "failed") continue; if (job.state === "queued") { if (expire || now >= job.launchBy) fail(job, "取得開始の待機上限"); continue; } const transfer = transfers[job.key]; if (!transfer || transfer.state === "failed") {fail(job, transfer?.reason || "ブラウザー取得情報が失われました"); continue;} const candidates = snapshot.downloads.filter(file => file.name.toLowerCase().startsWith(job.key.toLowerCase())); if (transfer.state === "save_requested") { if (!job.receipt) job.receipt = Object.freeze({bytes:transfer.bytes, received:transfer.received, started:transfer.started}); if (transfer.bytes !== job.receipt.bytes || transfer.received !== job.receipt.received || transfer.started !== job.receipt.started) {fail(job, "ブラウザーの取得情報が変わりました"); continue;} if (candidates.length > 1) {fail(job, "予定名の候補が複数あります"); continue;} const candidate = candidates[0]; if (candidate && candidate.name === job.key && candidate.size === job.receipt.bytes && candidate.size > 0) { job.stable = candidate.size === job.lastSize ? job.stable + 1 : 0; job.lastSize = candidate.size; if (job.stable >= 2) {job.state = "ready"; job.bytes = candidate.size;} } else {job.stable = 0; job.lastSize = -1; job.state = "pending";} } else if (job.receipt) {fail(job, "完了済みの取得状態が変わりました"); continue;} if (job.state !== "ready" && (expire || now >= Math.max(job.expires, observationGrace))) fail(job, "保存未完了・受け取り先不一致・許可待ち、または待機上限"); } const done = jobs.every(job => job.state === "ready" || job.state === "failed"); return {done, count:jobs.length}; } function results(plan) { if (!sealed) throw new Error("Manifest not sealed"); const manifest = checked(plan); const record = records.get(plan.idNumber); if (!record || record.signature !== JSON.stringify(manifest) || record.jobs.some(job => job.state !== "ready" && job.state !== "failed")) throw new Error("ID results unavailable or manifest changed"); if (record.jobs.some((job, index) => identity(job) !== identity(record.manifest[index]))) throw new Error("Reservation identity changed"); return JSON.stringify(record.jobs); } function cancel(reason) { cancelled = true; for (const record of records.values()) for (const job of record.jobs) if (job.state !== "ready" && job.state !== "failed") { fail(job, reason); const transfer = readTransfers()[job.key]; if (transfer?.state === "receiving") {transfer.state = "failed"; transfer.reason = reason;} } return "cancelled"; } return Object.freeze({accept, seal, poll, results, cancel}); } function beginBrowserDownload(url, name, key, scope = window) { scope.__padTransfers ??= {}; if (scope.__padTransfers[key]) throw new Error("Duplicate transfer key"); const transfer = {state:"receiving", name, bytes:0, reason:"", started:Date.now()}; scope.__padTransfers[key] = transfer; void (async()=>{ const controller = scope.AbortController ? new scope.AbortController() : null; const timeout = scope.setTimeout(() => { transfer.state = "failed"; transfer.reason = "本文受信の待機上限"; controller?.abort(); }, 90000); try { const target = new URL(url, scope.location.href); if (target.origin !== scope.location.origin) throw new Error("Different origin is not allowed"); const response = await scope.fetch(target.href, {credentials:"same-origin", cache:"no-store", signal:controller?.signal}); if (!response.ok) throw new Error("HTTP " + response.status); if ((response.headers.get("content-type") || "").split(";")[0].trim().toLowerCase() !== "application/pdf") throw new Error("Not a PDF response; session/login may be required"); const blob = await response.blob(); const bytes = new Uint8Array(await blob.arrayBuffer()); const header = new TextDecoder().decode(bytes.slice(0, 8)); const tail = new TextDecoder().decode(bytes.slice(-1024)); if (!header.startsWith(String.fromCharCode(37)+"PDF-") || !/\x25\x25EOF\s*$/.test(tail)) throw new Error("PDF signature/end marker missing"); if (transfer.state !== "receiving") throw new Error(transfer.reason || "Transfer cancelled"); transfer.bytes = bytes.length; const objectUrl = scope.URL.createObjectURL(blob); const anchor = scope.document.createElement("a"); anchor.href = objectUrl; anchor.download = name; scope.document.body.append(anchor); anchor.click(); anchor.remove(); transfer.state = "save_requested"; transfer.received = Date.now(); scope.setTimeout(()=>scope.URL.revokeObjectURL(objectUrl), 60000); } catch (error) {transfer.state = "failed"; transfer.reason = error.message;} finally {scope.clearTimeout?.(timeout);} })(); return "started"; } if (window.__padPipeline) throw new Error("Pipeline already exists"); window.__padPipeline=createDownloadPipeline("%RunStamp%",location.origin,job=>beginBrowserDownload(job.url,job.key,job.key),()=>window.__padTransfers || {}); return "initialized"; }''' Result=> PipelineState
ON ERROR
    GOTO RunProcessingFailed
END
LOOP FOREACH IdNumber IN IdNumbers
    Variables.CreateNewList List=> TargetLinks
    SET IdLog TO $''''''
    Variables.ConvertJsonToCustomObject Json: $'''{"idNumber":"%IdNumber%","runStamp":"%RunStamp%","request":"%RunStamp%_%IdNumber%","jobs":[],"logGroups":[],"noMatchWords":[],"noMatchCount":0,"noMatchLog":"","pageLog":"","startFailure":""}''' CustomObject=> IdPlan
    SET SearchRequest TO $'''%RunStamp%_%IdNumber%'''
    SET IdTabUrl TO $'''%BaseUrl%?mode=search&run=%RunStamp%&tab=%IdNumber%&profile=%WaitScenario%&protected=1&transport=%SearchTransport%&responseDelayMs=%SiteConfig['ResponseDelayMilliseconds']%'''
    SET ListingState TO $'''failed'''
    # 作成したタブだけを使う。既存タブへ接続・移動・終了せず、IDごとの参照を保持する。
    WebAutomation.CreateNewTab.CreateNewTab BrowserInstance: Browser Url: IdTabUrl WaitForPageToLoadTimeout: 60 NewBrowserInstance=> IdBrowser
    ON ERROR
        GOTO IdSearchFailed
    END
    WebAutomation.ExecuteJavascript BrowserInstance: IdBrowser Javascript: $'''function ExecuteScript() { const area = document.querySelector("%SiteConfig['ResultAreaSelector']%"); const context = {started:Date.now(), documentToken:document.documentElement.dataset.documentToken || String(performance.timeOrigin), previousGeneration:area ? area.getAttribute("%SiteConfig['ResultGenerationAttribute']%") || "" : "", timeOrigin:performance.timeOrigin}; const input = document.getElementById("%SiteConfig['SearchInputId']%"); const button = document.getElementById("%SiteConfig['SearchButtonId']%"); if (!input || !button || button.disabled) throw new Error("Search controls unavailable"); Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set.call(input, "%IdNumber%"); input.dataset.request = "%SearchRequest%"; input.dispatchEvent(new Event("input", {bubbles:true})); input.dispatchEvent(new Event("change", {bubbles:true})); window.__padOldDocumentSentinel = "%SearchRequest%"; setTimeout(function() { button.click(); }, 100); return JSON.stringify(context); }''' Result=> LoadState
    ON ERROR
        GOTO IdSearchFailed
    END
    Variables.ConvertJsonToCustomObject Json: LoadState CustomObject=> SearchContext
    # 新HTMLでJS状態が消えるため毎回初期化可。検索前ID/世代・開始時刻はPAD側で保持する。
    SET ListingState TO $'''waiting'''
    LOOP WaitIndex FROM 1 TO ListingPollLimit STEP 1
        WebAutomation.ExecuteJavascript BrowserInstance: IdBrowser Javascript: $'''function ExecuteScript() { const config = {startEpoch:%SearchContext['started']%, previousGeneration:"%SearchContext['previousGeneration']%", previousDocument:"%SearchContext['documentToken']%", requireNewDocument:"%SearchTransport%" === "navigate", currentTimeOrigin:performance.timeOrigin, textArea:"%SiteConfig['ArrivalAreaSelector']%", arrivalText:"%ArrivalText%", mode:"%WaitMode%", area:"%SiteConfig['ResultAreaSelector']%", links:"%SiteConfig['AreaLinkSelector']%", statusId:"%SiteConfig['SearchStatusId']%", idAttribute:"%SiteConfig['ResultIdAttribute']%", requestAttribute:"%SiteConfig['ResultRequestAttribute']%", generationAttribute:"%SiteConfig['ResultGenerationAttribute']%", stateAttribute:"%SiteConfig['StateAttribute']%", ready:"%SiteConfig['ReadyState']%", empty:"%SiteConfig['EmptyState']%", failed:"%SiteConfig['FailedState']%", maximumSeconds:%MaximumWaitSeconds%, stableSeconds:%StableSeconds%, minimumSeconds:%MinimumWaitSeconds%, fixedSeconds:%FixedWaitSeconds%}; function createWaiter(root, config, idNumber, request, clock = () => Date.now()) { const started = config.startEpoch ?? clock(); const original = root.querySelector(config.area); const generation = config.previousGeneration ?? original?.getAttribute(config.generationAttribute); let previous = null; let unchangedSince = started; let arrivalObservedAt = null; const waiter = {acceptedLinks:null, acceptedRequest:null, acceptedGeneration:null}; waiter.getAccepted = (expectedId, expectedRequest) => { const area = root.querySelector(config.area); if (!waiter.acceptedLinks || expectedId !== idNumber || expectedRequest !== waiter.acceptedRequest || !area || area.getAttribute(config.idAttribute) !== idNumber || area.getAttribute(config.requestAttribute) !== request || area.getAttribute(config.generationAttribute) !== waiter.acceptedGeneration) throw new Error("Search identity or readiness changed"); return waiter.acceptedLinks.map(link => ({...link})); }; waiter.poll = () => { const current = clock(); const elapsed = (current - started) / 1000; const area = root.querySelector(config.area); const status = root.getElementById(config.statusId); const documentToken = root.documentElement?.dataset.documentToken || String(config.currentTimeOrigin || ""); const documentChanged = Boolean(config.previousDocument && documentToken && documentToken !== config.previousDocument); const identity = (!config.requireNewDocument || documentChanged) && area && area.getAttribute(config.idAttribute) === idNumber && area.getAttribute(config.requestAttribute) === request && area.getAttribute(config.generationAttribute) !== generation; const statusIdentity = status && status.getAttribute(config.idAttribute) === idNumber && status.getAttribute(config.requestAttribute) === request; const state = statusIdentity ? status.getAttribute(config.stateAttribute) : null; const links = identity ? Array.from(area.querySelectorAll(config.links)).filter(anchor => anchor.getAttribute(config.idAttribute) === idNumber && anchor.getAttribute(config.requestAttribute) === request).map(anchor => ({pdfId:anchor.dataset.pdfId || "", sourceName:anchor.textContent.trim(), url:anchor.href})) : []; const signature = JSON.stringify(links.map(link => [link.sourceName, link.url])); if (signature !== previous || !identity) {previous = signature; unchangedSince = current;} const arrival = config.textArea ? root.querySelector(config.textArea) : null; const textSeen = Boolean(config.arrivalText && identity && arrival && arrival.getAttribute(config.idAttribute) === idNumber && arrival.getAttribute(config.requestAttribute) === request && arrival.getAttribute(config.generationAttribute) === area.getAttribute(config.generationAttribute) && arrival.textContent.includes(config.arrivalText)); if (!textSeen) arrivalObservedAt = null; else if (arrivalObservedAt === null) arrivalObservedAt = current; const stable = (current - (config.mode === "text-stable" ? Math.max(unchangedSince, arrivalObservedAt ?? current) : unchangedSince)) / 1000; const result = {state:"waiting", mode:config.mode, reason:!area ? "area_missing" : !identity ? "new_result_identity_unconfirmed" : config.mode === "text-stable" && !textSeen ? "arrival_text_missing" : links.length ? "collecting" : "zero_unconfirmed", elapsed, stable, textSeen, count:links.length, documentChanged, documentToken, previousDocument:config.previousDocument || "", warning:config.mode === "explicit" ? "explicit_marker" : "heuristic_not_complete"}; if ((!config.requireNewDocument || documentChanged) && statusIdentity && state === config.failed) {result.state = "failed"; result.reason = "explicit_failure";} else if (identity && state === config.empty && links.length === 0) {result.state = "ready"; result.reason = "explicit_empty";} else if (identity && config.mode === "explicit" && state === config.ready && links.length > 0) {result.state = "ready"; result.reason = "explicit_ready";} else if (identity && links.length > 0 && elapsed >= config.minimumSeconds && config.mode === "stable" && stable >= config.stableSeconds) {result.state = "ready"; result.reason = "nonzero_signature_stable";} else if (identity && textSeen && links.length > 0 && elapsed >= config.minimumSeconds && config.mode === "text-stable" && stable >= config.stableSeconds) {result.state = "ready"; result.reason = "arrival_text_then_nonzero_stable";} else if (identity && links.length > 0 && elapsed >= Math.max(config.minimumSeconds, config.fixedSeconds) && config.mode === "fixed") {result.state = "ready"; result.reason = "fixed_comparison_only";} if (elapsed >= config.maximumSeconds) {result.state = "timeout"; result.reason = links.length ? "deadline_exceeded" : "zero_or_identity_unconfirmed_timeout";} if (result.state === "ready") {waiter.acceptedLinks = links.map(link => ({...link})); waiter.acceptedRequest = request; waiter.acceptedGeneration = area.getAttribute(config.generationAttribute);} return result; }; return waiter; } if (!window.__padWait || window.__padWait.searchRequest !== "%SearchRequest%") { window.__padWait = createWaiter(document, config, "%IdNumber%", "%SearchRequest%"); window.__padWait.searchRequest = "%SearchRequest%"; } const state = window.__padWait.poll(); return JSON.stringify(state); }''' Result=> WaitResultJson
        ON ERROR
            GOTO NavigationPollRetry
        END
        Variables.ConvertJsonToCustomObject Json: WaitResultJson CustomObject=> WaitDecision
        IF WaitDecision['state'] <> $'''waiting''' THEN
            SET ListingState TO WaitDecision['state']
            EXIT LOOP
        END
        GOTO WaitPollDelay
        LABEL NavigationPollRetry
        ERROR => NavigationPollError Reset: True
        SET ListingState TO $'''waiting'''
        LABEL WaitPollDelay
        WAIT PollSeconds
    END
    GOTO IdSearchChecked
    LABEL IdSearchFailed
    ERROR => IdSearchError Reset: True
    SET ListingState TO $'''failed'''
    LABEL IdSearchChecked
    IF ListingState <> $'''ready''' THEN
        Variables.IncreaseVariable Value: PageFailureCount IncrementValue: 1
        SET IdLog TO $'''%IdLog%
        (。•́ - •̀。) ページ読取失敗 (一覧取得エラーまたは待機時間超過)'''
    ELSE
        WebAutomation.ExecuteJavascript BrowserInstance: IdBrowser Javascript: $'''function ExecuteScript() { function createIdPlan(links, words, idNumber, runStamp, origin) { const jobs = []; const seen = new Set(); const matchedWords = new Set(); const logGroupWords = []; const logGroups = []; for (const link of links) { const matches = words.filter(word => link.sourceName.includes(word)); if (!matches.length || !/_[0-9]{4}-[0-9]{2}-[0-9]{2}[.]pdf$/.test(link.sourceName) || new URL(link.url).origin !== origin) continue; matches.forEach(word => matchedWords.add(word)); const identity = JSON.stringify([link.url, link.sourceName]); if (seen.has(identity)) continue; seen.add(identity); const name = idNumber + "_" + link.sourceName; const searchWord = matches.join(" / "); let logGroupIndex = logGroupWords.indexOf(searchWord); if (logGroupIndex < 0) { logGroupIndex = logGroups.length; logGroupWords.push(searchWord); logGroups.push(String.fromCharCode(10) + "        [検索ワード] " + searchWord); } jobs.push({id: link.pdfId, name, url: link.url, searchWord, logGroupIndex, plannedKey: idNumber + " | " + searchWord + " | " + name, key: "PAD_SESSION_" + runStamp + "_" + idNumber + "_" + jobs.length + ".pdf", state: "queued", reason: "", bytes: 0, stable: 0, lastSize: -1, receipt: null}); } const noMatchWords = words.filter(word => !matchedWords.has(word)); const noMatchLog = noMatchWords.map(word => String.fromCharCode(10) + "        [検索ワード] " + word + String.fromCharCode(10) + "            ― 該当なし").join(""); return {jobs, logGroups, noMatchWords, noMatchCount: noMatchWords.length, noMatchLog}; } const plan=createIdPlan(window.__padWait.getAccepted("%IdNumber%","%SearchRequest%"),(%SearchWordsJson%).words,"%IdNumber%","%RunStamp%",location.origin); Object.assign(plan,{idNumber:"%IdNumber%",runStamp:"%RunStamp%",request:"%SearchRequest%",pageLog:"",startFailure:""}); return JSON.stringify(plan); }''' Result=> IdPlanJson
        ON ERROR
            GOTO IdPlanFailed
        END
        Variables.ConvertJsonToCustomObject Json: IdPlanJson CustomObject=> IdPlan
        ON ERROR
            GOTO IdPlanFailed
        END
        SET TargetLinks TO IdPlan['jobs']
        Variables.IncreaseVariable Value: ExpectedCount IncrementValue: TargetLinks.Count
        LOOP FOREACH PlannedLink IN TargetLinks
            Variables.AddItemToList Item: PlannedLink['plannedKey'] List: PlannedKeys
        END
        IF DownloadMode = $'''browser''' THEN
            Scripting.RunVBScript.RunVBScript VBScriptCode: $'''Option Explicit
Dim files, entry, downloadRoot, outputRoot, prefix, downloads, output, separator
Set files = CreateObject("Scripting.FileSystemObject")
downloadRoot = "%BrowserDownloadFolder%"
outputRoot = "%RunFolder.FullName%"
prefix = "PAD_SESSION_%RunStamp%_"
downloads = ""
separator = ""
For Each entry In files.GetFolder(downloadRoot).Files
    If StrComp(Left(entry.Name, Len(prefix)), prefix, vbTextCompare) = 0 Then
        downloads = downloads & separator & "{""name"":" & Quote(entry.Name) & ",""size"":" & CStr(entry.Size) & "}"
        separator = ","
    End If
Next
output = ""
separator = ""
For Each entry In files.GetFolder(outputRoot).Files
    output = output & separator & "{""name"":" & Quote(entry.Name) & "}"
    separator = ","
Next
WScript.Echo "{""downloads"":[" & downloads & "],""output"":[" & output & "]}"
Function Quote(value)
    Dim index, character, code, result
    result = ""
    For index = 1 To Len(value)
        character = Mid(value, index, 1)
        code = AscW(character)
        If character = Chr(34) Or character = Chr(92) Then
            result = result & Chr(92) & character
        ElseIf code >= 0 And code < 32 Then
            result = result & Chr(92) & "u" & Right("0000" & Hex(code), 4)
        Else
            result = result & character
        End If
    Next
    Quote = Chr(34) & result & Chr(34)
End Function''' ScriptOutput=> DiskSnapshotJson
            ON ERROR
                GOTO IdStartFailed
            END
            WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { const plan=%IdPlanJson%; const ack=JSON.parse(window.__padPipeline.accept(plan,%DiskSnapshotJson%)); if (ack.idNumber!==plan.idNumber || ack.request!==plan.request || ack.count!==plan.jobs.length || JSON.stringify(ack.keys)!==JSON.stringify(plan.jobs.map(job=>job.key))) throw new Error("Start acknowledgement mismatch"); return "accepted"; }''' Result=> IdStartAck
            ON ERROR
                GOTO IdStartFailed
            END
            IF IdStartAck <> $'''accepted''' THEN
                SET IdPlan['startFailure'] TO $'''取得開始の応答を確認できません'''
            END
        END
    END
    GOTO IdCollected
    LABEL IdStartFailed
    ERROR => IdStartError Reset: True
    SET IdPlan['startFailure'] TO IdStartError.Message
    GOTO IdCollected
    LABEL IdPlanFailed
    ERROR => IdPlanError Reset: True
    Variables.IncreaseVariable Value: PageFailureCount IncrementValue: 1
    SET IdLog TO $'''
        (。•́ - •̀。) ページ読取失敗 (一覧取得エラーまたは待機時間超過)'''
    LABEL IdCollected
    SET IdPlan['pageLog'] TO IdLog
    Variables.AddItemToList Item: IdPlan List: AllIdPlans
END
Variables.ConvertJsonToCustomObject Json: $'''{}''' CustomObject=> AllPlansEnvelope
SET AllPlansEnvelope['plans'] TO AllIdPlans
Variables.ConvertCustomObjectToJson CustomObject: AllPlansEnvelope Json=> AllIdPlansJson
IF DownloadMode = $'''browser''' THEN
    WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { return window.__padPipeline.seal((%AllIdPlansJson%).plans); }''' Result=> PipelineState
    ON ERROR
        GOTO PipelineFailed
    END
    IF PipelineState <> $'''sealed''' THEN
        GOTO PipelineRejected
    END
    SET CompletionPollLimit TO ExpectedCount
    Variables.IncreaseVariable Value: CompletionPollLimit IncrementValue: 360
    LOOP DiskIndex FROM 1 TO CompletionPollLimit STEP 1
        Scripting.RunVBScript.RunVBScript VBScriptCode: $'''Option Explicit
Dim files, entry, downloadRoot, outputRoot, prefix, downloads, output, separator
Set files = CreateObject("Scripting.FileSystemObject")
downloadRoot = "%BrowserDownloadFolder%"
outputRoot = "%RunFolder.FullName%"
prefix = "PAD_SESSION_%RunStamp%_"
downloads = ""
separator = ""
For Each entry In files.GetFolder(downloadRoot).Files
    If StrComp(Left(entry.Name, Len(prefix)), prefix, vbTextCompare) = 0 Then
        downloads = downloads & separator & "{""name"":" & Quote(entry.Name) & ",""size"":" & CStr(entry.Size) & "}"
        separator = ","
    End If
Next
output = ""
separator = ""
For Each entry In files.GetFolder(outputRoot).Files
    output = output & separator & "{""name"":" & Quote(entry.Name) & "}"
    separator = ","
Next
WScript.Echo "{""downloads"":[" & downloads & "],""output"":[" & output & "]}"
Function Quote(value)
    Dim index, character, code, result
    result = ""
    For index = 1 To Len(value)
        character = Mid(value, index, 1)
        code = AscW(character)
        If character = Chr(34) Or character = Chr(92) Then
            result = result & Chr(92) & character
        ElseIf code >= 0 And code < 32 Then
            result = result & Chr(92) & "u" & Right("0000" & Hex(code), 4)
        Else
            result = result & character
        End If
    Next
    Quote = Chr(34) & result & Chr(34)
End Function''' ScriptOutput=> DiskSnapshotJson
        ON ERROR
            GOTO PipelineFailed
        END
        WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { return JSON.stringify(window.__padPipeline.poll(%DiskSnapshotJson%,%DiskIndex% === %CompletionPollLimit%)); }''' Result=> PipelinePollJson
        ON ERROR
            GOTO PipelineFailed
        END
        Variables.ConvertJsonToCustomObject Json: PipelinePollJson CustomObject=> PipelinePoll
        IF PipelinePoll['done'] = True THEN
            EXIT LOOP
        END
        WAIT 0.5
    END
END
GOTO PipelineChecked
LABEL PipelineFailed
ERROR => PipelineError Reset: True
SET PipelineFailure TO PipelineError.Message
GOTO PipelineCancel
LABEL PipelineRejected
SET PipelineFailure TO $'''全体の予定一覧を照合できません'''
LABEL PipelineCancel
WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { return window.__padPipeline ? window.__padPipeline.cancel("PAD monitoring failed") : "missing"; }''' Result=> PipelineState
ON ERROR
    GOTO PipelineChecked
END
LABEL PipelineChecked
Scripting.RunVBScript.RunVBScript VBScriptCode: $'''Option Explicit
Dim files, entry, downloadRoot, outputRoot, prefix, downloads, output, separator
Set files = CreateObject("Scripting.FileSystemObject")
downloadRoot = "%BrowserDownloadFolder%"
outputRoot = "%RunFolder.FullName%"
prefix = "PAD_SESSION_%RunStamp%_"
downloads = ""
separator = ""
For Each entry In files.GetFolder(downloadRoot).Files
    If StrComp(Left(entry.Name, Len(prefix)), prefix, vbTextCompare) = 0 Then
        downloads = downloads & separator & "{""name"":" & Quote(entry.Name) & ",""size"":" & CStr(entry.Size) & ",""modified"":" & Quote(CStr(CDbl(entry.DateLastModified))) & "}"
        separator = ","
    End If
Next
output = ""
separator = ""
For Each entry In files.GetFolder(outputRoot).Files
    output = output & separator & "{""name"":" & Quote(entry.Name) & "}"
    separator = ","
Next
WScript.Echo "{""downloads"":[" & downloads & "],""output"":[" & output & "]}"
Function Quote(value)
    Dim index, character, code, result
    result = ""
    For index = 1 To Len(value)
        character = Mid(value, index, 1)
        code = AscW(character)
        If character = Chr(34) Or character = Chr(92) Then
            result = result & Chr(92) & character
        ElseIf code >= 0 And code < 32 Then
            result = result & Chr(92) & "u" & Right("0000" & Hex(code), 4)
        Else
            result = result & character
        End If
    Next
    Quote = Chr(34) & result & Chr(34)
End Function''' ScriptOutput=> ValidationSnapshotJson
ON ERROR
    GOTO RunProcessingFailed
END
SET AllPlansEnvelope['downloadRoot'] TO BrowserDownloadFolder
SET AllPlansEnvelope['outputRoot'] TO RunFolder.FullName
SET AllPlansEnvelope['context'] TO IdLogContext
SET AllPlansEnvelope['mode'] TO DownloadMode
SET AllPlansEnvelope['runStamp'] TO RunStamp
SET AllPlansEnvelope['pipelineFailure'] TO PipelineFailure
Variables.ConvertCustomObjectToJson CustomObject: AllPlansEnvelope Json=> AllIdPlansJson
WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { function prepareValidation(plans, mode, downloadRoot, outputRoot, pipelineFailure, getResults, disk) { const tasks = []; const keys = new Set(); const names = new Set(); for (const plan of plans) { if (plan.pageLog) continue; let results = []; let error = pipelineFailure || plan.startFailure; if (mode === "browser" && !error) { try { results = getResults(plan); } catch { error = "取得結果を確認できません"; } } for (const job of plan.jobs) { if (!/^[^\\\\/:*?"<>|\\x00-\\x1f]+[.]pdf$/.test(job.name) || !/^PAD_SESSION_[A-Za-z0-9_-]+[.]pdf$/.test(job.key) || keys.has(job.key) || names.has(job.name.toLowerCase())) throw new Error("Invalid or duplicate job"); keys.add(job.key); names.add(job.name.toLowerCase()); const result = results.find(value => value.key === job.key); const matched = result && ["name","url","plannedKey","searchWord","logGroupIndex"].every(field => result[field] === job[field]); const candidates = disk.downloads.filter(file => file.name.toLowerCase().startsWith(job.key.toLowerCase())); const candidate = candidates[0]; const eligible = !error && (mode === "http" || (matched && result.state === "ready" && result.bytes > 0 && candidates.length === 1 && candidate.name === job.key && candidate.size === result.bytes && Number(candidate.modified) > 0)); tasks.push({...job, index:tasks.length, idNumber:plan.idNumber, bytes:result?.bytes || 0, modified:candidate?.modified || "0", receipt:result?.receipt || null, eligible, validated:false, reason:eligible ? "PDFとして検証できません" : "ブラウザー保存を確認できません", sourcePath:mode === "browser" ? downloadRoot + "\\\\" + job.key : outputRoot + "\\\\" + job.name, targetPath:outputRoot + "\\\\" + job.name}); } } return tasks; } const data=%AllIdPlansJson%; const tasks=prepareValidation(data.plans,data.mode,data.downloadRoot,data.outputRoot,data.pipelineFailure,plan=>JSON.parse(window.__padPipeline.results(plan)),%ValidationSnapshotJson%); return JSON.stringify(tasks); }''' Result=> ValidationTasksJson
ON ERROR
    GOTO RunProcessingFailed
END
Variables.ConvertJsonToCustomObject Json: ValidationTasksJson CustomObject=> ValidationTasks
ON ERROR
    GOTO RunProcessingFailed
END
Variables.CreateNewList List=> ValidatedTasks
LOOP FOREACH CurrentLink IN ValidationTasks
    IF CurrentLink['eligible'] = True THEN
        SET ValidationPath TO CurrentLink['sourcePath']
        IF (File.IfFile.Exists File: CurrentLink['targetPath']) THEN
            GOTO BulkCandidateConflict
        END
        IF DownloadMode = $'''http''' THEN
            Web.DownloadFromWeb.DownloadToFile Url: CurrentLink['url'] FilePath: CurrentLink['targetPath'] ConnectionTimeout: 130 FollowRedirection: True ClearCookies: False UserAgent: $'''PAD-PDF-Lab''' Encoding: Web.Encoding.AutoDetect AcceptUntrustedCertificates: False DownloadedFile=> DownloadedFile
            ON ERROR
                GOTO BulkDownloadFailed
            END
        ELSE
            Folder.GetFiles Folder: BrowserDownloadFolder FileFilter: $'''%CurrentLink['key']%*''' IncludeSubfolders: False FailOnAccessDenied: True SortBy1: Folder.SortBy.NoSort SortDescending1: False SortBy2: Folder.SortBy.NoSort SortDescending2: False SortBy3: Folder.SortBy.NoSort SortDescending3: False Files=> DiskCandidates
            ON ERROR
                GOTO BulkCandidateConflict
            END
            IF DiskCandidates.Count <> 1 THEN
                GOTO BulkCandidateConflict
            END
            IF DiskCandidates[0].FullName <> ValidationPath OR DiskCandidates[0].Size <> CurrentLink['bytes'] OR DiskCandidates[0].Size <= 0 THEN
                GOTO BulkCandidateConflict
            END
        END
        File.ReadTextFromFile.ReadText File: ValidationPath Encoding: File.TextFileEncoding.UTF8 Content=> PdfSignatureText
        ON ERROR
            GOTO BulkInvalidPdf
        END
        Text.ParseText.RegexParse Text: PdfSignatureText TextToFind: $'''\A\x25PDF-(?:1[.]\d|2[.]0)''' StartingPosition: 0 IgnoreCase: False Matches=> PdfHeaderMatches
        Text.ParseText.RegexParse Text: PdfSignatureText TextToFind: $'''\x25\x25EOF\s*\z''' StartingPosition: 0 IgnoreCase: False Matches=> PdfEndMatches
        IF PdfHeaderMatches.Count <> 1 OR PdfEndMatches.Count <> 1 THEN
            GOTO BulkInvalidPdf
        END
        Pdf.ExtractTextFromPDF.ExtractText PDFFile: ValidationPath DetectLayout: False ExtractedText=> ParsedPdfText
        ON ERROR
            GOTO BulkInvalidPdf
        END
        SET CurrentLink['validated'] TO 1
        GOTO BulkTaskRecorded
        LABEL BulkCandidateConflict
        SET CurrentLink['reason'] TO $'''候補不明・既存名のため操作なし'''
        GOTO BulkTaskRecorded
        LABEL BulkDownloadFailed
        ERROR => DownloadError Reset: True
        SET CurrentLink['reason'] TO $'''サーバーから取得できませんでした'''
        GOTO BulkTaskRecorded
        LABEL BulkInvalidPdf
        ERROR => PdfValidationError Reset: True
    END
    LABEL BulkTaskRecorded
    Variables.AddItemToList Item: CurrentLink List: ValidatedTasks
END
SET AllPlansEnvelope['tasks'] TO ValidatedTasks
Variables.ConvertCustomObjectToJson CustomObject: AllPlansEnvelope Json=> ValidatedTasksJson
SET BulkMoveFailed TO $'''true'''
SET BulkMovesJson TO $'''[]'''
WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { function buildMoveData(tasks, plans, mode, transfers) { const expected = plans.filter(plan => !plan.pageLog).flatMap(plan => plan.jobs.map(job => ({...job, idNumber:plan.idNumber}))); if (tasks.length !== expected.length || tasks.some((task,index) => task.index !== index || ["idNumber","key","name","plannedKey","url","searchWord","logGroupIndex"].some(field => task[field] !== expected[index][field]))) throw new Error("Validation manifest mismatch"); const literal = value => { if (/[\\r\\n\\x00]/.test(String(value))) throw new Error("Invalid script data"); return "\\"" + String(value).replaceAll("\\"","\\"\\"") + "\\""; }; const lines = ["taskCount = " + tasks.length, "ReDim tasks(" + Math.max(0,tasks.length-1) + ")"]; for (const task of tasks) { let validated = task.validated === 1; let reason = task.reason; if (validated && mode === "browser") { const current = transfers[task.key]; const receipt = task.receipt; if (!current || !receipt || current.state !== "save_requested" || ["bytes","started","received"].some(field => current[field] !== receipt[field])) { validated = false; reason = "移動前にブラウザー取得情報が変わりました"; } } lines.push("tasks(" + task.index + ") = Array(" + [task.index,validated ? "true":"false",task.name,task.key,task.bytes,reason,task.modified].map(literal).join(",") + ")"); } return lines.join("\\r\\n"); } const data=%ValidatedTasksJson%; return buildMoveData(data.tasks,data.plans,"%DownloadMode%",window.__padTransfers || {}); }''' Result=> BulkMoveData
ON ERROR
    GOTO BulkMovesFinished
END
Scripting.RunVBScript.RunVBScript VBScriptCode: $'''Option Explicit
Dim files, tasks, taskCount, taskIndex, task, outputRoot, downloadRoot, mode, reports, result, errorCode
Set files = CreateObject("Scripting.FileSystemObject")
outputRoot = "%RunFolder.FullName%"
downloadRoot = "%BrowserDownloadFolder%"
mode = "%DownloadMode%"
%BulkMoveData%
reports = ""
For taskIndex = 0 To taskCount - 1
    task = tasks(taskIndex)
    result = ""
    On Error Resume Next
    result = MoveOne(task)
    errorCode = Err.Number
    Err.Clear
    On Error GoTo 0
    If errorCode <> 0 Or result = "" Then result = Outcome(task(0), False, task(2), "保存先操作に失敗しました(移動済みの可能性あり)")
    If taskIndex > 0 Then reports = reports & ","
    reports = reports & result
Next
WScript.Echo "[" & reports & "]"

Function MoveOne(item)
    Dim sourcePath, sourceFile, sourceSize, entry, candidates, originalPath, finalName, targetPath, verifiedAt, stamp, destinationFile
    finalName = item(2)
    MoveOne = Outcome(item(0), False, finalName, item(5))
    If item(1) <> "true" Then Exit Function
    If files.GetFileName(finalName) <> finalName Or InStr(finalName, ":") > 0 Or Right(finalName, 4) <> ".pdf" Then Exit Function
    originalPath = files.BuildPath(outputRoot, finalName)
    If mode = "browser" Then
        If files.GetFileName(item(3)) <> item(3) Or InStr(item(3), ":") > 0 Then Exit Function
        sourcePath = files.BuildPath(downloadRoot, item(3))
        If files.FileExists(originalPath) Or files.FolderExists(originalPath) Then
            MoveOne = Outcome(item(0), False, finalName, "既存名を上書きしません")
            Exit Function
        End If
    Else
        sourcePath = originalPath
    End If
    If Not files.FileExists(sourcePath) Or Not files.FolderExists(outputRoot) Then
        MoveOne = Outcome(item(0), False, finalName, "元ファイルまたは保存先が見つかりません")
        Exit Function
    End If
    Set sourceFile = files.GetFile(sourcePath)
    sourceSize = sourceFile.Size
    If sourceSize <= 0 Then Exit Function
    If mode = "browser" Then
        If CStr(CDbl(sourceFile.DateLastModified)) <> CStr(item(6)) Then
            MoveOne = Outcome(item(0), False, finalName, "検証前からファイル更新日時が変化しました")
            Exit Function
        End If
        If sourceSize <> CDbl(item(4)) Then
            MoveOne = Outcome(item(0), False, finalName, "受信サイズが変化しました")
            Exit Function
        End If
        candidates = 0
        For Each entry In files.GetFolder(downloadRoot).Files
            If StrComp(Left(entry.Name, Len(item(3))), item(3), vbTextCompare) = 0 Then candidates = candidates + 1
        Next
        If candidates <> 1 Then
            MoveOne = Outcome(item(0), False, finalName, "受信候補が一意ではありません")
            Exit Function
        End If
    End If
    verifiedAt = Now
    stamp = CStr(Year(verifiedAt)) & Right("0" & Month(verifiedAt), 2) & Right("0" & Day(verifiedAt), 2) & "_" & Right("0" & Hour(verifiedAt), 2) & Right("0" & Minute(verifiedAt), 2) & Right("0" & Second(verifiedAt), 2)
    finalName = Left(finalName, Len(finalName) - 4) & "(DL=" & stamp & ").pdf"
    targetPath = files.BuildPath(outputRoot, finalName)
    If files.FileExists(targetPath) Or files.FolderExists(targetPath) Then
        MoveOne = Outcome(item(0), False, item(2), "日時付き既存名を上書きしません")
        Exit Function
    End If
    files.MoveFile sourcePath, targetPath
    If files.FileExists(sourcePath) Or Not files.FileExists(targetPath) Then Exit Function
    Set destinationFile = files.GetFile(targetPath)
    If destinationFile.Path <> targetPath Or destinationFile.Size <> sourceSize Then Exit Function
    MoveOne = Outcome(item(0), True, finalName, "")
End Function

Function Outcome(index, saved, name, reason)
    Outcome = "{""index"":" & index & ",""saved"":" & LCase(CStr(saved)) & ",""name"":" & Quote(name) & ",""reason"":" & Quote(reason) & "}"
End Function

Function Quote(value)
    Quote = Chr(34) & Replace(Replace(value, Chr(92), Chr(92) & Chr(92)), Chr(34), Chr(92) & Chr(34)) & Chr(34)
End Function''' ScriptOutput=> BulkMovesJson
ON ERROR
    GOTO BulkMovesFinished
END
SET BulkMoveFailed TO $'''false'''
LABEL BulkMovesFinished
WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { function aggregateResults(plans, tasks, moves, context, outputRoot, runStamp, moveFailure) { const expected = plans.filter(plan => !plan.pageLog).flatMap(plan => plan.jobs); if (expected.length !== tasks.length || tasks.some((task,index) => task.index !== index || task.plannedKey !== expected[index].plannedKey)) throw new Error("Result manifest mismatch"); const validMoves = !moveFailure && Array.isArray(moves) && moves.length === tasks.length && moves.every((move,index) => move.index === index && typeof move.saved === "boolean" && typeof move.name === "string" && typeof move.reason === "string"); const report = {success:0,failure:0,noMatch:0,plannedKeys:[],recordedKeys:[],savedNames:[],logs:[]}; let index = 0; for (const plan of plans) { const groups = [...plan.logGroups]; if (!plan.pageLog) { report.noMatch += plan.noMatchCount; for (const job of plan.jobs) { const task = tasks[index]; const move = validMoves ? moves[index] : null; const saved = task.validated === 1 && move?.saved === true && new RegExp("^" + job.name.slice(0,-4).replace(/[.*+?^${}()|[\\]\\\\]/g,"\\\\$&") + "\\\\(DL=[0-9]{8}_[0-9]{6}\\\\)\\\\.pdf$").test(move.name); const name = saved ? move.name : job.name; const reason = saved ? "" : (!task.validated ? task.reason : (!validMoves ? "一括移動の結果を確認できません(移動済みの可能性あり)" : move.reason || "保存結果が不正です")); report[saved ? "success":"failure"]++; if (saved) report.savedNames.push(name); report.plannedKeys.push(plan.idNumber + " | " + job.searchWord + " | " + name); report.recordedKeys.push(task.idNumber + " | " + task.searchWord + " | " + name); groups[job.logGroupIndex] += "\\n            " + (saved ? "〇 成功":"X 失敗") + "  |  " + name + (reason ? "(" + reason + ")":""); index++; } } report.logs.push({path:outputRoot + "\\\\" + plan.idNumber + "_ダウンロード結果_" + runStamp + ".txt",text:"=========================================================\\n【" + plan.idNumber + "】  " + context + (plan.pageLog || groups.join("") + plan.noMatchLog) + "\\n"}); } return report; } const data=%ValidatedTasksJson%; return JSON.stringify(aggregateResults(data.plans,data.tasks,%BulkMovesJson%,data.context,data.outputRoot,data.runStamp,"%BulkMoveFailed%" === "true")); }''' Result=> BulkReportJson
ON ERROR
    GOTO RunProcessingFailed
END
Variables.ConvertJsonToCustomObject Json: BulkReportJson CustomObject=> BulkReport
ON ERROR
    GOTO RunProcessingFailed
END
SET SuccessCount TO BulkReport['success']
SET FailureCount TO BulkReport['failure']
SET NoMatchCount TO BulkReport['noMatch']
SET PlannedKeys TO BulkReport['plannedKeys']
SET RecordedKeys TO BulkReport['recordedKeys']
SET SavedFileNames TO BulkReport['savedNames']
LOOP FOREACH IdReport IN BulkReport['logs']
    SET IdLog TO IdReport['text']
    File.WriteText File: IdReport['path'] TextToWrite: $'''%IdLog%''' AppendNewLine: False IfFileExists: File.IfFileExists.Overwrite Encoding: File.FileEncoding.UTF8
    ON ERROR
        GOTO IdLogWriteFailed
    END
    GOTO IdLogWritten
    LABEL IdLogWriteFailed
    ERROR => IdLogError Reset: True
    SET IdLog TO $'''%IdLog%
【ログ保存失敗】%IdLogError.Message%'''
    LABEL IdLogWritten
    SET DetailLog TO $'''%DetailLog%%IdLog%'''
END
**ENDREGION
**REGION 05 件数照合とログ原本保存
# 予定と結果の全キーを順番まで照合し、件数と実PDFも確認する。機械用ファイルは増やさない。
Folder.GetFiles Folder: RunFolder.FullName FileFilter: $'''*.pdf''' IncludeSubfolders: True FailOnAccessDenied: True SortBy1: Folder.SortBy.NoSort SortDescending1: False SortBy2: Folder.SortBy.NoSort SortDescending2: False SortBy3: Folder.SortBy.NoSort SortDescending3: False Files=> FinalPdfFiles
SET AllPlansEnvelope['plannedKeys'] TO PlannedKeys
SET AllPlansEnvelope['recordedKeys'] TO RecordedKeys
SET AllPlansEnvelope['savedNames'] TO SavedFileNames
Variables.ConvertCustomObjectToJson CustomObject: AllPlansEnvelope Json=> ReconcileInputJson
SET Reconciled TO $'''false'''
Scripting.RunVBScript.RunVBScript VBScriptCode: $'''Option Explicit
Dim files, entry, downloadRoot, outputRoot, prefix, downloads, output, separator
Set files = CreateObject("Scripting.FileSystemObject")
downloadRoot = "%BrowserDownloadFolder%"
outputRoot = "%RunFolder.FullName%"
prefix = "PAD_SESSION_%RunStamp%_"
downloads = ""
separator = ""
For Each entry In files.GetFolder(downloadRoot).Files
    If StrComp(Left(entry.Name, Len(prefix)), prefix, vbTextCompare) = 0 Then
        downloads = downloads & separator & "{""name"":" & Quote(entry.Name) & ",""size"":" & CStr(entry.Size) & "}"
        separator = ","
    End If
Next
output = ""
separator = ""
For Each entry In files.GetFolder(outputRoot).Files
    output = output & separator & "{""name"":" & Quote(entry.Name) & "}"
    separator = ","
Next
WScript.Echo "{""downloads"":[" & downloads & "],""output"":[" & output & "]}"
Function Quote(value)
    Dim index, character, code, result
    result = ""
    For index = 1 To Len(value)
        character = Mid(value, index, 1)
        code = AscW(character)
        If character = Chr(34) Or character = Chr(92) Then
            result = result & Chr(92) & character
        ElseIf code >= 0 And code < 32 Then
            result = result & Chr(92) & "u" & Right("0000" & Hex(code), 4)
        Else
            result = result & character
        End If
    Next
    Quote = Chr(34) & result & Chr(34)
End Function''' ScriptOutput=> DiskSnapshotJson
ON ERROR
    GOTO ReconcileFinished
END
WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { function reconcileResults(input, disk, expected, successes, failures, pdfCount) { const planned = input.plannedKeys; const recorded = input.recordedKeys; const names = input.savedNames; if (![planned, recorded, names, disk.output].every(Array.isArray)) return false; if (successes + failures !== expected || pdfCount !== successes || planned.length !== expected || recorded.length !== expected || names.length !== successes) return false; if (new Set(planned).size !== expected || new Set(names.map(name => name.toLowerCase())).size !== successes) return false; if (!planned.every((key, index) => key === recorded[index])) return false; const actual = new Map(); for (const file of disk.output) actual.set(file.name, (actual.get(file.name) || 0) + 1); return names.every(name => actual.get(name) === 1); } return String(reconcileResults(%ReconcileInputJson%,%DiskSnapshotJson%,%ExpectedCount%,%SuccessCount%,%FailureCount%,%FinalPdfFiles.Count%)); }''' Result=> Reconciled
ON ERROR
    GOTO ReconcileFinished
END
LABEL ReconcileFinished
DateTime.GetCurrentDateTime.Local DateTimeFormat: DateTime.DateTimeFormat.DateAndTime CurrentDateTime=> RunFinished
Text.ConvertDateTimeToText.FromCustomDateTime DateTime: RunFinished CustomFormat: $'''yyyy-MM-dd HH:mm:ss.fff''' Result=> FinishedText
SET SummaryText TO $'''全体:対象%ExpectedCount%件/成功%SuccessCount%件/失敗%FailureCount%件・ページ読取失敗%PageFailureCount%件・該当なし%NoMatchCount%件'''
IF WaitMode <> $'''explicit''' THEN
    SET RunLogContext TO $'''%RunLogContext%
[参考判定] %WaitMode%方式・完全件数未保証(判定後の追加は今回の対象に含めません)'''
END
SET LogDisplay TO $'''%SummaryText%
%RunLogContext%
実行終了:%FinishedText%
最終照合:%Reconciled%

%DetailLog%========================================================='''
File.WriteText File: ReportPath TextToWrite: LogDisplay AppendNewLine: False IfFileExists: File.IfFileExists.Overwrite Encoding: File.FileEncoding.UTF8
ON ERROR
    GOTO SummaryWriteFailed
END
GOTO SummaryWritten
LABEL SummaryWriteFailed
ERROR => SummaryWriteError Reset: True
SET LogDisplay TO $'''%LogDisplay%
【集計ログ保存失敗】%SummaryWriteError.Message%'''
LABEL SummaryWritten
**ENDREGION
**REGION 06 人による最終確認
# URLボタンは送信せず表示を保つ。振り分けを選んだ場合だけ独立の補助処理へ進み、表示欄の編集は原本へ書き戻さない。
SET ReviewSummary TO SummaryText
Variables.ConvertJsonToCustomObject Json: $'''{"crlf":"\\r\\n"}''' CustomObject=> ReviewLineBreaks
SET ReviewLogDisplay TO $'''%LogDisplay%%ReviewLineBreaks['crlf']%'''
SET ReviewButton TO $'''not_submitted'''
@@statistics_TextBlock: '1'
@@statistics_Input_Text: '1'
@@statistics_Action_OpenUrl: '1'
@@statistics_Action_Submit: '2'
Display.ShowCustomDialog CardTemplateJson: '''{
  "type": "AdaptiveCard",
  "version": "1.4",
  "id": "AdaptiveCard",
  "body": [
    { "type": "TextBlock", "id": "Summary", "text": "${Summary_Text}", "wrap": true },
    { "type": "Input.Text", "id": "LogText", "label": "確認・コピー用(編集してもログ原本は変わりません)", "isMultiline": true, "value": "${LogText_Value}" }
  ],
  "actions": [
    { "type": "Action.OpenUrl", "id": "OpenFolder", "title": "保存先を開く", "url": "${FolderUri_Url}" },
    { "type": "Action.Submit", "id": "Finish", "title": "そのまま終了", "associatedInputs": "none" },
    { "type": "Action.Submit", "id": "Sort", "title": "振り分けて終了", "associatedInputs": "none" }
  ],
  "FormTitle": "PAD PDF Lab 結果"
}''' CustomFormData=> ReviewOnlyFormData ButtonPressed=> ReviewButton @Summary_Text: ReviewSummary @FolderUri_Url: RunFolderUri @LogText_Value: ReviewLogDisplay
ON ERROR
    GOTO RunReviewClosed
END
IF ReviewButton = $'''Sort''' THEN
    SET SortSourceFolder TO RunFolder.FullName
    GOTO StartPdfSort
END
GOTO RunReviewDone
LABEL RunReviewClosed
ERROR => ReviewDialogError Reset: True
GOTO RunReviewDone
**ENDREGION

LABEL PreparationBrowserFailed
ERROR => PreparationBrowserError Reset: True
SET InputOutcome TO $'''error_terminated'''
SET FatalMessage TO $'''ブラウザーの開始準備を続行できないため、エラー終了しました。
サイトの起動状態とEdge・PADの接続をご確認ください。PDFの取得は開始していません。'''
GOTO ShowFatalNotice
LABEL BrowserStartupFailed
ERROR => BrowserOperationError Reset: True
SET FatalMessage TO $'''ブラウザーを操作できないため、エラー終了しました。
サイトの起動状態、ログイン状態、EdgeとPADの接続をご確認ください。原因がログインとは限りません。'''
GOTO RunFatalNotice
LABEL BrowserSessionUnavailable
SET FatalMessage TO $'''ログイン・セッションの準備完了を確認できず、待機上限に達したため、エラー終了しました。
サイトの起動状態とログイン状態を確認してから、再実行してください。'''
GOTO RunFatalNotice
LABEL RunProcessingFailed
ERROR => ProcessingError Reset: True
SET FatalMessage TO $'''ブラウザー操作、取得結果の確認、または結果の作成を続行できないため、エラー終了しました。
サイト・ログイン・EdgeとPADの接続、および保存先の状態をご確認ください。'''
LABEL RunFatalNotice
SET InputOutcome TO $'''error_terminated'''
SET FatalMessage TO $'''%FatalMessage%

保存先:%RunFolder.FullName%
保存済みファイルは削除していません。開始済みのダウンロードが残っている場合があります。
保存先とブラウザーのダウンロード状況を確認してから再実行してください。'''
File.WriteText File: DetailLogPath TextToWrite: FatalMessage AppendNewLine: True IfFileExists: File.IfFileExists.Append Encoding: File.FileEncoding.UTF8
ON ERROR
    GOTO RunFatalLogFailed
END
GOTO ShowFatalNotice
LABEL RunFatalLogFailed
ERROR => FatalLogError Reset: True
SET FatalMessage TO $'''%FatalMessage%
注意:エラー終了のログも保存できませんでした。画面の内容をご確認ください。'''
LABEL ShowFatalNotice
IF PreparationBrowserReady = True THEN
    WebAutomation.ExecuteJavascript BrowserInstance: Browser Javascript: $'''function ExecuteScript() { if (window.__padPreparationPanel) window.__padPreparationPanel.remove(); delete window.__padPreparationPanel; return "cleared"; }''' Result=> PreparationNoticeState
    ON ERROR
        GOTO PreparationFatalCleanup
    END
    GOTO PreparationFatalCleanupDone
    LABEL PreparationFatalCleanup
    ERROR => PreparationNoticeError Reset: True
    LABEL PreparationFatalCleanupDone
END
@@statistics_TextBlock: '1'
@@statistics_Action_Submit: '1'
Display.ShowCustomDialog CardTemplateJson: '''{
  "type": "AdaptiveCard",
  "version": "1.4",
  "id": "AdaptiveCard",
  "body": [
    {
      "type": "TextBlock",
      "id": "Failure",
      "text": "${Failure_Text}",
      "wrap": true
    }
  ],
  "actions": [
    {
      "type": "Action.Submit",
      "id": "Close",
      "title": "確認して終了",
      "associatedInputs": "none"
    }
  ],
  "FormTitle": "PDF取得|エラー終了"
}''' CustomFormData=> FatalFormData ButtonPressed=> FatalButton @Failure_Text: FatalMessage
ON ERROR
    GOTO RunFatalExit
END
LABEL RunFatalExit
EXIT Code: 1

**REGION 07 任意のPDF振り分け補助処理
LABEL StartPdfSort
SET SortParentFolder TO $''''''
SET SortIdDigits TO $''''''
SET SortChildFolder TO $''''''
SET SortIncludeLogs TO $'''true'''
SET SortInputNotice TO $'''移動元直下に残っているPDFと、選択時はID別ログを、IDフォルダー内へ移動します。不足するIDフォルダーと指定した子フォルダーは作成します。上書きはしません。'''
LABEL ShowSortInput
SET SortInputButton TO $'''not_submitted'''
@@statistics_TextBlock: '3'
@@statistics_Input_Text: '3'
@@statistics_Input_Toggle: '1'
@@statistics_Action_Submit: '2'
Display.ShowCustomDialog CardTemplateJson: '''{
  "type": "AdaptiveCard",
  "version": "1.4",
  "id": "AdaptiveCard",
  "body": [
    { "type": "TextBlock", "id": "Notice", "text": "${Notice_Text}", "wrap": true },
    { "type": "TextBlock", "id": "Source", "text": "移動元:${Source_Text}", "wrap": true },
    { "type": "Input.Text", "id": "Parent", "label": "振り分け先の親フォルダー(既存の絶対パス)", "value": "${Parent_Value}", "isRequired": true, "maxLength": 1024 },
    { "type": "Input.Text", "id": "Digits", "label": "ファイル名先頭からIDとして読む桁数(例:0007なら4)", "value": "${Digits_Value}", "isRequired": true, "maxLength": 3 },
    { "type": "Input.Text", "id": "Child", "label": "IDフォルダーからの相対パス", "value": "${Child_Value}", "isRequired": true, "maxLength": 32767 },
    { "type": "Input.Toggle", "id": "IncludeLogs", "title": "ログも振り分ける(ID別のダウンロード結果のみ)", "value": "${IncludeLogs_Value}", "valueOn": "true", "valueOff": "false" },
    { "type": "TextBlock", "id": "ChildExample", "text": "例:保管/2026年度/報告書。IDフォルダーの中から指定します。", "wrap": true }
  ],
  "actions": [
    { "type": "Action.Submit", "id": "SortOK", "title": "振り分け開始" },
    { "type": "Action.Submit", "id": "SortCancel", "title": "キャンセル", "associatedInputs": "none" }
  ],
  "FormTitle": "PDF振り分けの設定"
}''' CustomFormData=> SortInputData ButtonPressed=> SortInputButton @Notice_Text: SortInputNotice @Source_Text: SortSourceFolder @Parent_Value: SortParentFolder @Digits_Value: SortIdDigits @Child_Value: SortChildFolder @IncludeLogs_Value: SortIncludeLogs
ON ERROR
    GOTO RunSortInputClosed
END
IF SortInputButton <> $'''SortOK''' THEN
    GOTO RunReviewDone
END
SET SortParentFolder TO SortInputData['Parent']
SET SortIdDigits TO SortInputData['Digits']
SET SortChildFolder TO SortInputData['Child']
SET SortIncludeLogs TO SortInputData['IncludeLogs']
Variables.ConvertJsonToCustomObject Json: $'''{"crlf":"\\r\\n"}''' CustomObject=> SortLineBreaks
SET SortInputText TO $'''%SortSourceFolder%%SortLineBreaks['crlf']%%SortParentFolder%%SortLineBreaks['crlf']%%SortIdDigits%%SortLineBreaks['crlf']%%SortChildFolder%%SortLineBreaks['crlf']%%SortIncludeLogs%'''
SET SortInputFile TO $''''''
File.GetTempPath TempFile=> SortInputFile
ON ERROR
    GOTO SortExecutionFailed
END
File.WriteText File: SortInputFile TextToWrite: SortInputText AppendNewLine: False IfFileExists: File.IfFileExists.Overwrite Encoding: File.FileEncoding.UTF8
ON ERROR
    GOTO SortExecutionFailed
END
Scripting.RunVBScript.RunVBScript VBScriptCode: $'''Option Explicit
Dim files, stream, inputText, values, sourceRoot, parentRoot, digitsText, childName, digits, check, slash, childParts, part
Dim snapshot, snapshotPaths, entry, originalPath, expectedSize, fileName, extractedId, targetPath, reason, errorCode
Dim total, succeeded, failed, lines, itemLine, summary, header, reportPath, reportFile, reportName, currentPlace, targetPlace, stamp, reportError
Dim includeLogs, logPattern, reportTime, finalPath, finalText, savedText
Set files = CreateObject("Scripting.FileSystemObject")
Set stream = CreateObject("ADODB.Stream")
stream.Type = 2
stream.Charset = "utf-8"
stream.Open
stream.LoadFromFile "%SortInputFile.FullName%"
inputText = stream.ReadText
stream.Close
values = Split(Replace(Replace(inputText, vbCrLf, vbLf), vbCr, vbLf), vbLf)
If UBound(values) <> 3 And UBound(values) <> 4 Then Finish "invalid", "振り分け入力の形式が不正です。", "", ""
includeLogs = False
If UBound(values) = 4 Then
    If values(4) <> "true" And values(4) <> "false" Then Finish "invalid", "ログ振り分けの選択が不正です。", "", ""
    includeLogs = (values(4) = "true")
End If
Set logPattern = New RegExp
logPattern.Pattern = "^.+_ダウンロード結果_[0-9]{8}_[0-9]{6}_[0-9]{3}[.]txt$"
sourceRoot = values(0)
parentRoot = Trim(values(1))
digitsText = Trim(values(2))
childName = Trim(values(3))
slash = Chr(92)
childName = Replace(childName, "/", slash)
Set check = New RegExp
check.Pattern = Replace("^(?:[A-Za-z]:~|~~[^~/:*?""<>|]+~[^~/:*?""<>|]+(?:~|$))", "~", slash & slash)
If Not check.Test(parentRoot) Then Finish "invalid", "振分先の親フォルダーを絶対パスで入力してください。", "", ""
If Not files.FolderExists(parentRoot) Then Finish "invalid", "振分先の親フォルダーが見つかりません。", "", ""
check.Pattern = "^[0-9]{1,3}$"
If Not check.Test(digitsText) Then Finish "invalid", "ID桁数は1~255の整数で入力してください。", "", ""
digits = CLng(digitsText)
If digits < 1 Or digits > 255 Then Finish "invalid", "ID桁数は1~255の整数で入力してください。", "", ""
If Len(childName) = 0 Then Finish "invalid", "IDフォルダーからの相対パスを入力してください。", "", ""
childParts = Split(childName, slash)
For Each part In childParts
    If Not ValidName(part) Then Finish "invalid", "IDフォルダーからの相対パスを入力してください。絶対パス・空の階層・点のみ・末尾空白は使えません。", "", ""
Next
If Not files.FolderExists(sourceRoot) Then Finish "failed", "今回の移動元が見つかりません。移動していません。", "", ""
sourceRoot = files.GetAbsolutePathName(sourceRoot)
parentRoot = files.GetAbsolutePathName(parentRoot)
Set snapshot = CreateObject("Scripting.Dictionary")
On Error Resume Next
For Each entry In files.GetFolder(sourceRoot).Files
    If StrComp(files.GetExtensionName(entry.Name), "pdf", vbTextCompare) = 0 Or (includeLogs And logPattern.Test(entry.Name)) Then snapshot.Add entry.Path, entry.Size
Next
errorCode = Err.Number
Err.Clear
On Error GoTo 0
If errorCode <> 0 Then Finish "failed", "移動元直下の対象一覧を確定できません。移動していません。", "", ""
snapshotPaths = snapshot.Keys
total = snapshot.Count
succeeded = 0
failed = 0
lines = ""
reportTime = Now
stamp = CStr(Year(reportTime)) & Right("0" & Month(reportTime), 2) & Right("0" & Day(reportTime), 2) & "_" & Right("0" & Hour(reportTime), 2) & Right("0" & Minute(reportTime), 2) & Right("0" & Second(reportTime), 2)
reportName = "振り分け結果_" & stamp & "_" & files.GetBaseName(files.GetTempName) & ".txt"
finalPath = files.BuildPath(sourceRoot, reportName)
reportPath = finalPath & ".処理中.txt"
If files.FileExists(finalPath) Or files.FolderExists(finalPath) Then Finish "failed", "同名の振分ログがあるため移動していません。", "", finalPath
header = "[移動処理ログファイル]" & vbCrLf & reportPath & vbCrLf & vbCrLf & _
    "[移動条件]" & vbCrLf & _
    "移動元フォルダ    : " & sourceRoot & vbCrLf & _
    "移動先フォルダ(親): " & parentRoot & vbCrLf & _
    "移動先フォルダ(子): 各ファイル先頭 " & CStr(digits) & " 桁" & vbCrLf & _
    "移動先フォルダ(孫): " & childName & vbCrLf & _
    "ID別ログも振り分け: " & CStr(includeLogs) & vbCrLf & vbCrLf & _
    "[ファイル別結果]" & vbCrLf
On Error Resume Next
Set reportFile = files.CreateTextFile(reportPath, False, True)
errorCode = Err.Number
If errorCode = 0 Then reportFile.Write "[処理中・未確定] 完了ログではありません。" & vbCrLf & vbCrLf & header
If Err.Number <> 0 Then errorCode = Err.Number
Err.Clear
On Error GoTo 0
If errorCode <> 0 Then Finish "failed", "振り分け専用ログを作成できません。移動していません。", "", reportPath
For Each originalPath In snapshotPaths
    fileName = files.GetFileName(originalPath)
    expectedSize = snapshot(originalPath)
    extractedId = ""
    targetPath = ""
    reason = ""
    If Len(files.GetBaseName(fileName)) < digits Then
        reason = "ID抽出不能:ファイル名が指定桁数より短いです。元に残しました。"
    Else
        extractedId = Left(fileName, digits)
        On Error Resume Next
        reason = MoveOne(originalPath, expectedSize, extractedId, targetPath)
        If Err.Number <> 0 Then reason = "移動不能:" & Err.Description & "。元と移動先を確認してください。"
        Err.Clear
        On Error GoTo 0
    End If
    If Len(reason) = 0 Then
        succeeded = succeeded + 1
        currentPlace = targetPath
        targetPlace = targetPath
        itemLine = "【〇 成功】" & extractedId & " | " & fileName & vbCrLf
    Else
        failed = failed + 1
        currentPlace = "未確認(元に見つかりません)"
        If files.FileExists(originalPath) Then
            currentPlace = originalPath
        ElseIf Len(targetPath) > 0 Then
            If files.FileExists(targetPath) Then currentPlace = targetPath & "(存在確認済み・移動元との同一性未確認)"
        End If
        targetPlace = "未特定"
        If Len(targetPath) > 0 Then targetPlace = "予定:" & targetPath
        If Len(extractedId) = 0 Then extractedId = "ID抽出不能"
        itemLine = "【X 失敗】" & extractedId & " | " & fileName & vbCrLf & _
            "    理由=" & reason & vbCrLf & "    現在=" & currentPlace & vbCrLf & _
            "    移動先=" & targetPlace & vbCrLf
    End If
    lines = lines & itemLine
    On Error Resume Next
    reportFile.Write itemLine
    reportError = Err.Number
    Err.Clear
    On Error GoTo 0
    If reportError <> 0 Then
        On Error Resume Next
        reportFile.Close
        On Error GoTo 0
        Finish "failed", "ログ追記に失敗したため以後の移動を中止しました。元・移動先と専用ログを確認してください。", header & lines, reportPath
    End If
Next
summary = "対象" & CStr(total) & "件/成功" & CStr(succeeded) & "件/失敗" & CStr(failed) & "件"
On Error Resume Next
reportFile.Close
reportError = Err.Number
Err.Clear
On Error GoTo 0
If reportError <> 0 Then Finish "failed", "移動処理後のログ保存を確認できません。元・移動先と専用ログを確認してください。", header & lines, reportPath
finalText = "[処理結果まとめ]" & vbCrLf & summary & vbCrLf & vbCrLf & Replace(header, reportPath, finalPath) & lines
On Error Resume Next
Set reportFile = files.CreateTextFile(finalPath, False, True)
reportError = Err.Number
If reportError = 0 Then
    reportFile.Write finalText
    reportError = Err.Number
    Err.Clear
    reportFile.Close
    If Err.Number <> 0 Then reportError = Err.Number
End If
Err.Clear
If reportError = 0 Then
    Set reportFile = files.OpenTextFile(finalPath, 1, False, -1)
    savedText = reportFile.ReadAll
    reportFile.Close
    reportError = Err.Number
    If StrComp(savedText, finalText, vbBinaryCompare) <> 0 Then reportError = 1
End If
Err.Clear
On Error GoTo 0
If reportError <> 0 Then Finish "failed", "最終ログ保存を確認できません。移動は再実行せず、処理中ログと実ファイルを確認してください。", "[処理結果まとめ]" & vbCrLf & summary & vbCrLf & vbCrLf & header & lines, reportPath
On Error Resume Next
files.DeleteFile reportPath, False
reportError = Err.Number
Err.Clear
On Error GoTo 0
If reportError <> 0 Then Finish "failed", "最終ログは保存済みですが処理中ログの削除に失敗しました。移動の再実行は不要です。", finalText, finalPath
Finish "complete", "振り分け完了", finalText, finalPath

Function ValidName(name)
    Dim position, character, code, deviceName, reserved
    ValidName = False
    If Len(name) = 0 Or Len(name) > 255 Or name = "." Or name = ".." Then Exit Function
    If Right(name, 1) = " " Or Right(name, 1) = "." Then Exit Function
    deviceName = UCase(Trim(Split(name, ".")(0)))
    Set reserved = New RegExp
    reserved.Pattern = "^(CON|PRN|AUX|NUL|CONIN[$]|CONOUT[$]|COM[1-9¹²³]|LPT[1-9¹²³])$"
    If reserved.Test(deviceName) Then Exit Function
    For position = 1 To Len(name)
        character = Mid(name, position, 1)
        code = AscW(character)
        If code >= 0 And code < 32 Then Exit Function
        If InStr(Chr(92) & "/:*?""<>|", character) > 0 Then Exit Function
    Next
    ValidName = True
End Function

Function ExactChild(folderPath, name)
    Dim child
    ExactChild = ""
    For Each child In files.GetFolder(folderPath).SubFolders
        If StrComp(child.Name, name, vbBinaryCompare) = 0 Then
            RequireLocalEntry child
            ExactChild = child.Path
            Exit Function
        End If
    Next
End Function

Sub RequireFolderChain(folderPath)
    Dim ancestor, nextPath
    ancestor = files.GetAbsolutePathName(folderPath)
    Do While Len(ancestor) > 0
        RequireLocalEntry files.GetFolder(ancestor)
        nextPath = files.GetParentFolderName(ancestor)
        If StrComp(nextPath, ancestor, vbTextCompare) = 0 Then Exit Do
        ancestor = nextPath
    Loop
End Sub

Function EnsureExactChild(folderPath, name)
    Dim existing, candidate, created
    If Not ValidName(name) Then Err.Raise 5, , "フォルダー名が不正です。"
    RequireLocalEntry files.GetFolder(folderPath)
    existing = ExactChild(folderPath, name)
    If Len(existing) > 0 Then
        EnsureExactChild = existing
        Exit Function
    End If
    candidate = files.BuildPath(folderPath, name)
    If files.FileExists(candidate) Or files.FolderExists(candidate) Then Err.Raise 5, , "同名ファイルまたは大文字・小文字が異なるフォルダーがあります:" & candidate
    Set created = files.CreateFolder(candidate)
    RequireLocalEntry created
    EnsureExactChild = ExactChild(folderPath, name)
    If Len(EnsureExactChild) = 0 Then Err.Raise 5, , "作成したフォルダーを確認できません:" & candidate
End Function

Sub RequireLocalEntry(item)
    Dim shell, process, output, matcher, matches, tag
    If (item.Attributes And 1024) = 0 Then Exit Sub
    Set shell = CreateObject("WScript.Shell")
    Set process = shell.Exec(Chr(34) & shell.ExpandEnvironmentStrings(Chr(37) & "SystemRoot" & Chr(37)) & Chr(92) & "System32" & Chr(92) & "fsutil.exe" & Chr(34) & " reparsepoint query " & Chr(34) & item.Path & Chr(34))
    output = process.StdOut.ReadAll
    Do While process.Status = 0
        WScript.Sleep 20
    Loop
    If process.ExitCode <> 0 Then Err.Raise 5, , "再解析タグを確認できないため移動しません:" & item.Path
    Set matcher = New RegExp
    matcher.Pattern = "0x[0-9A-Fa-f]{8}"
    Set matches = matcher.Execute(output)
    If matches.Count = 0 Then Err.Raise 5, , "再解析タグが不明なため移動しません:" & item.Path
    tag = CLng("&H" & Mid(matches(0).Value, 3))
    If (tag And &HFFFF0FFF) <> &H9000001A Then Err.Raise 5, , "リンクまたは未対応の再解析ポイントは対象外です(タグ=" & matches(0).Value & "):" & item.Path
End Sub

Function MoveOne(originalPath, size, id, ByRef destination)
    Dim idFolder, childFolder, originalFile, destinationFile, segment
    MoveOne = ""
    If Not ValidName(id) Then
        MoveOne = "ID抽出不能:先頭の指定桁がフォルダー名として使えません。元に残しました。"
        Exit Function
    End If
    If Not files.FileExists(originalPath) Then
        MoveOne = "確定した元ファイルが見つかりません。"
        Exit Function
    End If
    Set originalFile = files.GetFile(originalPath)
    RequireLocalEntry originalFile
    If (originalFile.Attributes And 4096) <> 0 Or (originalFile.Attributes And &H400000) <> 0 Then
        MoveOne = "元ファイルがオンラインのみの状態です。ローカルで利用可能にして再試行してください。移動していません。"
        Exit Function
    End If
    If originalFile.Size <> size Then
        MoveOne = "元ファイルのサイズ・実体が一覧確定時と一致しません。移動していません。"
        Exit Function
    End If
    RequireFolderChain parentRoot
    RequireFolderChain sourceRoot
    idFolder = EnsureExactChild(parentRoot, id)
    childFolder = idFolder
    For Each segment In childParts
        childFolder = EnsureExactChild(childFolder, segment)
    Next
    destination = files.BuildPath(childFolder, files.GetFileName(originalPath))
    If files.FileExists(destination) Or files.FolderExists(destination) Then
        MoveOne = "移動先に同名ファイル等があります。上書きせず元に残しました。"
        Exit Function
    End If
    files.MoveFile originalPath, destination
    If files.FileExists(originalPath) Or Not files.FileExists(destination) Then
        MoveOne = "移動後の所在を確認できません。元と移動先を確認してください。"
        Exit Function
    End If
    Set destinationFile = files.GetFile(destination)
    If destinationFile.Size <> size Then MoveOne = "移動後のサイズが一致しません。移動先を確認してください。"
End Function

Function JsonText(value)
    Dim result, character, position, code, escape
    result = ""
    escape = Chr(92)
    For position = 1 To Len(value)
        character = Mid(value, position, 1)
        code = AscW(character)
        If character = Chr(34) Or character = escape Then
            result = result & escape & character
        ElseIf code >= 0 And code < 32 Then
            result = result & escape & "u" & Right("0000" & Hex(code), 4)
        Else
            result = result & character
        End If
    Next
    JsonText = Chr(34) & result & Chr(34)
End Function

Sub Finish(status, notice, display, logPath)
    WScript.Echo "{" & JsonText("status") & ":" & JsonText(status) & "," & JsonText("summary") & ":" & JsonText(notice) & "," & JsonText("display") & ":" & JsonText(display) & "," & JsonText("logPath") & ":" & JsonText(logPath) & "}"
    WScript.Quit 0
End Sub''' ScriptOutput=> SortJson
ON ERROR
    GOTO SortExecutionFailed
END
Variables.ConvertJsonToCustomObject Json: SortJson CustomObject=> SortResult
ON ERROR
    GOTO SortExecutionFailed
END
GOTO SortCleanup
LABEL SortExecutionFailed
ERROR => SortExecutionError Reset: True
Variables.ConvertJsonToCustomObject Json: $'''{"status":"failed","summary":"振分結果を確認できません。処理を中止しました。","display":"元と振分先のPDF、および移動元の振り分け専用ログを確認してください。","logPath":""}''' CustomObject=> SortResult
LABEL SortCleanup
IF SortInputFile <> $'''''' THEN
    File.Delete Files: SortInputFile
    ON ERROR
        GOTO SortCleanupFailed
    END
END
GOTO SortOutcomeReady
LABEL SortCleanupFailed
ERROR => SortCleanupError Reset: True
LABEL SortOutcomeReady
IF SortResult['status'] = $'''invalid''' THEN
    SET SortInputNotice TO SortResult['summary']
    GOTO ShowSortInput
END
SET SortSummary TO SortResult['summary']
SET SortLogDisplay TO SortResult['display']
@@statistics_TextBlock: '1'
@@statistics_Input_Text: '1'
@@statistics_Action_Submit: '2'
Display.ShowCustomDialog CardTemplateJson: '''{
  "type": "AdaptiveCard",
  "version": "1.4",
  "id": "AdaptiveCard",
  "body": [
    { "type": "TextBlock", "id": "Summary", "text": "${Summary_Text}", "wrap": true },
    { "type": "Input.Text", "id": "SortLog", "label": "振り分け結果(編集しても専用ログ原本は変わりません)", "isMultiline": true, "value": "${LogText_Value}" }
  ],
  "actions": [
    { "type": "Action.Submit", "id": "SortRetry", "title": "再試行", "associatedInputs": "none" },
    { "type": "Action.Submit", "id": "SortClose", "title": "閉じる", "associatedInputs": "none" }
  ],
  "FormTitle": "PDF振り分け結果"
}''' CustomFormData=> SortReviewData ButtonPressed=> SortReviewButton @Summary_Text: SortSummary @LogText_Value: SortLogDisplay
ON ERROR
    GOTO RunSortInputClosed
END
IF SortReviewButton = $'''SortRetry''' THEN
    SET SortInputNotice TO $'''前回の入力を修正して再実行できます。移動元直下に残るPDFと、選択時はID別ログが対象です。前回の専用ログも残ります。'''
    GOTO ShowSortInput
END
GOTO RunReviewDone
LABEL RunSortInputClosed
ERROR => SortDialogError Reset: True
GOTO RunReviewDone
**ENDREGION
LABEL RunReviewDone

コメント

タイトルとURLをコピーしました