2026年3月11日 星期三

[macos] 快速設定 Mac Mini M4 32GB/1TB 並安裝設定 OpenClaw + Telegram + FileVault 排錯 @ Macbook M1


昨天弄出一個簡單的工具 macstrap ,方便未來快速批次處理 mac mini M4。然而,mac mini M4 一開始還是有幾個步驟需要人工處理的。

初次把 Mac mini 開機,過程會設定網路連線,以及設定一組帳密登入。進入系統後:
  • Sound: 聲音調成靜音
  • Energy: 開啟避免自動睡眠、電源失敗後可自動開機、網路喚醒
  • General: Sharing -> Remote Login: On
  • General: Sharing -> Remote Management: On -> 選項全開, 密碼設置, 指定使用者使用且選項全開
  • General: About -> Name -> 可以改成簡短的名稱,如 mac-mini ,好處就是可以用 ssh mac-mini.local 連線
  • General: Users & Group -> Automatically log in as -> user
  • 開啟 termnial -> 輸入 python3 -> 會觸動透過 Xcode 安裝 python3 環境 



如此就完成 mac mini 可被遠端操作的基本設定了,可以測試:
  • ssh user@mac-mini.local 
  • 開啟 螢幕共享 -> 輸入 mac-mini.local -> 選擇帳號登入
以上兩種就是可遠端的配置了,而多了 python3 的安裝,是為了用 macstrap 快速安置開發環境

```
% mkdir openclaw-on-mac-mini && cd openclaw-on-mac-mini
% python3 -m venv venv && source venv/bin/activate && pip install macstrap
% macstrap init --examples
% macstrap ssh-auth mac-mini.local --user user
...
✓  Passwordless SSH to mac-mini.local working.
...
✓  Sudo password stored in macOS Keychain
% macstrap run --config examples/ai-cli --config examples/openclaw  mac-mini.local 
...
```

如此系統就會安裝一些常見的 AI cli (OpenAI - Codex CLI, Google - Gemini CLI, Microsoft - Copilot CLI), Openclaw 跟 Docker Desktop 也安裝好了,而 Docker Desktop 需要登入 macOS 上先來個手動啟動,啟動後可以在設定區開啟 Start Docker Desktop when you sign into your computer,而這台 mac mini M4 是 32GB RAM 跟 1TB SSD ,因此可以多善用 Docker 來封裝一些任務做風險管控。

接著就是啟動 openclaw 的部分了,可以在 macos Desktop 啟動,也可以用 ssh 登入啟動,在此用後者:

```
% ssh user@mac-mini.local
user@mac-mini ~ % openclaw onboard

🦞 OpenClaw 2026.3.8 (3caab92)
   I'm not AI-powered, I'm AI-possessed. Big difference.

▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██░▄▄▄░██░▄▄░██░▄▄▄██░▀██░██░▄▄▀██░████░▄▄▀██░███░██
██░███░██░▀▀░██░▄▄▄██░█░█░██░█████░████░▀▀░██░█░█░██
██░▀▀▀░██░█████░▀▀▀██░██▄░██░▀▀▄██░▀▀░█░██░██▄▀▄▀▄██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                  🦞 OPENCLAW 🦞                    
 
┌  OpenClaw onboarding
│...

```

我們採用 QuickStart ,以及 Copilot (Github + local proxy) ,而 Copilot CLI 認證方式採用 `GitHub Copilot (GitHub device login) (Uses GitHub device flow)`,且模型就挑 `GPT-5 mini` 好了

主因是 Github Copilot 在 2026-03-11 的模型費用:

```
 Search models...

  Claude Sonnet 4.5 (default) ✓                                      1x
  Claude Haiku 4.5                                                0.33x
  Claude Opus 4.6                                                    3x
  Claude Opus 4.6 (fast mode) (Preview) (requires enablement)       30x
  Claude Opus 4.5                                                    3x
  Claude Sonnet 4                                                    1x
  Gemini 3 Pro (Preview)                                             1x
  GPT-5.3-Codex                                                      1x
  GPT-5.2-Codex                                                      1x
  GPT-5.2                                                            1x
  GPT-5.1-Codex-Max                                                  1x
  GPT-5.1-Codex                                                      1x
  GPT-5.1                                                            1x
  GPT-5.1-Codex-Mini (Preview)                                    0.33x
❯ GPT-5 mini                                                         0x
  GPT-4.1                                                            0x
```

若表現太差就換到 `Claude Sonnet 4.5`

接著就是跟龍蝦溝通的方式 `Select channel (QuickStart)`

```
◆  Select channel (QuickStart)
│  ● Telegram (Bot API) (recommended · newcomer-friendly)
│  ○ WhatsApp (QR link)
│  ○ Discord (Bot API)
│  ○ IRC (Server + Nick)
│  ○ Google Chat (Chat API)
│  ○ Slack (Socket Mode)
│  ○ Signal (signal-cli)
│  ○ iMessage (imsg)
│  ○ LINE (Messaging API)
│  ○ Feishu/Lark (飞书)
│  ○ Nostr (NIP-04 DMs)
│  ○ Microsoft Teams (Bot Framework)
│  ○ Mattermost (plugin)
│  ○ Nextcloud Talk (self-hosted)
│  ○ Matrix (plugin)
│  ○ BlueBubbles (macOS app)
│  ○ Zalo (Bot API)
│  ○ Zalo (Personal Account)
│  ○ Synology Chat (Webhook)
│  ○ Tlon (Urbit)
│  ○ Skip for now
```

就是一下 Telegram 吧,設定滿簡單的,只要到 Telegram BotFather 上註冊一個 Bot 即可,過程會給予 HTTP API token ,回 OpenClaw 貼上即可。

接著 Install missing skill dependencies 的部分,就挑一下 macOS 系列: apple-notes, apple-reminders, openai-whisper 等等:

◆  Install missing skill dependencies
│  ◻ Skip for now (Continue without installing dependencies)
│  ◻ 🔐 1password
│  ◼ 📝 apple-notes (Manage Apple Notes via the `memo` CLI on macOS 
│  (create, view, edit, delete, search, move,…)
│  ◼ ⏰ apple-reminders (Manage Apple Reminders via remindctl CLI 
│  (list, add, edit, complete, delete). Supports li…)
│  ◻ 🐻 bear-notes
│  ◻ 📰 blogwatcher
│  ◻ 🫐 blucli
│  ◻ 📸 camsnap
│  ◻ 🧩 clawhub
│  ◻ 🎛️ eightctl
│  ◻ 🧲 gifgrep
│  ◻ 🎮 gog
│  ◻ 📍 goplaces
│  ◻ 📧 himalaya
│  ◻ 📨 imsg
│  ◻ 📦 mcporter
│  ◻ 📊 model-usage
│  ◻ 🍌 nano-banana-pro
│  ◻ 📄 nano-pdf
│  ◻ 💎 obsidian
│  ◼ 🎙️ openai-whisper (Local speech-to-text with the Whisper CLI (no
│   API key). — Install OpenAI Whisper (brew))
│  ◻ 💡 openhue
│  ◻ 🧿 oracle
│  ◻ 🛵 ordercli
│  ◻ 👀 peekaboo
│  ◻ 🗣️ sag
│  ◻ 🌊 songsee
│  ◻ 🔊 sonoscli
│  ◻ 🧾 summarize
│  ◻ ✅ things-mac
│  ◻ 🎞️ video-frames
│  ◻ 📱 wacli
│  ◻ 𝕏 xurl

接著 hooks 就全裝,最後就差不多搞定了。

未來想要打開設定,可以改用 openclaw dashboard 指令,將允許在 http://localhost:12345/ 類似的網址瀏覽。

這邊提一下 telegram 連線有問題的處理方式:
  1. 先在 Telegram BotFather 那邊查看 Bot 的情況,像是用 `/mybots` ,接著可以查看指定 bot 的 api token
  2. 直接在 mac mini 上編輯 ~/.openclaw/openclaw.json 檔案,確認:
    • 查看 channels.telegram 的設定,如 enabled: true, dmPolicy: pairing, 以及可以順便更新一下 botToken 等
  3. 在 mac mini 使用指令 `openclaw gateway` 運行
  4. 在 Telegram Bot 上,打 `/start` 指令
  5. 在 mac mini 使用指令 `openclaw pairing list telegram`
  6. 如果有看到配對碼,就用 `openclaw pairing approve telegram <CODE>` 生效他
如果 mac mini 重啟後,容易 Telegram 沒反應,這時可以用 ssh user@mac-mini.local 試試,若會看到 `System successfully unlocked. You may now use SSH to authenticate normally.` ,大概是因為 FileVault 的關係,可以評估一下是否關閉:

user@mac-mini ~ % fdesetup status
FileVault is On.
user@mac-mini ~ % sudo fdesetup disable
Enter the user name:user
Enter the password for user 'user':
FileVault has been disabled.
user@mac-mini ~ % fdesetup status      
FileVault is Off.

最後,設定一下開機自動執行 openclaw ,這邊單純用 crontab 管理,還有其他更佳的方案,之後再來研究:

```
user@mac-mini ~ % which openclaw
/Users/user/.nvm/versions/node/v22.22.1/bin/openclaw

user@mac-mini ~ % crontab -l
@reboot PATH=$PATH:$HOME/.nvm/versions/node/v22.22.1/bin/ $HOME/.nvm/versions/node/v22.22.1/bin/openclaw gateway >> $HOME/openclaw.log 2>&1
```

2026年3月10日 星期二

[macOS] 使用 macstrap 批次快速設定 Mac Mini M4 與架設 OpenClaw 環境 @ macOS

最近拿到一台 Mac mini M4 32GB/1TB ,之前很習慣用 Notes 貼一下快速安裝的指令,想著想著自己十多年前都在用 ansible 管理百台 Ubuntu servers ,這個年代一直貼指令微遜 XD 就 vibe coding 一下,請 AI 小弟幫忙實現工具,於是乎就長出個 macstrap 工具,其特色就是讓不熟悉 ansible 的人也能快速上手,而安裝途徑是透過 ssh remote command ,這樣搞定一台後,未來有多台 mac mini 要初始化,也可以快速搞定。

首先,先把 Mac mini 開箱後,設定好登入的帳號密碼,接著在 System Settings -> General -> Sharing -> Remote login 打開,這樣就完成很基本的 ssh 可登入機制,然而 macstrap 是基於 python 的工具,所以 Mac mini 本地需要故意打一下 python 指令觸發系統去下載安裝 Python3

接下來就是 macstrap 領域了:

```
% cd /tmp
% python3 -m venv venv
% source /tmp/venv/bin/activate
% pip install macstrap
...
% macstrap --version 
macstrap, version 1.1.0
```

初始化一些範例來用:

```
% macstrap init --exmaples
% tree -L 1 examples
examples
├── ai-cli
├── openclaw
├── php8.3-dev
└── utilities-dev

5 directories, 0 files
```

設置連上 Mac mini 的帳密,假設 Mac mini IP 是 192.168.1.100 (或 mac-mini.local) 登入帳號是 macuser 且擁有 sudo 權限:

```
% macstrap ssh-auth mac-mini.local --user macuser
```

替 192.168.1.100 (或 mac-mini.local) 安裝 AI CLI:

```
% macstrap run --config examples/ai-cli mac-mini.local
...

TASK [nvm : Install global npm packages] *****
ok: [mac-mini.local] => (item=@github/copilot)
ok: [mac-mini.local] => (item=@anthropic-ai/claude-code)
ok: [mac-mini.local] => (item=@openai/codex)
ok: [mac-mini.local] => (item=@google/gemini-cli)

...

```

安裝 OpenClaw:

```
% macstrap run --config examples/openclaw mac-mini.local
...
TASK [nvm : Install global npm packages] *****
ok: [user-macmini.local] => (item=openclaw)
...
```

接著登入 mac-mini.local 後,就可以用 openclaw onboard 啟動他來做設定:

```
% ssh macuser@mac-mini.local
Now using node v22.22.1 (npm v10.9.4)
[env] node: v22.22.1 | npm: 10.9.4 | Python 3.9.6

% openclaw onboard

🦞 OpenClaw 2026.3.8 (3caab92)
   Runs on a Raspberry Pi. Dreams of a rack in Iceland.

▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██░▄▄▄░██░▄▄░██░▄▄▄██░▀██░██░▄▄▀██░████░▄▄▀██░███░██
██░███░██░▀▀░██░▄▄▄██░█░█░██░█████░████░▀▀░██░█░█░██
██░▀▀▀░██░█████░▀▀▀██░██▄░██░▀▀▄██░▀▀░█░██░██▄▀▄▀▄██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                  🦞 OPENCLAW 🦞                    
 
┌  OpenClaw onboarding
◇  Security ─────────────────────────────────────────────────────────────╮
│                                                                        │
│  Security warning — please read.                                       │
│                                                                        │
│  OpenClaw is a hobby project and still in beta. Expect sharp edges.    │
│  By default, OpenClaw is a personal agent: one trusted operator        │
│  boundary.                                                             │
│  This bot can read files and run actions if tools are enabled.         │
│  A bad prompt can trick it into doing unsafe things.                   │
│                                                                        │
│  OpenClaw is not a hostile multi-tenant boundary by default.           │
│  If multiple users can message one tool-enabled agent, they share      │
│  that delegated tool authority.                                        │
│                                                                        │
│  If you’re not comfortable with security hardening and access          │
│  control, don’t run OpenClaw.                                          │
│  Ask someone experienced to help before enabling tools or exposing it  │
│  to the internet.                                                      │
│                                                                        │
│  Recommended baseline:                                                 │
│  - Pairing/allowlists + mention gating.                                │
│  - Multi-user/shared inbox: split trust boundaries (separate           │
│    gateway/credentials, ideally separate OS users/hosts).              │
│  - Sandbox + least-privilege tools.                                    │
│  - Shared inboxes: isolate DM sessions (`session.dmScope:              │
│    per-channel-peer`) and keep tool access minimal.                    │
│  - Keep secrets out of the agent’s reachable filesystem.               │
│  - Use the strongest available model for any bot with tools or         │
│    untrusted inboxes.                                                  │
│                                                                        │
│  Run regularly:                                                        │
│  openclaw security audit --deep                                        │
│  openclaw security audit --fix                                         │
│                                                                        │
│  Must read: https://docs.openclaw.ai/gateway/security                  │
│                                                                        │
├────────────────────────────────────────────────────────────────────────╯
◆  I understand this is personal-by-default and shared/multi-user use requires
 lock-down. Continue?
│  ○ Yes / ● No

```

2026年2月13日 星期五

[Linux] 安裝 OpenClaw @ raspberrypi Debian 12




之前有一台 Pi 3 閒置,查了一下記憶體有 1GB ,試試看吧!先把它更新到 Debian GNU/Linux 12 (bookworm) ,接著用 nvm 安裝出 node.js v22 環境,再調整 swap 到 1GB 來跑:

```
@raspberrypi:~ $ uname -a
Linux raspberrypi 6.1.21-v8+ #1642 SMP PREEMPT Mon Apr  3 17:24:16 BST 2023 aarch64 GNU/Linux

@raspberrypi:~ $ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 12 (bookworm)
Release: 12
Codename: bookworm

@raspberrypi:~ $ apt policy raspberrypi-bootloader
raspberrypi-bootloader:
  Installed: 1:1.20230405-1
  Candidate: 1:1.20230405-1
  Version table:
 *** 1:1.20230405-1 500
        500 http://archive.raspberrypi.org/debian bookworm/main arm64 Packages
        100 /var/lib/dpkg/status

@raspberrypi:~ $ cat /etc/apt/sources.list.d/raspi.list
deb http://archive.raspberrypi.org/debian/ bookworm main

@raspberrypi:~ $ cat /etc/apt/sources.list
deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware
deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware

@raspberrypi:~ $ lscpu
Architecture:            aarch64
  CPU op-mode(s):        32-bit, 64-bit
  Byte Order:            Little Endian
CPU(s):                  4
  On-line CPU(s) list:   0-3
Vendor ID:               ARM
  Model name:            Cortex-A53
    Model:               4
    Thread(s) per core:  1
    Core(s) per cluster: 4
    Socket(s):           -
    Cluster(s):          1
    Stepping:            r0p4
    CPU(s) scaling MHz:  100%
    CPU max MHz:         1200.0000
    CPU min MHz:         600.0000
    BogoMIPS:            38.40
    Flags:               fp asimd evtstrm crc32 cpuid
Caches (sum of all):     
  L1d:                   128 KiB (4 instances)
  L1i:                   128 KiB (4 instances)
  L2:                    512 KiB (1 instance)
Vulnerabilities:         
  Itlb multihit:         Not affected
  L1tf:                  Not affected
  Mds:                   Not affected
  Meltdown:              Not affected
  Mmio stale data:       Not affected
  Retbleed:              Not affected
  Spec store bypass:     Not affected
  Spectre v1:            Mitigation; __user pointer sanitization
  Spectre v2:            Not affected
  Srbds:                 Not affected
  Tsx async abort:       Not affected

@raspberrypi:~ $ sudo vcgencmd measure_temp
temp=54.8'C

@raspberrypi:~ $ sudo vcgencmd measure_clock arm
frequency(48)=600062000

@raspberrypi:~ $ sudo vcgencmd get_mem gpu
gpu=76M

@raspberrypi:~ $ cat /proc/device-tree/model
Raspberry Pi 3 Model B Rev 1.2

$ cat /proc/cpuinfo | grep Model
Model : Raspberry Pi 3 Model B Rev 1.2
```

---

```
@raspberrypi:~ $ openclaw onboard

<--- Last few GCs --->

[17543:0x7fb04c0000]   229321 ms: Scavenge 451.3 (463.5) -> 450.8 (464.3) MB, pooled: 0 MB, 21.16 / 0.00 ms  (average mu = 0.614, current mu = 0.204) allocation failure; 
[17543:0x7fb04c0000]   229421 ms: Scavenge 452.0 (464.3) -> 451.5 (469.0) MB, pooled: 0 MB, 40.55 / 0.00 ms  (average mu = 0.614, current mu = 0.204) allocation failure; 


<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----

 1: 0xe13218 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [openclaw]
 2: 0x11a7f8c v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [openclaw]
 3: 0x11a813c v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [openclaw]
 4: 0x13ccadc  [openclaw]
 5: 0x13e4a50 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [openclaw]
 6: 0x13bb378 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [openclaw]
 7: 0x13bc1b0 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [openclaw]
 8: 0x13972d0 v8::internal::Factory::NewFillerObject(int, v8::internal::AllocationAlignment, v8::internal::AllocationType, v8::internal::AllocationOrigin) [openclaw]
 9: 0x17b0b5c v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [openclaw]
10: 0x1cc19d4  [openclaw]
```

```
@raspberrypi:~ $ free -h
               total        used        free      shared  buff/cache   available
Mem:           909Mi       209Mi       555Mi       3.8Mi       212Mi       700Mi
Swap:           99Mi        99Mi       160Ki


@raspberrypi:~ $ sudo dphys-swapfile swapoff
@raspberrypi:~ $ sudo vim /etc/dphys-swapfile
CONF_SWAPSIZE=1024
@raspberrypi:~ $ sudo dphys-swapfile setup
@raspberrypi:~ $ sudo dphys-swapfile swapon

@raspberrypi:~ $ free -h
               total        used        free      shared  buff/cache   available
Mem:           909Mi       313Mi       428Mi       5.0Mi       236Mi       596Mi
Swap:          1.0Gi          0B       1.0Gi
```

```
@raspberrypi:~ $ NODE_OPTIONS="--max-old-space-size=768" openclaw onboard

🦞 OpenClaw 2026.2.9 (33c75cb) — I read logs so you can keep pretending you don't have to.

▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██░▄▄▄░██░▄▄░██░▄▄▄██░▀██░██░▄▄▀██░████░▄▄▀██░███░██
██░███░██░▀▀░██░▄▄▄██░█░█░██░█████░████░▀▀░██░█░█░██
██░▀▀▀░██░█████░▀▀▀██░██▄░██░▀▀▄██░▀▀░█░██░██▄▀▄▀▄██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                  🦞 OPENCLAW 🦞                    
 
┌  OpenClaw onboarding
◇  Security ──────────────────────────────────────────────────────────────────────────────╮
│                                                                                         │
│  Security warning — please read.                                                        │
│                                                                                         │
│  OpenClaw is a hobby project and still in beta. Expect sharp edges.                     │
│  This bot can read files and run actions if tools are enabled.                          │
│  A bad prompt can trick it into doing unsafe things.                                    │
│                                                                                         │
│  If you’re not comfortable with basic security and access control, don’t run OpenClaw.  │
│  Ask someone experienced to help before enabling tools or exposing it to the internet.  │
│                                                                                         │
│  Recommended baseline:                                                                  │
│  - Pairing/allowlists + mention gating.                                                 │
│  - Sandbox + least-privilege tools.                                                     │
│  - Keep secrets out of the agent’s reachable filesystem.                                │
│  - Use the strongest available model for any bot with tools or untrusted inboxes.       │
│                                                                                         │
│  Run regularly:                                                                         │
│  openclaw security audit --deep                                                         │
│  openclaw security audit --fix                                                          │
│                                                                                         │
│  Must read: https://docs.openclaw.ai/gateway/security                                   │
│                                                                                         │
├─────────────────────────────────────────────────────────────────────────────────────────╯
◆  I understand this is powerful and inherently risky. Continue?
│  ○ Yes / ● No
```

2026年1月1日 星期四

2026年,突然想用這個字了...

新年快樂

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;
}

收工

其他資訊:

2025年3月7日 星期五

Docker 開發筆記 - 在 Synology NAS 運行 immich 照片服務與 Symbolic link 管理方式 @ Synology DS723+


周邊有強者好友真不錯,時常分享把一堆服務都搞自建方案,目的不是省錢,而是追求資料握在自己手中的架構。挑選 immich 大概算是公認做得不錯的相簿管理服務,其介面跟 Google Photos 很像,且支援人臉辨識也有依照地圖(OpenStreetMap)顯示照片拍攝位置


而 immich 官方文件有非常方便架設 Docker 的筆記:
在 Synology NAS 上,若有支援 Docker 的,可以在 Container Manager 中新增專案,這時要上傳 docker-composer.yml 檔案,則是在剛剛的教學網站有顯示:
在此先決定了 NAS 上的檔案位置,把 Docker 運行所需的環境擺在 /docker/immich-app 目錄(這是 File Station 看到的路徑),他實際的路徑位置會是 /volume1/docker/immich-app 。


這時 docker-compose.yml 可透過 Container Manager 上傳好,而 example.env 則是透過 File Station 拖拉進去改名成 .env

這時直接在 Container Manager 運行時,應當會踩到問題,因為 library 或 postgres 目錄不存在,這時也繼續靠 File Station 建立,後續 Container Manager 運行就會正常了,正常到最後可以用 NAS_IP:2283 瀏覽起來,可以看到註冊畫面等。

這邊有幾個議題記錄一下:
  1. 使用 NAS 反向代理伺服器,提供 https 連線 immich 服務
  2. 在 immich 使用外部圖庫 (External Library) ,將原本的 Synology Photos 匯入
  3. 在 immich External Library 如何使用 Symbolic link 來管理
首先,NAS 反向代理伺服器的設定還滿簡單的,找一個 port 來服務 https 連線,例如 2284 ,直接把它導向到 2283 ,這樣就搞定收工,未來就有 https://NAS_IP:2284 可用了!


接著,使用外部圖庫部分,這邊跟 Docker 設定有關,必須把額外的資料像掛載進來,例如

    volumes:
      # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
      - /etc/localtime:/etc/localtime:ro
      - /var/services/homes/UserID/Photos:/synology-photo:ro

透過把 Synology 在使用者家目錄的位置掛進來,這樣對 immich docker 環境中,就多了 /synology-photo 路徑可以查看資料,這時在 immich 網頁設定上,直接設定外部位置在 /synology-photo ,就可以讓 immich 掃描到照片資料來分析了

然而,對於資料管理上會想要慢慢實驗,例如少量的把目錄資料加入到 immich 外部圖庫,且不需要一直改 YAML volumes?通常對系統熟悉的,就會想試試 symbolic link 架構,還能避免 NAS 上有重複資料佔著空間,然而,symbolic link 在 Docker 環境上有使用限制,有一些討論串:
我這邊的解法,其實是設法先排除 Docker 限制,只要繞過限制後,Symbolic link 還是可以使用的。目前的設計就先在 ~/Photos/ 建立一個目錄,如 immich ,接著在裡面建立 Symbolic link 到上一層 ~/Photos 想要的資料就好,而掛載到 Docker 裡仍維持在 ~/Photos 位置,而 immich 網頁上就設定 /synology-photo/immich 即可。

總結一下 Symbolic link 使用資訊:
  • Docker volumes YAML 設定不變,概念上要把 Symbolic link 來源也包覆到
    • /var/services/homes/UserID/Photos:/synology-photo:ro
  • 建立 /var/services/homes/UserID/Photos/immich 目錄
  • 在 immich 網頁上,將外部圖庫設定在 /synology-photo/immich
  • 未來想動態將資料交給 immich 服務時,就只須在 /var/services/homes/UserID/Photos/immich 內建立向上一層的目錄,確保那些享用 symbolic link 的資料都有在 Docker volumes 內即可

UserID@NAS:~/Photos/immich$ ls -l
lrwxrwxrwx+ 1 UserID users 27 Mar  6 23:25 '2020-01-01' -> '../2020-01-01/'
lrwxrwxrwx  1 UserID users 24 Mar  6 23:28 '2021-02-02' -> '../2021-02-02/'
lrwxrwxrwx  1 UserID users 27 Mar  6 23:28 '2022-03-03' -> '../2022-03-03/'
  
收工

2025年2月26日 星期三

[macOS] 使用 Windows APP (原 Microsoft RDP app) 遠端登入 Ubuntu 24.04 Desktop 遠端桌面 及 Error code: 0x207 問題排除 @ macOS 15.3.1


把自己的 Pi 5 安裝 Ubuntu 24.04 Desktop 後,原本想要裝 VNC 來提供遠端桌面的,搞了半天才發現已經有內建的 RDP 服務,接著當然就是在 macOS 上找尋很多年前已裝過的 Microsoft Windows Remote Desktop app ,找了半天也沒找到,最後才發現 macOS App Store 已更名為 Windows App

第一次在 macOS 上 Windows APP 登入到 Ubuntu 24.04 Desktop 是正常的,但很詭異的是之後一直會收到錯誤訊息:

We couldn't connect to the remote PC. This might be due to an expired password. If this keeps happening, contact your network administrator for assistance. Error code: 0x207

我也是趁這次才知道,原來 Ubuntu 24.04 Desktop 內建的 RDP 服務,必須使用者先登入過桌面才行,並且鎖住螢幕也不行。事後發現有一連串的方式解套。


首先是使用者必須先登入過桌面,那就開啟自動登入就好,剛好在 Pi 5 實驗裝置不會踩到資安議題。接著是鎖住螢幕的問題,有一派是調整成不自動鎖住,此時發現還可以透過額外安裝 GNOME Extension Manager,並且在裡頭下載 Allow Locked Remote Desktop 就好,設定好重啟。

$ sudo apt install gnome-shell-extension-manager

最後,關於 macOS 上第二次起登入 Ubuntu Desktop 總是碰到的 Error code: 0x207 問題,神人解法是先把設定檔匯出,接著修改關鍵設定,把 "use redirection server name:i:0" 更新成 "use redirection server name:i:1" ,再匯入回去即可搞定。有些討論串認為是 macOS - Windows app 的 bug


搞定

ref:

2025年2月24日 星期一

[Linux] 樹莓派5開發筆記 - Raspberry Pi 5 + Pi AI kit + Pi Mini Tower kit @ Ubuntu 24.04 Desktop



二月初手癢,敗了個 Raspberry Pi AI kit Hailo-8 ,接著發現以前買的 Pi 5 小機殼不能用了,裸機運行了一下,就手癢找找有沒有比較大的機殼,就看到了 樹莓派 5 塔式散熱器外殼組 | Raspberry Pi 5 Mini Tower Kit with ICE Tower ,接著再去蝦皮逛一眼,有看到遠方發貨的,就試了一下。


實際上排線是非常緊繃地連著:


有一邊的腳架不鎖(無法鎖):



最終就這樣被我硬拼了出來,上方的 Pi AI kit 的架子有兩隻腳是懸空的,懸空那面剛好有排線針腳固定著也不太會晃,就這樣胡亂用了起來。

這邊就簡單的完成 Mini Tower Kit 的面板(OLED)初始化,順便小改 code 來達成顯示 IP 和收到關機時可正常退出使螢幕變黑。


目前就包成一個 script 來運行:

user@user-desktop:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 24.04.2 LTS
Release: 24.04
Codename: noble
user@user-desktop:~$ curl https://raw.githubusercontent.com/changyy/study-pi5/refs/heads/main/script/pi5-mini-tower-kit-with-oled-only.sh > /tmp/pi5-mini-tower-kit-with-oled-only.sh
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  6184  100  6184    0     0  11814      0 --:--:-- --:--:-- --:--:-- 11801
user@user-desktop:~$ bash /tmp/pi5-mini-tower-kit-with-oled-only.sh 
...

收工

2025年2月21日 星期五

Python 開發筆記 - promptpack-for-code 一個將專案的目錄結構和指定檔案輸出單一文字檔,讓 AI 點評跟建議的小工具


近半年已經極高的比例都會用 claude.ai 做事,最近用量太大後,再加上 grok.com 免費中,就想到如果碰到付費額度用原被凍時,該怎樣快速換另一家 AI 服務使用,也可以避免與 AI 輔助努力了老半天,重啟對話又要打得落落長的內容。


因此就開發了一個小工具:promptpack-for-code

他的用法就是把指定專案的目錄結構、指定的程式碼輸出成 output.txt ,接著就可以上傳 output.txt 跟 AI 互動,成果包括可以請他點評哪邊可以優化、撰寫系統架構、教學文件等等,十分方便

```
% python3 -m venv venv
% source venv/bin/activate
(venv) /tmp % pip install promptpack-for-code
% git clone https://github.com/changyy/py-promptpack-for-code
% promptpack-for-code py-promptpack-for-code -r py-promptpack-for-code 
Successfully created output.txt
```

問問此時此刻(2025-02-21)最強的 Grok 3 AI:

```
### 專案概述

`py-promptpack-for-code` 是一個 Python 命令列工具,旨在將多個程式碼檔案或目錄中的檔案打包成一個結構化的單一文字檔,特別針對與 AI 語言模型進行程式碼審查或分析的需求進行優化。這個工具的主要功能包括:

1. **檔案結構生成**:根據指定的根目錄生成樹狀目錄結構,方便理解專案的組織。
2. **內容整合**:將指定目錄中的程式碼檔案內容合併到輸出檔案,並保留相對路徑。
3. **過濾與忽略**:支援自訂忽略模式(例如 `.git`、`.pyc` 等),避免不必要的檔案內容。
4. **進度顯示**:提供進度條選項,適合處理大型專案時使用。
5. **易於與 AI 整合**:輸出格式規範且結構化,便於直接輸入到 AI 聊天平台進行分析。

### 專案評論

#### 優點
1. **實用性強**:
   - 該工具解決了一個具體且常見的痛點:如何快速準備程式碼供 AI 分析。它不僅合併檔案,還提供了目錄結構,這對理解上下文非常有幫助。
   - 支援多目錄處理、進度條、強制覆蓋等功能,滿足不同使用場景的需求。

2. **結構清晰**:
   - 專案採用標準 Python 包結構(`src/`、`tests/` 等),易於維護和擴展。
   - 文件(如 `README.md` 和 `LICENSE`)完整,提供了詳細的使用說明和授權資訊。

3. **測試覆蓋良好**:
   - `tests/test_core.py` 包含多個單元測試,涵蓋核心功能(樹狀結構生成、檔案處理、忽略模式等),提高了程式碼的可靠性。

4. **部署與分享便利**:
   - 透過 `setup.py` 和 GitHub Actions(`python-publish.yml`)實現 PyPI 發佈,使用者只需 `pip install` 即可使用。
   - MIT 許可證允許廣泛的開源使用和修改。

5. **效能考慮**:
   - 使用 `ThreadPoolExecutor` 實現並行處理檔案,對於大型專案具有一定的效能優勢。

#### 不足之處
1. **功能單一性**:
   - 目前工具主要專注於生成文字輸出,缺乏更高級的分析或格式化選項(例如 JSON 輸出或程式碼摘要生成)。
   - 對於非文字檔案的處理較為簡單(僅跳過),可能無法滿足需要處理混合內容的需求。

2. **錯誤處理有限**:
   - 雖然有基本的日誌記錄(`logging`),但對於檔案編碼錯誤(`UnicodeDecodeError`)或權限問題的處理僅限於跳過或警告,缺乏更靈活的應對策略。

3. **文件不夠詳盡**:
   - `README.md` 提供了基本用法,但對於進階功能(例如自訂忽略模式的具體範例)或輸出檔案的詳細格式說明不夠深入。
   - 缺少 API 文件,對於希望程式化調用的開發者不夠友好。

4. **忽略模式的靈活性**:
   - 雖然支援 `fnmatch` 通配符,但忽略模式僅限於檔案名稱,無法根據檔案內容或大小進行過濾。

### 優化建議

1. **增加輸出格式選項**:
   - 除了目前的純文字輸出,新增 JSON 或 YAML 格式選項,讓使用者可以更方便地將結果用於其他工具或程式化處理。
   - 例如:
     ```json
     {
       "tree": {
         "project-name": {
           "src": {
             "main.py": "def main():\n    print('Hello')\n",
             "utils": {
               "helper.py": "def helper():\n    return True\n"
             }
           }
         }
       }
     }
     ```

2. **增強錯誤處理與回饋**:
   - 提供選項讓使用者決定如何處理非 UTF-8 檔案(例如轉換編碼或以二進位模式顯示部分內容)。
   - 對於權限錯誤,記錄到輸出檔案中,而不是僅在日誌中警告,這樣使用者能更容易追蹤問題。

3. **擴展忽略模式功能**:
   - 支援基於檔案大小、修改時間或內容關鍵字的過濾。例如:
     ```bash
     promptpack-for-code /path/to/src --ignore-size ">10MB" --ignore-modified "<2023-01-01"
     ```
   - 允許從外部檔案(如 `.gitignore`)載入忽略模式,增強與現有工作流的兼容性。

4. **改善文件與範例**:
   - 在 `README.md` 中新增更多實際範例,例如處理大型專案或與 AI 工具(如 ChatGPT 或 GitHub Copilot)配合的具體流程。
   - 提供一份獨立的 `CONTRIBUTING.md`,詳細說明如何提交問題或貢獻程式碼。

5. **新增摘要功能**:
   - 提供選項生成程式碼摘要(例如每個檔案的前幾行或函數簽名),減少輸出檔案的大小,方便快速預覽。
     ```bash
     promptpack-for-code /path/to/src --summary
     ```

6. **支援增量更新**:
   - 增加一個模式,僅處理自上次運行以來有變更的檔案(例如根據檔案修改時間戳記),提高處理效率。
     ```bash
     promptpack-for-code /path/to/src --incremental
     ```

7. **UI 或互動模式**:
   - 開發一個簡單的 TUI(文字使用者介面)或互動模式,讓使用者在命令列中動態選擇要處理的目錄或檔案。

### 總結

`py-promptpack-for-code` 是一個設計良好且實用的工具,特別適合需要與 AI 進行程式碼分析的開發者。其核心功能已經相當成熟,但在靈活性、錯誤處理和文件方面仍有提升空間。透過上述建議,這個專案可以進一步滿足更廣泛的使用場景,並提升使用者的體驗。

如果你有具體的使用需求或想討論某個優化方向,我可以進一步提供更詳細的建議或程式碼範例!
```

2025年2月17日 星期一

Docker 開發筆記 - 使用 aws cli 和 Docker Exec 進入 AWS ECS Container @ macOS

這個議題比我想像中麻煩了點,讓我回憶起 2009 年寫的 AWS 筆記,那時同事一開始還只在用 Firefox extension 管理 AWS EC2 呢 XD 我覺得這理論上要能都透過網頁搞定才對,先把目前研究的過程筆記一下。

總之,要能像 ssh 遠端(docker exec -it ContainerID bash)進去 AWS ECS container 的關鍵之處:
  • 使用 awscli 做事
  • AWS ECS 的 Task 定義,基礎設施需求 -> 任務角色,需要指定一下角色,例如 ecsTaskExecutionRole
  • AWS IAM -> ecsTaskExecutionRole -> 添加 AmazonSSMManagedInstanceCore 權限
  • AWS ECS -> Cluster -> Service ,需要用 awscli 啟動 enable-execute-command ,並且重新更新服務,使之生效
  • 使用 aws 指令登入
首先,先下載 awscli 並且版本要夠新:
安裝後檢查版本,版本太低會無法完成任務:

% aws --version
aws-cli/2.24.5 Python/3.12.6 Darwin/24.3.0 exe/x86_64

接著還要安裝 Session Manager plugin,此例紀錄 Mac with Apple silicon 版:
% curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/mac_arm64/sessionmanager-bundle.zip" -o "sessionmanager-bundle.zip"
% unzip sessionmanager-bundle.zip
% sudo ./sessionmanager-bundle/install -i /usr/local/sessionmanagerplugin -b /usr/local/bin/session-manager-plugin

基本的環境已準備好了,下一刻是查看自己的 AWS ECS 的任務定義是否有把 任務角色 設定好,這部就維持用網頁吧:



若你的 AWS ECS 上定義的 Task 只有一個,也可以偷懶靠 awscli 操作(在此就不贅述 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION 部分),會用到的指令:

aws ecs list-task-definition-families --status ACTIVE
aws ecs list-task-definitions --family-prefix webapp
aws ecs describe-task-definition --task-definition webapp:3

連續技,快速檢查 taskRoleArn 跟 executionRoleArn:

% NamespaceID=$(aws ecs list-task-definition-families --status ACTIVE | jq -r '.families[0]') ; echo "Namespace: $NamespaceID" ; TaskID=$(aws ecs list-task-definitions --family-prefix "$NamespaceID" | jq -r '.taskDefinitionArns[0]') ; echo "Task: $TaskID" ; aws ecs describe-task-definition --task-definition "$TaskID" | jq ".taskDefinition | { taskRoleArn: .taskRoleArn, executionRoleArn: .executionRoleArn}"
Namespace: myapp-task
Task: arn:aws:ecs:ap-northeast-1:####:task-definition/myapp-task:2
{
  "taskRoleArn": "arn:aws:iam::####:role/ecsTaskExecutionRole",
  "executionRoleArn": "arn:aws:iam::####:role/ecsTaskExecutionRole"
}

接下來,若 AWS ECS Cluster 還沒有建立任何 Service,用指令查:

% aws ecs list-tasks --cluster myapp-cluster  
{
    "taskArns": []
}

接著我們在 AWS ECS 網頁端起了一個 Service 名為 myapp-service ,在創建過程中沒看到啟用 enable-execute-command,這時在網頁上查看也是顯示 "ECS 執行: 關閉"



接下來用 aws cli 查詢:

% aws ecs list-tasks --cluster myapp-cluster   
{
    "taskArns": [
        "arn:aws:ecs:ap-northeast-1:####:task/myapp-cluster/#TASKID#"
    ]
}

% TaskID=$(aws ecs list-tasks --cluster myapp-cluster | jq -r '.taskArns[0]') ; echo "TaskID: $TaskID" ; aws ecs describe-tasks --cluster myapp-cluster --tasks $TaskID | jq '.tasks[0] | { "clusterArn": .clusterArn, "taskArn": .taskArn, "taskDefinitionArn": .taskDefinitionArn, "group": .group, "healthStatus": .healthStatus, "desiredStatus": .desiredStatus, "enableExecuteCommand": .enableExecuteCommand, "containers-name": [.containers |.[] | { "name":.name, "runtimeId": .runtimeId} ] }'
TaskID: arn:aws:ecs:ap-northeast-1:####:task/myapp-cluster/#TASKID#
{
  "clusterArn": "arn:aws:ecs:ap-northeast-1:####:cluster/myapp-cluster",
  "taskArn": "arn:aws:ecs:ap-northeast-1:####:task/myapp-cluster/#TASKID#",
  "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:####:task-definition/myapp-task:2",
  "group": "service:myapp-service",
  "healthStatus": "HEALTHY",
  "desiredStatus": "RUNNING",
  "enableExecuteCommand": false,
  "containers-name": [
    {
      "name": "php-fpm-docker",
      "runtimeId": "#TASKID#-#ContainerID#"
    },
    {
      "name": "web-docker",
      "runtimeId": "#TASKID#-#ContainerID#"
    }
  ]
}

可以看到 enableExecuteCommand 為 false

這時候,如果透過 aws cli 來設法登入到 Container:

% aws ecs execute-command --cluster myapp-cluster --task #TASKID# --container #containers#name# --command "/bin/bash" --interactive


The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


An error occurred (InvalidParameterException) when calling the ExecuteCommand operation: Unable to start session because the container doesn’t exist. Specify a valid container and try again.

接著,使用 aws cli 來啟動 enableExecuteCommand 吧:

% aws ecs update-service --cluster myapp-cluster --service arn:aws:ecs:ap-northeast-1:####:service/myapp-cluster/myapp-service --enable-execute-command 
{
    "service": {
        ... 
        "enableExecuteCommand": true,
        ...
    }
}

可以看到 enableExecuteCommand 被標記成 true 了,這時還需要重新發布服務,可以重網頁去觸發,或是靠指令觸發:

% aws ecs update-service --cluster myapp-cluster --service arn:aws:ecs:ap-northeast-1:####:service/myapp-cluster/myapp-service --force-new-deployment

當服務發布完畢後,在網頁上就可以看到改變,或是用指令在查一次:

% TaskID=$(aws ecs list-tasks --cluster myapp-cluster | jq -r '.taskArns[0]') ; echo "TaskID: $TaskID" ; aws ecs describe-tasks --cluster myapp-cluster --tasks $TaskID | jq '.tasks[0] | { "clusterArn": .clusterArn, "taskArn": .taskArn, "taskDefinitionArn": .taskDefinitionArn, "group": .group, "healthStatus": .healthStatus, "desiredStatus": .desiredStatus, "enableExecuteCommand": .enableExecuteCommand, "containers-name": [.containers |.[] | { "name":.name, "runtimeId": .runtimeId} ] }'
TaskID: arn:aws:ecs:ap-northeast-1:####:task/myapp-cluster/#TASKID#
{
  "clusterArn": "arn:aws:ecs:ap-northeast-1:####:cluster/myapp-cluster",
  "taskArn": "arn:aws:ecs:ap-northeast-1:####:task/myapp-cluster/#TASKID#",
  "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:####:task-definition/myapp-task:2",
  "group": "service:myapp-service",
  "healthStatus": "HEALTHY",
  "desiredStatus": "RUNNING",
  "enableExecuteCommand": true,
  "containers-name": [
    {
      "name": "php-fpm-docker",
      "runtimeId": "#TASKID#-#CONTAINERID#"
    },
    {
      "name": "web-docker",
      "runtimeId": "#TASKID#-#CONTAINERID#"
    }
  ]
}

如此,就可以正式遠端進去一下:

% aws ecs execute-command --cluster myapp-cluster --task #TASKID# --container web-docker --command "/bin/bash" --interactive

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-################
ip-123-45-6-123:/var/www/html# 

收工

參考資料: