2025年9月21日 星期日

重度使用 Claude Code 五個月後...


累積到上個月,已經掏錢給 Claude 滿一年了,從一開始 claude.ai 的用法,不停地在瀏覽器上詢問,自己也開發了簡單的小工具,目的是把程式碼打包成單一檔案,方便超額後可以續作 XD 不過,這樣的生態慢慢在 2025.02 之後都改變了。我也從 vim 模式轉成 vscode + github copilot + agent mode ,然而,當我又摸熟 github copilot + agent mode 後,又轉進了 claude code 的世界,並在 2025.07 也提播助理費到百元美金 XD 好用,當然要給他加薪一下

如果要說最大的生活改變?可能就是進入到定義任務的開發模式了,以及享受慣老闆機制,凡事避開自己親自 coding ,自己不 coding 也能欣賞 AI coding,也是一種自我成長的過程,可以接納更多種的解方。

慣老闆方面也要體驗一下的,當然就想要極致壓榨 AI ,倒垃圾前、洗澡前、睡前下個指令是基本的,同時開 2-4 個 Terminal 做不同的事也是合情合理,只是到頭來自己的 context switch 反而吃重,並且隨著 AI 能力越來越強後,漸漸地收斂到同時做 1~2 件事就好,放過自己吧!

放過自己後,就是享受生活的開始,不再計較要怎樣操爆 AI ,反而是在生活時間的碎片化下,如何靠 AI 輔助串起一切,凡事盡可能規劃 Save/Load 架構,做事做到一個地步肯定要叫 AI 更新 CLAUDE.md 檔案,甚至另外規劃個 PLAN.md ,就像小老闆時時刻刻盯著 KPI 。那何謂享受?既然等待 AI 的時間慢慢拉長(AI處理的任務深度加深),就開始看一堆動畫 XD 想想倒看了不少:

  • 膽大黨
  • 阪本的日常
  • 怪獸8號
  • 薰香花朵凜然綻放
  • 治癒魔法的錯誤使用法
  • 我要架招一切
  • 我獨自升級
  • ...

現在是 2025-09-21 ,打算讓 Claude code 卸下重擔。開始試試 OpenAI codex ,從一開始對話窗上不能上傳圖片、不能工作時先打好下一道指令等等, codex 版本迭代後,基本上跟 Claude Code 的差距越來越小了。再加上 OpenAI GPT-5 的成效,其實很接近 Claude Sonnect 4 / Opus 4.1 ,只要任務定義的清楚,基本上都是一樣的。

我想,這大概就是另類的小確幸吧?AI 輔助十分競爭,各家業者的差距也在縮減。

不過,也有些殘忍的事正在發生,美國已經大量關閉新鮮人的職缺,原本認為中高階者會被新人+AI輔助取代,隨著時間已經變成新人的職缺大量縮減,因為有經驗者 + AI 輔助可以做更多的事,企業也不用等新人犯錯練等。就像十年前有錢的新創,起手式先掏錢挖走有經驗的人。

這件事在工作上也滿有感受到的,觀察到同事正進行一個尚未有經驗的任務時,原本期待 AI輔助的過程,可以讓同事朝向有效的路線發展,事實上還是很吃“提問者的提問方式“,因為 AI 也會順著提問者的思維去規劃,回歸到下 prompt 的品質依舊重要。觀察到一些計劃方向也出現繞路的情況,所幸,隨著實務經驗增加後,還是回歸了正途。這件事代表:雇用有經驗者 + AI 輔助,還是跟以往的生態一樣的,只是一個人的經驗價值,仍有可能因為 AI 的出現被貶低。

目前 開發者 與 AI 輔助仍努力的找個平衡點,應該不是 AI 的泡沫化?略知很多 AI 服務(如 Perplexity)仍是努力燒錢中,尚未損益平衡,這也是恰巧聽到矽谷 podcast ,行內在搞新創的都有時時追蹤業內的頭部,據說 Anthropic 話語權仍很好的,而整合類(沒有研發自己的 LLM且本身也沒錢研發)都非常辛苦,也怕搞 LLM 大廠推出新服務就打亂了自己的步調。

而最底層食物鏈的開發者呢?我想,真的只剩投資了 :P 只能配置好資產(大盤指數也行也夠),避免產業鏈一個瞬間轉身就...受到重傷 Orz



2025年6月26日 星期四

兒童相機記憶卡損毀救援筆記 - 在 macOS 修復摔壞的 microSD 卡資料



兩三年前買了兒童相機給小孩亂拍,坊間大部分的兒童相機都大同小異,有的著重相機畫素,有的著重拍攝的方便性,像是可自拍、觸控螢幕等。後來,我選了觸控螢幕款,畢竟方便第一,至於畫素、閃光燈等等的就放掉,並買了一張 32GB microSD ,讓小孩大小可隨便錄影。

最近出遊時,兒童相機在開機的狀態下,不小心摔到地上,接著無法拍照(儲存),也看不到所有已拍攝的資料。

當下在想,這種 microSD 跟 SSD 的,不像以前的 HDD 有轉盤在那邊轉,應該不至於壞軌,再細想一下,有可能像 OS 優化連續錄影/開多檔快速寫檔案,但這時撞到地上導致 OS 狀態異常,沒有把資料收尾而損毀,可能導致 micorSD 檔案系統 Header 就被破壞掉,而不是 microSD 受到什麼物理性的破壞。

接著,就來煩 AI 了 XD 我自己是先把損毀的 microSD 透過 macOS 內建的 Disk Utility 先備份一份 Image 檔案(備份過程也不是很正常的結束,但至少產生了一個 img 檔案),接著拿另一張同樣容量的 32GB microSD 來處理,避免一開始就對原始 microSD 卡操作。

最後,透過跟 AI 聊天的過程,他認為我的想法/直覺有機會解掉,於是乎就操作看看,出發點是想重建 microSD header,流程: 
  • 先把損毀的 microSD卡(A),透過 macOS 內建 Disk Utility 先備份一份 Image 檔案,產出 a_card.img
  • 製作乾淨的 microSD卡(B),把額外一張 microSD 卡放入兒童相機格式化,確保資料檔案系統跟損毀的 microSD 卡一樣,在產出 clean_b_card.img
  • 後續則是在 microSD卡(B) 操作,用 a_card.img 資料覆蓋 microSD卡(B),這時 microSD卡(B) 插入兒童相機一樣會顯示檔案損毀的狀態
  • 最後則是拿 clean_b_card.img 部分資料去覆蓋 microSD卡(B) ,直到可以 OS 正常讀出資料
執行過程中,AI 其實會去猜檔案系統為何,然後摸索 Header 大小,像是建議一開始用 8MB 來猜,然後測試完看不到資料,又說可以試試 4MB (一樣看不到資料),又說要不要試試 2MB,這時我反而去問他應該要從 header size 小到大來測試,也讓 AI 想起可以用 macos 內建的 diskutil 指令查詢資料,此例是 microSD 插入後被系統安置在 /dev/disk10 ,相關指令如:

%  diskutil info /dev/disk10 | grep -E "(Block|Cluster|Sector)" 
   Device Block Size:         512 Bytes
   Allocation Block Size:     512 Bytes

不然根據 AI 的建議,他的操作每一次都要重新製作一次 microSD卡(B) ,滿費時的,最終我就只測試三次,分別是 8MB, 4MB ,接著到 512KB 就搞定了(過程重建 3次資料),而我請 AI 由小到大的 header size 測試,他給予的指令流程:

# 1. 先一次性寫入 A 卡資料(只做這一次)
diskutil unmountDisk /dev/disk10
sudo dd if=a_card.img of=/dev/rdisk10 bs=1m

# 2. 從最小範圍開始測試 - 只覆蓋 Boot Sector (512 bytes)
sudo dd if=clean_b_card.img of=/dev/rdisk10 bs=512 count=1 conv=notrunc
diskutil mountDisk /dev/disk10
# 檢查是否有資料,如果沒有繼續下一步

# 3. 如果 512 bytes 不夠,擴大到 40KB(到 FAT 表開始位置)
diskutil unmountDisk /dev/disk10
sudo dd if=clean_b_card.img of=/dev/rdisk10 bs=1k count=40 conv=notrunc
diskutil mountDisk /dev/disk10
# 檢查結果

# 4. 如果還不夠,擴大到 100KB
diskutil unmountDisk /dev/disk10
sudo dd if=clean_b_card.img of=/dev/rdisk10 bs=1k count=100 conv=notrunc
diskutil mountDisk /dev/disk10
# 檢查結果

# 5. 繼續擴大到 500KB
diskutil unmountDisk /dev/disk10
sudo dd if=clean_b_card.img of=/dev/rdisk10 bs=1k count=500 conv=notrunc
diskutil mountDisk /dev/disk10
# 檢查結果

# 6. 最後到 1MB
diskutil unmountDisk /dev/disk10
sudo dd if=clean_b_card.img of=/dev/rdisk10 bs=1k count=1024 conv=notrunc
diskutil mountDisk /dev/disk10

我後來只做這些,就看得到資料:

% diskutil unmountDisk /dev/disk10
% sudo dd if=a_card.img of=/dev/rdisk10 bs=1m
9297+1 records in
9297+1 records out
9748611584 bytes transferred in 642.300766 secs (15177643 bytes/sec)

% sudo dd if=clean_b_card.img of=/dev/rdisk10 bs=512 count=1 conv=notrunc
Password:
1+0 records in
1+0 records out
512 bytes transferred in 0.009456 secs (54146 bytes/sec)

% diskutil mountDisk /dev/disk10
Volume(s) mounted successfully

如此能看到資料後,也確定有些檔案相機的確沒正確寫入,顯示損毀,猜測應該是晚一點寫檔的機制(cache),導致有些檔案並未正確收尾。

最後在靠這些:

% diskutil unmountDisk /dev/disk10
Unmount of all volumes on disk10 was successful

% sudo fsck_msdos -f -y /dev/disk10
Password:
** /dev/rdisk10
** Phase 1 - Preparing FAT
** Phase 2 - Checking Directories
** Phase 3 - Checking for Orphan Clusters
Warning: 175 files, 31120736 KiB free (972523 clusters)

% brew install testdisk

% sudo photorec /dev/disk10

過程:

PhotoRec 7.2, Data Recovery Utility, February 2024
Christophe GRENIER <grenier@cgsecurity.org>
https://www.cgsecurity.org

Disk /dev/disk10 - 31 GB / 29 GiB (RO)
     Partition                  Start        End    Size in sectors
   P FAT32                          0   62333951   62333952 [NO NAME]

Destination /path/recup_dir

Pass 1 - Reading sector    6458368/62333952, 217 files found
Elapsed time 0h02m02s - Estimated time to completion 0h17m35
jpg: 101 recovered
txt: 94 recovered
mov: 17 recovered
gz: 5 recovered

希望可以救多一點 

2025年4月22日 星期二

Python 開發筆記 - 使用 http.server 建置嵌入式產品網頁開發環境與 Proxy 機制

這個需求快十年前弄過,那時是用 webpack proxy server,可以在習慣的桌機開發網頁,接著對於 CGI 查詢就透過 proxy 導向到 device CGI 

然後,現在弄一個 Python ,方便後續在老 server 上運行,畢竟肥肥的程式碼搭配檔案系統大小寫問題,還是交給 server 檔案系統處理吧,如此,未來有部分 html/js/css code 要快速測試,就只需要到指定機器上,運行

$ python3 proxy-server.py --port 12345 --proxy-target 'http://192.168.123.234:80' --proxy-paths '/cgi-bin/' -d project/path/html/code/
Serving files from directory: project/path/html/code/
Serving HTTP on 0.0.0.0 port 12345 (multi-threaded) ...
Proxying paths ['/cgi-bin/'] to http://192.168.123.234:80
^C
Server stopped.

如此對於 CGI request 就會自動導向到 'http://192.168.123.234:80' 

程式碼純筆記,這都是 AI 產的:

#!/usr/bin/env python3

import http.server
import socketserver
import http.client
import argparse
import os
import sys
import socket
from urllib.parse import urlparse

# 使用 ThreadingMixIn 來處理並發請求
class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    daemon_threads = True
    allow_reuse_address = True


class ProxyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
    # 代理目標地址
    proxy_target = None
    # 需要代理的路徑前綴
    proxy_paths = []
    # 連接池(簡單實現)
    conn_pool = {}
    
    def __init__(self, *args, **kwargs):
        # 確保可以正確處理目錄參數
        super().__init__(*args, **kwargs)
    
    def do_GET(self):
        # 檢查是否需要代理這個請求
        for path_prefix in self.proxy_paths:
            if self.path.startswith(path_prefix):
                self.proxy_request()
                return
        
        # 如果不需要代理,則使用默認處理方式
        super().do_GET()
    
    def do_POST(self):
        # 為 POST 請求也提供代理功能
        for path_prefix in self.proxy_paths:
            if self.path.startswith(path_prefix):
                self.proxy_request()
                return
        
        super().do_POST()
    
    def get_connection(self, host, is_ssl=False):
        """從連接池獲取連接,或建立新連接"""
        key = (host, is_ssl)
        
        if key not in self.conn_pool:
            if is_ssl:
                self.conn_pool[key] = http.client.HTTPSConnection(host)
            else:
                self.conn_pool[key] = http.client.HTTPConnection(host)
        
        # 檢查連接是否仍然有效
        conn = self.conn_pool[key]
        try:
            # 嘗試使用一個非阻塞的方式檢查連接狀態
            old_timeout = conn.sock.gettimeout()
            conn.sock.settimeout(0.01)
            conn.sock.recv(1, socket.MSG_PEEK)
            conn.sock.settimeout(old_timeout)
        except (socket.error, AttributeError):
            # 連接已關閉或存在問題,創建新連接
            if is_ssl:
                self.conn_pool[key] = http.client.HTTPSConnection(host)
            else:
                self.conn_pool[key] = http.client.HTTPConnection(host)
        except Exception:
            # 其他類型的錯誤,可能連接仍然有效
            pass
            
        return self.conn_pool[key]
    
    def proxy_request(self):
        """使用 http.client 處理代理請求,這在 Python 3.2 中更高效"""
        target_url = urlparse(self.proxy_target + self.path)
        
        # 獲取請求頭部
        headers = {}
        for key, value in self.headers.items():
            # 排除一些特定的頭部
            if key.lower() not in ('host', 'content-length'):
                headers[key] = value
        
        # 設置正確的 Host 頭部
        headers['Host'] = target_url.netloc
        
        # 讀取請求體(如果有)
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length) if content_length > 0 else None
        
        try:
            # 確定是否使用 HTTPS
            is_ssl = target_url.scheme == 'https'
            
            # 獲取或創建連接
            conn = self.get_connection(target_url.netloc, is_ssl)
            
            # 構建請求路徑
            request_path = target_url.path
            if target_url.query:
                request_path += '?' + target_url.query
            
            # 發送請求
            conn.request(
                method=self.command,
                url=request_path,
                body=body,
                headers=headers
            )
            
            # 獲取響應
            response = conn.getresponse()
            
            # 設置響應狀態碼
            self.send_response(response.status)
            
            # 設置響應頭部
            for header in response.getheaders():
                key, value = header
                if key.lower() != 'transfer-encoding':  # 排除特定頭部
                    self.send_header(key, value)
            self.end_headers()
            
            # 發送響應體
            self.wfile.write(response.read())
            
            # 不關閉連接,將其保留在連接池中
            
        except http.client.HTTPException as e:
            # 處理 HTTP 錯誤
            self.send_response(500)
            self.send_header('Content-Type', 'text/plain; charset=utf-8')
            self.end_headers()
            self.wfile.write("HTTP Error: {}".format(str(e)).encode('utf-8'))
            
        except Exception as e:
            # 處理其他錯誤
            self.send_response(500)
            self.send_header('Content-Type', 'text/plain; charset=utf-8')
            self.end_headers()
            self.wfile.write("Proxy error: {}".format(str(e)).encode('utf-8'))


def run_server(port=8000, proxy_target="http://localhost:8080", proxy_paths=["/cgi-bin/"], directory=None):
    # 設置代理目標和路徑
    ProxyHTTPRequestHandler.proxy_target = proxy_target
    ProxyHTTPRequestHandler.proxy_paths = proxy_paths
    
    # 確保代理目標是有效的 URL
    if not ProxyHTTPRequestHandler.proxy_target.startswith(('http://', 'https://')):
        ProxyHTTPRequestHandler.proxy_target = "http://{}".format(ProxyHTTPRequestHandler.proxy_target)
    
    # 移除代理目標末尾的斜線(如果有)
    if ProxyHTTPRequestHandler.proxy_target.endswith('/'):
        ProxyHTTPRequestHandler.proxy_target = ProxyHTTPRequestHandler.proxy_target[:-1]
    
    # 設置工作目錄
    if directory:
        os.chdir(directory)
        print("Serving files from directory: {}".format(directory))
    else:
        print("Serving files from current directory: {}".format(os.getcwd()))
    
    # 提高 socket 超時時間以處理慢速連接
    socket.setdefaulttimeout(60)
    
    # 建立服務器(使用線程化服務器)
    server_address = ("", port)
    httpd = ThreadedHTTPServer(server_address, ProxyHTTPRequestHandler)
    
    print("Serving HTTP on 0.0.0.0 port {} (multi-threaded) ...".format(port))
    print("Proxying paths {} to {}".format(proxy_paths, proxy_target))
    
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\nServer stopped.")
        httpd.server_close()


if __name__ == "__main__":
    # 解析命令行參數
    parser = argparse.ArgumentParser(description='HTTP Server with proxy capabilities (optimized for Python 3.2)')
    parser.add_argument('--port', type=int, default=8000, help='Port to listen on (default: 8000)')
    parser.add_argument('--proxy-target', type=str, default="http://localhost:8080", 
                        help='Target server to proxy to (default: http://localhost:8080)')
    parser.add_argument('--proxy-paths', type=str, default="/cgi-bin/", 
                        help='Comma-separated list of path prefixes to proxy (default: /cgi-bin/)')
    parser.add_argument('--directory', '-d', type=str, default=None,
                        help='Specify directory to serve files from (default: current directory)')
    
    args = parser.parse_args()
    proxy_paths = [path.strip() for path in args.proxy_paths.split(',')]
    
    run_server(args.port, args.proxy_target, proxy_paths, args.directory)

2025年4月17日 星期四

[macOS] 關閉 Android File Transfer Agent 自動啟動 @ MacBook Air M2 / macOS 15.4.1

雖然 Android File Transfer 差不多被官方捨棄掉了,但我有舊版 Android 開發機還是很依賴它。目前官方不能下載,想下載可參考這篇:macOS - android fileTransfer 下載位置 AndroidFileTransfer.dmg

這篇主要是要記錄關閉 Android File Transfer Agent 的方式,以往都是自己跑去系統位置去稍微破壞一下檔案名稱路徑等等的架構,這次就多了解一下系統架構:
  1. 系統 -> 一般 -> 登入項目與延伸功能 -> 在登入時打開,在此處進行關閉


  2. 把 Android File Transfer Agent 所在處移除
    • ~/Library/Application Support/Google/Android File Transfer/Android File Transfer Agent.app
    • /Applications/Android File Transfer.app/Contents/Helpers/Android File Transfer Agent.app


  3. 如此在應用程式開啟時,就不會重新再次註冊到 "在登入時打開"


這樣可以避免 android phone 插上連接線時,不斷觸發煩人的彈跳視窗(手機或筆電都有),直到需要時,自己再去應用程式開啟 Android File Transfer 就夠了

2025年4月11日 星期五

[macOS] 調整 MacBook screenshot 的畫質,從 png 調整成 jpg 縮小檔案大小 @ MacBook Air 15吋 / Apple M2

關於 MacBook Print Screen 的畫質太高這件事,其實困擾我很久了,以往都是 screenshot 後,自己縮一下圖再傳出去,一週做個不到五次。但如果和同事工作交流更加頻繁時,會懶得縮圖,使得 Mail/Line/Stack 傳的截圖傳遞檔案很大,觀看者也要花更多的時間處理

隨口問問 AI ,可以靠指令調整,像是把預設儲存方式改成 JPG 並調整品質,這樣就搞定了!以 MacBook Air M2 15吋為例,預設整個畫面 screenshot 是落在快 5MB 大小(跟畫面內容複雜度有關),調整後約 1.5MB

我自己就弄成兩個 script 方便開關調整:

% cat reduce-macos-screenshot-quality.sh
#!/bin/bash

defaults read com.apple.screencapture

defaults write com.apple.screencapture type jpg
killall SystemUIServer
defaults write com.apple.screencapture jpg-quality 60

defaults read com.apple.screencapture

% cat reset-macos-screenshot-quality.sh 
#!/bin/bash

defaults read com.apple.screencapture
#defaults write com.apple.screencapture type png

defaults delete com.apple.screencapture
killall SystemUIServer

defaults read com.apple.screencapture

預設的畫質:

% file raw.png 
~/User/Desktop/raw.png: PNG image data, 3420 x 2214, 8-bit/color RGBA, non-interlaced

調整後: 

% file reduce.jpg 
~/User/Desktop/reduce.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 144x144, segment length 16, Exif Standard: [TIFF image data, big-endian, direntries=4, xresolution=62, yresolution=70, resolutionunit=2], baseline, precision 8, 3420x2214, components 3

% defaults read com.apple.screencapture
{
    "jpg-quality" = 60;
    type = jpg;
}

收工

其他資訊: