EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Hardware / ลบใบหน้าในเครื่องสแกนไม่ได้ เพราะ API ตอบ notSupport
Hardware

ลบใบหน้าในเครื่องสแกนไม่ได้ เพราะ API ตอบ notSupport

โดย Ton-Or ·15 ต.ค. 2026·อ่าน 10 นาที
ลบใบหน้าในเครื่องสแกนไม่ได้ เพราะ API ตอบ notSupport

ปัญหาไม่ได้อยู่ที่เครื่องสแกนอ่านหน้าไม่แม่น มันแม่นเกินไปด้วยซ้ำ ปัญหาคือคนที่ลาออกไปเมื่อสองเดือนก่อนยังเดินผ่านประตูได้อยู่ เพราะคำสั่งลบใบหน้าที่เอกสารบอกว่ามี ยิงเข้าไปจริงแล้วเครื่องตอบกลับมาคำเดียวว่า notSupport

พอดีได้งานต่อยอดจากระบบทะเบียนพนักงานที่ทำค้างไว้ โจทย์คือให้ทะเบียนกลางกับเครื่องสแกนใบหน้าที่ประตูตรงกันโดยไม่ต้องมีใครเดินไปกดหน้าเครื่อง ผมนึกว่าเป็นงานครึ่งวัน อ่าน API แล้วเขียนลูป sync แค่นั้น แต่พอลงมือจริงกลับติดอยู่ตรงข้อเดียวคือ "ลบ" ไล่ยิงคำสั่งลบใบหน้าทุกแบบที่เอกสารระบุไว้ ได้คำตอบเดียวกันหมดคือ notSupport ทั้งที่หน้า capabilities ของเครื่องเองประกาศว่ารองรับ สุดท้ายทางที่ใช้ได้จริงไม่ใช่การลบ ผมเลยเอามาเขียนไว้ทั้งเส้นทาง เพราะกว่าจะเจอเสียเวลาไปสองวันเต็ม

เลข IP ในบทความเป็นค่าตัวอย่าง ส่วนคำสั่ง โครงสร้าง JSON และรหัสตอบกลับเป็นของจริงที่เจอมา

ขอบเขต: เครื่องของเราเอง กับรหัสที่ได้รับมอบมา

ต้องปักหมุดข้อนี้ก่อน สิ่งที่เล่าทั้งบทความคือการดูแลเครื่องควบคุมการเข้าออกที่อยู่ในความรับผิดชอบของเราเอง ในวงแลนที่เราดูแล ด้วยบัญชีผู้ดูแลที่ได้รับมอบมาพร้อมงาน มันคืองานทะเบียนทรัพย์สินกับทะเบียนคน ไม่ใช่การหาทางเข้าเครื่องที่ไม่ใช่ของเรา

เครื่องของเราเอง ด้วยสิทธิ์ที่เราถืออยู่แล้ว

โค้ดทุกชิ้นในบทความนี้ใช้บัญชีผู้ดูแลที่ได้รับมอบมาพร้อมงาน และยิงไปที่เครื่องในวงปิดที่เราดูแลเท่านั้น ระบบควบคุมการเข้าออกเป็นอุปกรณ์ที่กระทบสิทธิ์คน การเอาสคริปต์แบบนี้ไปยิงเครื่องที่ไม่ใช่ของตัวเองคือความผิดตามกฎหมาย และไม่ใช่สิ่งที่บทความนี้พูดถึง

กติกาที่ผมถือเสมอคือไม่ฝังรหัสผ่านลงไฟล์สคริปต์ อ่านจาก environment variable อย่างเดียว เพราะไฟล์พวกนี้สุดท้ายจะถูกก๊อปไปเครื่องอื่น ส่งต่อให้ช่างคนถัดไป หรือเผลอ commit ขึ้น git แล้วตามลบไม่ทัน

# บัญชีผู้ดูแลของเครื่องที่เราได้รับมอบมา วางไว้ใน environment ไม่ใช่ในไฟล์
export ACS_HOST=10.10.10.41
export ACS_USER=admin
export ACS_PASS='<PASSWORD>'
python face-sync.py --dry-run
import os, requests
from requests.auth import HTTPDigestAuth

HOST = os.environ["ACS_HOST"]
AUTH = HTTPDigestAuth(os.environ["ACS_USER"], os.environ["ACS_PASS"])
BASE = "http://%s/ISAPI" % HOST          # ISAPI ใช้ Digest ไม่ใช่ Basic

ทำไมต้อง sync สองทาง ไม่ใช่ยิงทางเดียว

ตอนออกแบบรอบแรกผมคิดแบบง่ายที่สุดคือให้ทะเบียนกลางเป็นเจ้าของความจริง แล้วยิงคำสั่งลงเครื่องอย่างเดียว ใครเข้าใหม่ก็เพิ่ม ใครออกก็ลบ จบ ปรากฏว่าใช้ไม่ได้ เพราะของจริงมีคนไปกดหน้าเครื่องด้วย ช่างที่ไปติดตั้งเพิ่มอีกประตูก็ลงทะเบียนคนที่เครื่องตรง ๆ ไม่ได้ผ่านระบบกลาง พอ sync ทางเดียวคนกลุ่มนั้นจะโดนลบทิ้งทุกคืนโดยไม่มีใครรู้ว่าหายไปไหน

sync ทางเดียว

ระบบกลางทับเครื่องเสมอ

เขียนง่ายและคาดเดาได้ แต่ทุกอย่างที่ถูกสร้างที่หน้าเครื่องจะหายเงียบ ๆ ในรอบถัดไป และเราจะไม่มีทางรู้เลยว่าเครื่องเคยมีอะไรอยู่บ้าง เพราะไม่เคยอ่านกลับ

sync สองทาง

อ่านของจริงก่อนตัดสินใจ

ดึงทะเบียนในเครื่องออกมาเทียบกับระบบกลางทุกรอบ แล้วแยกเป็นสามกอง คือต้องเพิ่ม ต้องปิดสิทธิ์ และที่เครื่องมีแต่กลางไม่มี กองสุดท้ายไม่แตะ แค่รายงานให้คนตัดสินใจ

กองที่สามสำคัญกว่าที่คิดครับ เพราะมันคือรายการที่บอกว่าระบบกลางไม่ครบ ไม่ใช่รายการที่บอกว่าเครื่องผิด ผมให้สคริปต์พ่นกองนี้ออกมาเป็นไฟล์แยกทุกคืน แล้วเดือนแรกได้รายชื่อกลับมาสิบกว่ารายการที่ต้องไปตามเก็บเข้าระบบกลาง

เทียบทะเบียนกลางกับของจริงในเครื่องทุกรอบมีในกลางเครื่องไม่มีต้องเพิ่มส่งเข้าเครื่องมีทั้งกลางและเครื่องไม่แตะยังทำงานอยู่มีในเครื่อง ระบบกลางบอกว่าออกแล้วปิดสิทธิ์Modify: enable = falseมีในเครื่อง กลางไม่มีลงที่หน้าเครื่องรายงานไม่แตะ ให้คนตัดสินใจกองสุดท้ายบอกว่าระบบกลางไม่ครบ ไม่ใช่ว่าเครื่องผิด ห้ามแตะ
สี่กองที่ได้จากการเทียบทะเบียนกลางกับเครื่อง มีเพียงกองที่ระบบกลางบอกว่าออกแล้วเท่านั้นที่ถูกปิดสิทธิ์

อ่านทะเบียนในเครื่องออกมาก่อน ด้วย UserInfo/Search

ขาแรกของวงจรคือดึงรายชื่อในเครื่องออกมาให้หมด endpoint คือ POST /ISAPI/AccessControl/UserInfo/Search?format=json ตัวนี้เป็น paging ต้องวนขอทีละหน้า และต้องส่ง searchID เป็นค่าคงที่ของรอบนั้นไปด้วย ถ้าเปลี่ยน searchID กลางคันเครื่องจะเริ่มนับใหม่แล้ววนไม่จบ

{
  "UserInfoSearchCond": {
    "searchID": "sync-20261030",
    "searchResultPosition": 0,
    "maxResults": 30
  }
}
def fetch_users(page_size=30):
    out, pos = [], 0
    while True:
        body = {"UserInfoSearchCond": {"searchID": "sync-20261030",
                                       "searchResultPosition": pos,
                                       "maxResults": page_size}}
        r = requests.post(BASE + "/AccessControl/UserInfo/Search?format=json",
                          auth=AUTH, json=body, timeout=10)
        r.raise_for_status()
        blk = r.json().get("UserInfoSearch", {})
        rows = blk.get("UserInfo", []) or []
        out.extend(rows)
        # responseStatusStrg = MORE แปลว่ายังมีหน้าถัดไป OK แปลว่าหมดแล้ว
        if blk.get("responseStatusStrg") != "MORE" or not rows:
            return out
        pos += len(rows)

ค่าที่ผมใช้จริงจากแต่ละแถวมีไม่กี่ตัว คือ employeeNo ที่เป็นกุญแจหลัก name และก้อน Valid ที่บอกว่าสิทธิ์ยังเปิดอยู่หรือไม่ กับช่วงเวลาที่ใช้ได้

{
  "employeeNo": "A1007",
  "name": "ตัวอย่าง ทดสอบ",
  "userType": "normal",
  "Valid": { "enable": true, "beginTime": "2026-01-01T00:00:00",
             "endTime": "2030-12-31T23:59:59", "timeType": "local" },
  "numOfFace": 1
}

numOfFace คือจำนวนใบหน้าที่ผูกกับคนนั้น ผมใช้ค่านี้เป็นตัวชี้ว่าคนไหนลงทะเบียนใบหน้าไว้แล้วบ้าง เพราะคนที่มีแต่บัตรไม่มีใบหน้าจะเป็นศูนย์

ทางที่ตัน: คำสั่งลบใบหน้าตอบ notSupport ทุกรูปแบบ

พอได้รายชื่อมาแล้ว ขั้นต่อไปที่คิดว่าง่ายที่สุดคือลบใบหน้าของคนที่ออกไปแล้ว เอกสารของตระกูลนี้บอกว่าคลังใบหน้าอยู่ใต้ FDLib แล้วลบด้วย FDSearch ได้ ผมเลยลองยิงตรง ๆ ก่อนด้วย curl

curl -s --digest -u "$ACS_USER:$ACS_PASS" -X PUT \
  "http://$ACS_HOST/ISAPI/Intelligent/FDLib/FDSearch/Delete?format=json&FDID=1&faceLibType=blackFD" \
  -H 'Content-Type: application/json' \
  -d '{"FPID":[{"value":"A1007"}]}'

ได้กลับมาแบบนี้ และได้แบบเดียวกันเป๊ะไม่ว่าจะเปลี่ยน faceLibType เป็นอะไร เปลี่ยน method เป็น DELETE ยิงแบบมี FDID หรือไม่มี ใส่ FPID เป็น employeeNo หรือเป็นค่า PID ที่อ่านมาจากคลังเอง

{
  "statusCode": 4,
  "statusString": "Invalid Operation",
  "subStatusCode": "notSupport",
  "errorCode": 1073741830,
  "errorMsg": "notSupport"
}

ที่ทำให้เสียเวลาคือหน้า capabilities ของเครื่องประกาศว่ามีความสามารถนี้ชัด ๆ ลองเรียกดูแล้วยังเห็นชื่อ operation ครบ

curl -s --digest -u "$ACS_USER:$ACS_PASS" \
  "http://$ACS_HOST/ISAPI/Intelligent/FDLib/capabilities?format=json" | head -30
capabilities บอกว่ารองรับ ไม่ได้แปลว่าเฟิร์มแวร์ตัวนั้นทำได้

ก้อน capabilities ของอุปกรณ์ตระกูลนี้เป็นสเปกของตระกูล ไม่ใช่ของรุ่น เครื่องปลายทางที่เป็นเทอร์มินัลควบคุมประตูใช้เฟิร์มแวร์คนละสายกับกล้องวิเคราะห์ภาพ คลังใบหน้าถูกผูกติดกับระเบียนผู้ใช้แน่นกว่าจนไม่มีคำสั่งลบเดี่ยว ๆ ให้ใช้ ผมเสียเวลาไล่ยิงอยู่เกือบวันเพราะเชื่อ capabilities มากเกินไป

แก้ไขเพิ่มเติมภายหลัง

ข้อสรุปว่า "ลบรูปใบหน้าเดี่ยว ๆ ไม่ได้" ในหัวข้อนี้ผมมาเจอทีหลังว่าไม่จริง ปุ่มลบในหน้าเว็บของเครื่องเองทำได้ และคำสั่งที่มันใช้ไม่อยู่ในเอกสาร วิธีหาคำสั่งนั้นผมเขียนแยกไว้ใน ตอนดัก HAR จากหน้าเว็บของเครื่อง ส่วนวิธีปิดสิทธิ์ข้างล่างนี้ยังใช้ได้และเร็วกว่าเมื่อโจทย์คือแค่ไม่ให้เข้าประตู

ทางที่ใช้ได้จริง: ไม่ลบ แต่ปิดสิทธิ์ทิ้ง

ทางออกที่ใช้ได้จริงคือเลิกคิดว่าต้องลบ แล้วไปแก้ที่ระเบียนผู้ใช้แทน endpoint คือ PUT /ISAPI/AccessControl/UserInfo/Modify?format=json แล้วสั่งปิด Valid.enable เป็น false ผลลัพธ์ที่ประตูคือคนนั้นสแกนแล้วไม่เปิด ซึ่งตรงกับสิ่งที่โจทย์ต้องการอยู่แล้ว

{
  "UserInfo": {
    "employeeNo": "A1007",
    "Valid": { "enable": false }
  }
}

จุดที่กินเวลาอีกครึ่งวันคือ body ต้องสั้นแค่นี้จริง ๆ รอบแรกผมส่ง Valid ไปทั้งก้อนตามที่อ่านกลับมาจาก Search คือมี beginTime endTime timeType ครบ เพราะคิดว่าส่งของเดิมกลับไปน่าจะปลอดภัยที่สุด ปรากฏว่าเด้ง

{
  "statusCode": 6,
  "statusString": "Invalid Content",
  "subStatusCode": "badJsonContent",
  "errorCode": 1073741825,
  "errorMsg": "badJsonContent"
}

ตัวที่ทำให้พังคือ beginTime ที่มีอยู่แล้วในเครื่อง พอส่งกลับไปพร้อมกับคำสั่งแก้ เครื่องตีความว่าเป็นการตั้งค่าช่วงเวลาใหม่แล้วตรวจรูปแบบเข้มกว่าตอนอ่านออกมา ผมเลยตัดทุกฟิลด์ที่ไม่ได้ตั้งใจแก้ออกให้หมด เหลือ employeeNo กับสิ่งที่จะเปลี่ยนเท่านั้น แล้วผ่านทันที

def disable_user(employee_no):
    """ปิดสิทธิ์แทนการลบ body ต้องสั้นที่สุด มีแค่กุญแจกับฟิลด์ที่จะเปลี่ยน"""
    body = {"UserInfo": {"employeeNo": str(employee_no),
                         "Valid": {"enable": False}}}
    r = requests.put(BASE + "/AccessControl/UserInfo/Modify?format=json",
                     auth=AUTH, json=body, timeout=10)
    j = r.json()
    if j.get("statusCode") != 1:
        raise RuntimeError("%s / %s" % (j.get("subStatusCode"), j.get("errorMsg")))
    return True
กฎ minimal body สำหรับ ISAPI ฝั่ง JSON

ของที่อ่านออกมาได้ ไม่ได้แปลว่าส่งกลับเข้าไปได้ทั้งก้อน ตอนแก้ค่าให้ส่งเฉพาะกุญแจ กับฟิลด์ที่ตั้งใจจะเปลี่ยน เท่านั้น ฟิลด์เวลาทุกตัวคือตัวจุดชนวน badJsonContent ที่เจอบ่อยที่สุด กฎนี้ใช้ได้กับ endpoint อื่นในตระกูลเดียวกันด้วย ไม่ใช่เฉพาะ UserInfo

ข้อดีที่ตามมาโดยไม่ได้ตั้งใจคือการปิดสิทธิ์เก็บร่องรอยไว้ ต่างจากการลบทิ้ง ถ้าวันหนึ่งต้องย้อนดูว่าคนนี้เคยมีสิทธิ์เข้าประตูไหนบ้างจนถึงวันไหน ระเบียนยังอยู่ครบ ส่วนตัวผมว่าในระบบที่กระทบสิทธิ์คน การปิดดีกว่าการลบอยู่แล้ว

วงจร sync ทั้งรอบ กับกติกาที่ต้องมี

พอได้ทั้งขาอ่านและขาเขียนแล้ว ที่เหลือคือประกอบเป็นรอบเดียวจบ ผมให้มันรันตีหนึ่งของทุกคืน รอบหนึ่งใช้เวลาไม่ถึงนาทีสำหรับทะเบียนหลักร้อยคน

วงจร sync หนึ่งรอบทะเบียนกลางคนที่ยังอยู่→UserInfo/Searchของจริงในเครื่อง→เทียบสามกองเพิ่ม ปิด ไม่รู้จัก→Modifyปิดสิทธิ์→รายงานกองที่ไม่แตะ
def sync(hr_active, dry_run=True):
    on_device = {u["employeeNo"]: u for u in fetch_users()}
    to_disable, unknown, plan = [], [], []

    for emp, u in on_device.items():
        enabled = u.get("Valid", {}).get("enable", False)
        if emp in hr_active:
            continue                      # ยังทำงานอยู่ ไม่แตะ
        if emp.startswith("A") and enabled:
            to_disable.append(emp)        # กองที่ระบบกลางบอกว่าออกแล้ว
        elif emp not in hr_active:
            unknown.append(emp)           # ลงทะเบียนที่หน้าเครื่อง ไม่แตะ

    for emp in to_disable:
        plan.append(("disable", emp))
        if not dry_run:
            disable_user(emp)
    return plan, unknown

กติกาสามข้อที่ผมบังคับใส่ไว้ในสคริปต์ตัวนี้ หนึ่ง ค่า default ของสคริปต์คือ --dry-run ต้องพิมพ์ --apply เท่านั้นถึงจะเขียนจริง สอง ถ้ารอบไหนจะปิดสิทธิ์เกินสิบคนพร้อมกันให้หยุดแล้วรายงานเฉย ๆ เพราะนั่นแปลว่าทะเบียนกลางเพี้ยน ไม่ใช่คนลาออกพร้อมกันสิบคน สาม ห้ามแตะระเบียนที่ไม่ได้ขึ้นต้นด้วยรหัสชุดที่ระบบกลางเป็นคนออกให้

# รันจริงต้องพิมพ์ apply เท่านั้น และมีเพดานกันทะเบียนกลางเพี้ยน
python face-sync.py --apply --max-disable 10 | tee /var/log/face-sync/$(date +%F).log

log ในเครื่องเต็มแล้วลบไม่ได้ เหลือแต่ล้างทั้งเครื่อง

เรื่องที่เจอตามมาหลังระบบเดินได้คือบันทึกเหตุการณ์ในตัวเครื่อง อ่านออกมาได้ผ่าน POST /ISAPI/AccessControl/AcsEvent?format=json ค้นย้อนหลังตามช่วงเวลาได้ปกติ แต่พอความจุใกล้เต็มแล้วอยากตัดของเก่าทิ้ง กลับไม่มีคำสั่งให้ลบเฉพาะช่วง มีแต่การล้างค่าโรงงานซึ่งล้างทะเบียนคนไปด้วยทั้งชุด

curl -s --digest -u "$ACS_USER:$ACS_PASS" -X POST \
  "http://$ACS_HOST/ISAPI/AccessControl/AcsEvent?format=json" \
  -H 'Content-Type: application/json' \
  -d '{"AcsEventCond":{"searchID":"logdump","searchResultPosition":0,
       "maxResults":24,"major":5,"minor":0,
       "startTime":"2026-10-01T00:00:00+07:00",
       "endTime":"2026-10-30T23:59:59+07:00"}}'
อย่าใช้การล้างค่าโรงงานเพื่อเคลียร์ log

คำสั่งล้างค่าโรงงานเป็นทางเดียวที่ล้างบันทึกเหตุการณ์ในเครื่องได้จริง แต่มันล้างทะเบียนผู้ใช้และใบหน้าทั้งหมดไปพร้อมกัน ถ้ากดไปแล้วไม่มีสำเนาทะเบียนอยู่นอกเครื่อง แปลว่าต้องเรียกคนทั้งหมดมาลงทะเบียนใบหน้าใหม่ทีละคน ทางที่ถูกคือดูดเก็บออกมาเป็นไฟล์ตามรอบ แล้วปล่อยให้ตัวเครื่องวนทับของเก่าเอง

ตัวเลขความจุของรุ่นที่ผมเจอคือเก็บได้ประมาณหนึ่งแสนห้าหมื่นเหตุการณ์แล้ววนทับ ประตูที่คนผ่านวันละพันกว่าครั้งแปลว่าเก็บย้อนหลังได้ราวสี่เดือน ซึ่งสั้นกว่าที่หลายที่ต้องเก็บตามนโยบายภายใน ผมเลยให้สคริปต์ตัวเดียวกันดูดบันทึกออกมาเป็น JSON รายวันเก็บไว้นอกเครื่องตั้งแต่วันแรก ไม่รอให้ถึงวันที่ต้องใช้

150,000เหตุการณ์ที่เครื่องเก็บได้
4 เดือนเท่ากับย้อนหลังได้จริง
notSupportคำตอบของคำสั่งลบ
2 ฟิลด์body ที่ Modify รับ

สรุปเป็นกติกาที่ผมยึดกับอุปกรณ์ตระกูลนี้

  1. ยืนยันขอบเขตก่อนเริ่มทุกครั้ง ว่าเครื่องอยู่ในความดูแลของเรา และบัญชีที่ใช้คือของที่ได้รับมอบมา วางไว้ใน environment ไม่ใช่ในไฟล์
  2. อย่าเชื่อ capabilities เต็มร้อย ทดสอบคำสั่งที่จะใช้จริงกับเครื่องรุ่นนั้นด้วย curl ก่อนลงมือเขียนสคริปต์
  3. ตอนแก้ค่า ส่ง body สั้นที่สุดเสมอ มีแค่กุญแจกับฟิลด์ที่ตั้งใจเปลี่ยน ฟิลด์เวลาเป็นตัวจุดชนวน badJsonContent
  4. ออกแบบเป็น sync สองทาง อ่านของจริงในเครื่องมาเทียบก่อนตัดสินใจ กองที่เครื่องมีแต่ระบบกลางไม่มี ให้รายงาน ไม่ใช่ลบ
  5. ปิดสิทธิ์แทนการลบ เก็บร่องรอยไว้ และได้ผลที่ประตูเหมือนกัน
  6. ดูดบันทึกเหตุการณ์ออกมาเก็บนอกเครื่องตั้งแต่วันแรก เพราะในเครื่องวนทับ และล้างเฉพาะ log ไม่ได้
  7. ทุกสคริปต์ที่เขียนลงอุปกรณ์ ต้อง dry-run เป็นค่าเริ่มต้น และมีเพดานจำนวนรายการต่อรอบ

ส่วนตัวผมคิดว่าบทเรียนจริงของงานนี้ไม่ใช่เรื่องยี่ห้อหรือ endpoint ไหนใช้ได้ไม่ได้ แต่เป็นเรื่องที่ว่าเอกสารกับของจริงไม่ตรงกันเป็นเรื่องปกติของอุปกรณ์ฝังตัว คนที่เขียนโค้ดคุยกับอุปกรณ์ต้องทดสอบคำสั่งจริงกับเครื่องจริงก่อนเสมอ แล้วออกแบบให้ระบบทำงานต่อได้แม้คำสั่งที่หวังไว้จะใช้ไม่ได้ ในเคสนี้ถ้าผมยืนยันว่าต้องลบให้ได้อย่างเดียว งานคงค้างอยู่จนทุกวันนี้

ที่ยังค้างในแผนของผมคือทำขาที่สามเพิ่ม ให้เครื่องยิงเหตุการณ์กลับมาหาเราเองแบบ event-driven แทนที่จะรอรอบตีหนึ่ง กับหาวิธีดึงรูปใบหน้าที่ลงทะเบียนไว้ออกมาสำรองนอกเครื่อง เผื่อวันที่ต้องล้างค่าโรงงานจริง ๆ จะได้ไม่ต้องเรียกคนมาถ่ายใหม่ทั้งหมด ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย