SLOPSHOPPER

trip-planner-th

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

newcommandtoastmodel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · trip-planner-th
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /trip ⎿ trip-planner-th: 🧳 Trip Planner TH (Mod 27) ⎿ trip-planner-th: ======================================== ⎿ trip-planner-th: /trip plan <ปลายทาง> <จำนวนวัน> [งบ/สไตล์...] ให้ Claude วางแผน แล้วบันทึกเป็น trip-<ปลายทาง>.md ⎿ trip-planner-th: เช่น /trip plan เชียงใหม่ 3 งบ 8000 ชอบคาเฟ่ ⎿ trip-planner-th: /trip search <ต้นทาง> <ปลายทาง> <วันที่> [flight|bus|train|hotel|all] ⎿ trip-planner-th: เช่น /trip search กรุงเทพ ภูเก็ต 15/12 flight ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Mod 27: Trip Planner TH

ผู้ช่วยวางแผนเที่ยว: ให้ 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. แปลงวันที่ (พ.ศ., วันนี้/พรุ่งนี้, ไม่ใส่ปีแล้ววันผ่านไปแล้ว = ปีหน้า)
  2. สร้างลิงก์ค้นหาไปเว็บจองจริง
  3. ให้ Claude วางแผนด้วย $.model.complete (มี model สำรอง)
  4. คำสั่ง /trip และการเตือนตอนเปิด session

หมายเหตุ

Mod จองตั๋วหรือรับเงินเองไม่ได้ เพราะไม่มี API สาธารณะของสายการบิน บขส. หรือ รฟท. จึงพาไปหน้าค้นหาของเว็บจองแทน แผนเที่ยวที่ Claude เขียน (ใช้ haiku) อาจมีรายละเอียดคลาดเคลื่อน เช่น เวลาเปิด ราคา หรือที่ตั้งร้าน ตรวจกับแหล่งข้อมูลจริงก่อนเดินทาง

Source 1 files
hooks/register.tsx 193 lines
1/**
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