Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 63 additions & 5 deletions docs/article-registry.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"_generated": "2026-06-04T15:46:06.237Z",
"_generated": "2026-06-18T15:48:23.845Z",
"_description": "Auto-generated from concepts.yaml + article frontmatter. LLM 生成文章時參考此檔案做跨文章連結。執行 node scripts/generate-article-registry.mjs 重新產生。",
"concepts": {
"OpenClaw": {
Expand Down Expand Up @@ -182,6 +182,30 @@
"canonicalArticle": "pkm-system",
"link": "/articles/pkm-system"
},
"Webhook": {
"displayName": "Webhook",
"shortDesc": "事件發生時由服務端主動把資料 POST 到你指定網址的回呼機制,Bot 即時收訊的基礎",
"canonicalArticle": "deploy-line-bot-cloudflare-workers",
"link": "/articles/deploy-line-bot-cloudflare-workers"
},
"LINE Bot": {
"displayName": "LINE Bot",
"shortDesc": "透過 LINE 官方帳號的 Messaging API 接上後端,讓 LINE 帳號變成可自動回應的 AI 機器人",
"canonicalArticle": "deploy-line-bot-cloudflare-workers",
"link": "/articles/deploy-line-bot-cloudflare-workers"
},
"Cloudflare Workers": {
"displayName": "Cloudflare Workers",
"shortDesc": "Cloudflare 的邊緣運算平台,部署即得 HTTPS 網址,免維護伺服器,適合當 Bot 後端",
"canonicalArticle": "deploy-line-bot-cloudflare-workers",
"link": "/articles/deploy-line-bot-cloudflare-workers"
},
"wrangler": {
"displayName": "wrangler",
"shortDesc": "Cloudflare Workers 的命令列工具,負責登入、設定機密與一行指令部署",
"canonicalArticle": "deploy-line-bot-cloudflare-workers",
"link": "/articles/deploy-line-bot-cloudflare-workers"
},
"Zeabur": {
"displayName": "Zeabur",
"shortDesc": "台灣團隊打造的雲端部署平台,一鍵部署 OpenClaw",
Expand Down Expand Up @@ -329,7 +353,9 @@
"Docker"
],
"concepts_referenced": [
"OpenClaw"
"OpenClaw",
"Cloudflare Workers",
"wrangler"
],
"link": "/articles/deploy-openclaw-cloud"
},
Expand Down Expand Up @@ -690,7 +716,9 @@
],
"concepts_referenced": [
"Harness",
"Telegram Bot"
"Telegram Bot",
"Webhook",
"LINE Bot"
],
"link": "/articles/mcp-protocol"
},
Expand Down Expand Up @@ -1014,6 +1042,32 @@
],
"link": "/articles/token-economics"
},
{
"slug": "deploy-line-bot-cloudflare-workers",
"title": "用 Cloudflare Workers 當 LINE Bot 後端:部署 Worker + 設定官方帳號 Webhook",
"description": "用 Claude Code 一行指令把 Cloudflare Worker 部署上線,再到 LINE 官方帳號後台開啟 Messaging API 與 Webhook,讓你的 LINE 帳號變成 24 小時在線的 AI 機器人。",
"scene": "整合與自動化",
"difficulty": "中級",
"contentType": "tutorial",
"tags": [
"LINE",
"Cloudflare",
"Webhook",
"部署",
"整合"
],
"prerequisites": [
"telegram-integration"
],
"concepts_defined": [
"Webhook",
"LINE Bot",
"Cloudflare Workers",
"wrangler"
],
"concepts_referenced": [],
"link": "/articles/deploy-line-bot-cloudflare-workers"
},
{
"slug": "gemini-gas-ordering-system",
"title": "用 Gemini 打造 AI 雲端訂餐系統:從菜單到廚房螢幕的 4 步驟實戰",
Expand Down Expand Up @@ -1125,7 +1179,10 @@
"Telegram Bot"
],
"concepts_referenced": [
"MCP"
"MCP",
"Webhook",
"LINE Bot",
"Cloudflare Workers"
],
"link": "/articles/telegram-integration"
},
Expand Down Expand Up @@ -1194,7 +1251,7 @@
{
"slug": "computex-2026-ai-era",
"title": "Computex 2026:Nvidia 打破龍蝦瓶頸,個人 AI 助理元年正式開始",
"description": "四個月前我們在 LINE 群組預測:等技術成熟加上好用的地端模型,就是龍蝦普及的時刻。Computex 2026 的 Nvidia RTX Spark,讓這個預測提前兌現。",
"description": "四個月前我在課程中預測:等技術成熟加上好用的地端模型,就是龍蝦普及的時刻。Computex 2026 的 Nvidia RTX Spark,讓這個預測提前兌現,也將改變個人電腦的歷史。",
"scene": "鴨編的碎碎念",
"difficulty": "入門",
"contentType": "guide",
Expand Down Expand Up @@ -1340,6 +1397,7 @@
"Shell"
],
"concepts_referenced": [
"wrangler",
"WSL",
"Homebrew"
],
Expand Down
112 changes: 112 additions & 0 deletions docs/reverse-article-from-screenshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 反向工作流程:從截圖反推教學文章

> 既有截圖 → 反推出一篇符合 LaunchDock 統一風格的文章。
> 與正向流程(先寫文章帶 `@img` → 再截圖配對,見 `article-workflow-guide.md`)相反。

---

## 🔴 為什麼這件事必須在 Claude Code 做,不能在 cowork 做

1. **遮蔽是本機工具**:機敏資訊遮蔽靠 `auto-capture`(macOS Vision OCR,本機 Python),雲端的 cowork 跑不了。
2. **順序問題**:一旦把**未遮蔽**的原始截圖上傳到雲端,機敏資訊在那一刻就已外流——遮蔽再好也來不及。**遮蔽必須在圖片離開機器之前、於本機完成。**
3. **風格一致性免費**:Claude Code 進 repo 自動讀 `CLAUDE.md`、`article-registry.json`、`concepts.yaml`,cowork 得手動貼且仍跑不了 `npm run registry`。

---

## ⚠️ 自動遮蔽的已知盲點(必讀)

`auto-capture` 預設 regex 只抓:**信用卡 / API key(OpenAI/Anthropic/Google/AWS/GitHub)/ email / 通用 secret**。

**它「抓不到」**:用戶名、主機名、`/Users/...` 路徑、終端機提示字元(如 `joseph@MacBook`)、瀏覽器登入的個人名稱、人臉。

➡️ 所以一定要做 **AI 視覺第二輪**:人眼掃過每張圖,把上述個資用 `--mask-text` 指定 token,由 OCR 定位後整行馬賽克。

---

## 完整流程

### Step 0:環境(一次性)

```bash
git clone https://github.com/589411/auto-capture.git ~/Documents/github/auto-capture
cd ~/Documents/github/auto-capture
python3 -m venv .venv && .venv/bin/pip install -e .
```

`scripts/redact-screenshots.py` 會在偵測不到套件時自動切換到這個 venv,所以平常用系統 `python3` 跑即可。

### Step 1:暫存原圖(先別進 `public/`)

把要用的截圖複製到 `~/Desktop/captures/_staging/<slug>/`,依操作順序命名 `01-raw.png`、`02-raw.png`…(`_staging` 與 `*-raw.png` 都不該被 commit)。

### Step 2:先掃描看哪裡有個資

```bash
python3 scripts/redact-screenshots.py ~/Desktop/captures/_staging/<slug> --scan --suffix
```

讓 Claude Code 逐張看圖,列出**自動工具抓不到**的個資 token(用戶名、主機名等)。

### Step 3:兩輪遮蔽

```bash
# 預設 patterns + 人工 token,產出 NN-clean.png
python3 scripts/redact-screenshots.py ~/Desktop/captures/_staging/<slug> \
--suffix --mask-text joseph --mask-text MacBook
```

`--mask-text` 可重複,大小寫不拘;任何 OCR 文字行含該 token 就整行馬賽克。

### Step 4:上架前最終掃描(必須 0 命中)

```bash
python3 scripts/redact-screenshots.py ~/Desktop/captures/_staging/<slug> --scan --mask-text joseph
```

顯示「✅ 全部 clean」才能繼續。

### Step 5:複製乾淨圖進 `public/`

用**語意化 kebab-case 檔名**(對應文章 @img 標記):

```bash
DEST=public/images/articles/<slug>
mkdir -p "$DEST"
cp ~/Desktop/captures/_staging/<slug>/01-clean.png "$DEST/<描述性檔名>.png"
# …其餘類推
```

### Step 6:反推文章(套統一風格)

依 `CLAUDE.md` + `article-registry.json` + `concepts.yaml`:

- 動筆前讀 registry,沿用既有概念用語、別重複解釋
- 同步產出**中英文兩版**(`src/content/articles/<slug>.md`、`src/content/articles/en/<slug>.md`),scene/difficulty 用對應語言 key
- 完整 frontmatter;風格:繁中口語但專業、用「你」、步驟編號、常見錯誤用 `### 🚨`
- 圖片直接寫成已配對語法 `![alt](/images/articles/<slug>/<檔名>.png)`

### Step 7:新概念登錄

若引入 `concepts.yaml` 沒有的新名詞 → 補上,再跑:

```bash
npm run registry
```

---

## 與正向流程的對照

| | 正向(既有) | 反向(本文) |
|---|---|---|
| 起點 | 先寫文章帶 `@img` 佔位 | 先有截圖 |
| 圖片 | 照標記去截圖、再配對 | 反推文字去對應既有圖 |
| 遮蔽 | 同樣必做 | 同樣必做(且更關鍵,個資已存在圖中) |
| 工具 | `scripts/add-image.sh` | `scripts/redact-screenshots.py` + 手動配對 |

---

**首次實作產物**:`deploy-line-bot-cloudflare-workers`(中英文版)。
**相關**:`article-workflow-guide.md`、`llm-article-prompt.md`、`image-workflow.md`、`CLAUDE.md`。

**Last updated:** 2026-06-18
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 147 additions & 0 deletions scripts/redact-screenshots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Redact sensitive info from a folder of screenshots (reverse-article workflow).

Two-pass redaction, matching the LaunchDock security rule:
1. auto-capture's default OCR patterns (credit cards / API keys / emails / secrets)
2. AI-vision second pass: mask any OCR text line containing a personal token
you pass with --mask-text (usernames, hostnames, real names — the default
regexes DO NOT catch these).
Then a final scan reports whether the folder is clean.

Usage:
# scan only (no changes), report what would be flagged
python3 scripts/redact-screenshots.py ~/Desktop/captures/_staging/<slug> --scan

# redact in place: default patterns + mask lines containing "joseph"/"MacBook"
python3 scripts/redact-screenshots.py ~/Desktop/captures/_staging/<slug> \
--mask-text joseph --mask-text MacBook

Input images: <dir>/*.png (and .jpg/.jpeg). With --suffix, reads NN-raw.png and
writes NN-clean.png; otherwise edits files in place.

Requires the auto-capture package (macOS Vision OCR). This script auto-re-execs
under ~/Documents/github/auto-capture/.venv if that venv exists.
"""
from __future__ import annotations

import argparse
import os
import re
import shutil
import sys
from pathlib import Path

AUTO_CAPTURE_DIR = Path.home() / "Documents/github/auto-capture"
VENV_PY = AUTO_CAPTURE_DIR / ".venv/bin/python"


def _ensure_deps():
"""Re-exec under the auto-capture venv if auto_capture isn't importable.

Uses an env sentinel (not interpreter-path comparison) because on macOS a
venv built on the CommandLineTools python resolves to the same binary as
/usr/bin/python3, yet only the venv has the deps on its path.
"""
sys.path.insert(0, str(AUTO_CAPTURE_DIR))
try:
import auto_capture.redact # noqa: F401
return
except ImportError:
pass
if VENV_PY.exists() and not os.environ.get("_REDACT_REEXEC"):
os.environ["_REDACT_REEXEC"] = "1"
os.execv(str(VENV_PY), [str(VENV_PY), *sys.argv])
sys.exit(
"❌ 找不到 auto-capture 套件。請先安裝:\n"
f" git clone https://github.com/589411/auto-capture.git {AUTO_CAPTURE_DIR}\n"
f" cd {AUTO_CAPTURE_DIR} && python3 -m venv .venv && .venv/bin/pip install -e ."
)


def main() -> int:
_ensure_deps()
from auto_capture.redact import (
_apply_mosaic,
_bbox_to_pixels,
_ocr_image,
SensitiveRegion,
redact_image,
)
from auto_capture.config import DEFAULT_REDACT_PATTERNS, RedactConfig
from PIL import Image

ap = argparse.ArgumentParser(description="Redact screenshots for LaunchDock articles")
ap.add_argument("dir", type=Path, help="folder of screenshots")
ap.add_argument("--mask-text", action="append", default=[],
help="personal token; any OCR line containing it gets mosaicked "
"(repeatable, case-insensitive)")
ap.add_argument("--scan", action="store_true", help="scan only, do not modify images")
ap.add_argument("--suffix", action="store_true",
help="read NN-raw.png, write NN-clean.png (default: edit in place)")
ap.add_argument("--block-size", type=int, default=14, help="mosaic block size")
args = ap.parse_args()

stage: Path = args.dir
if not stage.is_dir():
return print(f"❌ 找不到資料夾:{stage}") or 1

pat = "*-raw.png" if args.suffix else "*.png"
imgs = sorted(p for p in stage.glob(pat)
if "-clean" not in p.name) + \
sorted(stage.glob("*.jpg")) + sorted(stage.glob("*.jpeg"))
if not imgs:
return print(f"❌ {stage} 內沒有圖片(pattern: {pat})") or 1

config = RedactConfig(enabled=True, block_size=args.block_size)
mask_tokens = [t.lower() for t in args.mask_text]
total = 0

print(f"{'🔍 掃描' if args.scan else '🔒 遮蔽'} {len(imgs)} 張圖片"
f"{'(含人工 token: ' + ', '.join(args.mask_text) + ')' if mask_tokens else ''}\n")

for src in imgs:
dst = src.with_name(src.name.replace("-raw", "-clean")) if args.suffix else src
if not args.scan and args.suffix:
shutil.copy(src, dst)

# Pass 1: default patterns (only writes when not --scan)
regions = []
if not args.scan:
_, regions = redact_image(dst if args.suffix else src, config,
output_path=dst)

# Pass 2: personal-token line masking
target = dst if (not args.scan and args.suffix) else src
ocr, (w, h) = _ocr_image(target)
line_hits = []
if mask_tokens:
img = Image.open(target)
for r in ocr:
t = r["text"].lower()
if any(tok in t for tok in mask_tokens):
x, y, bw, bh = _bbox_to_pixels(r["bbox"], w, h, padding=6)
if bw > 0 and bh > 0:
line_hits.append(r["text"])
if not args.scan:
_apply_mosaic(img, SensitiveRegion(x, y, bw, bh,
"personal_line", ""), args.block_size)
if not args.scan and line_hits:
img.save(target)
img.close()

n = len(regions) + len(line_hits)
total += n
tag = "⚠️ " if (args.scan and n) else ("🔒" if n else "✅")
print(f"{tag} {src.name}: {n} 區"
+ (f" {[r.pattern_name for r in regions] + line_hits}" if n else ""))

print(f"\n📊 {'發現' if args.scan else '遮蔽'} {total} 個敏感區域,共 {len(imgs)} 張")
if args.scan and total:
print("➡️ 重跑時拿掉 --scan 即會實際遮蔽。")
elif not total:
print("✅ 全部 clean。")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading