│ ┌── hour (0-23)
│ │ ┌─ day/month (1-31, L)
│ │ │ ┌ month (1-12)
│ │ │ │ ┌ day/week (0-7, 0=Sun)
* * * * *
หรือเลือกไฟล์ด้านล่าง
แล้วคลิกเพื่อดูดสี
routingKey="order.created" → Exchange ส่งเฉพาะ Queue ที่ binding key ตรงกัน queue อื่นไม่ได้รับ* แทน 1 word, # แทน 0 คำขึ้นไปword.word.word เช่น order.th.createdQueue subscribe ด้วย pattern เช่น
order.# หรือ *.th.*{"format":"pdf"} ใช้ x-match: all (AND) หรือ x-match: any (OR) เหมาะกับ routing ซับซ้อนQueue พิเศษที่รับ message ที่ "ตาย" ไปแล้ว กล่าวคือ message ที่ถูก reject, หมดอายุ (TTL) หรือ queue เต็ม
// ตั้ง DLQ ใน application.properties (Quarkus) mp.messaging.incoming.orders.x-dead-letter-exchange=dlx mp.messaging.incoming.orders.x-dead-letter-routing-key=dead
จำกัดจำนวน message ที่ RabbitMQ ส่งให้ Consumer ก่อนจะรอ ack กลับ — ป้องกัน Consumer ถูก flood
// prefetch = 10 → consumer ได้รับสูงสุด 10 msg // ก่อน ack ใด ก็ไม่ได้รับ msg ที่ 11 mp.messaging.incoming.orders.prefetch=10
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
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
LPUSH jobs "email-123" # push หัว RPOP jobs # pop ท้าย = FIFO queue BLPOP jobs 30 # block รอสูงสุด 30s LRANGE feed:user:1 0 99 # ดู 100 รายการแรก
SADD online:users "user:123" "user:456" SISMEMBER online:users "user:123" # 1 (ใช่) SMEMBERS online:users # ดูทุกคน SINTER tags:java tags:backend # intersection
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
ตั้งเวลาหมดอายุของ key — Redis ลบ key อัตโนมัติหลังหมดเวลา ไม่ต้องเขียน cleanup code
SET session:abc "data" EX 3600 # หมดใน 1 ชั่วโมง EXPIRE key 300 # ตั้ง TTL ทีหลัง TTL key # เหลืออีกกี่วินาที PERSIST key # ยกเลิก TTL
Publisher ส่ง message ไปยัง channel → Subscriber ทุกตัวที่ subscribe channel นั้นได้รับพร้อมกัน
./mvnw quarkus:add-extension -Dextensions="smallrye-reactive-messaging-rabbitmq"./mvnw quarkus:add-extension -Dextensions="redis-client"# ── 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
@Channel + Emitter<String> ใน REST endpoint เพื่อส่ง message เข้า RabbitMQ แบบ non-blockingimport 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
@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
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 + "\"}";
}
}
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);
}
}
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
// }
| Storage | ขนาด | ประเภทข้อมูล | Lifetime | Async | ค้นหาได้ | เหมาะกับ |
|---|---|---|---|---|---|---|
| localStorage | 5–10 MB | string | ถาวร | ❌ | ❌ | settings, theme, preferences |
| sessionStorage | 5 MB | string | ปิด tab = หาย | ❌ | ❌ | wizard, multi-step form |
| IndexedDB | GB+ | Object, Blob, File… | ถาวร | ✅ | ✅ | offline data, draft, profile cache |
| Cookie | 4 KB | string | มี expires | ✅ | ❌ | auth token (httpOnly) |
| Cache API | GB+ | HTTP Request/Response | ถาวร | ✅ | ❌ | PWA offline, static assets |
| OPFS | GB+ | File (binary) | ถาวร | ✅ | ❌ | ไฟล์ใหญ่, SQLite-in-browser |
_sp_)// ── เก็บข้อมูล ──
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));
}
// ── 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;
| id | name | role | cachedAt |
|---|
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();
// 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');
};
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;
});
})
);
});
Secure + SameSite=Strict กับทุก auth cookieESAN AI AGENT ออฟฟิศ 8 บิตของ Claude Code
ทุก session ของ Claude Code ที่เปิดอยู่บนคอมคุณ จะกลายเป็นตัวละคร 1 ตัวในออฟฟิศ มีงานก็เดินไปนั่งโต๊ะ ว่างก็ไปพัก ดูได้ว่าใครกำลังใช้เครื่องมืออะไร context เหลือเท่าไร และสร้าง agent ประจำออฟฟิศไว้สั่งงานจากหน้าเว็บได้
http://localhost:4567
ไม่ได้เปิดที่เว็บนี้ หน้า อีสาน DevTools บน github.io เป็นแค่คู่มือ ตัวออฟฟิศต้องคุยกับ bridge (โปรแกรมเล็ก ๆ ที่ plugin เปิดไว้ในคอมคุณ) ข้อมูล session ทั้งหมดอยู่ในเครื่อง ไม่ขึ้นอินเทอร์เน็ต
ทำงานยังไง
- Claude Codeทุก session ส่งเหตุการณ์ (พิมพ์ prompt, ใช้เครื่องมือ, ตอบเสร็จ) ผ่าน hook ของ plugin
- bridgeรันเบื้องหลังที่
127.0.0.1:4567เปิดเองตอนเริ่ม session รับเหตุการณ์แล้วเก็บสถานะไว้ - หน้าออฟฟิศเปิด
http://localhost:4567ในเบราว์เซอร์ เห็นทุก session เป็นตัวละครแบบสด
ติดตั้งครั้งแรก
ทำครั้งเดียว หลังจากนั้นเปิด Claude Code ตามปกติ ออฟฟิศจะทำงานเอง
-
เช็กของที่ต้องมี
Node.js 18 ขึ้นไป (ใช้รัน bridge) และคำสั่ง
claudeต้องใช้ได้ใน terminal ถ้าจะติดตั้งเป็นแอปใช้ Chrome หรือ Edgenode --versionclaude --version -
เพิ่ม marketplace ของ อีสาน DevTools
บอก Claude Code ว่า plugin นี้มาจาก repo ไหน พิมพ์ใน terminal (cmd, PowerShell หรือ Terminal ของ macOS ก็ได้)
claude plugin marketplace add devgreenpink/dev-utility-toolkit -
ติดตั้ง plugin
claude plugin install esan-office@esan-devtoolsหรือพิมพ์
/pluginในหน้าจอ Claude Code แล้วเพิ่ม marketplace กับติดตั้งจากเมนูแทน 2 คำสั่งข้างบน -
เปิด Claude Code session ใหม่
พิมพ์
claudeใน terminal หรือเปิดแท็บ Code ในแอป Claude desktop ก็ได้ พอเริ่ม session bridge จะเปิดเองเบื้องหลัง ไม่ต้องรันอะไรเพิ่ม -
เปิดออฟฟิศ
เข้า http://localhost:4567 จะเห็นตัวละครของ session นั้นเดินเข้าประตูมา ถ้าอยากได้เป็นแอปบนเดสก์ท็อป กดปุ่ม ติดตั้งเป็นแอป ข้างปุ่มธีมมุมขวาบน
อัปเดตเป็นเวอร์ชันใหม่
plugin ไม่อัปเดตเอง ทำตามนี้ทุกครั้งที่มีเวอร์ชันใหม่
-
ดึงรายการ plugin ล่าสุดจาก GitHub
claude plugin marketplace update esan-devtools -
อัปเดต plugin
claude plugin update esan-office@esan-devtoolsถ้าขึ้นว่า
already at the latest versionแปลว่าไฟล์ใหม่ลงแล้ว ไปข้อต่อไปได้เลย -
เปิด Claude Code session ใหม่ 1 ครั้ง
คำสั่ง update แค่เปลี่ยนไฟล์ ส่วน bridge ตัวเก่ายังรันอยู่ ตั้งแต่ 0.4.1 ขึ้นไป session ใหม่จะขอให้ตัวเก่าปิดเองแล้วเปิดตัวใหม่แทน
-
รีเฟรชหน้าออฟฟิศ
กด Ctrl+F5 ที่
localhost:4567แล้วเช็กว่าได้เวอร์ชันใหม่จริงที่ localhost:4567/api/ping (ดูค่าversion)
bridge รุ่นเก่ายังไม่รู้จักคำขอให้ปิดตัว หน้าเลยยังเป็นของเก่าแม้อัปเดตแล้ว ให้ปิด process ที่ใช้ port 4567 เองก่อน แล้วค่อยเปิด session ใหม่
netstat -ano | findstr :4567taskkill /PID 3160 /Flsof -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
"statusLine": { "type": "command", "command": "node \"C:/Users/<ชื่อผู้ใช้>/.claude/plugins/marketplaces/esan-devtools/plugins/esan-office/scripts/statusline.js\"" }"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