eCourt-note/scripts/install-wordaddin.mjs
Suphonchai Phoonsawat b1449ab668
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m17s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
Refactor ticket verification and installation scripts
- Split `verifyAddinTicket` into two functions: `verifyAddinTicketHmac` (ignores expiration) and `verifyAddinTicket` (checks expiration).
- Add `hasVersionByUser` function to check if a document has been previously worked on by a user, allowing expired tickets to be accepted if the user has a history with the document.
- Update OpenAPI specification to reflect changes in ticket expiration handling.
- Introduce new scripts for installing and uninstalling the Word add-in on Windows and macOS, providing a streamlined process for sideloading the add-in.
- Enhance tests for ticket verification to cover new functionality regarding expired tickets and user history.
2026-10-28 14:42:03 +07:00

246 lines
11 KiB
JavaScript

#!/usr/bin/env node
// Dev tool ติดตั้ง (sideload) / ถอด Word add-in "e-Court Documents Versioning"
// บนเครื่องทดสอบ Windows + macOS — โหลด manifest จาก server (default ตัวทดสอบ
// ecourt-note.chamomind.com ที่ app/wordaddin/manifest.xml/route.ts rewrite origin
// ให้ตรงโฮสต์ผู้ขออยู่แล้ว) แล้วจดทะเบียนให้ Word Desktop เห็น add-in
//
// กลไกตรงตาม office-addin-dev-settings@4 (อ่านจาก source ใน node_modules) แต่
// เขียนเองด้วย node builtins ล้วน เพื่อใช้ได้ทั้งรันใน repo (npm run addin:install)
// และ copy ไฟล์เดี่ยวนี้ไปรัน `node install-wordaddin.mjs` บนเครื่องที่ไม่มี repo:
// - Windows: registry HKCU\Software\Microsoft\Office\16.0\Wef\Developer
// value ชื่อ = GUID ของ add-in, REG_SZ, data = path manifest บนดิสก์ (ต้องอยู่ถาวร)
// - macOS: วางไฟล์ <GUID>.manifest.xml ใน
// ~/Library/Containers/com.microsoft.Word/Data/Documents/wef/ (Word สแกน *.xml ทุกไฟล์)
//
// ใช้: node scripts/install-wordaddin.mjs [options]
// --url <manifest-url> default https://ecourt-note.chamomind.com/docedit/wordaddin/manifest.xml
// (dev: https://localhost:3000/docedit/wordaddin/manifest.xml — ต้อง npm run dev:https ก่อน)
// --remove ถอดการติดตั้ง (ไม่ต้องต่อเน็ต — ใช้ manifest ที่เก็บไว้ตอนลง)
// --help | -h
//
// หลังลง/ถอดต้อง restart Word ทุกครั้ง · รายละเอียดเต็ม: docs/WORDADDIN-DEV.md
import { spawnSync } from "node:child_process";
import { mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
const PREFIX = "[install-addin]";
const DEFAULT_MANIFEST_URL = "https://ecourt-note.chamomind.com/docedit/wordaddin/manifest.xml";
/** ตรงคีย์ที่ office-addin-dev-settings ใช้ (Office 2016+ ทุกรุ่นอยู่ภายใต้ 16.0) */
const WIN_DEV_KEY = "HKCU\\Software\\Microsoft\\Office\\16.0\\Wef\\Developer";
/** โฟลเดอร์เก็บ manifest ถาวรบน Windows — path เดียวกับที่ docs/WORDADDIN-DEV.md สอน manual */
const winManifestDir = () => path.join(os.homedir(), "Documents", "wordaddin-manifest");
const winManifestPath = () => path.join(winManifestDir(), "manifest.xml");
/** โฟลเดอร์ที่ Word for Mac สแกนหา add-in ที่ sideload */
const macWefDir = () =>
path.join(os.homedir(), "Library", "Containers", "com.microsoft.Word", "Data", "Documents", "wef");
function usage(code = 1) {
console.log(`ใช้: node scripts/install-wordaddin.mjs [options]
--url <manifest-url> default ${DEFAULT_MANIFEST_URL}
--remove ถอดการติดตั้ง
--help | -h`);
process.exit(code);
}
function parseArgs(argv) {
const options = { url: DEFAULT_MANIFEST_URL, remove: false };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
const next = () => {
const value = argv[++i];
if (value === undefined) usage();
return value;
};
switch (arg) {
case "--url": options.url = next(); break;
case "--remove": options.remove = true; break;
case "--help": case "-h": usage(0); break;
default: usage();
}
}
return options;
}
function fail(message) {
console.error(`${PREFIX} ✗ ${message}`);
process.exit(1);
}
/** โหลด manifest จาก server แล้วดึง GUID ของ add-in จาก <Id> — ไม่ hardcode เพราะเปลี่ยน environment ได้ */
async function downloadManifest(url) {
let response;
try {
response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30_000) });
} catch (error) {
fail(`โหลด manifest จาก ${url} ไม่ได้ (${error instanceof Error ? error.message : String(error)})`);
}
if (!response.ok) {
fail(`server ตอบ HTTP ${response.status} จาก ${url} — เช็คว่า environment นั้นรันอยู่`);
}
const xml = await response.text();
if (!xml.includes("<OfficeApp")) {
fail(`response จาก ${url} ไม่ใช่ manifest ของ Office add-in (ไม่มี <OfficeApp)`);
}
const idMatch = xml.match(/<Id>([0-9a-fA-F-]{36})<\/Id>/);
if (!idMatch) {
fail(`หา <Id> (GUID) ใน manifest ไม่เจอจาก ${url}`);
}
return { xml, guid: idMatch[1].toLowerCase() };
}
/** อ่าน GUID จากไฟล์ manifest ที่เก็บไว้ตอนติดตั้ง (สำหรับ --remove ที่ไม่ต้องต่อเน็ต) */
function readStoredGuid(manifestPath) {
try {
const xml = readFileSync(manifestPath, "utf8");
const idMatch = xml.match(/<Id>([0-9a-fA-F-]{36})<\/Id>/);
return idMatch ? idMatch[1].toLowerCase() : null;
} catch {
return null;
}
}
/** reg.exe ผ่าน args array เสมอ (ไม่ใช่ shell string — path มี space ก็ไม่พัง) */
function reg(args) {
return spawnSync("reg", args, { encoding: "utf8" });
}
function installWindows(xml, guid) {
const dest = winManifestPath();
mkdirSync(winManifestDir(), { recursive: true });
writeFileSync(dest, xml);
console.log(`${PREFIX} ✓ เก็บ manifest ไว้ที่ ${dest}`);
const add = reg(["add", WIN_DEV_KEY, "/v", guid, "/t", "REG_SZ", "/d", dest, "/f"]);
if (add.status !== 0) {
fail(`เขียน registry ไม่สำเร็จ (reg add exit ${add.status}): ${add.stderr?.trim() ?? "ไม่มี stderr"}`);
}
const query = reg(["query", WIN_DEV_KEY, "/v", guid]);
if (query.status !== 0) {
fail(`reg add สำเร็จแต่ยืนยันด้วย reg query ไม่ผ่าน (exit ${query.status})`);
}
console.log(`${PREFIX} ✓ จดทะเบียน add-in ใน registry (${WIN_DEV_KEY} → ${guid})`);
return dest;
}
function removeWindows() {
const stored = winManifestPath();
const guid = readStoredGuid(stored);
if (!guid) {
fail(
`ไม่พบ manifest ที่เก็บไว้ (${stored}) จึงไม่รู้ GUID ของ add-in — ` +
`ลบเองได้ที่ reg delete "${WIN_DEV_KEY}" /v <GUID> /f`,
);
}
const del = reg(["delete", WIN_DEV_KEY, "/v", guid, "/f"]);
if (del.status !== 0) {
fail(`reg delete ไม่สำเร็จ (exit ${del.status}): ${del.stderr?.trim() ?? "ไม่มี stderr"}`);
}
try {
unlinkSync(stored);
} catch {
// registry ถอดแล้ว — ไฟล์ค้างไม่ทำให้ Word เห็น add-in
}
console.log(`${PREFIX} ✓ ถอดการติดตั้งแล้ว (registry ${guid} + ไฟล์ ${stored})`);
}
/** ไฟล์ sideload ของ GUID นี้ทั้งหมดใน wef — ทั้งของเราและชื่อ basename เก่า ๆ ที่เคยใช้ */
function macSideloadFiles(guid) {
const dir = macWefDir();
try {
return readdirSync(dir)
.filter((name) => name === `${guid}.manifest.xml` || name.startsWith(`${guid}.`))
.map((name) => path.join(dir, name));
} catch {
return [];
}
}
function installMac(xml, guid) {
const dir = macWefDir();
mkdirSync(dir, { recursive: true });
// ลบของเก่าก่อนเขียนใหม่ — idempotent และไม่มีไฟล์ซ้ำ GUID ค้างในโฟลเดอร์
for (const stale of macSideloadFiles(guid)) {
try {
unlinkSync(stale);
} catch {
console.warn(`${PREFIX} ⚠ ลบไฟล์เก่าไม่ได้: ${stale}`);
}
}
const dest = path.join(dir, `${guid}.manifest.xml`);
writeFileSync(dest, xml);
console.log(`${PREFIX} ✓ วาง manifest ไว้ที่ ${dest}`);
return dest;
}
function removeMac() {
const dir = macWefDir();
// บน Mac ไม่มี manifest เก็บแยกที่อื่น (ตัวไฟล์ใน wef คือของที่ลงไว้) — หาทุก *.xml
// ในโฟลเดอร์ที่เป็น manifest ของ add-in นี้จากชื่อในไฟล์เอง จึงไม่ต้อง hardcode GUID
let files;
try {
files = readdirSync(dir)
.filter((name) => name.toLowerCase().endsWith(".xml"))
.map((name) => path.join(dir, name))
.filter((file) => {
try {
const xml = readFileSync(file, "utf8");
return xml.includes("<OfficeApp") && xml.includes("e-Court Documents Versioning");
} catch {
return false;
}
});
} catch {
files = [];
}
if (files.length === 0) {
console.log(`${PREFIX} ไม่พบไฟล์ sideload ของ add-in นี้ใน ${dir} — ถือว่าถอดแล้ว`);
return;
}
for (const file of files) {
unlinkSync(file);
console.log(`${PREFIX} ✓ ลบ ${file}`);
}
}
function printNextSteps() {
const where = process.platform === "win32" ? "Insert > My Add-ins > SHARED FOLDER" : "Insert > My Add-ins";
console.log(`
ขั้นต่อไป:
1. restart Word ให้ครบทุกหน้าต่าง (จำเป็นทุกครั้งหลังลง/ถอด)
2. ${where} > เปิด "e-Court Documents Versioning"
3. เปิดไฟล์ .docx ที่มี ticket เพื่อทดสอบ เช่นสร้างใหม่ด้วย:
node scripts/make-ticket-docx.mjs file.docx --fid DEMO_FID_001 --tid T12
รายละเอียด: docs/WORDADDIN-DEV.md`);
}
async function main() {
const options = parseArgs(process.argv.slice(2));
console.log(`${PREFIX} platform: ${process.platform}${options.remove ? " · โหมดถอดการติดตั้ง" : ""}`);
if (process.platform === "win32") {
if (options.remove) {
removeWindows();
} else {
const { xml, guid } = await downloadManifest(options.url);
installWindows(xml, guid);
}
} else if (process.platform === "darwin") {
if (options.remove) {
removeMac();
} else {
const { xml, guid } = await downloadManifest(options.url);
installMac(xml, guid);
}
} else {
fail(`รองรับเฉพาะ Windows (win32) และ macOS (darwin) — platform นี้คือ ${process.platform}`);
}
console.log(`${PREFIX} ✓ เสร็จ`);
printNextSteps();
}
main().catch((error) => {
fail(error instanceof Error ? error.stack ?? error.message : String(error));
});