/** * batch-refresh-507570.js * ------------------------------------------------------------ * SiteId 507570(事業計画v2)専用のワンオフスクリプト。 * ブラウザで手動実施している「1) レコードを開く→実績計上(263191)読込→ * 2) 保存→集計テーブル(507676)へ反映」を、scripts/1_事業計画テーブル.jsの * getJissekiData()・summaryCalc()・summaryRecord()/bulkUpsertRecords()と * 同一ロジックでAPI経由で再現し、対象レコードを一括処理する。 * * 処理内容(1レコードあたり): * Phase A: 263191から受注(本契約/設計契約)・着工・完成を取得し、 * 月別にDescription021-032(受注実績)/081-092(着工実績)/141-152(完成実績)へ格納 * ※ getJissekiData()と同一ロジック(年度範囲10/1-9/30、月境界チェック等) * Phase B: 既存の計画(Description001-012等)+Phase Aの実績から、 * 受注/着工/完成それぞれの月別合計・累計をsummaryCalc()と同一ロジックで計算し * Description041-052/101-112/161-172(合計)へ格納 * Phase C: Phase Bの合計(12ヶ月×3系統=36件)をsummaryRecord()と同一ロジックで * ClassHash/NumHashに組み立て、507676へbulkupsertで1回送信 * Phase D: Phase A+Bの結果を507570へ1回のupdateでまとめて保存 * * 使い方: * node batch-refresh-507570.js … 全レコード対象、プレビューのみ(送信なし) * node batch-refresh-507570.js --only=507580 … 指定ResultIdのみ対象(テスト用) * node batch-refresh-507570.js --execute … 実際に507570 update + 507676 bulkupsert を送信 * --only と --execute は併用可 * ------------------------------------------------------------ */ const fs = require("fs"); const path = require("path"); const https = require("https"); const PROJECT_NAME = "NEXTGROUP経営計画"; const ENV = "production"; const SITE_ID = 507570; const JISSEKI_SITE_ID = 263191; const SHUKEI_SITE_ID = 507676; const REPO_ROOT = path.join(__dirname, "..", "..", "..", ".."); const baseDir = path.join(REPO_ROOT, PROJECT_NAME); const siteDir = path.join(baseDir, "configs", ENV, `site-${SITE_ID}_事業計画v2`); const modifyDir = path.join(siteDir, "modify", "batch-refresh"); const configPath = path.join(REPO_ROOT, `config_${ENV}.json`); const serverConfig = JSON.parse(fs.readFileSync(configPath, "utf-8")); const apiKey = serverConfig.ApiKey; const baseUrl = serverConfig.BaseUrl; const argv = process.argv.slice(2); const execute = argv.includes("--execute"); const onlyArg = argv.find((a) => a.startsWith("--only=")); const onlyId = onlyArg ? parseInt(onlyArg.split("=")[1], 10) : null; function post(pathName, body) { return new Promise((resolve, reject) => { const url = new URL(baseUrl + pathName); const payload = JSON.stringify(body); const req = https.request( { hostname: url.hostname, path: url.pathname, method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) }, }, (res) => { let c = ""; res.on("data", (d) => (c += d)); res.on("end", () => { try { resolve({ status: res.statusCode, body: JSON.parse(c) }); } catch (e) { resolve({ status: res.statusCode, body: c }); } }); } ); req.on("error", reject); req.write(payload); req.end(); }); } function pad3(n) { return String(n).padStart(3, "0"); } // getJissekiData()の空行テンプレート(1-10) function emptyJissekiTemplate() { const rows = []; for (let k = 1; k <= 10; k++) { rows.push({ 0: String(k), 1: "", 2: "", 3: "", 4: "", 5: "", 6: "", 7: "" }); } return rows; } // jissekiA/B/C[m]配列(最大10件超は元コードも考慮していないため同様に踏襲)を // 10行テンプレートへ整形する(getJissekiData()の394-462行と同一ロジック) function buildJissekiRows(rows) { if (!rows.length) return emptyJissekiTemplate(); const out = rows.map((r, idx) => ({ ...r, 0: String(idx + 1) })); for (let k = 0; k < 10; k++) { if (k >= out.length) { out[k] = { 0: String(k + 1), 1: "", 2: "", 3: "", 4: "", 5: "", 6: "", 7: "" }; } } return out; } // SiteSettings.Columnsを見ずに済むよう、getJigyouKeikakuData同様Description番号直指定で読み書きする async function fetchJisseki(classE, sYear, eYear, sDate, eDate, eDate1) { const jissekiA = {}, jissekiB = {}, jissekiC = {}; for (let m = 1; m <= 12; m++) { jissekiA[m] = []; jissekiB[m] = []; jissekiC[m] = []; } // 受注(本契約のみ) { const res = await post(`/api/items/${JISSEKI_SITE_ID}/get`, { ApiVersion: 1.1, ApiKey: apiKey, View: { ColumnFilterHash: { ClassE: `["${classE}"]`, ClassO: `["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]`, DateA: `["${sDate},${eDate}"]`, DateB: `["\t"]`, }, ColumnSorterHash: { DateA: "asc" }, }, }); for (const value of res.body.Response.Data) { const countMonth = parseFloat(value.ClassHash.ClassO.replace("月", "")); const date0 = new Date(value.DateHash.DateA); date0.setHours(date0.getHours() + 9); const countYear = date0.getFullYear(); if (countMonth <= 9 && countYear == sYear) continue; if (countMonth == 10 && countYear == eYear) continue; const kubun = value.ClassHash.ClassD; const cname = value.ClassHash.ClassC; const ukeoi = parseFloat(value.NumHash.NumA); const arari = parseFloat(value.NumHash.NumB); const arariPer = Math.floor((arari / ukeoi) * 1000) / 10; const tanto = value.ClassHash.ClassF; jissekiA[countMonth].push({ 0: "0", 1: date0, 2: kubun, 3: cname, 4: ukeoi, 5: arariPer, 6: arari, 7: tanto }); } } // 受注(設計契約) { const res = await post(`/api/items/${JISSEKI_SITE_ID}/get`, { ApiVersion: 1.1, ApiKey: apiKey, View: { ColumnFilterHash: { ClassE: `["${classE}"]`, ClassO: `["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]`, DateB: `["${sDate},${eDate}"]`, }, ColumnSorterHash: { DateB: "asc" }, }, }); for (const value of res.body.Response.Data) { const countMonth = parseFloat(value.ClassHash.ClassO.replace("月", "")); const date0 = new Date(value.DateHash.DateB); date0.setHours(date0.getHours() + 9); const countYear = date0.getFullYear(); if (countMonth <= 9 && countYear == sYear) continue; if (countMonth == 10 && countYear == eYear) continue; const kubun = value.ClassHash.ClassD; let cname, ukeoi, arari; if (value.DateHash.DateA != "1899-12-30T00:00:00") { cname = "[本]" + value.ClassHash.ClassC; ukeoi = parseFloat(value.NumHash.NumA); arari = parseFloat(value.NumHash.NumB); } else { cname = "[設]" + value.ClassHash.ClassC; ukeoi = parseFloat(value.NumHash.NumC); arari = parseFloat(value.NumHash.NumD); } const tanto = value.ClassHash.ClassF; const arariPer = Math.floor((arari / ukeoi) * 1000) / 10; jissekiA[countMonth].push({ 0: "0", 1: date0, 2: kubun, 3: cname, 4: ukeoi, 5: arariPer, 6: arari, 7: tanto }); } } // 着工 { const res = await post(`/api/items/${JISSEKI_SITE_ID}/get`, { ApiVersion: 1.1, ApiKey: apiKey, View: { ColumnFilterHash: { ClassE: `["${classE}"]`, DateC: `["${sDate},${eDate1}"]` }, ColumnSorterHash: { DateC: "asc" }, }, }); let num = 1, month0 = 0; res.body.Response.Data.forEach((value, index) => { const date0 = new Date(value.DateHash.DateC); date0.setHours(date0.getHours() + 9); const month = date0.getMonth() + 1; const kubun = value.ClassHash.ClassD; const cname = value.ClassHash.ClassC; const ukeoi = parseFloat(value.NumHash.NumA); const arari = parseFloat(value.NumHash.NumB); const arariPer = Math.floor((arari / ukeoi) * 1000) / 10; const tanto = value.ClassHash.ClassF; if (index > 0 && month0 != month) num = 1; jissekiB[month].push({ 0: num, 1: date0, 2: kubun, 3: cname, 4: ukeoi, 5: arariPer, 6: arari, 7: tanto }); num++; month0 = month; }); } // 完成 { const res = await post(`/api/items/${JISSEKI_SITE_ID}/get`, { ApiVersion: 1.1, ApiKey: apiKey, View: { ColumnFilterHash: { ClassE: `["${classE}"]`, DateD: `["${sDate},${eDate1}"]` }, ColumnSorterHash: { DateD: "asc" }, }, }); let num = 1, month0 = 0; res.body.Response.Data.forEach((value, index) => { const date0 = new Date(value.DateHash.DateD); date0.setHours(date0.getHours() + 9); const month = date0.getMonth() + 1; const kubun = value.ClassHash.ClassD; const cname = value.ClassHash.ClassC; const ukeoi = parseFloat(value.NumHash.NumA); const arari = parseFloat(value.NumHash.NumB); const arariPer = Math.floor((arari / ukeoi) * 1000) / 10; const tanto = value.ClassHash.ClassF; if (index > 0 && month0 != month) num = 1; jissekiC[month].push({ 0: num, 1: date0, 2: kubun, 3: cname, 4: ukeoi, 5: arariPer, 6: arari, 7: tanto }); num++; month0 = month; }); } return { jissekiA, jissekiB, jissekiC }; } // summaryCalc()と同一ロジック。DescriptionHash(既存の計画+今回の実績)からgoukeiDataを計算する function calcGoukei(descriptionHash) { const keikakuCumulative = {}; const jissekiCumulative = {}; for (let month1 = 10; month1 < 22; month1++) { const month2 = month1 >= 13 ? month1 - 12 : month1; for (let i = 40; i <= 160; i += 60) { const tableId = pad3(month2 + i); keikakuCumulative[tableId] = [0, 0, 0, 0, 0, 0]; jissekiCumulative[tableId] = [0, 0, 0, 0, 0, 0]; } } const goukeiResult = {}; // tableId -> 6行配列 for (let month1 = 10; month1 < 22; month1++) { const month2 = month1 >= 13 ? month1 - 12 : month1; for (let i = 0; i <= 120; i += 60) { const keikakuTableId = pad3(month2 + i); const jissekiTableId = pad3(month2 + i + 20); const goukeiTableId = pad3(month2 + i + 40); const keikakuData = JSON.parse(descriptionHash["Description" + keikakuTableId] || "[]"); const jissekiDataArr = JSON.parse(descriptionHash["Description" + jissekiTableId] || "[]"); const existingGoukei = descriptionHash["Description" + goukeiTableId] ? JSON.parse(descriptionHash["Description" + goukeiTableId]) : JSON.parse( '[{"0":"当月","1":"計画","2":"","3":"","4":"","5":"","6":"請負金額"},{"0":"","1":"実績","2":"","3":"","4":"","5":"","6":"0%"},{"0":"","1":"差異","2":"","3":"","4":"","5":"","6":""},{"0":"累計","1":"計画","2":"","3":"","4":"","5":"","6":"棟数"},{"0":"","1":"実績","2":"","3":"","4":"","5":"","6":"0%"},{"0":"","1":"差異","2":"","3":"","4":"","5":"","6":""}]' ); const goukeiData = existingGoukei; // ラベル([0],[1]列、[0][6]/[3][6])を保持したまま数値部分だけ上書きする const keikakuSum = [0, 0, 0, 0, 0, 0]; const jissekiSum = [0, 0, 0, 0, 0, 0]; for (const row of keikakuData) { const v4 = parseFloat(String(row[4]).toString().replace(/[¥,]/g, "")); if (v4 > 0) { const v6 = parseFloat(String(row[6]).toString().replace(/[¥,]/g, "")); keikakuSum[1] += v4; keikakuSum[3] += v6; if (row[2] != "カウント外") { keikakuSum[0]++; keikakuSum[4] += v4; keikakuSum[5] += v6; } } } if (keikakuSum[4] > 0 && keikakuSum[5] > 0) { keikakuSum[2] = Math.floor((keikakuSum[5] / keikakuSum[4]) * 10000) / 100; } for (const row of jissekiDataArr) { const v4 = parseFloat(String(row[4]).toString().replace(/[¥,]/g, "")); if (v4 > 0) { const v6 = parseFloat(String(row[6]).toString().replace(/[¥,]/g, "")); jissekiSum[1] += v4; jissekiSum[3] += v6; if (row[2] != "カウント外") { jissekiSum[0]++; jissekiSum[4] += v4; jissekiSum[5] += v6; } } } if (jissekiSum[4] > 0 && jissekiSum[5] > 0) { jissekiSum[2] = Math.floor((jissekiSum[5] / jissekiSum[4]) * 10000) / 100; } goukeiData[0][2] = keikakuSum[0]; goukeiData[0][3] = keikakuSum[1]; goukeiData[0][4] = keikakuSum[2]; goukeiData[0][5] = keikakuSum[3]; goukeiData[1][2] = jissekiSum[0]; goukeiData[1][3] = jissekiSum[1]; goukeiData[1][4] = jissekiSum[2]; goukeiData[1][5] = jissekiSum[3]; goukeiData[2][2] = jissekiSum[0] - keikakuSum[0]; goukeiData[2][3] = jissekiSum[1] - keikakuSum[1]; goukeiData[2][4] = jissekiSum[2] - keikakuSum[2]; goukeiData[2][5] = jissekiSum[3] - keikakuSum[3]; for (let month3 = month1; month3 < 22; month3++) { const month2b = month3 >= 13 ? month3 - 12 : month3; const tableId = pad3(month2b + i + 40); keikakuCumulative[tableId][0] += keikakuSum[0]; keikakuCumulative[tableId][1] += keikakuSum[1]; keikakuCumulative[tableId][3] += keikakuSum[3]; keikakuCumulative[tableId][4] += keikakuSum[4]; keikakuCumulative[tableId][5] += keikakuSum[5]; keikakuCumulative[tableId][2] = keikakuCumulative[tableId][1] > 0 ? Math.floor((keikakuCumulative[tableId][5] / keikakuCumulative[tableId][4]) * 10000) / 100 : 0; jissekiCumulative[tableId][0] += jissekiSum[0]; jissekiCumulative[tableId][1] += jissekiSum[1]; jissekiCumulative[tableId][3] += jissekiSum[3]; jissekiCumulative[tableId][4] += jissekiSum[4]; jissekiCumulative[tableId][5] += jissekiSum[5]; jissekiCumulative[tableId][2] = jissekiCumulative[tableId][1] > 0 ? Math.floor((jissekiCumulative[tableId][5] / jissekiCumulative[tableId][4]) * 10000) / 100 : 0; } goukeiData[3][2] = keikakuCumulative[goukeiTableId][0]; goukeiData[3][3] = keikakuCumulative[goukeiTableId][1]; goukeiData[3][4] = keikakuCumulative[goukeiTableId][2]; goukeiData[3][5] = keikakuCumulative[goukeiTableId][3]; goukeiData[4][2] = jissekiCumulative[goukeiTableId][0]; goukeiData[4][3] = jissekiCumulative[goukeiTableId][1]; goukeiData[4][4] = jissekiCumulative[goukeiTableId][2]; goukeiData[4][5] = jissekiCumulative[goukeiTableId][3]; goukeiData[5][2] = jissekiCumulative[goukeiTableId][0] - keikakuCumulative[goukeiTableId][0]; goukeiData[5][3] = jissekiCumulative[goukeiTableId][1] - keikakuCumulative[goukeiTableId][1]; goukeiData[5][4] = jissekiCumulative[goukeiTableId][2] - keikakuCumulative[goukeiTableId][2]; goukeiData[5][5] = jissekiCumulative[goukeiTableId][3] - keikakuCumulative[goukeiTableId][3]; if (keikakuCumulative[goukeiTableId][1] > 0 && jissekiCumulative[goukeiTableId][1] > 0) { goukeiData[1][6] = (Math.floor((jissekiCumulative[goukeiTableId][1] / keikakuCumulative[goukeiTableId][1]) * 10000) / 100).toString() + "%"; } else { goukeiData[1][6] = "0%"; } if (keikakuCumulative[goukeiTableId][0] > 0 && jissekiCumulative[goukeiTableId][0] > 0) { goukeiData[4][6] = (Math.floor((jissekiCumulative[goukeiTableId][0] / keikakuCumulative[goukeiTableId][0]) * 10000) / 100).toString() + "%"; } else { goukeiData[4][6] = "0%"; } goukeiResult[goukeiTableId] = goukeiData; // 直接descriptionHashへも書き戻す(次のmonth1ループが最新の計画/実績を参照できるよう。 // ただしkeikaku/jissekiは今回変更しないので実質goukei追記のみ) descriptionHash["Description" + goukeiTableId] = JSON.stringify(goukeiData); } } return goukeiResult; } // summaryRecord()と同一ロジック。goukeiResult(受注/着工/完成 x 12ヶ月)からbulkupsert用Dataを組み立てる function buildBulkData(goukeiResult, classHashBase) { const data = []; for (let i = 1; i <= 12; i++) { let sortKey; if (i >= 10) sortKey = pad3(i - 9); else sortKey = pad3(i + 3); let count = 1; for (let j = 40; j <= 160; j += 60) { const tableId = pad3(i + j); const goukeiData = goukeiResult[tableId]; const classHash = { ClassA: classHashBase.ClassA, ClassB: classHashBase.ClassB, ClassC: classHashBase.ClassC, ClassD: classHashBase.ClassD, ClassE: classHashBase.ClassE, ClassH: classHashBase.ClassH, ClassK: sortKey, ClassY: i, ClassV: count, ClassZ: classHashBase.ClassZ, }; const numHash = { Num001: goukeiData[0][2], Num002: goukeiData[0][3], Num003: goukeiData[0][4], Num004: goukeiData[0][5], Num011: goukeiData[1][2], Num012: goukeiData[1][3], Num013: goukeiData[1][4], Num014: goukeiData[1][5], Num021: goukeiData[3][2], Num022: goukeiData[3][3], Num023: goukeiData[3][4], Num024: goukeiData[3][5], Num031: goukeiData[4][2], Num032: goukeiData[4][3], Num033: goukeiData[4][4], Num034: goukeiData[4][5], }; data.push({ ClassHash: classHash, NumHash: numHash }); count++; } } return data; } async function processRecord(it, log) { const resultId = it.ResultId; const classHashBase = it.ClassHash; const classE = classHashBase.ClassB; // getJissekiData()同様、自レコードのClassB値を263191のClassEフィルタに使う const sYear = classHashBase.ClassZ; const eYear = parseFloat(classHashBase.ClassZ) + 1; const sDate = `${sYear}/10/01 00:00:00`; const eDate = `${eYear}/09/30 23:59:59`; const sDate0 = new Date(sDate); const eDate0 = new Date(eDate); const thisMonthLastDay = new Date(); thisMonthLastDay.setMonth(thisMonthLastDay.getMonth() + 1, 0); let eDate1 = `${thisMonthLastDay.getFullYear()}/${String(thisMonthLastDay.getMonth() + 1).padStart(2, "0")}/${String( thisMonthLastDay.getDate() ).padStart(2, "0")} 23:59:59`; const eDate1D = new Date(eDate1); if (eDate0.getTime() < eDate1D.getTime()) { eDate1 = eDate; } const { jissekiA, jissekiB, jissekiC } = await fetchJisseki(classE, sYear, eYear, sDate, eDate, eDate1); // Phase A: DescriptionHashへ実績を組み立て(既存の計画等はit.DescriptionHashからコピーして保持) const descriptionHash = { ...it.DescriptionHash }; for (let i = 1; i <= 12; i++) { descriptionHash["Description" + pad3(i + 20)] = JSON.stringify(buildJissekiRows(jissekiA[i])); descriptionHash["Description" + pad3(i + 80)] = JSON.stringify(buildJissekiRows(jissekiB[i])); descriptionHash["Description" + pad3(i + 140)] = JSON.stringify(buildJissekiRows(jissekiC[i])); } // Phase B: 合計(累計込み)を計算し、descriptionHashへ反映(calcGoukei内で書き戻す) const goukeiResult = calcGoukei(descriptionHash); // 更新するDescriptionフィールドだけを送信用に抜き出す(実績36 + 合計36 = 72) const updateDescriptionHash = {}; for (let i = 1; i <= 12; i++) { for (const off of [20, 80, 140]) updateDescriptionHash["Description" + pad3(i + off)] = descriptionHash["Description" + pad3(i + off)]; for (const off of [40, 100, 160]) updateDescriptionHash["Description" + pad3(i + off)] = descriptionHash["Description" + pad3(i + off)]; } // Phase C: 507676向けbulkupsertデータ const bulkData = buildBulkData(goukeiResult, classHashBase); log(`ResultId ${resultId} (${classHashBase.ClassB}): 受注/着工/完成 実績${["A", "B", "C"].map((x) => "OK").join("/")}, 合計36件, bulkupsert対象${bulkData.length}件`); return { resultId, updateDescriptionHash, bulkData }; } (async () => { fs.mkdirSync(modifyDir, { recursive: true }); const ts = new Date().toISOString().replace(/[:.]/g, "-"); const logLines = []; const log = (s) => { console.log(s); logLines.push(s); }; log(`=== batch-refresh-507570.js 実行開始 (execute=${execute}, only=${onlyId || "全件"}) ===`); const listRes = await post(`/api/items/${SITE_ID}/get`, { ApiVersion: 1.1, ApiKey: apiKey }); let items = listRes.body.Response.Data; if (onlyId) items = items.filter((it) => it.ResultId === onlyId); log(`対象レコード数: ${items.length}`); const perRecordResults = []; for (const it of items) { try { const r = await processRecord(it, log); perRecordResults.push(r); } catch (e) { log(`ResultId ${it.ResultId}: エラー ${e.message}`); } } const previewPath = path.join(modifyDir, `preview_${ts}.json`); fs.writeFileSync( previewPath, JSON.stringify( { targetCount: items.length, updates: perRecordResults.map((r) => ({ resultId: r.resultId, descriptionKeys: Object.keys(r.updateDescriptionHash), bulkDataCount: r.bulkData.length })), sampleUpdate: perRecordResults[0], }, null, 1 ) ); log(`[OK] プレビュー保存: ${previewPath}`); if (!execute) { log("[注意] --execute が指定されていないため、送信は行っていません。"); fs.writeFileSync(path.join(modifyDir, `log_${ts}.txt`), logLines.join("\n")); return; } // レコードごとに 507570 update → 507676 bulkupsert(36件)を実行 // (全レコード分をまとめて1回のbulkupsertにすると件数超過でPleasanter側のBulkUpsertMaxに // 引っかかる恐れがあるため、レコード単位=36件ずつに区切って送信する) let updateOk = 0, updateNg = []; let bulkOk = 0, bulkNg = []; for (const r of perRecordResults) { const updateRes = await post(`/api/items/${r.resultId}/update`, { ApiVersion: 1.1, ApiKey: apiKey, DescriptionHash: r.updateDescriptionHash, }); if (updateRes.status === 200 && updateRes.body && updateRes.body.StatusCode === 200) updateOk++; else updateNg.push({ resultId: r.resultId, res: updateRes }); const bulkRes = await post(`/api/items/${SHUKEI_SITE_ID}/bulkupsert`, { ApiVersion: 1.1, ApiKey: apiKey, Keys: ["ClassA", "ClassB", "ClassV", "ClassY", "ClassZ"], KeyNotFoundCreate: true, Data: r.bulkData, }); if (bulkRes.status === 200 && bulkRes.body && bulkRes.body.StatusCode === 200) bulkOk++; else bulkNg.push({ resultId: r.resultId, res: bulkRes }); log(`ResultId ${r.resultId}: 507570update=${updateRes.status} / 507676bulkupsert=${bulkRes.status} ${bulkRes.body && bulkRes.body.Message ? bulkRes.body.Message : ""}`); } log(`507570 update: 成功${updateOk}件 / 失敗${updateNg.length}件`); log(`507676 bulkupsert: 成功${bulkOk}件 / 失敗${bulkNg.length}件`); if (updateNg.length) log("updateNg: " + JSON.stringify(updateNg, null, 1).slice(0, 2000)); if (bulkNg.length) log("bulkNg: " + JSON.stringify(bulkNg, null, 1).slice(0, 2000)); fs.writeFileSync( path.join(modifyDir, `result_${ts}.json`), JSON.stringify({ updateOk, updateNg, bulkOk, bulkNg }, null, 1) ); fs.writeFileSync(path.join(modifyDir, `log_${ts}.txt`), logLines.join("\n")); })();