27 KiB
e-Court Documents Versioning (Word Add-in)
ระบบจัดการ version history ของเอกสาร Word — สร้างเอกสารจาก template ผ่านเว็บ ติดตามทุกการแก้ไขผ่าน add-in ใน Word และ sync ข้อมูลสำคัญกลับฐานข้อมูลอัตโนมัติ
คุณสมบัติหลัก
- 📝 สร้างเอกสารจาก template — กรอก form บนเว็บ → ระบบ generate .docx จาก template (แทนที่ค่าใน content controls) พร้อม stamp metadata และขึ้นเป็น version 1 อัตโนมัติ
- 🗂️ Template management — upload ไฟล์ .docx ที่มี content controls แล้วระบบ detect fields อัตโนมัติ ตั้งชื่อ/ชนิด/required ต่อ field ผ่านหน้าเว็บ
- 📌 ตรวจจับ tracked document — เปิดไฟล์ใน Word (Desktop Win/Mac หรือ Online) แล้ว add-in รู้ทันทีว่าไฟล์นี้อยู่ในระบบ พร้อมแสดง version history
- 💾 Save & Version — ปุ่มใน task pane สร้าง version ใหม่ (stamp metadata + อัปโหลด snapshot ไป backend)
- 🔄 Field sync — ทุกครั้งที่สร้าง version ระบบ extract ค่าจาก content controls ของเอกสารไป update fields ใน DB ให้ sync กับเนื้อหาจริงเสมอ
- ⬇️ ดาวน์โหลด version ย้อนหลัง — ทุก version เก็บครบพร้อม SHA256 hash ตรวจสอบความถูกต้องได้
สถาปัตยกรรม
| ส่วน | Stack | หน้าที่ |
|---|---|---|
Add-in (/addin) |
Office.js, TypeScript, React, Fluent UI | Task pane ใน Word — ตรวจจับ document, Save & Version, version history, field extraction |
Backend (/backend) |
ASP.NET Core 8, EF Core, PostgreSQL, MinIO, OpenXML | Version API, template system, docx generation, JWT validation |
Web Portal (/web) |
Nuxt.js, TailwindCSS | จัดการ template + สร้างเอกสารจาก form (light/dark theme) |
| Auth | Keycloak (OIDC + PKCE) | Public clients ทั้ง add-in และเว็บ, backend validate ผ่าน JWKS |
| Storage | MinIO (S3-compatible) | bucket docx-versions (snapshot ต่อ version) + docx-templates (ไฟล์ template) — private ทั้งคู่ |
Flow หลัก:
- Admin upload template (.docx ที่มี content controls) → ระบบ detect fields อัตโนมัติ
- ผู้ใช้กรอก form บนเว็บ → ได้ไฟล์ .docx ที่เป็น version 1 แล้ว → เปิดใน Word
- Add-in รู้จักเอกสารทันที → แก้ไขเนื้อหา → กด Save & Version → เกิด version ใหม่ + fields ใน DB sync ตามเนื้อหาล่าสุด
สถานะโปรเจกต์
| Phase | เนื้อหา | สถานะ |
|---|---|---|
| 0 | Scaffold 3 โปรเจกต์ (addin / backend / web) | ✅ เสร็จ |
| 1 | Local dev infra — docker-compose + Keycloak (realm, 3 clients + PKCE) + MinIO (buckets) | ✅ เสร็จ |
| 2 | Backend core — version API (FR-5–7) + JWT auth (FR-8) | ✅ เสร็จ |
| 3 | Template system + docx generation + field sync (FR-9–11) | ✅ เสร็จ |
| 4 | Add-in services layer — auth (PKCE + Dialog API), metadata, file/API, field extraction (FR-8, FR-11) | ✅ เสร็จ |
| 5 | Task pane UI — sign-in, Start Versioning, Save & Version, history (FR-1–4) | ✅ เสร็จ — ทดสอบจริงบน Word for Mac แล้ว (login/tracking/Save & Version ผ่าน, แก้ปัญหาที่เจอ 4 ประเด็น — ดู Troubleshooting) เหลือยืนยัน download + AC เต็มชุด |
| 6 | Web Portal UI (FR-9–10) | ⏳ รอ implement |
| 7–8 | Tests ครบชุด + เอกสารฉบับเต็ม | ⏳ รอ implement |
ใช้ได้แล้วตอนนี้: infra ทั้งชุด + backend API ครบทั้ง version API (upload / history / download) และ template system (upload template พร้อม auto-detect fields, generate เอกสารเป็น v1, field sync ตอนสร้าง version) — ลองเล่นได้ผ่าน Swagger หรือ curl (ตัวอย่างด้านล่าง) · ฝั่ง add-in ครบทั้ง services layer และ task pane UI พร้อม unit tests 45 ตัว
เอกสาร
- specs/spec-officejs.md — spec เต็มรูปแบบ: functional requirements (FR-1–11), data models, acceptance criteria (1–14)
- docs/plans/officejs-implementation-plan.md — implementation plan แบ่ง 9 phases พร้อม definition of done
สำหรับผู้เริ่มพัฒนา: อ่าน spec ก่อน แล้วตามด้วย plan (เริ่มจาก Phase 0)
การพัฒนา (local)
Prerequisites
- Docker Desktop
- .NET 8 SDK +
dotnet-eftool (สำหรับ migration):dotnet tool install --global dotnet-ef --version 8.0.11 - Node.js LTS (add-in และ web)
- Dev cert สำหรับ localhost (ต้องทำก่อน
docker compose up— Keycloak mount cert จากที่นี่):
ทำครั้งเดียวต่อเครื่อง: สร้าง CA + cert ที่ trust ไว้ใน keychain ใช้ทั้ง add-in dev server (https://localhost:3000) และ Keycloak (https://localhost:8443)cd addin && npx office-addin-dev-certs install
1) รัน infrastructure (PostgreSQL + MinIO + Keycloak)
cp .env.example .env # ครั้งแรกครั้งเดียว — แก้ password ได้ตามต้องการ
docker compose up -d
เมื่อขึ้นครบแล้ว:
| บริการ | URL | เข้าด้วย |
|---|---|---|
| Keycloak admin console | https://localhost:8443/admin/ | KC_BOOTSTRAP_ADMIN_USERNAME / KC_BOOTSTRAP_ADMIN_PASSWORD จาก .env |
| MinIO console | http://localhost:9001 | MINIO_ROOT_USER / MINIO_ROOT_PASSWORD จาก .env |
| PostgreSQL | localhost:5432 |
POSTGRES_USER / POSTGRES_PASSWORD จาก .env, ฐานข้อมูล wordversioning |
ทำไม Keycloak ต้องเป็น HTTPS: Office Dialog API ที่ add-in ใช้เปิดหน้า login ปฏิเสธ http:// โดยสมบูรณ์ (error: "The HTTP protocol is not supported. Use HTTPS instead") — dev จึงให้ Keycloak เสิร์ฟ HTTPS ที่ port 8443 ด้วย cert ของ
office-addin-dev-certs(CA ถูก trust ในเครื่องแล้ว Word/webview จึงเชื่อ) · ตัว issuer ใน token จะเป็นhttps://localhost:8443/realms/word-versioningซึ่ง backend ตั้งAuthentication:Authorityตรงกัน
Keycloak จะ import realm word-versioning อัตโนมัติ (clients 3 ตัว + test user) จาก keycloak/realm-export.json — ไฟล์นี้คือ source of truth ของ realm ฝั่ง dev: แก้ realm ให้แก้ที่ไฟล์นี้แล้วรัน docker compose rm -sf keycloak && docker compose up -d เพื่อ import ใหม่ (การแก้ผ่าน admin console จะหายเมื่อ recreate container)
Test user สำหรับล็อกอิน: testuser / Test1234! (เปลี่ยนได้ใน realm-export.json)
MinIO จะมี bucket docx-versions และ docx-templates (private ทั้งคู่) สร้างให้อัตโนมัติ
2) Backend API
cd backend/WordVersioning.Api
dotnet ef database update # ครั้งแรก / เมื่อมี migration ใหม่
dotnet run # รันทั้ง http://localhost:5075 และ https://localhost:7232 (Swagger ใช้ https ก็ได้)
ทำไม dev ต้องมี endpoint HTTPS ด้วย: task pane ของ add-in เป็นหน้า
https://localhost:3000และ webview ของ Word (WKWebView บน Mac) บล็อก fetch จากหน้า https ไป http แบบเงียบๆ — error ที่เห็นคือ "Load failed" — ส่วน CORS ก็ไม่ช่วยเพราะ request ไม่เคยถึง server · dev จึงเปิด endpointhttps://localhost:7232ด้วย cert ชุดเดียวกับ Keycloak (~/.office-addin-dev-certs, ปิดได้ผ่านDevHttps:Enabled=falseในappsettings.Development.json) · add-in ชี้ API ที่ https ตัวนี้อัตโนมัติ
ค่าเชื่อมต่อ (DB/MinIO/Keycloak) อยู่ใน appsettings.json + appsettings.Development.json — override ผ่าน env ได้ทั้งหมด เช่น ConnectionStrings__Default, MinIO__Endpoint, Authentication__Authority
API ที่มีตอนนี้ (ทุก endpoint ต้องแนบ Authorization: Bearer <token>):
# Documents / versions (FR-5–7, 11)
POST /api/documents/{documentId}/versions # อัปโหลด version ใหม่ + field sync
GET /api/documents/{documentId}/versions # version history ใหม่ → เก่า (พร้อม snapshot fields)
GET /api/documents/{documentId}/versions/{versionId}/download # ดาวน์โหลด version (proxy stream จาก MinIO)
GET /api/documents/{documentId}/versions/{versionId}/download-link # presigned URL อายุสั้น (ให้เบราว์เซอร์โหลดตรงจาก MinIO)
# Generate จาก template (FR-10)
POST /api/documents/generate # { templateId, values } → เอกสาร v1 พร้อม metadata
# Templates (FR-9)
GET /api/templates # รายการ (?includeInactive=true สำหรับ admin)
GET /api/templates/{id} # รายละเอียด + field definitions
POST /api/templates # upload .docx → detect fields อัตโนมัติ
PUT /api/templates/{id} # แก้ metadata + field definitions (label/type/required/ลำดับ)
PUT /api/templates/{id}/file # แทนที่ไฟล์ → re-detect (บอก tag ที่เพิ่ม/หาย)
DELETE /api/templates/{id} # deactivate (soft-delete)
- Upload เป็น multipart/form-data:
file(.docx เท่านั้น, จำกัด 100MB) +clientVersionId(GUID) +fields(optional, JSON map tag→text สำหรับ field sync) documentIdยังไม่มีในระบบ = สร้างใหม่อัตโนมัติ ·versionNumberเพิ่มอัตโนมัติต่อเอกสาร · เก็บ SHA256 ของทุก version- Field sync policy (FR-11): field ที่ส่งมาขาด (ผู้ใช้ลบ content control ใน Word) → เก็บค่าเดิม + mark missing ใน snapshot ไม่ fail request · ส่งเกินนิยาม → ignore
- Generate (FR-10): backend เปิด template ด้วย OpenXML → แทนที่ค่าใน SDT ที่ run level (รักษา formatting) → stamp custom properties (
SavedByAddIn,DocumentTrackingId) → upload เป็น v1 → response{ documentId, versionId, versionNumber, downloadUrl }— เปิดไฟล์ใน Word แล้ว add-in (Phase 4–5) จะรู้จักทันที
ขอ token ทดสอบ (dev)
ทุก client ใน realm บังคับ PKCE (ปิด direct grants ตาม spec) และ token ของ admin-cli ไม่มี claim aud จึงเรียก backend ไม่ผ่าน (401) — ใช้สคริปต์นี้เดิน PKCE flow จริงแทน (login ด้วย test user ให้อัตโนมัติ):
python3 scripts/dev-token.py # ปริ้น access token
python3 scripts/dev-token.py --clip # หรือคัดลอกเข้า clipboard
TOKEN=$(python3 scripts/dev-token.py) # เก็บใส่ตัวแปร
แล้วลอง upload:
DOC_ID=$(uuidgen | tr 'A-Z' 'a-z')
curl -X POST http://localhost:5075/api/documents/$DOC_ID/versions \
-H "Authorization: Bearer $TOKEN" \
-F "file=@เอกสาร.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
-F "clientVersionId=$(uuidgen)"
# → { "versionId": "...", "versionNumber": 1 }
ลองเล่น template → generate → field sync (Phase 3)
0) เตรียมไฟล์ template — เขียน .docx ใน Word แล้วแทรก content controls ในจุดที่จะเป็น field (Developer tab → Rich Text Content Control → Properties ตั้ง Tag ให้ unique ต่อไฟล์ เช่น employee_name, salary)
1) Upload template → ระบบ detect fields อัตโนมัติ:
curl -X POST http://localhost:5075/api/templates \
-H "Authorization: Bearer $TOKEN" \
-F "name=สัญญาจ้างงาน" \
-F "file=@template.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document"
# → { "id": "...", "fields": [ { "key": "employee_name", ... }, ... ] }
2) ตั้งค่า field definitions (label ที่แสดงในฟอร์ม / ชนิด text|number|date / required / ลำดับ):
curl -X PUT http://localhost:5075/api/templates/$TPL_ID \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"fields": [
{ "key": "employee_name", "label": "ชื่อ-นามสกุล", "type": "text", "isRequired": true, "sortOrder": 0 },
{ "key": "salary", "label": "เงินเดือน", "type": "number", "isRequired": true, "sortOrder": 1 } ]}'
3) Generate เอกสาร — ได้ .docx ที่เป็น version 1 พร้อม metadata:
curl -X POST http://localhost:5075/api/documents/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"templateId\":\"$TPL_ID\",\"values\":{\"employee_name\":\"สมชาย ใจดี\",\"salary\":\"50000\"}}"
# → { "documentId": "...", "versionId": "...", "versionNumber": 1, "downloadUrl": "/api/documents/.../download" }
curl -H "Authorization: Bearer $TOKEN" -o ไฟล์ที่สร้าง.docx \
"http://localhost:5075$DOWNLOAD_URL"
เปิดไฟล์ที่ได้ตรวจได้ว่า: ค่าถูกทุก field (formatting ของ template คงเดิม) และมี custom properties SavedByAddIn / DocumentTrackingId (unzip ดู docProps/custom.xml หรือ Word → File → Info → Properties → Advanced Properties)
4) Field sync ตอนสร้าง version ถัดไป — upload v2 พร้อมแนบ fields (map tag→text ที่ extract จาก content controls — ในอนาคตฝั่ง add-in ทำให้อัตโนมัติ):
curl -X POST http://localhost:5075/api/documents/$DOC_ID/versions \
-H "Authorization: Bearer $TOKEN" \
-F "file=@ไฟล์แก้แล้ว.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
-F "clientVersionId=$(uuidgen)" \
-F 'fields={"employee_name":"สมชาย ใจดี มากกว่าเดิม","salary":"60000"}'
แล้วดู GET /api/documents/$DOC_ID/versions — แต่ละ version มี snapshot fields ของตัวเอง หากลบ content control ออกจาก Word ก่อน upload field นั้นจะเห็น "missing": true โดยค่าเดิมยังคงอยู่ (ไม่ error)
3) Add-in (ครบทั้ง services layer และ task pane UI)
cd addin
npx office-addin-dev-certs install # ครั้งแรกครั้งเดียว (https localhost)
npm run build # webpack build + type-check ทั้ง program
npm test # Jest — unit tests (45 ตัว)
npm run lint # eslint (ใช้ eslint.config.mjs ของโปรเจกต์)
npm run validate # ตรวจ manifest.xml
npm start # dev server https://localhost:3000 — sideload ผ่าน manifest.xml
Services layer ที่มีแล้ว (addin/src/services/) — ทุกตัวมี unit tests ครอบ (mock Word.run, Office.context.ui, fetch):
| ไฟล์ | หน้าที่ |
|---|---|
authService.ts |
OIDC Authorization Code + PKCE (S256) ผ่าน Office Dialog API, token ใน memory เท่านั้น, silent refresh, state กัน CSRF |
metadataService.ts |
อ่าน/stamp custom properties (SavedByAddIn ฯลฯ), Custom XML Part แบบ delete-then-add (fail เงียบๆ ตาม known bug บน Mac), readContentControlFields() (FR-11) |
fileService.ts |
ดึงไฟล์ปัจจุบันผ่าน File/Slice API ประกอบเป็น Blob — เรียก closeAsync เสมอ |
apiClient.ts |
fetch wrapper แนบ Bearer, โดน 401 → refresh + retry ครั้งเดียว, ยัง fail → AuthRequiredError ให้ UI กลับหน้า sign-in |
versioningService.ts |
uploadVersion (แนบ fields), getVersions, downloadVersion + getDownloadLink (presigned) |
versionFlow.ts |
orchestration ของ Start Versioning / Save & Version ตามลำดับ 7 ขั้นตอนของ spec |
หน้า login ของ add-in คือ addin/src/taskpane/auth-callback.html — redirect target ของ Keycloak (ตรง https://localhost:3000/auth-callback.html ใน realm-export.json)
Config ฝั่ง add-in ฉีดตอน build ผ่าน webpack DefinePlugin (อ่านจาก env — default ชี้ dev ในพื้นที่):
KEYCLOAK_AUTHORITY=https://localhost:8443/realms/word-versioning \
API_BASE_URL=https://localhost:7232 \
npm run build
(ตัวแปรที่เหลือ: KEYCLOAK_CLIENT_ID — default word-addin-officejs)
Task pane UI (Phase 5) — state machine checking → signed-out → untracked → tracking:
- Sign-in gate (FR-1) — ยังไม่ login → ปุ่ม Sign in (เปิด Keycloak ผ่าน Office Dialog API) · login แล้วตรวจ property
SavedByAddInในไฟล์: ตรง → โหลด version history ทันที (ไฟล์ที่สร้างจากเว็บเข้า state นี้เลย เห็น v1 โดยไม่ต้องกดอะไร) / ไม่ตรง → ปุ่ม Start Versioning This Document - Save & Version (FR-2) — confirm dialog → stamp properties → อัปเดต Custom XML Part → native save → ดึงไฟล์ + extract fields (FR-11) → upload → refresh history · ทุกขั้นมี loading + error ไม่มี unhandled rejection
- Soft reminder (FR-3) — ระหว่าง tracking ถามทุก 30 วิ ว่ามีแก้ไขที่ยังไม่ได้ version → banner เตือน (ไม่ block)
- Version history (FR-4) — รายการ v เวลา/ผู้สร้าง + ปุ่มดาวน์โหลด version นั้น (ขอ presigned URL อายุ 2 นาทีแล้วเปิดในเบราว์เซอร์ของเครื่อง — webview ของ Word ทิ้ง blob download แบบเงียบๆ ไฟล์จึงต้องมาจากเบราว์เซอร์จริง แล้วหาได้ในโฟลเดอร์ Downloads ของเบราว์เซอร์)
- Session หมดอายุ (AC-9) — 401 → silent refresh → ยังไม่ได้ → กลับหน้า sign-in พร้อมข้อความ
Sideload เพื่อทดสอบใน Word จริง (ต้องรัน infra + backend + npm start ก่อน):
- Word Desktop (Mac/Windows):
cd addin && npm start(ใช้ office-addin-debugging sideload ให้อัตโนมัติ) หรือ insert manual: Word → Insert → My Add-ins → Upload My Add-in → เลือกaddin/manifest.xml - Word Online: ไฟล์ใดก็ได้ใน OneDrive/SharePoint → Insert → Office Add-ins → Upload My Add-in → เลือก
addin/manifest.xml - ไล่ทดสอบตาม acceptance criteria: login ผ่าน dialog → เปิดไฟล์ใหม่เห็นปุ่ม Start Versioning → กดแล้วได้ v1 → แก้ไขแล้ว Save & Version → history เพิ่ม + ดาวน์โหลด version เก่าเปิดได้
- ⚠️ ห้าม
cpทับ manifest ใน~/Library/Containers/com.microsoft.Word/Data/Documents/wef/—npm startสร้าง symlink ไว้ การทับทำให้ครั้งถัดไปขึ้นEEXIST— ถาวรแก้ที่addin/manifest.xmlต้นทางแล้ว restart Word (ถ้าเจอ EEXIST แล้ว: ลบไฟล์ใน wef ทิ้งแล้วnpm startใหม่)
4) Web Portal (ยังเป็น scaffold)
cd web && npm run dev # http://localhost:5173 (ห้ามใช้ 3000 — ชน add-in)
logic จริง (login, template UI, dynamic form) มาตาม Phase 6
คำแนะนำฉบับเต็ม (sideload บน Word Desktop/Online, สร้าง template แรก, สร้างเอกสารจากเว็บ) จะเติมเมื่อ implement ครบตาม Phase 8 ของแผน
Troubleshooting (จากการทดสอบจริงบน Word for Mac)
ปัญหาที่เจอตอน sideload test รอบแรกๆ พร้อมสาเหตุ/วิธีแก้ — แก้ไขในโค้ดแล้วทั้งหมด เก็บไว้เป็นบทเรียน:
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| กด Sign in แล้ว "The HTTP protocol is not supported. Use HTTPS instead" | Office Dialog API ปฏิเสธ http:// โดยสมบูรณ์ แต่ Keycloak dev เดิมรัน http://8080 | Keycloak เสิร์ฟ HTTPS ที่ https://localhost:8443 ด้วย cert ของ office-addin-dev-certs (docker-compose mount ~/.office-addin-dev-certs เข้า container) |
| Login ผ่านแต่ทุก API call ขึ้น "Load failed" | WKWebView บล็อก fetch จากหน้า https ไป http (mixed content) — task pane เป็น https://localhost:3000 แต่ API เป็น http://localhost:5075 (CORS ไม่เกี่ยว — ตรวจแล้วเรียบร้อย) |
backend เปิด endpoint https://localhost:7232 ด้วย cert ชุดเดียวกัน (DevHttps:* ใน appsettings.Development.json) — add-in ชี้อันนี้อัตโนมัติ |
| กด Start Versioning แล้ว "รูปแบบข้อมูลของ slice ไม่รองรับ" | host แต่ละตัวส่ง slice.data คนละรูป (Win = ArrayBuffer/view, Mac = base64 string หรือ number[]) |
fileService.sliceDataToBytes() normalize ครบทั้ง 4 รูปแบบ |
| กดดาวน์โหลด version แล้วไม่มีไฟล์โผล่ที่ไหนเลย | webview ของ Word ทิ้ง blob download (a[download]) แบบเงียบๆ |
ขอ presigned URL อายุ 2 นาทีจาก download-link แล้วเปิดในเบราว์เซอร์ของเครื่อง (openBrowserWindow) — ไฟล์ลงโฟลเดอร์ Downloads ของเบราว์เซอร์ |
| แก้ webpack.config.js แล้วค่าเก่ายังติดอยู่ | webpack ไม่ restart เองเมื่อ config เปลี่ยน + dev-server ตัวเก่าถือ port 3000 อยู่ (ตัวใหม่ขึ้นไม่ได้) | kill dev-server เดิม (`lsof -ti :3000 |
npm start ขึ้น EEXIST ... wef/...manifest.xml |
มีการ cp ทับ symlink ของ sideload จนกลายเป็นไฟล์ธรรมดา |
ลบไฟล์นั้นใน wef folder ทิ้งแล้ว npm start ใหม่ (ดูคำเตือนในหัวข้อ Sideload) |
curl token จาก admin-cli แล้วเรียก backend ได้ 401 |
token ของ admin-cli ไม่มี claim aud (ไม่ได้อยู่ใน realm-export จึงไม่มี audience mapper) |
ใช้ python3 scripts/dev-token.py (เดิน PKCE จริงด้วย client word-versioning-web) |
กติกาที่สรุปได้: ใน dev ทุก service ที่ task pane (หน้า https) จะเรียก ต้องเป็น HTTPS ด้วย cert จาก office-addin-dev-certs ทั้งหมด — Keycloak (8443) และ backend (7232)
เทคโนโลยี
- Add-in: Office.js (WordApi 1.5+), TypeScript, React, Fluent UI, webpack — scaffold ด้วย
yo office - Backend: .NET 8, EF Core + Npgsql (PostgreSQL), AWSSDK.S3 → MinIO (
ForcePathStyle = true), DocumentFormat.OpenXml, JwtBearer - Web: Nuxt.js (Vue 3), TailwindCSS (
darkMode: 'class') - Auth: Keycloak — realm
word-versioning, clients:word-addin-officejs,word-versioning-web,word-addin-vsto(public + PKCE S256 ทุกตัว)