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

ผู้ช่วยวางแผนเที่ยว: ให้ Claude วางแผนทริปรายวันพร้อมงบประมาณและของที่ต้องเตรียม แล้วบันทึกเป็นไฟล์ Markdown, ทำลิงก์ค้นตั๋วเครื่องบิน รถทัวร์ รถไฟ และที่พักไปยังเว็บจองจริง และเก็บตั๋วที่จองแล้ว (เลขที่จอง ที่นั่ง) พร้อมนับถอยหลัง เปิด Claude Code วันก่อนเดินทางจะเด้งเตือน
| คำสั่ง | ทำอะไร | ||||
|---|---|---|---|---|---|
/trip plan เชียงใหม่ 3 งบ 8000 ชอบคาเฟ่ | ให้ Claude วางแผน บันทึกเป็น trip-เชียงใหม่.md | ||||
| `/trip search กรุงเทพ ภูเก็ต 15/12 [flight\ | bus\ | train\ | hotel\ | all]` | ลิงก์ค้นตั๋วและที่พัก |
/trip add บิน DMK-CNX 15/12 08:30 FD3433 ที่นั่ง 12A | เก็บตั๋ว (บิน รถทัวร์ รถไฟ รถตู้ เรือ) | ||||
/trip tickets | ตั๋วที่จะถึง พร้อมนับถอยหลัง | ||||
/trip remove <id> | ลบตั๋ว |
/plugin install trip-planner-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
Mod จองตั๋วหรือรับเงินเองไม่ได้ เพราะไม่มี API สาธารณะของสายการบิน บขส. หรือ รฟท. จึงพาไปหน้าค้นหาของเว็บจองแทน แผนเที่ยวที่ Claude เขียน (ใช้ haiku) อาจมีรายละเอียดคลาดเคลื่อน เช่น เวลาเปิด ราคา หรือที่ตั้งร้าน ตรวจกับแหล่งข้อมูลจริงก่อนเดินทาง
hooks/register.tsx 193 lines1/**
2 * ============================================================================
3 * MOD 27: Trip Planner TH - ผู้ช่วยวางแผนเที่ยว (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: Google Travel, TripIt
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. /trip plan ให้ Claude วางแผนทริปรายวัน งบประมาณ ของที่ต้องเตรียม แล้วบันทึกเป็นไฟล์ .md
9 * 2. /trip search ทำลิงก์ค้นตั๋วเครื่องบิน รถทัวร์ รถไฟ และที่พัก ไปยังเว็บจองจริง
10 * 3. กระเป๋าตั๋ว: เก็บตั๋วที่จองแล้ว (เลขที่จอง ที่นั่ง) พร้อมนับถอยหลัง และเตือนเมื่อใกล้เดินทาง
11 *
12 * หมายเหตุ: Mod จองตั๋วหรือรับเงินเองไม่ได้ (ไม่มี API สาธารณะของสายการบิน/บขส./รฟท.)
13 * จึงพาไปหน้าค้นหาของเว็บจองจริงแทน ราคาและที่นั่งให้ดูจากเว็บนั้น
14 */
15
16import type { EngineInterface, Register } from 'claude-code'
17
18type Mode = 'flight' | 'bus' | 'train' | 'van' | 'boat' | 'other'
19interface Ticket {
20 id: number
21 mode: Mode
22 from: string
23 to: string
24 at: number // เวลาออกเดินทาง (ms)
25 ref: string // เลขที่จอง / ที่นั่ง / หมายเหตุ
26}
27
28const ICON: Record<Mode, string> = { flight: '✈️', bus: '🚌', train: '🚆', van: '🚐', boat: '⛴️', other: '🎫' }
29const MODE_ALIAS: Record<string, Mode> = { flight: 'flight', บิน: 'flight', เครื่องบิน: 'flight', bus: 'bus', รถทัวร์: 'bus', train: 'train', รถไฟ: 'train', van: 'van', รถตู้: 'van', boat: 'boat', เรือ: 'boat' }
30let tickets: Ticket[] = []
31let nextId = 1
32let projectRoot = ''
33
34async function save($: EngineInterface): Promise<void> {
35 await $.store.set('trip', { tickets, nextId })
36}
37
38// ==========================================================================
39// บทที่ 1: แปลงวันที่และเวลา (รับ พ.ศ., วันนี้/พรุ่งนี้)
40// ==========================================================================
41function parseDate(text: string | undefined, now: number): Date | undefined {
42 const base = new Date(now)
43 if (!text) return undefined
44 if (text === 'วันนี้' || text === 'today') return new Date(base.getFullYear(), base.getMonth(), base.getDate())
45 if (text === 'พรุ่งนี้' || text === 'tomorrow') return new Date(base.getFullYear(), base.getMonth(), base.getDate() + 1)
46 let m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(text)
47 if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))
48 m = /^(\d{1,2})\/(\d{1,2})(?:\/(\d{4}))?$/.exec(text)
49 if (!m) return undefined
50 let year = Number(m[3] ?? base.getFullYear())
51 if (year > 2400) year -= 543
52 const d = new Date(year, Number(m[2]) - 1, Number(m[1]))
53 // ไม่ใส่ปีและวันนั้นผ่านไปแล้ว = ปีหน้า
54 if (!m[3] && d.getTime() < new Date(base.getFullYear(), base.getMonth(), base.getDate()).getTime()) d.setFullYear(d.getFullYear() + 1)
55 return d
56}
57
58function isoDate(d: Date): string {
59 return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
60}
61
62function countdown(ms: number): string {
63 if (ms < 0) return 'ออกเดินทางไปแล้ว'
64 const h = Math.floor(ms / 3_600_000)
65 return h < 24 ? `อีก ${h} ชม. ${Math.floor((ms % 3_600_000) / 60_000)} นาที` : `อีก ${Math.floor(h / 24)} วัน ${h % 24} ชม.`
66}
67
68// ==========================================================================
69// บทที่ 2: ลิงก์ค้นหาไปเว็บจองจริง (ใส่ต้นทาง ปลายทาง วันที่ให้แล้ว)
70// ==========================================================================
71function searchLinks(from: string, to: string, date: string, mode: string): string[] {
72 const q = (s: string) => encodeURIComponent(s)
73 const links: string[] = []
74 if (mode === 'all' || mode === 'flight') links.push(`✈️ เครื่องบิน (Google Flights): https://www.google.com/travel/flights?q=${q(`Flights from ${from} to ${to} on ${date}`)}`)
75 if (mode === 'all' || mode === 'bus' || mode === 'van') {
76 links.push(`🚌 รถทัวร์/รถตู้ (12Go): https://12go.asia/th`)
77 links.push(`🚌 ค้นรอบรถ: https://www.google.com/search?q=${q(`รถทัวร์ ${from} ไป ${to} ${date}`)}`)
78 }
79 if (mode === 'all' || mode === 'train') links.push(`🚆 รถไฟ (D-Ticket การรถไฟฯ): https://dticket.railway.co.th`)
80 if (mode === 'all' || mode === 'hotel') links.push(`🏨 ที่พัก (Google Hotels): https://www.google.com/travel/hotels?q=${q(`hotels in ${to}`)}`)
81 return links
82}
83
84// ==========================================================================
85// บทที่ 3: ให้ Claude วางแผนทริป (haiku ก่อน ใช้ไม่ได้ค่อยใช้ model ของ session)
86// ==========================================================================
87async function planTrip($: EngineInterface, dest: string, days: number, extra: string): Promise<string> {
88 const system = 'You are a careful Thai travel planner. Write in Thai, in Markdown. Give a day-by-day plan, a rough budget table in baht, transport between places, a packing list, and local tips. Use real, well-known places only; where opening hours or prices may change, say to check before going. Keep it practical.'
89 const prompt = `ปลายทาง: ${dest}\nจำนวนวัน: ${days}\nเพิ่มเติม: ${extra || 'ไม่มี'}`
90 for (const model of ['haiku', await $.session.model()]) {
91 const r = await $.model.complete({ model, system, prompt, maxTokens: 3000, timeoutMs: 120_000 }).catch(() => undefined)
92 if (r?.isAnswered) return r.text.trim()
93 }
94 throw new Error('เรียก model ไม่สำเร็จ')
95}
96
97function ticketLine(t: Ticket, now: number): string {
98 const d = new Date(t.at)
99 return `${ICON[t.mode]} #${t.id} ${t.from} → ${t.to} ${d.toLocaleDateString('th-TH')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')} (${countdown(t.at - now)})${t.ref ? `\n ${t.ref}` : ''}`
100}
101
102const HELP = `🧳 Trip Planner TH (Mod 27)
103========================================
104/trip plan <ปลายทาง> <จำนวนวัน> [งบ/สไตล์...] ให้ Claude วางแผน แล้วบันทึกเป็น trip-<ปลายทาง>.md
105 เช่น /trip plan เชียงใหม่ 3 งบ 8000 ชอบคาเฟ่
106/trip search <ต้นทาง> <ปลายทาง> <วันที่> [flight|bus|train|hotel|all]
107 เช่น /trip search กรุงเทพ ภูเก็ต 15/12 flight
108/trip add <ประเภท> <ต้นทาง>-<ปลายทาง> <วันที่> <เวลา> [เลขที่จอง/ที่นั่ง...]
109 ประเภท: flight bus train van boat (หรือ บิน รถทัวร์ รถไฟ รถตู้ เรือ)
110 เช่น /trip add บิน DMK-CNX 15/12 08:30 FD3433 PNR ABC123 ที่นั่ง 12A
111/trip tickets | remove <id>
112วันที่: วันนี้ | พรุ่งนี้ | 15/12 | 15/12/2569 | 2026-12-15`
113
114export const register: Register = on => {
115 on('session.start', async ($, e, next) => {
116 projectRoot = e.cwd.replace(/[\\/]$/, '')
117 const saved = (await $.store.get('trip')) as { tickets?: Ticket[]; nextId?: number } | undefined
118 tickets = saved?.tickets ?? []
119 nextId = saved?.nextId ?? 1
120 // เตือนตั๋วที่จะออกเดินทางภายใน 24 ชม.
121 const now = await $.clock.now()
122 const soon = tickets.filter(t => t.at > now && t.at - now < 24 * 3_600_000)
123 for (const t of soon) $.ui.toast(`🧳 ใกล้เดินทางแล้ว: ${t.from} → ${t.to} ${countdown(t.at - now)}`)
124 await $.command.register({ name: 'trip', description: 'วางแผนเที่ยว ค้นตั๋ว และเก็บตั๋วที่จองแล้ว (/trip help)', argumentHint: 'plan|search|add|tickets' })
125 return next(e)
126 })
127
128 // ==========================================================================
129 // บทที่ 4: คำสั่ง /trip
130 // ==========================================================================
131 on('command.run', { command: 'trip' }, async ($, e) => {
132 const [sub = 'help', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
133 const now = await $.clock.now()
134
135 try {
136 switch (sub) {
137 case 'plan': {
138 const [dest, daysText, ...extra] = rest
139 const days = Number(daysText)
140 if (!dest || !Number.isInteger(days) || days < 1 || days > 14) return { text: 'วิธีใช้: /trip plan เชียงใหม่ 3 งบ 8000 ชอบคาเฟ่ (1-14 วัน)' }
141 const plan = await planTrip($, dest, days, extra.join(' '))
142 const path = `${projectRoot}${projectRoot.includes('\\') ? '\\' : '/'}trip-${dest.replace(/[\\/:*?"<>|\s]/g, '_')}.md`
143 await $.fs.write(path, `${plan}\n`)
144 return { text: `${plan.slice(0, 2500)}${plan.length > 2500 ? '\n...' : ''}\n\n💾 บันทึกแผนเต็มไว้ที่ ${path}` }
145 }
146 case 'search': {
147 const [from, to, dateText, mode = 'all'] = rest
148 const date = parseDate(dateText, now)
149 if (!from || !to || !date) return { text: 'วิธีใช้: /trip search กรุงเทพ ภูเก็ต 15/12 [flight|bus|train|hotel|all]' }
150 return { text: [`🔎 ${from} → ${to} ${date.toLocaleDateString('th-TH', { dateStyle: 'full' })}`, ...searchLinks(from, to, isoDate(date), mode), '', 'จองแล้วเก็บตั๋วไว้ด้วย /trip add ...'].join('\n') }
151 }
152 case 'add': {
153 const [modeText = '', route = '', dateText, timeText = '', ...refWords] = rest
154 const mode = MODE_ALIAS[modeText.toLowerCase()] ?? 'other'
155 const [from, to] = route.split('-')
156 const date = parseDate(dateText, now)
157 const time = /^([01]?\d|2[0-3])[:.]([0-5]\d)$/.exec(timeText)
158 if (!from || !to || !date || !time) return { text: 'วิธีใช้: /trip add บิน DMK-CNX 15/12 08:30 FD3433 ที่นั่ง 12A' }
159 date.setHours(Number(time[1]), Number(time[2]))
160 const t: Ticket = { id: nextId++, mode, from, to, at: date.getTime(), ref: refWords.join(' ') }
161 tickets.push(t)
162 tickets.sort((a, b) => a.at - b.at)
163 await save($)
164 return { text: `✅ เก็บตั๋วแล้ว\n${ticketLine(t, now)}` }
165 }
166 case 'tickets': {
167 const upcoming = tickets.filter(t => t.at > now - 24 * 3_600_000)
168 return { text: upcoming.length ? ['🎫 ตั๋วของฉัน', ...upcoming.map(t => ticketLine(t, now))].join('\n') : 'ยังไม่มีตั๋ว เก็บด้วย /trip add ...' }
169 }
170 case 'remove': {
171 const before = tickets.length
172 tickets = tickets.filter(t => t.id !== Number(rest[0]))
173 await save($)
174 return { text: before === tickets.length ? `❌ ไม่พบตั๋ว #${rest[0] ?? ''}` : `🗑️ ลบตั๋ว #${rest[0]} แล้ว` }
175 }
176 default:
177 return { text: HELP }
178 }
179 } catch (err) {
180 return { text: `❌ ${sub} ไม่สำเร็จ: ${err instanceof Error ? err.message : String(err)}` }
181 }
182 })
183}
184
185/**
186 * ========================================================================
187 * การบ้าน / ต่อยอด
188 * ========================================================================
189 * 1. ใส่พยากรณ์อากาศของปลายทางในวันเดินทาง (ใช้ Open-Meteo แบบ Mod 29)
190 * 2. แจ้งเตือนไปมือถือก่อนออกเดินทาง 3 ชม. (ต่อกับ Mod 22)
191 * 3. ให้ Claude อ่านอีเมลยืนยันการจอง (copy มาวาง) แล้วเติมข้อมูลตั๋วให้เอง
192 */
193