2 · Core pipeline — AI text + job-post graphic
ai-recruitment-api ไม่มี workflow แบบหลายสถานะเอกสารเหมือน benefit-api / recruitment-api แต่เป็นชุด on-demand endpoints ที่ back-office เรียกทีละขั้นตอนสร้างประกาศรับสมัคร บทนี้ trace แต่ละขั้นจาก handler จริงบน branch prod พร้อม call site ของ OpenAI / remove.bg / Puppeteer
แผนที่ไฟล์: 01 Repository map
Auth / secret map: 04 Auth + integrations
ภาพรวมลำดับที่สมเหตุสมผลจาก I/O
ลำดับด้านล่างเป็น ลำดับใช้งานที่อนุมานจากชื่อ endpoint และ input/output — ไม่พบ orchestrator กลางใน repo ที่บังคับลำดับนี้
vtrc-backoffice (CORS whitelist)
│ Authorization: Bearer <JWT>
▼
1) POST .../generate-job-description
│ MSSQL jobReqPosition + OpenAI ×4 (gpt-4o via callOpenAI)
▼
2) POST .../get-post-summarize-job-description
POST .../get-post-summarize-job-qualification
│ OpenAI gpt-3.5-turbo (axios ตรง) — ย่อข้อความใส่กราฟิก
▼
3) POST .../get-post-background
│ สุ่ม 4 จาก b1–b29 (ไม่เรียก AI)
▼
4) POST .../get-post-character
│ first-time → คลัง allImages
│ else → callOpenAI (career) + callDalle ×3 + removeBgAndSave ×N
▼
5) POST .../get-post-upload_image (ทางเลือก — upload เอง)
│ remove.bg → เซฟใต้ storage/
▼
6) POST .../get-post-combine
│ qrserver.com + Puppeteer screenshot → base64
▼
7) POST .../generate-interview-questions (อิสระจากกราฟิก)
│ MSSQL หลายตาราง + OpenAI gpt-3.5-turbo หลายรอบ
▼
8) POST .../save-file-to-api
│ writeFileAsync (fileName hardcode 'doctor')Pipeline A — Generate job description
Route: POST /generate-job-description — routes/index.js:53
Handler: generateJobDescription — controllers/generateJobDescriptionController.js:83-385
Input ขั้นต่ำ
บังคับ Position ไม่ว่าง (:117-123) — ถ้าขาด → 400
Body รับฟิลด์ org / level / education / skills จำนวนมากเพื่อประกอบ buildExtraInfo() (:25-80, ใช้ที่ :245, :318)
Step A1 — อ่าน MSSQL
const pool = await sql.connect({
...dbConfig,
options: {
encrypt: true,
trustServerCertificate: true,
enableArithAbort: true,
...(dbConfig.options || {})
}
});Query parameterized:
SELECT TOP (1)
[id], [positionCareerName], [gender], [ageFrom], [ageTo],
[experience], [responsibility], [qualification],
[isAiDescription], [isDescription],
[aiResponsibility], [aiQualification], [aiDescription]
FROM [dbo].[jobReqPosition]
WHERE [positionCareerName] = @Positionแหล่ง: generateJobDescriptionController.js:138-159
- ถ้า query fail → 500
{ message: 'Database query failed' }(:173-176) - ถ้าไม่พบแถว → 200 success พร้อม string ว่าง (
:180-187) — ไม่ใช่ 404
Step A2 — OpenAI สี่ครั้ง (sequential, fail-soft)
ทุกครั้งผ่าน callOpenAI จาก config/openai.js (model gpt-4o)
| ลำดับ | ตัวแปรผลลัพธ์ | บรรทัด await | จุดประสงค์สั้น ๆ |
|---|---|---|---|
| 1.1 | responsibilities_others | :230 | diff NEW vs OLD responsibilities → bullet ไทย + | |
| 1.2 | recruitment_ai | :258 | คิด responsibilities เพิ่ม ≥5 ข้อจาก context |
| 2.1 | qualification_others | :296 | diff NEW vs OLD qualifications |
| 2.2 | qualification_ai | :334 | คิด qualifications เพิ่ม ≥5 ข้อ (+ guidance ประสบการณ์) |
แต่ละบล็อกมี try/catch แยก — error → log แล้วตั้งค่าเป็น '' (:231-233 ฯลฯ) ดังนั้น HTTP ยัง 200 ได้แม้ OpenAI ล้มบางส่วน
callOpenAI เองก็ fail-soft เป็น '' อีกชั้น (config/openai.js:24-27)
Step A3 — ประกอบ response
โครงสร้างสามตอนสำหรับทั้ง description และ qualification:
TRC จากหน่วยงานต้นสังกัด— จาก body เดิมหลังnormalizeBulletsTRC จากประกาศภายในสภากาชาด— จาก OpenAI othersข้อมูลจาก AI— จาก OpenAI ai
แหล่ง: generateJobDescriptionController.js:346-376
return res.json({
status: 'success',
response: { job_description_ai, job_qualification_ai }
});ต้นทุน: อย่างน้อย 4 Chat Completions ต่อ request ที่พบแถวใน DB
Pipeline B — Summarize สำหรับใส่ภาพ
B1 — Job description summary
Route: POST /get-post-summarize-job-description — routes/index.js:124
Handler: summarizeJobDescriptionController.js:7-84
- บังคับ
job_description(:10-16) - Model:
gpt-3.5-turbo(:46) — ไม่ใช้callOpenAI/ ไม่ใช่ gpt-4o - Axios ตรงไป
https://api.openai.com/v1/chat/completions(:44-60) - Prompt สั่งเลือก 4 bullet สำคัญ ภาษาไทย สั้น ๆ (
:19-32) - แปลง newline เป็น
|ก่อนคืน (:66)
B2 — Job qualification summary
Route: POST /get-post-summarize-job-qualification — routes/index.js:129
โครงสร้างสมมาตรกับ B1 (summarizeJobQualificationController.js — pattern เดียวกัน: gpt-3.5-turbo + axios)
ต้นทุน: +1 completion ต่อ endpoint
Pipeline C — Background (ไม่เรียก AI)
Route: POST /get-post-background — routes/index.js:58
Handler: module2_1_get_background.js:59-97
- Fisher–Yates shuffle รายการ
/Background/b1.png…b29.png(:7-37,:62-63) - เลือก 4 รายการ (
:63) - ใส่
id: uuidv4(),is_serious/is_minimalแบบสุ่ม yes/no (:66-73) - คืน
backgroundเป็น string ของ JSON objects คั่นด้วย comma (:78-88)
ไม่แตะ OpenAI / DB / disk write
เวอร์ชัน v2 (ไม่ได้ mount)
module2_1_get_background_v2.js ใช้ metadata seriousness / decoration ต่อไฟล์ (:7-37) และ export getBackgroundV2 (:102) — ไม่มี route ชี้มา ณ prod
Pipeline D — Character (AI + remove.bg หรือคลัง local)
Route: POST /get-post-character — routes/index.js:63-78
Handler: module2_2_controller.js:69-214
Lazy-load dependencies
Handler โหลด callOpenAI, callDalle, removeBgAndSave, allImages ภายในฟังก์ชัน พร้อม 500 ถ้า require ล้ม (:71-98) — สอดคล้องกับ diagnostic guard ใน router
D0 — ตรวจโหมด first-time
isFirstTime(description) เทียบ description กับ prompt สำเร็จรูปยาว ๆ แบบ hardcode (:24-28)
ถ้าตรง → ใช้คลังภาพ; ไม่ตรง → generate ใหม่
D1 — จัดประเภทอาชีพด้วย OpenAI
Prompt ให้เลือก keyword จากรายการตายตัว (Office worker, Doctor, Nurse, …) (:105-111)await callOpenAI(promptCareer) (:115) — ล้มแล้ว default 'Office worker' (:117-119)
D2a — First-time: สุ่มจาก allImages
กรอง position + gender แล้ว pickRandom 3 ภาพ (:124-149)
fallback เป็น Office worker (:146-149)
คืน string รูปแบบ "image1":"url"|"image2":"..." (:155-157)
ไม่เรียก DALL-E / remove.bg ใน path นี้
D2b — Not first-time: DALL-E ×3 + remove.bg
sanitizeDescription(description)— กฎPROMPT_RULES(:31-44, เรียก:161)detectGender+ bodygender→genderHint(:46-51,:163-169)buildFinalPrompt— ภาพสมจริงครึ่งตัว พื้นหลังเรียบ (:53-63,:171)- Payload:
{ model: 'gpt-image-1', prompt, n: 1, size: '1024x1536' }(:172) Promise.allสร้าง 3 ภาพขนาน ด้วยcallDalle(:174-184)- แต่ละ URL ที่ได้ →
removeBgAndSave(url)(:191-201) - คืน path ที่ขึ้นต้นด้วย
/ในรูปแบบ image string เดียวกัน (:203-209)
Prompt sanitization (business rule)
| Pattern | Replacement (ย่อ) |
|---|---|
| คนขับรถ / driver | asian age 40 khaki shirt |
| คนสวน / gardener | asian age 40 khaki shirt |
| บัญชี / accountant | asian age 40 white shirt |
| ภารโรง / ครัว | asian navy workshirt + jean |
แหล่ง: module2_2_controller.js:31-36
ถ้า match จะ แทนทั้ง description ด้วย replacement ไม่ใช่แค่แก้คำ (:40-41)
ต้นทุน path D2b: 1× gpt-4o (career) + 3× image generation + สูงสุด 3× remove.bg
remove.bg ใน path นี้
services/removeBgService.js → POST https://api.remove.bg/v1.0/removebg (:66-78)
timeout 30s (:75)
เซฟผ่าน writeFileAsync โฟลเดอร์ Module2_2 (:139-181)
Pipeline E — Upload image → remove.bg
Route: POST /get-post-upload_image — routes/index.js:99-117
Handler: removeBgUploadController.js:88-181
Input ที่รับ
หนึ่งใน: multipart file(s) จาก upload.any(), หรือ image_url, หรือ base64 (:107-134)
Call site
callRemoveBg ภายในไฟล์เดียวกัน → REMOVE_BG_API_URL = 'https://api.remove.bg/v1.0/removebg' (:16, :42, :80)
Header: X-Api-Key (:32)
timeout 45s (:45, :83)
Output
เขียนไฟล์ใต้ storage/{YYYY-MM-DD}/{folder}/ default folder Module2_3 (:17, :155-165)
คืน { response: { url: '/storage/...' } } (:167-169)
หมายเหตุ static: server.js เสิร์ฟ ./storage ที่ prefix เดียวกัน (server.js:18) แต่ .gitignore ละเว้น storage/ — ไฟล์ runtime อาจไม่อยู่ใน git
Pipeline F — Combine posting (Puppeteer)
Route: POST /get-post-combine — routes/index.js:134
Handler: combineJobPostingController.js:36-448
Input สำคัญ
job_link บังคับ (:57-62)
อื่น ๆ: Division, Position, summaries, Contact, Background/Character URLs, hiring_type/QTY, start/endDate (:41-54)
F1 — QR code
const qrServerUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(job_link)}`;
const qrResponse = await axios.get(qrServerUrl, { responseType: 'arraybuffer' });แหล่ง: combineJobPostingController.js:65-75
ฝังเป็น data URL ใน HTML (:75)
F2 — HTML template + วันที่ พ.ศ.
convertDateToThaiFormat บวกปี 543 (:16-34)
ประกอบ list จาก summary ที่แยกด้วย | (:86-92)
F3 — Puppeteer screenshot
browser = await puppeteer.launch({
executablePath: '/usr/bin/chromium-browser',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
headless: 'new'
});แหล่ง: combineJobPostingController.js:377-386
ตรงกับ Dockerfile PUPPETEER_EXECUTABLE_PATH (Dockerfile:20-21)
- viewport กว้าง 900,
deviceScaleFactor: 2(:390-394) page.setContent(..., { waitUntil: 'networkidle0' })(:397-399)- screenshot element
.container(:402-421) - คืน
result_image_urlเป็น base64 ดิบ (client ต้องเติม data URL prefix เอง — comment ที่:430-431) finallyปิด browser เสมอ (:442-446)
ความเสี่ยง performance: เปิด Chromium ใหม่ทุก request (browser = null ที่ต้น handler :37)
Orphan twin
module_2_3_combine_image.js มี QR URL เดียวกันที่บรรทัด 65 และ puppeteer.launch ที่ ~376 — logic คล้ายกันแต่ ไม่ถูก mount
Pipeline G — Interview questions
Route: POST /generate-interview-questions — routes/index.js:139
Handler: generateInterviewQuestionsController.js:8 … (ไฟล์ 570 บรรทัด)
G1 — Connect MSSQL ด้วย config inline
สร้าง dbConfig จาก env เอง ไม่ผ่าน config/db.js (:29-42) — encrypt: false
G2 — โหลด candidate / job
| Query | ตาราง | เงื่อนไข | บรรทัด |
|---|---|---|---|
| candidate | [appProfileHd] | id = @CandidateID | :56-62 |
| job | [jobReqPosition] | hdid = @JobID | :79-85 |
หมายเหตุ: job description controller ค้นด้วย positionCareerName; ที่นี่ค้นด้วย hdid — คนละคีย์
G3 — OpenAI หลายรอบด้วย helper ภายใน
Helper callChatGPT (:106-134):
- Model:
gpt-3.5-turbo(:108) - System: บังคับ output ภาษาไทย (
:110) - Axios ตรงไป Chat Completions (
:116-124) - แปลง newline →
|(:128)
รอบที่ยืนยันด้วย await callChatGPT:
| ชื่อในโค้ด | บรรทัดประมาณ | หัวข้อ |
|---|---|---|
generalQuestion | :156 | soft skills ทั่วไป |
technicalQuestion | :169 | ชื่อตัวแปร technical แต่ prompt ยังเขียนว่า "General interview question" (:159-168) — ข้อความ prompt อาจคัดลอกผิด |
situationalQuestion | :182 | situational |
personalityQuestion | :195 | personality |
personalityDetailQuestion | :263 | แบบฟอร์ม [title]… ยาว |
coreCompetencyQuestion | :291 | core competency 5 หัวข้อ × 10 คำถาม |
G4 — Concern questions จากข้อมูลโปรไฟล์
Query เพิ่ม (fully-qualified recruitment_ai.dbo.*):
| ตาราง | บรรทัด |
|---|---|
appProfileEdu | :300-303 |
appProfileProLicense | :311-314 |
appProfileExp | :322-325 |
jobReqDegree | :333-336 |
jobReqField | :344-347 |
appProfileCapabilityLang | :356-362 |
จากนั้นประกอบ concernMessages และอาจมี OpenAI เพิ่มสำหรับ concern (อ่านต่อในไฟล์ช่วง :372+) — ใช้ parameterized @CandidateID / @JobID / @ProfileID
ต้นทุน: อย่างน้อย 6 Chat Completions ต่อ request สำเร็จ + DB round-trips หลายครั้ง
Pipeline H — Save file
Route: POST /save-file-to-api — routes/index.js:144
Handler: uploadController.js:4-27
const fileName = 'doctor' // hardcode
const folder = 'Module2_2' // hardcode
const params = { base64: req?.body?.base64, fileName, folder }
const result = await writeFileAsync(params)แหล่ง: uploadController.js:6-13writeFileAsync เขียนใต้ storage/{date}/{folder}/ (utils/file-util.js:47-83)
แผนที่ OpenAI call sites (ครบทั้ง repo)
| # | ไฟล์ | กลไก | Model | บรรทัดหลัก |
|---|---|---|---|---|
| 1 | config/openai.js | axios wrapper callOpenAI | gpt-4o | :7-28 |
| 2 | generateJobDescriptionController.js | เรียก callOpenAI ×4 | gpt-4o | :230,:258,:296,:334 |
| 3 | module2_2_controller.js | callOpenAI (career) | gpt-4o | :115 |
| 4 | config/dalle.js | openai.images.generate | gpt-image-1 default | :47-52 |
| 5 | module2_2_controller.js | callDalle ×3 | gpt-image-1 | :177 |
| 6 | summarizeJobDescriptionController.js | axios ตรง | gpt-3.5-turbo | :55-60 |
| 7 | summarizeJobQualificationController.js | axios ตรง | gpt-3.5-turbo | :58+ |
| 8 | generateInterviewQuestionsController.js | callChatGPT ×6+ | gpt-3.5-turbo | :116-124, awaits :156+ |
ไม่มีที่อื่นใน .js ที่เรียก OpenAI API (ยืนยันด้วยการค้นทั้ง repo)
แผนที่ remove.bg call sites (ครบทั้ง repo)
| # | ไฟล์ | ฟังก์ชัน | URL | เรียกจาก |
|---|---|---|---|---|
| 1 | services/removeBgService.js | callRemoveBg / removeBgAndSave | api.remove.bg/v1.0/removebg | module2_2_controller.js:194 |
| 2 | controllers/removeBgUploadController.js | callRemoveBg | เดียวกัน | route /get-post-upload_image |
| 3 | controllers/get-post-upload_image.js | สำเนา logic ใกล้เคียง | เดียวกัน | ไม่ถูกใช้เป็น handler จริง (export ชื่อไม่ตรง require) |
Failure modes ที่สำคัญ
| สถานการณ์ | พฤติกรรมจริง |
|---|---|
| ไม่มี JWT | 401 จาก verifyJWT ก่อนเข้า pipeline |
| Position ไม่มีใน DB | 200 + ข้อความว่าง (job description) |
| OpenAI ล้มบางส่วนใน job description | 200 + section ว่าง |
| module2_2 require ล้มตอน boot | route ยังมี แต่ตอบ 500 diagnostic |
| DALL-E ไม่คืน URL | { status: 'error', message: 'Image generation returned no usable URLs' } |
| remove.bg ล้มหลัง generate | อาจได้ error "Background removal produced no images" |
ไม่มี job_link ใน combine | 400 |
| Chromium path ผิด (นอก Docker) | Puppeteer launch fail → 500 |
ไม่มี OPENAI_API_KEY | summarize/interview ตอบ 500; callOpenAI ยังยิงแล้วได้ '' |
เคล็ดลับตอนทำงานจริง
- ทดสอบ prompt กับคีย์จริงก่อน deploy — ไม่มี unit test ครอบ prompt
- แยกต้นทุน: job description (4× gpt-4o) + character (1× gpt-4o + 3× image + remove.bg) + interview (6× gpt-3.5) — อย่าเรียกซ้ำโดยไม่ตั้งใจ
- บนเครื่อง local ที่ไม่มี
/usr/bin/chromium-browserการ combine จะพังแม้ logic HTML ถูก - อย่าแก้ orphan
module_2_3_*แล้วคาดว่า production จะเปลี่ยน — ต้อง mount ในroutes/index.jsก่อน
ไป บท 3 Persistence →