SLOPSHOPPER

terminal-browser-th

เปิดเว็บจาก Claude Code: ให้ Claude สรุปเว็บเป็นไทย, ดูข้อความดิบ, เว็บโปรด, ค้นหา และประวัติ

newguardcommandnetworktimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · terminal-browser-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 › /browser ⎿ terminal-browser-th: วิธีใช้: /browser react.dev | /browser raw example.com | /browser fav github | /browser search วิธีใช้ Claude ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Mod 08: Terminal Browser TH

เปิดเว็บโดยไม่ต้องออกจาก Claude Code ให้ Claude เปิดแล้วสรุปเป็นภาษาไทย หรือดึงข้อความดิบมาดูเองโดยไม่ใช้ token มีเว็บโปรดสาย dev ไทย ค้นหาเว็บ และประวัติ (รวมเว็บที่ Claude เปิดเองด้วย WebFetch)

คำสั่ง

คำสั่งทำอะไร
/browser react.devให้ Claude เปิดและสรุปเป็นไทย
/browser raw example.comดึงข้อความดิบเอง
/browser fav [ชื่อ]ดู/เปิดเว็บโปรด
/browser search <คำ>ให้ Claude ค้นเว็บแล้วสรุป
/browser historyประวัติ

ติดตั้ง

/plugin install terminal-browser-th --marketplace tksoft11/claude-code-mods-th

ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)

โค้ดสอนอะไร

เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. ดัก tool เดียวด้วย matcher { tool: "WebFetch" }
  2. สั่งให้ Claude ทำงานด้วย $.prompt.submit
  3. ดึงเว็บเองด้วย $.http.fetch
  4. ตรวจ URL ด้วย new URL()

หมายเหตุ

raw แสดงสูงสุด 3,000 ตัวอักษร และตัด HTML แบบง่าย

Source 1 files
hooks/register.tsx 175 lines
1/**
2 * ============================================================================
3 * MOD 08: Terminal Browser TH - เปิดเว็บโดยไม่ต้องออกจาก Claude Code (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: https://github.com/zenbu-labs/terminal-browser
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. /browser <url>          ให้ Claude เปิดเว็บ (WebFetch) แล้วสรุปเป็นภาษาไทย
9 * 2. /browser raw <url>      ดึงหน้าเว็บเองด้วย $.http.fetch แล้วแสดงข้อความดิบ (ไม่ใช้ token)
10 * 3. /browser fav [ชื่อ]     เว็บโปรดคนไทยสาย dev
11 * 4. /browser search <คำ>    ให้ Claude ค้นเว็บแล้วสรุป
12 * 5. /browser history        ประวัติเว็บที่เปิด (รวมที่ Claude เปิดเองด้วย WebFetch)
13 */
14
15import type { EngineInterface, Register } from 'claude-code'
16
17interface HistoryItem {
18  url: string
19  time: number
20  status: 'loading' | 'success' | 'error'
21}
22
23// ==========================================================================
24// บทที่ 1: เก็บประวัติ (เก็บแค่ 20 รายการล่าสุด)
25// ==========================================================================
26let history: HistoryItem[] = []
27
28function addToHistory(url: string, time: number): HistoryItem {
29  const item: HistoryItem = { url, time, status: 'loading' }
30  history.push(item)
31  if (history.length > 20) history.shift()
32  return item
33}
34
35// ==========================================================================
36// บทที่ 2: เว็บโปรดคนไทย - แก้/เพิ่มได้ตามใจ
37// ==========================================================================
38const THAI_FAVORITES = [
39  { name: 'Google TH', url: 'https://www.google.co.th' },
40  { name: 'Pantip', url: 'https://pantip.com' },
41  { name: 'GitHub', url: 'https://github.com' },
42  { name: 'MDN', url: 'https://developer.mozilla.org/th/' },
43  { name: 'NPM', url: 'https://www.npmjs.com' },
44  { name: 'React Docs', url: 'https://react.dev' },
45  { name: 'Claude Code Docs', url: 'https://code.claude.com/docs' },
46]
47
48function timeAgo(ms: number): string {
49  const sec = Math.floor(ms / 1000)
50  return sec < 60 ? `${sec}วิที่แล้ว` : sec < 3600 ? `${Math.floor(sec / 60)}นาทีที่แล้ว` : `${Math.floor(sec / 3600)}ชม.ที่แล้ว`
51}
52
53// รับ url แบบไม่มี https:// ก็ได้ เช่น react.dev
54function normalizeUrl(input: string): string | undefined {
55  const withScheme = /^https?:\/\//i.test(input) ? input : `https://${input}`
56  try {
57    return new URL(withScheme).href
58  } catch {
59    return undefined
60  }
61}
62
63// ==========================================================================
64// บทที่ 3: ให้ Claude ทำงานแทน - ส่ง prompt ใหม่ด้วย $.prompt.submit
65// ==========================================================================
66// prompt จะเริ่ม turn ใหม่เมื่อ session ว่าง จึงใช้ void (ไม่ต้องรอ)
67function askClaudeToOpen($: EngineInterface, url: string): void {
68  $.clock.after(50, () => void $.prompt.submit({ text: `ใช้ WebFetch เปิด ${url} แล้วสรุปเนื้อหาหลักเป็นภาษาไทยแบบกระชับ` }).catch(() => undefined)) // ส่งหลังคำสั่งตอบกลับแล้ว (ส่งระหว่าง command.run ไม่ได้)
69}
70
71// ==========================================================================
72// บทที่ 4: ดึงเว็บเองด้วย $.http.fetch แล้วตัด HTML ออกให้อ่านง่าย
73// ==========================================================================
74function htmlToText(html: string): string {
75  return html
76    .replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, '')
77    .replace(/<br\s*\/?>|<\/(p|div|h\d|li|tr)>/gi, '\n')
78    .replace(/<[^>]+>/g, '')
79    .replace(/&nbsp;/g, ' ')
80    .replace(/&amp;/g, '&')
81    .replace(/&lt;/g, '<')
82    .replace(/&gt;/g, '>')
83    .replace(/[ \t]+/g, ' ')
84    .replace(/\n\s*\n+/g, '\n')
85    .trim()
86}
87
88export const register: Register = on => {
89  on('session.start', async ($, e, next) => {
90    history = []
91    await $.command.register({
92      name: 'browser',
93      description: 'เปิดเว็บ: /browser <url> | raw <url> | fav [ชื่อ] | search <คำ> | history',
94      argumentHint: '<url>|raw|fav|search|history',
95    })
96    return next(e)
97  })
98
99  // ==========================================================================
100  // บทที่ 5: ดัก WebFetch ของ Claude เพื่อบันทึกประวัติ
101  // ==========================================================================
102  on('tool.call', { tool: 'WebFetch' }, async ($, e, next) => {
103    const item = addToHistory(e.url, await $.clock.now())
104    const ran = await next(e)
105    item.status = ran.deny === undefined && !ran.isError ? 'success' : 'error'
106    return ran
107  })
108
109  on('command.run', { command: 'browser' }, async ($, e) => {
110    const args = e.args.trim()
111    const [sub = '', ...restWords] = args.split(/\s+/)
112    const rest = restWords.join(' ')
113    const now = await $.clock.now()
114
115    if (sub === '') {
116      return { text: 'วิธีใช้: /browser react.dev | /browser raw example.com | /browser fav github | /browser search วิธีใช้ Claude Code | /browser history' }
117    }
118
119    if (sub === 'history') {
120      if (history.length === 0) return { text: '🌐 ยังไม่มีประวัติ' }
121      const icon = { loading: '⏳', success: '✅', error: '❌' }
122      const rows = [...history].reverse().map(h => `${icon[h.status]} ${h.url} (${timeAgo(now - h.time)})`)
123      return { text: ['🌐 ประวัติการเปิดเว็บ', '----------------------------------------', ...rows].join('\n') }
124    }
125
126    if (sub === 'fav') {
127      if (!rest) {
128        const rows = THAI_FAVORITES.map(f => `⭐ ${f.name.padEnd(17)} ${f.url}`)
129        return { text: ['⭐ เว็บโปรด (/browser fav <ชื่อ> เพื่อเปิด)', ...rows].join('\n') }
130      }
131      const found = THAI_FAVORITES.find(f => f.name.toLowerCase().includes(rest.toLowerCase()))
132      if (!found) return { text: `❌ ไม่เจอเว็บโปรดชื่อ "${rest}"` }
133      askClaudeToOpen($, found.url)
134      return { text: `🌐 ให้ Claude เปิด ${found.name} (${found.url})` }
135    }
136
137    if (sub === 'search') {
138      if (!rest) return { text: 'วิธีใช้: /browser search วิธีใช้ Claude Code' }
139      $.clock.after(50, () => void $.prompt.submit({ text: `ค้นเว็บเรื่อง "${rest}" แล้วสรุปผลเป็นภาษาไทย พร้อมลิงก์แหล่งที่มา` }).catch(() => undefined)) // ส่งหลังคำสั่งตอบกลับแล้ว (ส่งระหว่าง command.run ไม่ได้)
140      return { text: `🔎 ให้ Claude ค้นหา: ${rest}` }
141    }
142
143    if (sub === 'raw') {
144      const url = normalizeUrl(rest)
145      if (!url) return { text: 'วิธีใช้: /browser raw https://example.com' }
146      const item = addToHistory(url, now)
147      try {
148        const res = await $.http.fetch(url, { headers: { 'User-Agent': 'claude-code-terminal-browser-th' } })
149        item.status = res.ok ? 'success' : 'error'
150        const body = htmlToText(res.text)
151        const cut = body.length > 3000 ? body.slice(0, 3000) + `\n... (ตัดเหลือ 3,000 จาก ${body.length.toLocaleString('en-US')} ตัวอักษร)` : body
152        return { text: `🌐 ${url} [HTTP ${res.status}]\n----------------------------------------\n${cut}` }
153      } catch (err) {
154        item.status = 'error'
155        return { text: `❌ เปิด ${url} ไม่ได้: ${String(err)}` }
156      }
157    }
158
159    // ไม่ตรง subcommand ไหน = ถือว่าเป็น url
160    const url = normalizeUrl(args)
161    if (!url) return { text: `❌ "${args}" ไม่ใช่ URL ที่ถูกต้อง` }
162    askClaudeToOpen($, url)
163    return { text: `🌐 ให้ Claude เปิด ${url} และสรุปเป็นภาษาไทย` }
164  })
165}
166
167/**
168 * ========================================================================
169 * การบ้าน / ต่อยอด
170 * ========================================================================
171 * 1. ทำแผง (Pane) แสดงประวัติ มีปุ่มกดเปิดซ้ำ (ดูตัวอย่าง Pane ใน Mod 04)
172 * 2. เก็บเว็บโปรดใน $.store ให้ผู้ใช้เพิ่มเองได้ด้วย /browser fav add <ชื่อ> <url>
173 * 3. ใช้ $.model.complete แปลข้อความดิบจาก raw เป็นภาษาไทยโดยไม่เข้าบทสนทนาหลัก
174 */
175