ดู limit การใช้ Claude แบบน่ารัก: % ที่ใช้ไปของรอบ 5 ชม. และ 7 วัน เวลารีเซ็ต context ที่เหลือ และเตือนก่อนเต็ม

ดูโควตา Claude แบบน่ารัก: ลิมิตหน้าต่าง 5 ชั่วโมงและ 7 วันจากตัวเลขจริงของ Claude Code พร้อมเวลารีเซ็ต การใช้ context และค่าใช้จ่ายของ session แมวแม่มดน้อยเปลี่ยนหน้าตามโควตาที่เหลือ ย่อในแถบสถานะ และเตือนก่อนเต็ม (ค่าเริ่มต้น 80% และ 95%)
| คำสั่ง | ทำอะไร | |
|---|---|---|
/ailimit | ดูโควตา + เปิดแผงแมวแม่มด | |
| `/ailimit status on\ | off` | ย่อในแถบสถานะ |
/ailimit warn <%> <%> | ตั้งเกณฑ์เตือน | |
/ailimit budget <ดอลลาร์> | งบค่าใช้จ่ายต่อวัน รวมทุก session เตือนที่ 80% และเมื่อเกิน / budget off |
/plugin install ai-limit-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
ลิมิต 5 ชั่วโมง/7 วันมีเฉพาะแพ็กเกจสมาชิก (ใช้ API key จะไม่มี) และจะมีตัวเลขหลังคำตอบแรกของ session ได้แรงบันดาลใจจากแอป PIKORI ของคุณ Ron Thitisak
hooks/register.tsx 260 lines1/**
2 * ============================================================================
3 * MOD 46: AI Limit TH - ดูโควตา Claude แบบน่ารัก (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: แอปดูลิมิต AI แบบน่ารัก (ขอบคุณไอเดียจาก PIKORI ของคุณ Ron Thitisak)
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. อ่านลิมิตจริงจาก Claude Code: หน้าต่าง 5 ชั่วโมง และ 7 วัน (เฉพาะแพ็กเกจสมาชิก)
9 * พร้อมเวลารีเซ็ต, การใช้ context และค่าใช้จ่ายของ session
10 * 2. แมวแม่มดน้อยเปลี่ยนหน้าตามโควตาที่เหลือ: สบายๆ → เริ่มกังวล → ตกใจ
11 * 3. เตือนก่อนเต็ม (ค่าเริ่มต้น 80% และ 95%) และโชว์ย่อในแถบสถานะ
12 * 4. นักบัญชี: รวมค่าใช้จ่ายทุก session ของวันนี้ ตั้งงบรายวันได้ เตือนที่ 80% และเมื่อเกิน
13 *
14 * ข้อมูลมาจาก event session.measure ที่ engine ส่งมาทุกครั้งที่ตัวเลขขยับ
15 * ไม่ต้องยิง API เอง และไม่มีข้อมูลสมมติ
16 */
17
18import type { EngineInterface, Register, SessionMeasureInput } from 'claude-code'
19
20const PANE = 'ai-limit-th'
21const WINDOW_NAME: Record<string, string> = { five_hour: '5 ชั่วโมง', seven_day: '7 วัน', spend_limit: 'วงเงิน' }
22
23let usage: Omit<SessionMeasureInput, 'changed'> | undefined
24let settings: { status: boolean; warnAt: number[]; budget?: number } = { status: true, warnAt: [80, 95] }
25// จำว่าเตือนระดับไหนไปแล้วของหน้าต่างไหน (รีเซ็ตเมื่อหน้าต่างรีเซ็ต)
26let warned: Record<string, number> = {}
27// นักบัญชี: ยอดใช้จ่ายรวมของวันนี้ (ทุก session) และยอดของ session นี้ที่นับไปแล้ว
28let spend = { day: '', usd: 0, warned: 0 }
29let sessionUsd = 0
30
31function today(ms: number): string {
32 const d = new Date(ms)
33 return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
34}
35
36function money(usd: number): string {
37 return `$${usd.toFixed(usd < 1 ? 3 : 2)}`
38}
39
40// ==========================================================================
41// บทที่ 1: แมวแม่มดตามระดับโควตาที่ใช้ไป
42// ==========================================================================
43function witch(percent: number): { face: string; say: string; color: string } {
44 if (percent >= 95) return { face: '/\\_/\\\n(⊙_⊙;)🧹', say: 'ไม้กวาดจะหมดแรงแล้ว! พักก่อนนะ', color: '#e53935' }
45 if (percent >= 80) return { face: '/\\_/\\\n(・_・;)🧹', say: 'เริ่มเยอะแล้ว ใช้อย่างประหยัดหน่อย', color: '#fb8c00' }
46 if (percent >= 50) return { face: '/\\_/\\\n( •ω•)🧹', say: 'ใช้ไปครึ่งทางแล้ว ยังไหวๆ', color: '#fdd835' }
47 return { face: '/\\_/\\\n( ^ω^)🧹✨', say: 'โควตาเหลือเยอะ ลุยเลย!', color: '#7cb342' }
48}
49
50function bar(percent: number, width = 20): string {
51 const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * width)
52 return '█'.repeat(filled) + '░'.repeat(width - filled)
53}
54
55function untilReset(iso: string | undefined, now: number): string {
56 if (!iso) return ''
57 const ms = Date.parse(iso) - now
58 if (!(ms > 0)) return 'รีเซ็ตแล้ว'
59 const m = Math.ceil(ms / 60_000)
60 const d = Math.floor(m / 1440)
61 const h = Math.floor((m % 1440) / 60)
62 return `รีเซ็ตใน ${d ? `${d} วัน ` : ''}${h ? `${h} ชม. ` : ''}${m % 60} นาที`
63}
64
65function windowName(kind: string): string {
66 return WINDOW_NAME[kind] ?? kind
67}
68
69function maxPercent(): number {
70 return Math.max(0, ...(usage?.rateLimits ?? []).map(r => r.percentUsed))
71}
72
73// ==========================================================================
74// บทที่ 2: แถบสถานะย่อ + เตือนตอนข้ามเกณฑ์
75// ==========================================================================
76function updateStatus($: EngineInterface): void {
77 const limits = usage?.rateLimits ?? []
78 const wallet = settings.budget ? `💸 ${money(spend.usd)}/${money(settings.budget)}` : ''
79 if (!settings.status || (limits.length === 0 && !wallet)) {
80 $.ui.status(undefined)
81 return
82 }
83 const icon = maxPercent() >= 95 ? '🙀' : maxPercent() >= 80 ? '😿' : '😺'
84 const parts = limits.map(r => `${r.kind === 'five_hour' ? '5ชม' : r.kind === 'seven_day' ? '7วัน' : r.kind} ${Math.round(r.percentUsed)}%`)
85 $.ui.status([limits.length ? `${icon} ${parts.join(' · ')}` : '', wallet].filter(Boolean).join(' | '))
86}
87
88// ==========================================================================
89// บทที่ 2.5: นักบัญชี - รวมค่าใช้จ่ายทุก session ของวันนี้ แล้วเทียบกับงบที่ตั้งไว้
90// ==========================================================================
91async function account($: EngineInterface, sessionTotal: number): Promise<void> {
92 const delta = Math.max(0, sessionTotal - sessionUsd)
93 sessionUsd = sessionTotal
94 if (delta === 0) return
95 const day = today(await $.clock.now())
96 spend = spend.day === day ? { ...spend, usd: spend.usd + delta } : { day, usd: delta, warned: 0 }
97 if (settings.budget) {
98 const pct = (spend.usd / settings.budget) * 100
99 const level = pct >= 100 ? 100 : pct >= 80 ? 80 : 0
100 if (level > spend.warned) {
101 spend = { ...spend, warned: level }
102 $.ui.toast(level >= 100 ? `💸 ใช้เกินงบวันนี้แล้ว ${money(spend.usd)} / ${money(settings.budget)}` : `💰 ใช้ไป ${Math.round(pct)}% ของงบวันนี้ (${money(spend.usd)} / ${money(settings.budget)})`, { timeoutMs: 12_000 })
103 }
104 }
105 await $.store.set('spend', spend)
106}
107
108function checkWarnings($: EngineInterface): void {
109 for (const r of usage?.rateLimits ?? []) {
110 const key = `${r.kind}@${r.resetsAt ?? ''}`
111 const level = [...settings.warnAt].sort((a, b) => b - a).find(t => r.percentUsed >= t)
112 if (level !== undefined && (warned[key] ?? 0) < level) {
113 warned[key] = level
114 $.ui.toast(`${level >= 95 ? '🙀' : '😿'} ลิมิต ${windowName(r.kind)} ใช้ไป ${r.percentUsed}% แล้ว`, { timeoutMs: 12_000 })
115 }
116 }
117}
118
119const HELP = `🐱 AI Limit TH (Mod 46)
120========================================
121/ailimit ดูโควตา + เปิดแผงแมวแม่มด
122/ailimit status on|off ย่อในแถบสถานะ
123/ailimit warn 70 90 ตั้งเกณฑ์เตือน (%)
124/ailimit budget 5 งบค่าใช้จ่ายต่อวัน (ดอลลาร์) เตือนที่ 80% และเมื่อเกิน | budget off
125ลิมิต 5 ชั่วโมง/7 วันมีเฉพาะแพ็กเกจสมาชิก และจะมีตัวเลขหลังคำตอบแรกของ session`
126
127function summary(now: number): string {
128 if (!usage) return '⏳ ยังไม่มีข้อมูล รอคำตอบแรกของ Claude ก่อนนะ'
129 const lines = [`${witch(maxPercent()).face.replace('\n', ' ')} ${witch(maxPercent()).say}`, '']
130 if (usage.rateLimits.length === 0) lines.push('ไม่มีข้อมูลลิมิต (ใช้ API key หรือยังไม่ได้รับตัวเลข)')
131 for (const r of usage.rateLimits) lines.push(`${windowName(r.kind).padEnd(10)} ${bar(r.percentUsed)} ${String(r.percentUsed).padStart(5)}% ${untilReset(r.resetsAt, now)}`)
132 if (usage.context.percent !== undefined) lines.push(`${'context'.padEnd(10)} ${bar(usage.context.percent)} ${String(Math.round(usage.context.percent)).padStart(5)}% ของ ${usage.context.window.toLocaleString('en-US')} tokens`)
133 if (usage.cost) lines.push(`💸 session นี้ ${money(usage.cost.usd)} · วันนี้รวม ${money(spend.usd)}${settings.budget ? ` / งบ ${money(settings.budget)} (${Math.round((spend.usd / settings.budget) * 100)}%)` : ' (ตั้งงบ: /ailimit budget 5)'}`)
134 return lines.join('\n')
135}
136
137export const register: Register = on => {
138 on('session.start', async ($, e, next) => {
139 settings = { ...settings, ...((await $.store.get('settings')) as typeof settings | undefined) }
140 warned = {}
141 sessionUsd = 0
142 spend = { day: '', usd: 0, warned: 0, ...((await $.store.get('spend')) as typeof spend | undefined) }
143 if (spend.day !== today(await $.clock.now())) spend = { day: today(await $.clock.now()), usd: 0, warned: 0 }
144 await $.command.register({ name: 'ailimit', description: 'ดูโควตา Claude (5 ชม./7 วัน) แบบน่ารัก ค่าใช้จ่ายรายวัน และเตือนก่อนเต็ม (/ailimit help)', argumentHint: 'status on|off | warn <%> <%> | budget <usd>' })
145 return next(e)
146 })
147
148 // ==========================================================================
149 // บทที่ 3: รับตัวเลขจาก engine ทุกครั้งที่ขยับ
150 // ==========================================================================
151 on('session.measure', async ($, e, next) => {
152 usage = { context: e.context, rateLimits: e.rateLimits, cost: e.cost }
153 if (e.cost) await account($, e.cost.usd)
154 checkWarnings($)
155 updateStatus($)
156 $.ui.invalidate('ui.render')
157 return next(e)
158 })
159
160 // ==========================================================================
161 // บทที่ 4: แผงแมวแม่มด
162 // ==========================================================================
163 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
164 const { Box, Text } = $.ui.resolve(e)
165 const now = await $.clock.now()
166 const w = witch(maxPercent())
167 const limits = usage?.rateLimits ?? []
168 return (
169 <Box flexDirection="column">
170 <Box flexDirection="row" gap={2}>
171 <Box flexDirection="column">
172 {w.face.split('\n').map(l => (
173 <Text color="#9575cd">{l}</Text>
174 ))}
175 </Box>
176 <Text color={w.color}>{usage ? w.say : 'รอคำตอบแรกก่อนนะ…'}</Text>
177 </Box>
178 {usage && limits.length === 0 && <Text dimColor>ไม่มีข้อมูลลิมิต (API key หรือยังไม่ได้รับตัวเลข)</Text>}
179 {limits.map(r => (
180 <Box flexDirection="column" marginTop={1}>
181 <Text>
182 {windowName(r.kind)} <Text color={witch(r.percentUsed).color}>{bar(r.percentUsed)}</Text> {r.percentUsed}%
183 </Text>
184 <Text dimColor>{untilReset(r.resetsAt, now)}</Text>
185 </Box>
186 ))}
187 {usage?.context.percent !== undefined && (
188 <Box marginTop={1}>
189 <Text>
190 context <Text color="#4fc3f7">{bar(usage.context.percent)}</Text> {Math.round(usage.context.percent)}%
191 </Text>
192 </Box>
193 )}
194 {usage?.cost && (
195 <Text dimColor>
196 💸 session {money(usage.cost.usd)} · วันนี้ {money(spend.usd)}
197 {settings.budget ? ` / งบ ${money(settings.budget)}` : ''}
198 </Text>
199 )}
200 </Box>
201 )
202 })
203
204 on('command.run', { command: 'ailimit' }, async ($, e) => {
205 const [sub = '', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
206 switch (sub) {
207 case '': {
208 // ยังไม่มี measurement? ลองถามตรงๆ ก่อน (ฟรี ไม่เสีย token)
209 if (!usage) {
210 const u = await $.session.usage().catch(() => undefined)
211 if (u && (u.rateLimits.length || u.context.percent !== undefined)) usage = { context: u.context, rateLimits: u.rateLimits, cost: u.cost }
212 }
213 await $.ui.open({ id: PANE, title: '🐱 AI Limit' })
214 $.ui.invalidate('ui.render')
215 return { text: summary(await $.clock.now()) }
216 }
217 case 'status':
218 if (rest[0] !== 'on' && rest[0] !== 'off') return { text: 'วิธีใช้: /ailimit status on|off' }
219 settings = { ...settings, status: rest[0] === 'on' }
220 await $.store.set('settings', settings)
221 updateStatus($)
222 return { text: `แถบสถานะ: ${settings.status ? 'แสดง' : 'ซ่อน'}` }
223 case 'budget': {
224 if (rest[0] === 'off') {
225 settings = { ...settings, budget: undefined }
226 await $.store.set('settings', settings)
227 updateStatus($)
228 return { text: 'ยกเลิกงบรายวันแล้ว' }
229 }
230 const usd = Number(rest[0])
231 if (!(usd > 0 && usd <= 10_000)) return { text: `วิธีใช้: /ailimit budget 5 (ดอลลาร์ต่อวัน) | budget off\nวันนี้ใช้ไป ${money(spend.usd)}` }
232 settings = { ...settings, budget: usd }
233 spend = { ...spend, warned: 0 }
234 await $.store.set('settings', settings)
235 updateStatus($)
236 return { text: `💰 งบวันละ ${money(usd)} · วันนี้ใช้ไป ${money(spend.usd)} (${Math.round((spend.usd / usd) * 100)}%)` }
237 }
238 case 'warn': {
239 const levels = rest.map(Number).filter(n => Number.isFinite(n) && n > 0 && n <= 100)
240 if (levels.length === 0 || levels.length !== rest.length) return { text: `วิธีใช้: /ailimit warn 80 95 (ตอนนี้: ${settings.warnAt.join(', ')}%)` }
241 settings = { ...settings, warnAt: levels }
242 await $.store.set('settings', settings)
243 warned = {}
244 return { text: `🔔 เตือนที่ ${levels.join(', ')}%` }
245 }
246 default:
247 return { text: HELP }
248 }
249 })
250}
251
252/**
253 * ========================================================================
254 * การบ้าน / ต่อยอด
255 * ========================================================================
256 * 1. บันทึกเปอร์เซ็นต์ทุกชั่วโมงแล้ววาดกราฟการใช้ย้อนหลัง
257 * 2. ทำนายว่าจะเต็มเมื่อไหร่จากอัตราการใช้ช่วงล่าสุด
258 * 3. ส่งเตือนไปมือถือเมื่อเกิน 95% (ต่อกับ Mod 22)
259 */
260