ตั้งเวลาเปิดเว็บ แอป หรือไฟล์อัตโนมัติ รายวัน วันทำงาน หรือครั้งเดียว ขณะเปิด Claude Code อยู่

ตั้งเวลาเปิดเว็บ แอป หรือไฟล์อัตโนมัติเหมือนนาฬิกาปลุก: ทุกวัน, เฉพาะวันทำงาน, วันที่เลือก หรือครั้งเดียว มีเว็บยอดนิยมให้เลือก (gmail, youtube, line, tradingview ฯลฯ) ถึงเวลาแล้วจะเปิดให้และเด้งบอก
| คำสั่ง | ทำอะไร |
|---|---|
/autoopen add gmail weekdays 09:00 | เปิด Gmail วันทำงาน 9 โมง (ใช้ preset) |
/autoopen add งาน https://notion.so daily 08:30 | เปิดเว็บทุกวัน |
/autoopen add รายงาน C:\work\report.xlsx mon,fri 09:00 | เปิดไฟล์วันจันทร์กับศุกร์ |
/autoopen add ประชุม https://meet.google.com once 14:30 | ครั้งเดียว (วันนี้ หรือพรุ่งนี้ถ้าเลยเวลาแล้ว) |
/autoopen list / next | ดูรายการ / รายการถัดไป |
/autoopen run <id> | เปิดเดี๋ยวนี้ |
/autoopen on <id> / off <id> / delete <id> | เปิด/ปิด/ลบรายการ |
/autoopen presets | รายชื่อ preset |
/plugin install app-scheduler-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
ทำงานเฉพาะตอนเปิด Claude Code อยู่ ถ้าต้องการให้เปิดแม้ไม่ได้เปิด Claude Code ให้ใช้ Task Scheduler ของ Windows หรือ cron ใช้ชื่อ /autoopen เพราะ /schedule เป็นคำสั่งที่มีอยู่แล้ว
hooks/register.tsx 261 lines1/**
2 * ============================================================================
3 * MOD 24: App Scheduler TH - ตั้งเวลาเปิดเว็บ แอป หรือไฟล์อัตโนมัติ (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: นาฬิกาปลุก และ Task Scheduler ของ Windows
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. ตั้งเวลาเปิดเว็บ (URL), แอป หรือไฟล์: ทุกวัน, วันทำงาน, วันที่เลือก หรือครั้งเดียว
9 * 2. มีเว็บยอดนิยมให้เลือก (preset) เช่น gmail, youtube, line, tradingview
10 * 3. เช็คทุก 30 วินาที ถึงเวลาก็เปิดให้และเด้งบอก
11 * 4. สั่งเปิดทันทีได้ ดูรอบถัดไป เปิด/ปิด/ลบรายการได้
12 *
13 * ข้อจำกัด: ทำงานเฉพาะตอนเปิด Claude Code อยู่ (Mod ไม่มีโปรแกรมเบื้องหลังของตัวเอง)
14 * ถ้าต้องการให้เปิดแม้ไม่ได้เปิด Claude Code ใช้ Task Scheduler ของ Windows หรือ cron แทน
15 *
16 * หมายเหตุ: ใช้ชื่อ /autoopen เพราะ /schedule เป็นคำสั่งที่มีอยู่แล้ว
17 */
18
19import type { EngineInterface, Register, Timer } from 'claude-code'
20
21type Repeat = { kind: 'daily' } | { kind: 'weekdays' } | { kind: 'days'; days: number[] } | { kind: 'once'; date: string }
22interface Job {
23 id: number
24 name: string
25 target: string
26 time: string // "09:00"
27 repeat: Repeat
28 enabled: boolean
29 nextRun: number // ms
30 runs: number
31}
32
33const PRESETS: Record<string, string> = {
34 gmail: 'https://mail.google.com',
35 youtube: 'https://www.youtube.com',
36 line: 'https://chat.line.biz',
37 facebook: 'https://www.facebook.com',
38 shopee: 'https://shopee.co.th',
39 tradingview: 'https://www.tradingview.com',
40 notion: 'https://www.notion.so',
41 calendar: 'https://calendar.google.com',
42 github: 'https://github.com',
43}
44const DAY_NAMES: Record<string, number> = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6, อา: 0, จ: 1, อ: 2, พ: 3, พฤ: 4, ศ: 5, ส: 6 }
45const DAY_TH = ['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส']
46
47let jobs: Job[] = []
48let nextId = 0
49let loop: Timer | undefined
50let isWindows = false
51let isMac = false
52
53// ==========================================================================
54// บทที่ 1: คำนวณเวลารอบถัดไป (เวลาท้องถิ่นของเครื่อง)
55// ==========================================================================
56function nextRunOf(job: Pick<Job, 'time' | 'repeat'>, after: number): number {
57 const [h = 0, m = 0] = job.time.split(':').map(Number)
58 if (job.repeat.kind === 'once') {
59 const [y = 1970, mo = 1, d = 1] = job.repeat.date.split('-').map(Number)
60 const at = new Date(y, mo - 1, d, h, m).getTime()
61 return at > after ? at : 0 // 0 = ผ่านไปแล้ว ไม่มีรอบต่อไป
62 }
63 const start = new Date(after)
64 for (let add = 0; add <= 7; add++) {
65 const day = new Date(start.getFullYear(), start.getMonth(), start.getDate() + add, h, m)
66 if (day.getTime() <= after) continue
67 const dow = day.getDay()
68 const ok = job.repeat.kind === 'daily' || (job.repeat.kind === 'weekdays' && dow >= 1 && dow <= 5) || (job.repeat.kind === 'days' && job.repeat.days.includes(dow))
69 if (ok) return day.getTime()
70 }
71 return 0
72}
73
74function repeatText(r: Repeat): string {
75 if (r.kind === 'daily') return 'ทุกวัน'
76 if (r.kind === 'weekdays') return 'จ-ศ'
77 if (r.kind === 'days') return r.days.map(d => DAY_TH[d]).join(',')
78 return `ครั้งเดียว ${r.date}`
79}
80
81function when(ms: number): string {
82 if (!ms) return '-'
83 const d = new Date(ms)
84 return `${DAY_TH[d.getDay()]} ${d.getDate()}/${d.getMonth() + 1} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
85}
86
87// "weekdays 09:00", "daily 18:30", "mon,wed 07:00", "once 2026-12-31 23:59", "once 14:30"
88function parseWhen(words: string[], now: number): { time: string; repeat: Repeat } | string {
89 const [kind = '', a, b] = words
90 const isTime = (t?: string) => /^([01]?\d|2[0-3]):[0-5]\d$/.test(t ?? '')
91 if ((kind === 'daily' || kind === 'weekdays') && isTime(a)) return { time: a ?? '', repeat: { kind } }
92 if (kind === 'once') {
93 if (isTime(a)) {
94 // ไม่ใส่วันที่ = วันนี้ (ถ้าเลยเวลาแล้ว = พรุ่งนี้)
95 const [h = 0, m = 0] = (a ?? '').split(':').map(Number)
96 const today = new Date(now)
97 const at = new Date(today.getFullYear(), today.getMonth(), today.getDate(), h, m)
98 if (at.getTime() <= now) at.setDate(at.getDate() + 1)
99 return { time: a ?? '', repeat: { kind: 'once', date: `${at.getFullYear()}-${at.getMonth() + 1}-${at.getDate()}` } }
100 }
101 if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(a ?? '') && isTime(b)) return { time: b ?? '', repeat: { kind: 'once', date: a ?? '' } }
102 }
103 const days = kind.split(',').map(d => DAY_NAMES[d.toLowerCase()])
104 if (days.length && days.every(d => d !== undefined) && isTime(a)) return { time: a ?? '', repeat: { kind: 'days', days: days as number[] } }
105 return 'รูปแบบเวลาไม่ถูก ใช้: daily 09:00 | weekdays 09:00 | mon,wed 18:00 | once 14:30 | once 2026-12-31 23:59'
106}
107
108// ==========================================================================
109// บทที่ 2: เปิดเว็บ/แอป/ไฟล์ด้วยโปรแกรมของระบบ (ส่ง target ผ่าน env กันคำสั่งแทรก)
110// ==========================================================================
111async function openTarget($: EngineInterface, target: string): Promise<void> {
112 const argv = isWindows
113 ? ['powershell', '-NoProfile', '-NonInteractive', '-Command', "$ErrorActionPreference='Stop'; Start-Process -FilePath $env:AS_TARGET"]
114 : isMac
115 ? ['open', target]
116 : ['xdg-open', target]
117 const run = await $.process.run(argv, { env: { AS_TARGET: target }, timeoutMs: 20_000 })
118 if (run.exitCode !== 0) throw new Error(run.stderr.trim() || `exit ${run.exitCode}`)
119}
120
121async function save($: EngineInterface): Promise<void> {
122 await $.store.set('jobs', { jobs, nextId })
123}
124
125// ==========================================================================
126// บทที่ 3: วนเช็คทุก 30 วินาที - ถึงเวลาแล้วเปิด แล้วคำนวณรอบถัดไป
127// ==========================================================================
128async function tick($: EngineInterface): Promise<void> {
129 const now = await $.clock.now()
130 let changed = false
131 for (const job of jobs) {
132 if (!job.enabled || !job.nextRun || job.nextRun > now) continue
133 // พลาดไปนานเกิน 10 นาที (เช่น ปิดเครื่องไว้) ไม่เปิดย้อนหลัง แค่เลื่อนไปรอบถัดไป
134 if (now - job.nextRun < 10 * 60_000) {
135 try {
136 await openTarget($, job.target)
137 job.runs++
138 $.ui.toast(`⏰ เปิด ${job.name} ตามเวลา ${job.time}`)
139 } catch (err) {
140 $.ui.toast(`❌ เปิด ${job.name} ไม่ได้: ${err instanceof Error ? err.message : String(err)}`)
141 }
142 }
143 job.nextRun = nextRunOf(job, now)
144 if (!job.nextRun) job.enabled = false // ครั้งเดียวเสร็จแล้ว
145 changed = true
146 }
147 if (changed) await save($)
148}
149
150function listText(): string {
151 if (jobs.length === 0) return '⏰ ยังไม่มีรายการ ลอง /autoopen add gmail weekdays 09:00'
152 return ['⏰ ตารางเปิดอัตโนมัติ', '----------------------------------------', ...jobs.map(j => `${j.enabled ? '🟢' : '⚪'} #${j.id} ${j.name.padEnd(12)} ${j.time} ${repeatText(j.repeat).padEnd(10)} ถัดไป ${when(j.nextRun)} (เปิดแล้ว ${j.runs} ครั้ง)\n ${j.target}`)].join('\n')
153}
154
155const HELP = `⏰ App Scheduler TH (Mod 24)
156========================================
157/autoopen add <ชื่อ|preset> [เว็บ/ไฟล์/แอป] <เวลา>
158 เช่น /autoopen add gmail weekdays 09:00
159 /autoopen add งาน https://notion.so daily 08:30
160 /autoopen add รายงาน C:\\work\\report.xlsx mon,fri 09:00
161 /autoopen add ประชุม https://meet.google.com once 14:30
162 เวลา: daily HH:MM | weekdays HH:MM | mon,wed HH:MM | once HH:MM | once YYYY-MM-DD HH:MM
163/autoopen list | next
164/autoopen run <id> เปิดเดี๋ยวนี้
165/autoopen on <id> | off <id> | delete <id>
166/autoopen presets
167⚠️ ทำงานเฉพาะตอนเปิด Claude Code อยู่`
168
169export const register: Register = on => {
170 on('session.start', async ($, e, next) => {
171 isWindows = /^[A-Za-z]:/.test(e.cwd)
172 isMac = !isWindows && e.cwd.startsWith('/Users/')
173 const saved = (await $.store.get('jobs')) as { jobs?: Job[]; nextId?: number } | undefined
174 jobs = saved?.jobs ?? []
175 nextId = saved?.nextId ?? 0
176 // รอบที่ผ่านไปตอนปิด Claude Code -> คำนวณรอบใหม่
177 const now = await $.clock.now()
178 for (const j of jobs) if (j.enabled && j.nextRun && now - j.nextRun > 10 * 60_000) j.nextRun = nextRunOf(j, now)
179 loop?.cancel()
180 loop = $.clock.every(30_000, () => void tick($))
181 await $.command.register({ name: 'autoopen', description: 'ตั้งเวลาเปิดเว็บ แอป หรือไฟล์อัตโนมัติ (/autoopen help)', argumentHint: 'add|list|run|on|off|delete' })
182 return next(e)
183 })
184
185 // ==========================================================================
186 // บทที่ 4: คำสั่ง /autoopen
187 // ==========================================================================
188 on('command.run', { command: 'autoopen' }, async ($, e) => {
189 const [sub = 'list', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
190 const now = await $.clock.now()
191 const find = (arg?: string) => jobs.find(j => j.id === Number(arg))
192
193 switch (sub) {
194 case 'add': {
195 const [name = '', ...more] = rest
196 // คำถัดจากชื่อเป็นคำบอกเวลา (daily, weekdays, once, mon,wed) = ไม่ได้ใส่ target มา -> ใช้ preset
197 const startsWithWhen = /^(daily|weekdays|once)$/i.test(more[0] ?? '') || (more[0] ?? '').split(',').every(d => DAY_NAMES[d.toLowerCase()] !== undefined)
198 const preset = PRESETS[name.toLowerCase()]
199 const target = preset && startsWithWhen ? preset : more.shift()
200 if (!name || !target) return { text: HELP }
201 const parsed = parseWhen(more, now)
202 if (typeof parsed === 'string') return { text: `❌ ${parsed}` }
203 const job: Job = { id: nextId++, name, target, ...parsed, enabled: true, nextRun: nextRunOf(parsed, now), runs: 0 }
204 if (!job.nextRun) return { text: '❌ เวลานั้นผ่านไปแล้ว' }
205 jobs.push(job)
206 await save($)
207 return { text: `✅ #${job.id} เปิด ${name} ${repeatText(job.repeat)} ${job.time}\n ครั้งถัดไป ${when(job.nextRun)}\n ${target}` }
208 }
209 case 'list':
210 return { text: listText() }
211 case 'next': {
212 const upcoming = jobs.filter(j => j.enabled && j.nextRun).sort((a, b) => a.nextRun - b.nextRun)[0]
213 if (!upcoming) return { text: 'ไม่มีรายการที่รออยู่' }
214 const mins = Math.round((upcoming.nextRun - now) / 60_000)
215 return { text: `⏭️ ถัดไป: ${upcoming.name} ${when(upcoming.nextRun)} (อีก ${mins < 60 ? `${mins} นาที` : `${Math.floor(mins / 60)} ชม. ${mins % 60} นาที`})` }
216 }
217 case 'run': {
218 const job = find(rest[0])
219 if (!job) return { text: `❌ ไม่พบ #${rest[0] ?? ''}` }
220 try {
221 await openTarget($, job.target)
222 job.runs++
223 await save($)
224 return { text: `🚀 เปิด ${job.name} แล้ว` }
225 } catch (err) {
226 return { text: `❌ เปิดไม่ได้: ${err instanceof Error ? err.message : String(err)}` }
227 }
228 }
229 case 'on':
230 case 'off': {
231 const job = find(rest[0])
232 if (!job) return { text: `❌ ไม่พบ #${rest[0] ?? ''}` }
233 job.enabled = sub === 'on'
234 if (job.enabled) job.nextRun = nextRunOf(job, now)
235 await save($)
236 return { text: `${job.enabled ? '🟢 เปิด' : '⚪ ปิด'} #${job.id} ${job.name}${job.enabled ? ` ถัดไป ${when(job.nextRun)}` : ''}` }
237 }
238 case 'delete': {
239 const job = find(rest[0])
240 if (!job) return { text: `❌ ไม่พบ #${rest[0] ?? ''}` }
241 jobs = jobs.filter(j => j !== job)
242 await save($)
243 return { text: `🗑️ ลบ #${job.id} ${job.name} แล้ว` }
244 }
245 case 'presets':
246 return { text: ['📌 Preset (ใช้ชื่อแทน URL ได้เลย)', ...Object.entries(PRESETS).map(([k, v]) => ` ${k.padEnd(12)} ${v}`)].join('\n') }
247 default:
248 return { text: HELP }
249 }
250 })
251}
252
253/**
254 * ========================================================================
255 * การบ้าน / ต่อยอด
256 * ========================================================================
257 * 1. ให้ทำงานแม้ไม่ได้เปิด Claude Code: สร้าง Task ใน Windows ด้วย $.process.run(['schtasks', '/Create', ...])
258 * 2. เปิดหลายอย่างพร้อมกันเป็นชุด เช่น "เริ่มงาน" = Gmail + Notion + Calendar
259 * 3. ส่งแจ้งเตือนไปมือถือก่อนถึงเวลา 5 นาที (ต่อกับ Mod 22)
260 */
261