Skip to content

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-descriptionroutes/index.js:53
Handler: generateJobDescriptioncontrollers/generateJobDescriptionController.js:83-385

Input ขั้นต่ำ

บังคับ Position ไม่ว่าง (:117-123) — ถ้าขาด → 400

Body รับฟิลด์ org / level / education / skills จำนวนมากเพื่อประกอบ buildExtraInfo() (:25-80, ใช้ที่ :245, :318)

Step A1 — อ่าน MSSQL

javascript
const pool = await sql.connect({
  ...dbConfig,
  options: {
    encrypt: true,
    trustServerCertificate: true,
    enableArithAbort: true,
    ...(dbConfig.options || {})
  }
});

Query parameterized:

sql
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.1responsibilities_others:230diff NEW vs OLD responsibilities → bullet ไทย + |
1.2recruitment_ai:258คิด responsibilities เพิ่ม ≥5 ข้อจาก context
2.1qualification_others:296diff NEW vs OLD qualifications
2.2qualification_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:

  1. TRC จากหน่วยงานต้นสังกัด — จาก body เดิมหลัง normalizeBullets
  2. TRC จากประกาศภายในสภากาชาด — จาก OpenAI others
  3. ข้อมูลจาก AI — จาก OpenAI ai

แหล่ง: generateJobDescriptionController.js:346-376

javascript
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-descriptionroutes/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-qualificationroutes/index.js:129
โครงสร้างสมมาตรกับ B1 (summarizeJobQualificationController.js — pattern เดียวกัน: gpt-3.5-turbo + axios)

ต้นทุน: +1 completion ต่อ endpoint


Pipeline C — Background (ไม่เรียก AI)

Route: POST /get-post-backgroundroutes/index.js:58
Handler: module2_1_get_background.js:59-97

  1. Fisher–Yates shuffle รายการ /Background/b1.pngb29.png (:7-37, :62-63)
  2. เลือก 4 รายการ (:63)
  3. ใส่ id: uuidv4(), is_serious / is_minimal แบบสุ่ม yes/no (:66-73)
  4. คืน 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-characterroutes/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

  1. sanitizeDescription(description) — กฎ PROMPT_RULES (:31-44, เรียก :161)
  2. detectGender + body gendergenderHint (:46-51, :163-169)
  3. buildFinalPrompt — ภาพสมจริงครึ่งตัว พื้นหลังเรียบ (:53-63, :171)
  4. Payload: { model: 'gpt-image-1', prompt, n: 1, size: '1024x1536' } (:172)
  5. Promise.all สร้าง 3 ภาพขนาน ด้วย callDalle (:174-184)
  6. แต่ละ URL ที่ได้ → removeBgAndSave(url) (:191-201)
  7. คืน path ที่ขึ้นต้นด้วย / ในรูปแบบ image string เดียวกัน (:203-209)

Prompt sanitization (business rule)

PatternReplacement (ย่อ)
คนขับรถ / driverasian age 40 khaki shirt
คนสวน / gardenerasian age 40 khaki shirt
บัญชี / accountantasian 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.jsPOST 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_imageroutes/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-combineroutes/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

javascript
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

javascript
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-questionsroutes/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:156soft skills ทั่วไป
technicalQuestion:169ชื่อตัวแปร technical แต่ prompt ยังเขียนว่า "General interview question" (:159-168) — ข้อความ prompt อาจคัดลอกผิด
situationalQuestion:182situational
personalityQuestion:195personality
personalityDetailQuestion:263แบบฟอร์ม [title]… ยาว
coreCompetencyQuestion:291core 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-apiroutes/index.js:144
Handler: uploadController.js:4-27

javascript
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-13
writeFileAsync เขียนใต้ storage/{date}/{folder}/ (utils/file-util.js:47-83)


แผนที่ OpenAI call sites (ครบทั้ง repo)

#ไฟล์กลไกModelบรรทัดหลัก
1config/openai.jsaxios wrapper callOpenAIgpt-4o:7-28
2generateJobDescriptionController.jsเรียก callOpenAI ×4gpt-4o:230,:258,:296,:334
3module2_2_controller.jscallOpenAI (career)gpt-4o:115
4config/dalle.jsopenai.images.generategpt-image-1 default:47-52
5module2_2_controller.jscallDalle ×3gpt-image-1:177
6summarizeJobDescriptionController.jsaxios ตรงgpt-3.5-turbo:55-60
7summarizeJobQualificationController.jsaxios ตรงgpt-3.5-turbo:58+
8generateInterviewQuestionsController.jscallChatGPT ×6+gpt-3.5-turbo:116-124, awaits :156+

ไม่มีที่อื่นใน .js ที่เรียก OpenAI API (ยืนยันด้วยการค้นทั้ง repo)


แผนที่ remove.bg call sites (ครบทั้ง repo)

#ไฟล์ฟังก์ชันURLเรียกจาก
1services/removeBgService.jscallRemoveBg / removeBgAndSaveapi.remove.bg/v1.0/removebgmodule2_2_controller.js:194
2controllers/removeBgUploadController.jscallRemoveBgเดียวกันroute /get-post-upload_image
3controllers/get-post-upload_image.jsสำเนา logic ใกล้เคียงเดียวกันไม่ถูกใช้เป็น handler จริง (export ชื่อไม่ตรง require)

Failure modes ที่สำคัญ

สถานการณ์พฤติกรรมจริง
ไม่มี JWT401 จาก verifyJWT ก่อนเข้า pipeline
Position ไม่มีใน DB200 + ข้อความว่าง (job description)
OpenAI ล้มบางส่วนใน job description200 + section ว่าง
module2_2 require ล้มตอน bootroute ยังมี แต่ตอบ 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 ใน combine400
Chromium path ผิด (นอก Docker)Puppeteer launch fail → 500
ไม่มี OPENAI_API_KEYsummarize/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