Mock Data Generator
สร้างข้อมูลจำลองสำหรับทดสอบระบบ · รองรับภาษาไทย
Keyboard Shortcuts
CtrlEnterRun tool
CtrlKFocus search
จำนวน records
🪪 ข้อมูลส่วนตัว
บัตรประชาชน 13 หลัก + checksum
—
ชื่อ–นามสกุล ภาษาไทย
—
ฉายา สไตล์สายเทค
—
เพศ ชาย / หญิง / ไม่ระบุ
—
วันเกิด พ.ศ. + อายุ
—
📱 ติดต่อ
เบอร์โทรศัพท์ มือถือไทย
—
อีเมล สุ่ม domain
—
📍 ที่อยู่
จังหวัด 30 จังหวัด
—
รหัสไปรษณีย์ 5 หลัก
—
💼 ข้อมูลงาน
บริษัท ชื่อบริษัทไทย
—
เลขนิติบุคคล 13 หลัก
—
ตำแหน่ง IT / Dev roles
—
เงินเดือน 15k–180k บาท
—
💳 การเงิน
บัตรเครดิต Luhn-valid (test only)
—
⚙️ ระบบ
UUID v4 crypto random
—
โหมดที่ตรวจจับได้: —
ต้นฉบับ (Original)
ฉบับแก้ไข (Modified)
1
🔎 JSONPath Query
—
ข้อความ INPUT
OUTPUT
—
🌏 Timezone
⚡ PRESETS ยอดนิยม
พิมพ์ Expression เอง (5-field cron)
* * * * *
ทุกนาที
🕐 รอบรัน 5 ครั้งถัดไป
FORMAT REFERENCE
┌──── minute (0-59)
│ ┌── hour (0-23)
│ │ ┌─ day/month (1-31, L)
│ │ │ ┌ month (1-12)
│ │ │ │ ┌ day/week (0-7, 0=Sun)
* * * * *
PATTERN
/ /
⚡ PATTERN สำเร็จรูป
ข้อความที่ต้องการทดสอบ
ผลลัพธ์ที่ match 0 matches
✏️ JWT ENCODER (HS256)
PAYLOAD (JSON)
SECRET KEY
OUTPUT TOKEN
—
⚠ ใช้สำหรับทดสอบเท่านั้น — ประมวลผลใน browser ทั้งหมด ไม่ส่งข้อมูลออกไปไหน
INPUT
OUTPUT
—
H
A
R
G
B
#6366F1
rgb(99,102,241)
hsl(239,84%,67%)
rgba(99,102,241,1.00)
--color: #6366F1;
🖼️ Image Color Picker — คลิกบนภาพเพื่อดูดสี
🖼️
ลากภาพมาวางที่นี่
หรือกด Ctrl+V วางจาก Clipboard
หรือเลือกไฟล์ด้านล่าง
📸 โหลดภาพก่อน
แล้วคลิกเพื่อดูดสี
DECIMAL (Base 10)
HEX (Base 16)
0x
BINARY (Base 2)
0b
OCTAL (Base 8)
0o
📐 ค่าอ้างอิง Bit Width
⚡
RxJS Reference
คำอธิบายภาษาไทย · 57 operators · พร้อมตัวอย่างโค้ด
ลองดู:
57 operators
🔄
Angular Lifecycle Hooks
9 Hooks · Interactive Simulator · คำอธิบายภาษาไทย
▶ Simulate:
📋 Hook Reference
9 hooks
🐇
RabbitMQ · Redis · Quarkus
Concepts · Animations · Code Examples สำหรับมือใหม่
📦 Core Entities — ส่วนประกอบหลักของ RabbitMQ
📤
Producer
แอปที่ส่ง message เข้าระบบ — เหมือนผู้ส่งจดหมาย
📥
Consumer
แอปที่รับและประมวลผล message — เหมือนผู้รับจดหมาย
📋
Queue
คิวเก็บ message รอส่ง — เหมือนตู้ไปรษณีย์ message อยู่จนกว่า consumer จะดึงไป
🔀
Exchange
ตัวรับ message จาก producer แล้วกระจายไปยัง queue ตาม routing rules — เหมือนไปรษณีย์กลาง
🔗
Binding
การเชื่อม Exchange กับ Queue ด้วย routing key — เหมือนที่อยู่บนซอง
📡
Channel
ช่องทางสื่อสารเสมือนใน Connection หนึ่ง — ประหยัด resource กว่าการเปิด Connection ใหม่ทุกครั้ง
🔌
Connection
TCP connection จริงระหว่าง app กับ RabbitMQ broker — หนึ่ง Connection มีได้หลาย Channel
🏠
VHost
Virtual Host — แยก namespace ของ exchange/queue ออกจากกัน เหมือนห้องแยกในอาคาร เดียวกัน
🔀 Exchange Types — 4 ประเภทการกระจาย message
Direct Exchange point-to-point
📬 เหมือนส่งพัสดุ EMS — routing key ต้องตรงกัน 100% ถึงส่งถึง queue ปลายทาง
Producer ระบุ routingKey="order.created" → Exchange ส่งเฉพาะ Queue ที่ binding key ตรงกัน queue อื่นไม่ได้รับ
✓ Task queue ✓ Load balance workers ✓ Microservice events
📤
Producer
order.created
Exchange
direct
Queue A order.created ✅
Queue B order.updated ✗
Fanout Exchange broadcast
📢 เหมือนประกาศเสียงตามสาย — ส่งไปทุก Queue ที่ bind ไว้ ไม่สนใจ routing key เลย
Producer ส่ง 1 message → Exchange กระจายไปยังทุก Queue ที่ bind ไว้พร้อมกัน ไม่มีเงื่อนไข
✓ Broadcast event ✓ Multi-system notification ✓ Cache invalidation
📤
Producer
(any key)
Exchange
fanout
Queue A email service ✅
Queue B sms service ✅
Queue C push service ✅
Topic Exchange pattern routing
📰 เหมือนสมัครรับข่าวตามหัวข้อ — * แทน 1 word, # แทน 0 คำขึ้นไป
Routing key รูปแบบ word.word.word เช่น order.th.created
Queue subscribe ด้วย pattern เช่น order.# หรือ *.th.*
✓ Multi-tenant routing ✓ Microservice events ✓ Flexible subscription
📤
Producer
order.th.created
Exchange
topic
Queue A order.# ✅
Queue B *.th.* ✅
Queue C payment.# ✗
Headers Exchange content-based
🏷️ เหมือนกรองอีเมลด้วย label — match ตาม message headers แทน routing key
Route ตาม headers เช่น {"format":"pdf"} ใช้ x-match: all (AND) หรือ x-match: any (OR) เหมาะกับ routing ซับซ้อน
✓ Content-based routing ✓ Complex conditions ✓ Multi-attribute filter
📤
Producer
format: pdf
Exchange
headers
Queue A format=pdf ✅
Queue B format=csv ✗
🎬 Message Lifecycle Animation — lifecycle ของ message
กด ▶ เพื่อเริ่ม
📤
Producer
📨
🔀
Exchange
📨
📋
Queue
📨
📥
Consumer
ack ✓
1. Producer สร้าง message และ publish ไปที่ Exchange
2. Exchange ตรวจ routing key และส่งไปยัง Queue ที่ match
3. Message นั่งรอใน Queue จนกว่า Consumer พร้อม
4. Consumer ดึง message ออกมาประมวลผล
5. Consumer ส่ง ack (acknowledge) กลับ → RabbitMQ ลบ message ออกจาก Queue
🎬 Queue Backpressure — เมื่อ Consumer ช้ากว่า Producer
Queue ว่าง · Consumer หยุดรอ
📤 Producer
ส่ง 1 msg/s
Queue 0/10
📥 Consumer
ดึง 1 msg/3s
📚 Advanced Concepts
☠️ Dead Letter Queue (DLQ) สำคัญ

Queue พิเศษที่รับ message ที่ "ตาย" ไปแล้ว กล่าวคือ message ที่ถูก reject, หมดอายุ (TTL) หรือ queue เต็ม

เกิดเมื่อ:
• Consumer nack message โดยไม่ requeue
• Message หมด TTL (message-ttl)
• Queue เต็ม (x-max-length)
// ตั้ง DLQ ใน application.properties (Quarkus)
mp.messaging.incoming.orders.x-dead-letter-exchange=dlx
mp.messaging.incoming.orders.x-dead-letter-routing-key=dead
💾 Durability — ความทนทานของข้อมูล ต้องรู้
ประเภท
รอด restart?
ใช้เมื่อ
Durable Queue
✓ ใช่
งาน critical ที่ห้ามสูญ
Transient Queue
✗ ไม่
cache/temp ที่ไม่สำคัญ
Persistent Msg
✓ ใช่
delivery-mode=2 ใน message
Transient Msg
✗ ไม่
realtime/low-priority events
💡 ต้องทั้ง durable queue + persistent message ถึงจะปลอดภัยจริง
⚡ Prefetch / QoS — คุม throughput Performance

จำกัดจำนวน message ที่ RabbitMQ ส่งให้ Consumer ก่อนจะรอ ack กลับ — ป้องกัน Consumer ถูก flood

// prefetch = 10 → consumer ได้รับสูงสุด 10 msg
// ก่อน ack ใด ก็ไม่ได้รับ msg ที่ 11
mp.messaging.incoming.orders.prefetch=10
💡 prefetch=1 = round-robin ที่สมบูรณ์แบบ แต่ throughput ต่ำ | prefetch สูง = throughput สูง แต่อาจ unbalanced
🎯 Use Cases หลัก Real World
⚙️
Async Task Queue — ส่งงานหนัก (resize image, send email) ไปทำ background โดยไม่บล็อก HTTP response
🔗
Event-driven Microservices — service A publish event, service B/C/D consume แยกกัน ไม่ต้อง couple กัน
⚖️
Work Queue / Load Balancing — consumer หลาย instance แย่ง task จาก queue เดียว — scale out ได้ง่าย
🛡️
Circuit Breaker Buffer — เมื่อ downstream ล่ม message ค้างใน queue รอจนกว่าระบบกลับมา
🗂️ Data Structures — โครงสร้างข้อมูลใน Redis
📝 String พื้นฐาน
ค่าเดียวที่เป็น text, number, หรือ binary (สูงสุด 512MB) — รองรับ INCR/DECR แบบ atomic
📦 เหมือนกล่องเก็บของชิ้นเดียว
🔑 Cache ผลลัพธ์ SQL/API — SET product:123 "{json}" EX 300
🔢 Counter — INCR page:views → atomic, thread-safe
⏳ Rate limiting — INCR + EXPIRE ใน pipeline
🔐 Distributed Lock — SET lock:resource uuid NX EX 30
SET user:token:abc123 "eyJhbG..." EX 3600
GET user:token:abc123
INCR api:calls:2024-06-29
SETNX lock:order:789 "worker-1"  # NX = only if Not eXists
🗃️ Hash Session/Object
Map ของ field → value — เหมาะสำหรับเก็บ object ที่มีหลาย field โดยไม่ต้อง serialize ทั้งก้อน
📋 เหมือนแบบฟอร์มที่มีหลายช่อง — แก้ไขทีละช่องได้โดยไม่ต้องอ่านทั้งใบ
👤 User Session — HSET session:abc userId "123" role "admin" exp "1234567890"
🛒 Shopping Cart — HSET cart:user:1 product:456 "2" product:789 "1"
⚙️ Config Cache — เก็บ config หลาย key ใน hash เดียว
HSET session:abc userId "123" name "สมชาย" role "admin"
HGET session:abc role          # "admin"
HGETALL session:abc            # ได้ทุก field
HINCRBY cart:user:1 product:456 1   # เพิ่ม quantity
📜 List Queue/Stack
Linked list ของ string — push/pop จากหัว (L) หรือท้าย (R) ได้ O(1)
🚂 เหมือนรถไฟ — ต่อตู้หัว/ท้ายได้ตลอดเวลา
📬 Simple Queue — LPUSH + RPOP (FIFO)
📚 Stack — LPUSH + LPOP (LIFO)
📰 Activity Feed — LPUSH + LTRIM เก็บแค่ 100 รายการล่าสุด
⏳ Blocking Queue — BLPOP รอ message โดยไม่ poll
LPUSH jobs "email-123"        # push หัว
RPOP jobs                     # pop ท้าย = FIFO queue
BLPOP jobs 30                 # block รอสูงสุด 30s
LRANGE feed:user:1 0 99       # ดู 100 รายการแรก
🎯 Set Unique Members
Collection ของ string ที่ไม่ซ้ำกัน (unordered) — รองรับ union/intersection/difference
🏷️ เหมือนกล่อง tag — ใส่ซ้ำไม่ได้ สลับที่ได้
👥 Online Users — SADD online:users "user:123" → รู้ว่าใคร online
🏷️ Tags — SADD article:1:tags "java" "quarkus" "backend"
🔍 Unique Visitors — SADD visitors:2024-06-29 "ip:1.2.3.4"
🤝 Common Friends — SINTER friends:user1 friends:user2
SADD online:users "user:123" "user:456"
SISMEMBER online:users "user:123"   # 1 (ใช่)
SMEMBERS online:users               # ดูทุกคน
SINTER tags:java tags:backend       # intersection
🏆 Sorted Set Leaderboard
Set ที่แต่ละ member มี score (float) — เรียงลำดับตาม score อัตโนมัติ O(log N)
🏅 เหมือนตารางคะแนน — ใส่คะแนนแล้วได้ rank ทันที
🏆 Leaderboard — ZADD scores 9500 "player:1" → ได้ rank ทันที
⏰ Delayed Queue — score = timestamp → ดึง job ที่ถึงเวลา
📊 Rate Window — score = timestamp ของ request → นับ requests ใน window
ZADD leaderboard 9500 "player:1" 8200 "player:2"
ZREVRANK leaderboard "player:1"    # rank (0-based)
ZREVRANGE leaderboard 0 9 WITHSCORES  # top 10
ZINCRBY leaderboard 100 "player:2"    # เพิ่ม score
📚 Redis Concepts เพิ่มเติม
⏳ TTL / Expiration สำคัญ

ตั้งเวลาหมดอายุของ key — Redis ลบ key อัตโนมัติหลังหมดเวลา ไม่ต้องเขียน cleanup code

SET session:abc "data" EX 3600      # หมดใน 1 ชั่วโมง
EXPIRE key 300                       # ตั้ง TTL ทีหลัง
TTL key                              # เหลืออีกกี่วินาที
PERSIST key                          # ยกเลิก TTL
💾 Persistence: RDB vs AOF ต้องรู้
Mode
วิธีทำงาน
Trade-off
RDB
Snapshot ตาม schedule (BGSAVE)
เร็ว / อาจสูญข้อมูลช่วงสุดท้าย
AOF
Log ทุก write command
ปลอดภัย / file ใหญ่กว่า
💡 Production แนะนำ AOF + RDB ร่วมกัน (hybrid)
📡 Pub/Sub — vs RabbitMQ Realtime

Publisher ส่ง message ไปยัง channel → Subscriber ทุกตัวที่ subscribe channel นั้นได้รับพร้อมกัน

Redis Pub/Sub
RabbitMQ
Persistence
✗ ไม่มี
✓ มี
Ack / Retry
✗ ไม่มี
✓ มี
Miss if offline
✗ สูญ
✓ ค้างใน queue
Latency
~1ms
~5-10ms
💡 Redis Pub/Sub = fire-and-forget realtime. RabbitMQ = reliable delivery
🗄️ Cache Patterns Design
Cache-Aside (Lazy)
App → Redis (miss?) → DB → store in Redis → return
ใช้บ่อยที่สุด — App control cache เอง
Write-Through
App → เขียน DB + Redis พร้อมกัน
Cache ตรงกับ DB เสมอ — write ช้ากว่า
Write-Behind (Write-Back)
App → เขียน Redis → async flush → DB
Write เร็วมาก — risk data loss หาก crash
🎬 Cache Hit vs Miss Animation
เลือก Hit หรือ Miss เพื่อดู animation
🖥️
Application
⚡
Redis Cache
🗄️
Database
กด ✅ Cache Hit หรือ ❌ Cache Miss เพื่อดู flow
🎬 Redis Pub/Sub Animation
กด ▶ เพื่อเริ่ม
📢
Publisher
PUBLISH news "ข่าวด่วน"
Channel: news
📱
Sub A
💻
Sub B
🖥️
Sub C
☕ Quarkus Integration Overview
🐇 RabbitMQ
quarkus-smallrye-reactive-messaging-rabbitmq
./mvnw quarkus:add-extension -Dextensions="smallrye-reactive-messaging-rabbitmq"
SmallRye Reactive Messaging — annotation-driven, non-blocking, reactive โดย default
@Incoming / @Outgoing Emitter<T> ack / nack DLQ support
⚡ Redis
quarkus-redis-client
./mvnw quarkus:add-extension -Dextensions="redis-client"
Vert.x Redis client — รองรับทั้ง reactive และ blocking API พร้อม type-safe commands
StringCommands HashCommands KeyCommands TTL / Pub-Sub
⚙️ application.properties — Config Keys ที่ต้องรู้
# ── RabbitMQ Connection ──────────────────────────────────────
rabbitmq-host=localhost
rabbitmq-port=5672
rabbitmq-username=guest
rabbitmq-password=guest
rabbitmq-virtual-host=/

# ── Incoming channel (Consumer) ───────────────────────────────
mp.messaging.incoming.orders-in.connector=smallrye-rabbitmq
mp.messaging.incoming.orders-in.queue.name=orders
mp.messaging.incoming.orders-in.queue.durable=true
mp.messaging.incoming.orders-in.prefetch=10
# Dead Letter Queue
mp.messaging.incoming.orders-in.x-dead-letter-exchange=dlx
mp.messaging.incoming.orders-in.x-dead-letter-routing-key=dead.orders

# ── Outgoing channel (Producer) ───────────────────────────────
mp.messaging.outgoing.orders-out.connector=smallrye-rabbitmq
mp.messaging.outgoing.orders-out.exchange.name=orders-exchange
mp.messaging.outgoing.orders-out.exchange.type=direct
mp.messaging.outgoing.orders-out.exchange.durable=true
mp.messaging.outgoing.orders-out.routing-key=order.created

# ── Exchange routing (topic example) ─────────────────────────
mp.messaging.outgoing.events-out.connector=smallrye-rabbitmq
mp.messaging.outgoing.events-out.exchange.name=events-exchange
mp.messaging.outgoing.events-out.exchange.type=topic
mp.messaging.outgoing.events-out.routing-key=order.th.created
# ── Redis Connection ─────────────────────────────────────────
quarkus.redis.hosts=redis://localhost:6379

# ── Redis with password ───────────────────────────────────────
quarkus.redis.hosts=redis://:mypassword@localhost:6379

# ── Redis Cluster ─────────────────────────────────────────────
quarkus.redis.hosts=redis://node1:6379,redis://node2:6379

# ── Connection pool ───────────────────────────────────────────
quarkus.redis.max-pool-size=10
quarkus.redis.max-pool-waiting=50
quarkus.redis.connect-timeout=5S

# ── Named client (multiple Redis instances) ───────────────────
quarkus.redis.cache.hosts=redis://cache-server:6379
quarkus.redis.session.hosts=redis://session-server:6379

# ── TLS (production) ─────────────────────────────────────────
quarkus.redis.tls.enabled=true
💻 Code Examples — ตัวอย่างโค้ด Java / Quarkus
1 RabbitMQ Producer — REST Endpoint → Publish to Queue Emitter<T>
ใช้ @Channel + Emitter<String> ใน REST endpoint เพื่อส่ง message เข้า RabbitMQ แบบ non-blocking
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;

@Path("/orders")
@ApplicationScoped
public class OrderResource {

    // Channel name ต้องตรงกับ mp.messaging.outgoing.orders-out ใน application.properties
    @Inject
    @Channel("orders-out")
    Emitter<String> orderEmitter;

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response createOrder(OrderRequest req) {
        // สร้าง JSON payload (หรือ serialize ด้วย Jackson)
        String payload = "{\"orderId\":\"" + req.id + "\",\"amount\":" + req.amount + "}";

        // ส่ง message — non-blocking, fire-and-forget
        orderEmitter.send(payload);

        return Response.accepted()
            .entity("{\"status\":\"queued\",\"orderId\":\"" + req.id + "\"}")
            .build();
    }
}

// application.properties
// mp.messaging.outgoing.orders-out.connector=smallrye-rabbitmq
// mp.messaging.outgoing.orders-out.exchange.name=orders-exchange
// mp.messaging.outgoing.orders-out.routing-key=order.created
2 RabbitMQ Consumer — @Incoming + Message<T> wrapper @Incoming
ใช้ @Incoming เพื่อรับ message — ใช้ Message<T> wrapper เพื่อควบคุม ack/nack เองได้
import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.eclipse.microprofile.reactive.messaging.Message;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.concurrent.CompletionStage;

@ApplicationScoped
public class OrderConsumer {

    // ─── Pattern 1: plain payload (auto-ack on success, auto-nack on exception) ───
    @Incoming("orders-in")
    public void processOrder(String payload) {
        System.out.println("Got order: " + payload);
        // ถ้า throw exception → message จะถูก nack
        // ถ้าสำเร็จ → auto ack
    }

    // ─── Pattern 2: Message<T> wrapper (manual ack/nack) ───────────────────────
    @Incoming("orders-in-manual")
    public CompletionStage<Void> processOrderManual(Message<String> message) {
        String payload = message.getPayload();
        try {
            processBusinessLogic(payload);
            return message.ack();   // บอก RabbitMQ ว่าสำเร็จ → ลบออกจาก queue
        } catch (Exception e) {
            // nack(true) = requeue | nack(false) = drop / ส่งไป DLQ
            return message.nack(e);
        }
    }

    private void processBusinessLogic(String payload) {
        // parse JSON, save to DB, call API, etc.
    }
}

// application.properties
// mp.messaging.incoming.orders-in.connector=smallrye-rabbitmq
// mp.messaging.incoming.orders-in.queue.name=orders
// mp.messaging.incoming.orders-in.queue.durable=true
// mp.messaging.incoming.orders-in.prefetch=10
3 Redis Cache-Aside Pattern — ตรวจ Cache ก่อน ถ้า Miss ดึงจาก DB StringCommands
Pattern ที่ใช้บ่อยที่สุด — ลด load จาก DB สูงสุด 90% สำหรับข้อมูลที่อ่านบ่อย
import io.quarkus.redis.datasource.RedisDataSource;
import io.quarkus.redis.datasource.string.SetArgs;
import io.quarkus.redis.datasource.string.StringCommands;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class ProductService {

    private final StringCommands<String, String> stringCmds;

    @Inject
    ProductRepository productRepo; // ตัวอย่าง JPA repository

    public ProductService(RedisDataSource ds) {
        // inject RedisDataSource แล้ว get type-safe commands
        this.stringCmds = ds.string(String.class, String.class);
    }

    public String getProduct(String productId) {
        String cacheKey = "product:" + productId;

        // Step 1: ตรวจ Redis cache ก่อน
        String cached = stringCmds.get(cacheKey);
        if (cached != null) {
            System.out.println("Cache HIT for " + cacheKey);
            return cached; // คืนจาก cache ทันที ~1ms
        }

        // Step 2: Cache MISS → ดึงจาก DB
        System.out.println("Cache MISS — querying DB for " + cacheKey);
        Product product = productRepo.findById(productId);
        if (product == null) return null;

        // Step 3: แปลงเป็น JSON string และ store ใน Redis พร้อม TTL 5 นาที
        String json = toJson(product); // Jackson ObjectMapper.writeValueAsString
        SetArgs args = new SetArgs().ex(300); // EX 300 seconds
        stringCmds.set(cacheKey, json, args);

        return json;
    }

    public void invalidateProduct(String productId) {
        // เมื่อ update ข้อมูล → ลบ cache (cache invalidation)
        stringCmds.del("product:" + productId);
    }

    private String toJson(Product p) {
        // Jackson serialize
        return "{\"id\":\"" + p.id + "\",\"name\":\"" + p.name + "\"}";
    }
}
4 Redis Session Storage — Hash HSET/HGET HashCommands
เก็บ user session ใน Redis Hash — แก้ไขแต่ละ field ได้โดยไม่ต้อง serialize ทั้งก้อน
import io.quarkus.redis.datasource.RedisDataSource;
import io.quarkus.redis.datasource.hash.HashCommands;
import io.quarkus.redis.datasource.keys.KeyCommands;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.Map;
import java.util.UUID;

@ApplicationScoped
public class SessionService {

    private final HashCommands<String, String, String> hashCmds;
    private final KeyCommands<String> keyCmds;

    public SessionService(RedisDataSource ds) {
        this.hashCmds = ds.hash(String.class, String.class, String.class);
        this.keyCmds  = ds.key(String.class);
    }

    /** สร้าง session ใหม่หลัง login สำเร็จ */
    public String createSession(String userId, String role) {
        String sessionId = UUID.randomUUID().toString();
        String key = "session:" + sessionId;

        // HSET session:<id> userId "123" role "admin" createdAt "..."
        // hset รับ Map — ไม่มี varargs version ใน Quarkus Redis API
        hashCmds.hset(key, Map.of(
            "userId",    userId,
            "role",      role,
            "createdAt", String.valueOf(System.currentTimeMillis())
        ));

        // ตั้ง TTL 30 นาที ด้วย KeyCommands.expire()
        keyCmds.expire(key, 1800);

        return sessionId;
    }

    /** ดึงข้อมูล session ทั้งหมด */
    public Map<String, String> getSession(String sessionId) {
        return hashCmds.hgetall("session:" + sessionId);
    }

    /** ดึง field เดียว (เร็วกว่า HGETALL) */
    public String getSessionField(String sessionId, String field) {
        return hashCmds.hget("session:" + sessionId, field);
    }

    /** ลบ session เมื่อ logout */
    public void deleteSession(String sessionId) {
        keyCmds.del("session:" + sessionId);
    }
}
5 Rate Limiting ด้วย Redis INCR + EXPIRE INCR + TTL
จำกัด API calls ต่อ IP หรือ user — ใช้ INCR + EXPIRE แบบ atomic — ตัวอย่าง: 100 requests/minute
import io.quarkus.redis.datasource.RedisDataSource;
import io.quarkus.redis.datasource.keys.KeyCommands;
import io.quarkus.redis.datasource.string.StringCommands;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;

@ApplicationScoped
public class RateLimiter {

    private static final int MAX_REQUESTS   = 100; // สูงสุด 100 requests
    private static final int WINDOW_SECONDS = 60;  // ต่อ 1 นาที

    private final StringCommands<String, String> stringCmds;
    private final KeyCommands<String> keyCmds;

    public RateLimiter(RedisDataSource ds) {
        this.stringCmds = ds.string(String.class, String.class);
        this.keyCmds    = ds.key(String.class);
    }

    /**
     * ตรวจสอบ rate limit
     * @param identifier เช่น IP address หรือ userId
     * @throws WebApplicationException 429 Too Many Requests ถ้าเกินลิมิต
     */
    public void checkRateLimit(String identifier) {
        String key = "rate:" + identifier + ":" + getCurrentMinuteBucket();

        // INCR เป็น atomic — ได้ค่า count ใหม่ทันที ไม่มี race condition
        long count = stringCmds.incr(key);

        if (count == 1) {
            // request แรกใน window นี้ → ตั้ง TTL ด้วย KeyCommands
            keyCmds.expire(key, WINDOW_SECONDS);
        }

        if (count > MAX_REQUESTS) {
            throw new WebApplicationException(
                Response.status(429)
                    .header("X-RateLimit-Limit",     MAX_REQUESTS)
                    .header("X-RateLimit-Remaining", 0)
                    .header("Retry-After",           WINDOW_SECONDS)
                    .entity("{\"error\":\"Rate limit exceeded. Try again in " + WINDOW_SECONDS + "s\"}")
                    .build()
            );
        }
    }

    /** Bucket ต่อนาที — key เปลี่ยนทุก 60 วินาทีอัตโนมัติ */
    private String getCurrentMinuteBucket() {
        return String.valueOf(System.currentTimeMillis() / (WINDOW_SECONDS * 1000L));
    }
}

// การใช้งานใน JAX-RS resource:
// @Inject RateLimiter rateLimiter;
//
// @GET @Path("/api/data")
// public Response getData(@Context HttpHeaders headers) {
//     String ip = headers.getRequestHeader("X-Forwarded-For").get(0);
//     rateLimiter.checkRateLimit(ip);
//     // ... proceed
// }
📋 Annotation Quick Reference
@Incoming("channel")
รับ message จาก channel ที่กำหนด — ใส่บน method ใน @ApplicationScoped bean
method parameter = payload type หรือ Message<T>
@Outgoing("channel")
ส่ง message ออกไปยัง channel — ใส่บน method ที่ return ค่า
return type = T, Message<T>, Publisher<T>
@Channel("channel")
Inject Emitter หรือ Multi สำหรับส่ง/รับ message จาก code โดยตรง
ใช้คู่กับ Emitter<T> หรือ Multi<T>
@RedisDataSource
Inject RedisDataSource สำหรับ named Redis client (ถ้ามีหลาย instance)
@RedisDataSource("cache") สำหรับ quarkus.redis.cache.*
⏳ Storage Lifetime — ข้อมูลอยู่ได้นานแค่ไหน?
กด "ปิด Tab" หรือ "ล้างข้อมูล" ดูว่า storage แต่ละชนิดตอบสนองอย่างไร
🟢 สถานะข้อมูล
localStorage
ถาวร
sessionStorage
อยู่ใน tab นี้
IndexedDB
ถาวร
Cache API
ถาวร
📊 เปรียบเทียบทุกชนิด
คลิกแถวเพื่อไปดูรายละเอียด
Storageขนาดประเภทข้อมูลLifetimeAsyncค้นหาได้เหมาะกับ
localStorage 5–10 MBstringถาวร ❌❌ settings, theme, preferences
sessionStorage 5 MBstringปิด tab = หาย ❌❌ wizard, multi-step form
IndexedDB GB+Object, Blob, File…ถาวร ✅✅ offline data, draft, profile cache
Cookie 4 KBstringมี expires ✅❌ auth token (httpOnly)
Cache API GB+HTTP Request/Responseถาวร ✅❌ PWA offline, static assets
OPFS GB+File (binary)ถาวร ✅❌ ไฟล์ใหญ่, SQLite-in-browser
📦 localStorage คืออะไร?
เก็บ key-value แบบ string — ง่าย synchronous ไม่หายแม้ปิด browser
ขนาด
5–10 MB
ประเภท
string เท่านั้น
Lifetime
ถาวร
Blocking
ใช่ (sync)
🧪 ลองใช้จริงได้เลย
ข้อมูลที่บันทึกจะอยู่ใน localStorage จริงๆ (ใช้ prefix _sp_)
กด getItem เพื่อดูค่า…
💻 Code Example
// ── เก็บข้อมูล ──
localStorage.setItem('theme', 'dark');
localStorage.setItem('lang', 'th');

// ── อ่านข้อมูล ──
const theme = localStorage.getItem('theme'); // "dark"
const missing = localStorage.getItem('xxx'); // null

// ── เก็บ Object (ต้อง JSON.stringify) ──
const user = { id: 1, name: 'สมชาย' };
localStorage.setItem('user', JSON.stringify(user));
const loaded = JSON.parse(localStorage.getItem('user'));

// ── ลบ ──
localStorage.removeItem('theme');
localStorage.clear(); // ล้างทั้งหมด

// ── วนดูทุก key ──
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  console.log(key, localStorage.getItem(key));
}
✅ ใช้เมื่อ / ⚠️ อย่าใช้เมื่อ
✅เก็บ theme, language, UI preferences — ข้อมูลเล็กที่ user ตั้งค่าไว้
✅Last active tab, sidebar state, form draft เล็กๆ
❌เก็บ auth token หรือ JWT — XSS ขโมยได้ทันที ใช้ httpOnly cookie แทน
❌ข้อมูลใหญ่กว่า 5 MB — จะ throw QuotaExceededError
⚠️ข้อมูลที่ต้องการค้นหา/filter — ใช้ IndexedDB แทน
Theme switcher Language preference Last active tab Sidebar collapse state Dev-tools นี้ใช้อยู่! 🎉
🪟 sessionStorage คืออะไร?
เหมือน localStorage แต่ข้อมูลหายเมื่อปิด tab — แต่ละ tab มี storage แยกกัน
ขนาด
5 MB
ประเภท
string เท่านั้น
Lifetime
ปิด tab = หาย
Tab isolation
ใช่ — แยกต่อ tab
🪟 Tab Isolation — แต่ละ tab มีข้อมูลต่างกัน
เปิด tab ใหม่ → sessionStorage ใหม่ (ว่างเปล่า) แม้เปิด URL เดียวกัน
Tab A — Checkout Step 2
checkoutStep:"2"
cartItems:"[{...}]"
shippingAddr:"{...}"
Tab B — เพิ่งเปิดใหม่
sessionStorage ว่างเปล่า
(ไม่เห็นข้อมูลของ Tab A)
💻 Code Example
// ── Multi-step checkout wizard ──

// Step 1: บันทึก cart
sessionStorage.setItem('checkoutStep', '1');
sessionStorage.setItem('cartItems', JSON.stringify(items));

// Step 2: บันทึก shipping address
sessionStorage.setItem('checkoutStep', '2');
sessionStorage.setItem('shippingAddr', JSON.stringify(addr));

// Step 3: อ่านข้อมูลรวมเพื่อ submit
const step  = sessionStorage.getItem('checkoutStep');
const cart  = JSON.parse(sessionStorage.getItem('cartItems'));
const addr  = JSON.parse(sessionStorage.getItem('shippingAddr'));

// หลัง submit สำเร็จ ล้างข้อมูล
sessionStorage.clear();

// ── API เหมือน localStorage ทุกอย่าง ──
sessionStorage.setItem(key, value);
sessionStorage.getItem(key);
sessionStorage.removeItem(key);
sessionStorage.length;
✅ ใช้เมื่อ / ⚠️ อย่าใช้เมื่อ
✅Multi-step form / wizard — เก็บ state ระหว่างขั้นตอน ล้างหลัง submit
✅OTP flow, checkout — ข้อมูลชั่วคราวที่ควรหายเมื่อปิด tab
❌ข้อมูลที่ต้องอยู่ข้าม tab หรือหลังปิด browser — ใช้ localStorage แทน
⚠️Ctrl+D duplicate tab → sessionStorage ถูก clone ไปด้วย (ระวัง duplicate state)
🗃️ IndexedDB คืออะไร?
Database จริงๆ ใน browser — เก็บ Object ได้โดยตรง, มี index ค้นหาได้, async ทั้งหมด
ขนาด
GB+
ประเภท
ทุกอย่าง (Object, Blob, File)
Lifetime
ถาวร
Async
✅ ไม่ block UI
🎮 Object Store — ลอง CRUD ได้เลย
นี่คือ visual demo — เหมือนกับ IndexedDB Object Store ใน browser จริง
idnamerolecachedAt
💻 Code — Dexie.js (แนะนำ)
Dexie.js คือ wrapper บาง Promise-based ที่ทำให้ IndexedDB ง่ายมาก
import Dexie from 'dexie'; // npm install dexie

// ── 1. สร้าง database + schema ──
const db = new Dexie('myApp');
db.version(1).stores({
  users:    '++id, name, role, email',  // ++ = auto-increment PK
  products: '++id, name, price, *tags', // * = multi-value index
});

// ── 2. Add ──
await db.users.add({ name: 'สมชาย', role: 'admin', email: 'a@b.com' });
await db.users.bulkAdd([
  { name: 'มาลี', role: 'user', email: 'c@d.com' },
  { name: 'วิชัย', role: 'user', email: 'e@f.com' },
]);

// ── 3. Query ──
const all   = await db.users.toArray();
const admins= await db.users.where('role').equals('admin').toArray();
const found = await db.users.where('name').startsWith('ส').toArray();
const one   = await db.users.get(1); // by primary key

// ── 4. Update ──
await db.users.update(1, { role: 'superadmin' });
await db.users.where('role').equals('user').modify({ active: true });

// ── 5. Delete ──
await db.users.delete(1);
await db.users.where('role').equals('guest').delete();
💻 Code — Raw IndexedDB API (ไม่แนะนำ แต่รู้ไว้)
// Raw API verbose มาก — ใช้ Dexie แทนดีกว่า
const request = indexedDB.open('myApp', 1);

request.onupgradeneeded = (e) => {
  const db = e.target.result;
  const store = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
  store.createIndex('role', 'role', { unique: false });
};

request.onsuccess = (e) => {
  const db = e.target.result;
  const tx = db.transaction('users', 'readwrite');
  const store = tx.objectStore('users');
  store.add({ name: 'สมชาย', role: 'admin' });
  tx.oncomplete = () => console.log('done');
};
✅ ใช้เมื่อ / ⚠️ อย่าใช้เมื่อ
✅Cache API response — เก็บ product list, article, user profile ไว้ใช้ offline
✅Draft / unsent form — เก็บข้อมูลที่ยังไม่ submit ค้างไว้ระหว่าง session
✅รูปภาพ / ไฟล์ — เก็บ Blob ได้โดยตรง ไม่ต้อง base64 encode
❌Auth token — XSS อ่านได้ ใช้ httpOnly cookie เสมอ
⚠️ข้อมูลเล็กๆ เช่น theme, setting — localStorage ง่ายกว่า เหมาะกว่า
⚡ Cache API คืออะไร?
เก็บ HTTP Request/Response — ใช้กับ Service Worker เพื่อทำ PWA offline
ขนาด
GB+
ประเภท
HTTP Request/Response
Lifetime
ถาวร (จนกว่าล้าง)
dev-tools นี้
✅ ใช้อยู่ใน sw.js
🎬 Service Worker Intercept — Cache Hit vs Miss
กดปุ่มด้านบนเพื่อ simulate request…
💻 Code — Service Worker (sw.js ของ dev-tools นี้)
const CACHE = 'myapp-v1';
const ASSETS = ['/', '/index.html', '/app.css', '/app.js'];

// ── Install: pre-cache static assets ──
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open(CACHE).then(c => c.addAll(ASSETS))
  );
  self.skipWaiting();
});

// ── Activate: ลบ cache เก่า ──
self.addEventListener('activate', e => {
  e.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
    )
  );
  self.clients.claim();
});

// ── Fetch: Cache-first strategy ──
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(cached => {
      if (cached) return cached; // ← Cache HIT ⚡
      return fetch(e.request).then(res => {
        // Cache ก่อนส่งกลับ
        const clone = res.clone();
        caches.open(CACHE).then(c => c.put(e.request, clone));
        return res;
      });
    })
  );
});
✅ ใช้เมื่อ / ⚠️ อย่าใช้เมื่อ
✅PWA offline support — HTML, CSS, JS, รูปภาพ, fonts
✅Performance — ลด network round-trip สำหรับ static assets
❌เก็บ JSON data ทั่วไป — ใช้ IndexedDB แทน Cache API เก็บได้แค่ HTTP Response
⚠️Cache invalidation — ต้อง bump cache version เมื่อ deploy ใหม่ ไม่งั้น user ได้ของเก่า
😈 XSS Attack — ขโมยข้อมูลจาก Storage ได้ไหม?
เลือก storage type ดูว่า attacker อ่านได้ไหม
🔒 Best Practices — กฎที่ต้องจำ
✅Auth token → httpOnly Cookie เท่านั้น ไม่ใช่ localStorage, sessionStorage, หรือ IndexedDB
✅ใช้ Secure + SameSite=Strict กับทุก auth cookie
✅Sanitize ทุก user input ก่อนแสดงผล — ป้องกัน XSS ตั้งแต่ต้นทาง
✅ใช้ Content Security Policy (CSP) header — จำกัด script ที่รันได้
❌อย่าเก็บ password, เลขบัตรเครดิต, เลขบัตรประชาชน ใน storage ใดๆ ทั้งสิ้น
⚠️IndexedDB ไม่ encrypt — ใครเข้าถึงเครื่องได้อ่านข้อมูลได้ อย่าเก็บ sensitive data
🌳 Decision Tree — ใช้ storage ไหนดี?
ตอบคำถามเพื่อรับคำแนะนำ
📖 ดูใน Chrome DevTools
F12เปิด DevTools → แท็บ Application (หรือ Storage)
📦ซ้ายมือ: Local Storage, Session Storage, IndexedDB, Cookies, Cache Storage
🔍คลิกที่แต่ละ origin เพื่อดู/แก้ไข/ลบข้อมูลได้ทันที
⚠️httpOnly cookie จะแสดงใน DevTools แต่ JS อ่านไม่ได้ — นี่คือพฤติกรรมที่ถูกต้อง
ยังไม่ได้เลือกโฟลเดอร์
Claude Code plugin · esan-office

ESAN AI AGENT ออฟฟิศ 8 บิตของ Claude Code

ทุก session ของ Claude Code ที่เปิดอยู่บนคอมคุณ จะกลายเป็นตัวละคร 1 ตัวในออฟฟิศ มีงานก็เดินไปนั่งโต๊ะ ว่างก็ไปพัก ดูได้ว่าใครกำลังใช้เครื่องมืออะไร context เหลือเท่าไร และสร้าง agent ประจำออฟฟิศไว้สั่งงานจากหน้าเว็บได้

เปิดใช้งานที่
http://localhost:4567
บนคอมเครื่องที่ติดตั้ง plugin หลังเปิด Claude Code แล้วอย่างน้อย 1 session
เปิดออฟฟิศ

ไม่ได้เปิดที่เว็บนี้ หน้า อีสาน DevTools บน github.io เป็นแค่คู่มือ ตัวออฟฟิศต้องคุยกับ bridge (โปรแกรมเล็ก ๆ ที่ plugin เปิดไว้ในคอมคุณ) ข้อมูล session ทั้งหมดอยู่ในเครื่อง ไม่ขึ้นอินเทอร์เน็ต

ทำงานยังไง

  1. Claude Codeทุก session ส่งเหตุการณ์ (พิมพ์ prompt, ใช้เครื่องมือ, ตอบเสร็จ) ผ่าน hook ของ plugin
  2. bridgeรันเบื้องหลังที่ 127.0.0.1:4567 เปิดเองตอนเริ่ม session รับเหตุการณ์แล้วเก็บสถานะไว้
  3. หน้าออฟฟิศเปิด http://localhost:4567 ในเบราว์เซอร์ เห็นทุก session เป็นตัวละครแบบสด

ติดตั้งครั้งแรก

ทำครั้งเดียว หลังจากนั้นเปิด Claude Code ตามปกติ ออฟฟิศจะทำงานเอง

  1. เช็กของที่ต้องมี

    Node.js 18 ขึ้นไป (ใช้รัน bridge) และคำสั่ง claude ต้องใช้ได้ใน terminal ถ้าจะติดตั้งเป็นแอปใช้ Chrome หรือ Edge

    node --version
    claude --version
  2. เพิ่ม marketplace ของ อีสาน DevTools

    บอก Claude Code ว่า plugin นี้มาจาก repo ไหน พิมพ์ใน terminal (cmd, PowerShell หรือ Terminal ของ macOS ก็ได้)

    claude plugin marketplace add devgreenpink/dev-utility-toolkit
  3. ติดตั้ง plugin
    claude plugin install esan-office@esan-devtools

    หรือพิมพ์ /plugin ในหน้าจอ Claude Code แล้วเพิ่ม marketplace กับติดตั้งจากเมนูแทน 2 คำสั่งข้างบน

  4. เปิด Claude Code session ใหม่

    พิมพ์ claude ใน terminal หรือเปิดแท็บ Code ในแอป Claude desktop ก็ได้ พอเริ่ม session bridge จะเปิดเองเบื้องหลัง ไม่ต้องรันอะไรเพิ่ม

  5. เปิดออฟฟิศ

    เข้า http://localhost:4567 จะเห็นตัวละครของ session นั้นเดินเข้าประตูมา ถ้าอยากได้เป็นแอปบนเดสก์ท็อป กดปุ่ม ติดตั้งเป็นแอป ข้างปุ่มธีมมุมขวาบน

อัปเดตเป็นเวอร์ชันใหม่

plugin ไม่อัปเดตเอง ทำตามนี้ทุกครั้งที่มีเวอร์ชันใหม่

  1. ดึงรายการ plugin ล่าสุดจาก GitHub
    claude plugin marketplace update esan-devtools
  2. อัปเดต plugin
    claude plugin update esan-office@esan-devtools

    ถ้าขึ้นว่า already at the latest version แปลว่าไฟล์ใหม่ลงแล้ว ไปข้อต่อไปได้เลย

  3. เปิด Claude Code session ใหม่ 1 ครั้ง

    คำสั่ง update แค่เปลี่ยนไฟล์ ส่วน bridge ตัวเก่ายังรันอยู่ ตั้งแต่ 0.4.1 ขึ้นไป session ใหม่จะขอให้ตัวเก่าปิดเองแล้วเปิดตัวใหม่แทน

  4. รีเฟรชหน้าออฟฟิศ

    กด Ctrl+F5 ที่ localhost:4567 แล้วเช็กว่าได้เวอร์ชันใหม่จริงที่ localhost:4567/api/ping (ดูค่า version)

ทำครั้งเดียว ถ้าเครื่องยังรัน 0.4.0 หรือเก่ากว่า

bridge รุ่นเก่ายังไม่รู้จักคำขอให้ปิดตัว หน้าเลยยังเป็นของเก่าแม้อัปเดตแล้ว ให้ปิด process ที่ใช้ port 4567 เองก่อน แล้วค่อยเปิด session ใหม่

Windows (cmd หรือ PowerShell): ดูเลข PID ท้ายบรรทัด
netstat -ano | findstr :4567
แล้วปิดด้วยเลขนั้น เช่น PID 3160
taskkill /PID 3160 /F
macOS / Linux
lsof -ti:4567 | xargs kill

ถ้า agent ประจำออฟฟิศกำลังทำงานหรือมีคิวอยู่ bridge ตัวเก่าจะยังไม่ปิด (กันงานหาย) รอให้งานเสร็จแล้วเปิด session ใหม่อีกครั้ง

เปิดที่ไหนได้บ้าง

ที่อยู่ใช้ทำอะไรสั่งงานได้ไหม
http://localhost:4567ใช้งานจริง บนคอมเครื่องที่ติดตั้ง pluginได้
http://<IP ของคอม>:4567ดูจากมือถือหรือเครื่องอื่นใน Wi-Fi เดียวกัน ต้องรัน bridge ด้วย ESAN_OFFICE_LAN=1ดูอย่างเดียว
devgreenpink.github.io (เว็บนี้)คู่มือ และโค้ดของ pluginใช้งานออฟฟิศไม่ได้

ดูโควตา 5 ชม. และรายสัปดาห์

ออฟฟิศเก็บตัวเลขโควตาได้ 3 ทาง ใช้ค่าที่ใหม่ที่สุด ใต้ตัวเลขบอกว่าอัปเดตเมื่อไรและมาจากทางไหน

  • ปุ่ม อัปเดตโควตา กดที่หน้าออฟฟิศ ใช้ได้เลยแม้ใช้แต่แอป desktop bridge ถาม Haiku สั้น ๆ 1 ครั้ง กิน token นิดเดียว กดได้นาทีละครั้ง
  • งานของ agent สั่งงาน agent ประจำออฟฟิศหรือถามด่วนเมื่อไร โควตาอัปเดตเองทุกครั้ง ไม่กิน token เพิ่ม
  • statusline สำหรับคนใช้ claude ใน terminal อัปเดตเองทุกครั้งที่ตอบ และทำให้ context เป็นตัวเลขจริง (ไม่มี ≈) เพิ่มบรรทัดนี้ใน ~/.claude/settings.json
Windows (เปลี่ยน <ชื่อผู้ใช้> เป็นชื่อผู้ใช้ Windows ของคุณ)
"statusLine": { "type": "command", "command": "node \"C:/Users/<ชื่อผู้ใช้>/.claude/plugins/marketplaces/esan-devtools/plugins/esan-office/scripts/statusline.js\"" }
macOS / Linux
"statusLine": { "type": "command", "command": "node ~/.claude/plugins/marketplaces/esan-devtools/plugins/esan-office/scripts/statusline.js" }
  • ใช้ path ใน plugins/marketplaces/... ตามตัวอย่าง path นี้ไม่เปลี่ยนเวลาอัปเดต plugin
  • โควตามีเฉพาะแพ็กเกจ Pro/Max (login ด้วยบัญชี Claude) บัญชีแบบ API key ไม่มีตัวเลขนี้
  • session ในแอป desktop ไม่เรียก statusline ใช้ปุ่ม อัปเดตโควตา แทน

ใช้งานเบื้องต้น

  • ดู session ที่เปิดเอง เปิด Claude Code ตามปกติ ตัวละครจะเข้ามาเอง ดูได้อย่างเดียว ถ้าปิดหน้าต่างแล้วยังค้าง กด เอาออก ที่การ์ด (กด 2 ครั้งเพื่อยืนยัน)
  • agent ประจำออฟฟิศ กด เพิ่ม agent เลือกหน้าที่ ใส่โฟลเดอร์ แล้วส่งงานจากหน้าเว็บ ทุกงานกิน token จริง
  • ถามด่วน พิมพ์คำถามง่าย ๆ ในช่องถามด่วน ใช้ Haiku ค้นเว็บได้ ไม่แก้ไฟล์
  • แกล้ง agent แตะตัวที่เดินเล่นอยู่ เลือก ขัดขา จั๊กจี้ สาดน้ำ หรือหลอกผี เป็นแค่ภาพ ไม่กระทบงาน
  • ธีม อีสาน, CDG หรือใต้ ที่มุมขวาบน หรือเปิดด้วย ?theme=isan, ?theme=green, ?theme=south

แก้ปัญหาที่เจอบ่อย

อัปเดตแล้ว แต่หน้ายังเป็นของเก่า
bridge ตัวเก่ายังรันอยู่ เปิด localhost:4567/api/ping ดู version ถ้ายังเก่า ทำตามกล่อง "ทำครั้งเดียว" ในหัวข้ออัปเดต แล้วเปิด session ใหม่
เปิด localhost:4567 ไม่ขึ้น
ยังไม่มี session ไหนเปิดหลังติดตั้ง หรือเครื่องไม่มี Node.js เปิด Claude Code session ใหม่ 1 ครั้ง แล้วลองใหม่
Claude Code ขึ้น hook error
bridge ไม่ได้รัน เปิด session ใหม่ (bridge จะเปิดเอง) ดู log ได้ที่ ~/.esan-office/bridge.log
ช่องโควตาว่าง
ยังไม่มีข้อมูลจากทางไหนเลย กดปุ่ม อัปเดตโควตา ที่หน้าออฟฟิศ (ต้องเป็น plugin 0.5.0 ขึ้นไป) ถ้าขึ้นว่าไม่ได้ตัวเลขกลับมา แปลว่าบัญชีนี้ไม่มีโควตาแบบ Pro/Max
port 4567 ชนกับโปรแกรมอื่น
ปิดโปรแกรมนั้น หรือเปลี่ยน port ด้วย ESAN_OFFICE_PORT และแก้ url ใน hooks/hooks.json ของ plugin

รายละเอียดทั้งหมด (ตัวแปร environment, ความปลอดภัย, การทดสอบ) อยู่ใน README ของ plugin บน GitHub

✓ Copied
⚙️
ติดตั้งเป็น App
ใช้งาน offline ได้ · ไม่ต้องเปิด browser