LaunchDãæ5æã«èµ·åãããã³ã«ãç§ãå¯ãŠããéã«åçåèšäºãçããŠããŸããã
åç·šã§ã¯ãClaude Codeãæ žã«ããèšäºçæã®åºæ¬èšèšãšã`generate.sh` ã1æ¬ã®èšäºãã©ãããäœããŸã§ã®éªšæ Œã解説ããŸãããä»åã¯ãåçåãªã³ã¯ãã©ã確å®ã«åãããããæ¥œå€©APIã倱æãããšãã«ã©ãéãããããäœåºŠå®è¡ããŠã1æ¥5æ¬ã§åæããåªçèšèšããaudit-heal.shã«ããèªå·±ä¿®åŸ©ã«ãŒãããšãããã·ã¹ãã ãæ¬åœã«åããç¶ããããã®åŸåéšåããã¹ãŠå ¬éããŸãã
æ10äžã®å€§åŠçã ã£ãããã坿¥ã§ã³ã³ãã³ããæžããŠããç§ã®åå ¥æ¹çšåŒã¯åçŽã§ããããæéÃå䟡ãã§ããæ60äžãŸã§äŒžã°ããã®ã¯ãæãæã¡ã§çšŒåæéãéçãŸã§ç©ã¿äžããããã§ãã£ãŠãä»çµã¿ã§çšŒãã§ããããã§ã¯ãããŸããã§ããã
äŒç€Ÿéœåã§è§£éããããšããåå ¥ã¯äžç¬ã§ãŒãã«ãªããŸãããæéã売ã坿¥ã¯ã売ãå ããªããªããšå³åº§ã«åŽ©å£ããŸããããã§æ°ã¥ããã®ããã皌ãç°å¢ã建ãŠãããšããšã皌ãäœæ¥ãããããšãã¯ãŸã£ããå¥ã®æŽ»åã ãšããããšã§ãã
å幎éãClaude Codeã§èªåŸç°å¢ãçµã¿ç¶ããçµæãä»ã®æå120äžã®å€§éšåã¯ç§ãäœæ¥ããŠããªãæéã«ç©ã¿äžãã£ãŠããŸãããã®ã¢ãã£ãªãšã€ããã¡ã¯ããªãŒã¯ãã®äžæ žã®ã²ãšã€ã§ãã
ã¢ãã£ãªãšã€ãèšäºã§çšŒãæå€§ã®ããã«ããã¯ã¯ããæžãããšããã¢ã¯ã·ã§ã³ã§ãã1æ¬8,000ã10,000åã®ã¬ãã¥ãŒèšäºãæžãã«ã¯ã調æ»èŸŒã¿ã§æäœ3ã4æéããããŸãããããæ¯æ¥3ã5æ¬ç¶ããããšã¯ãå°æ¥ã§ãªããã°ç©ççã«äžå¯èœã§ãã
å€ãã®ã©ã€ã¿ãŒã¯ããã§ãã©ãããã°éãæžãããããšããæ¹åã«æé©åããŸãããã³ãã¬ãŒããäœãããªãµãŒããAIã«ä»»ãããé³å£°å ¥åã䜿ããããããæå¹ã§ããã倩äºãèŠããŠããŸãã1æ¥ã®äœæ¥æéã¯æéã ããã§ãã
ã¢ãããŒããæ ¹æ¬ããå€ãããšãåããå€ãããŸãããã©ãããã°éãæžããããã§ã¯ãªãããã©ãããã°èªåãæžããªããŠæžãããããã®åãã«æ£é¢ããçããã®ããä»åã®ã·ã¹ãã ã§ãã
ãã®ã·ã¹ãã ãã²ãšããšã§è¡šçŸãããšããLaunchDãæã»æŒã»å€ã«`daily.sh`ãå©ãã仿¥ãŸã è¶³ããªãæ¬æ°ã ãèšäºãçæã»å ¬éãããã§ãã
ãã€ã³ãã¯ç§ãäœãããªããŠãããšããç¹ã§ã¯ãããŸãããæ£ç¢ºã«ã¯ãç§ãé¢äžãããšã·ã¹ãã ãå£ãããšããèšèšã«ãªã£ãŠããç¹ã§ããæåã§ãã¡ã€ã«ãè¶³ãããæ¶ããããããšãåªçæ§ã厩ããŸããlaunchdã®ã¹ã±ãžã¥ãŒã«ãèªåã§åãããšãä¿¡é ŒããŠã人éã¯è§Šããªãããã®å²ãåããã·ã¹ãã ãå®å®ãããŠããŸãã
æ5æã«launchdã`daily.sh`ãå©ããŸããèšäºãçæãããŠã¯ãŠãªããã°ã«æçš¿ãããç£æ»ãã°ãæ®ããåé¡ãããã°macOSã®éç¥ã»ã³ã¿ãŒã«èŠåãå±ããç§ã¯ãã®éç¥ã7æã«èµ·ããŠç¢ºèªããã ãã§ãã確èªã«äœ¿ãæéã¯3å以äžã§ãã
åªçïŒã¹ããšãïŒ ã¯ããäœåºŠå®è¡ããŠãçµæãåãã«ãªãããšããæ§è³ªã§ãã`daily.sh`ã®ã³ã¡ã³ãã«ãæèšãããŠããŸãã
# 1æ¥è€æ°åå®è¡ãããèªå·±å埩ãžã§ããã仿¥ãŸã å
¬éã§ããŠããªãæ¬æ°ãã ãã
# çæâå
¬éããåªçèšèšãæã䜿çšéå¶éçã§ç©ºæ¯ãããŠããæŒ/å€ã®åå®è¡ã
# èªåã§æ®ããåãããããäœåºŠèµ°ãããŠã1æ¥ã¡ããã© TARGET æ¬ã§åæãããããã¯ãã ã®ã³ã¡ã³ãã§ã¯ãªããèšèšã®æ žå¿ã§ããæã®å®è¡ã§Claude Codeã®ã¬ãŒãå¶éã«åŒã£ããã0æ¬ããçæã§ããªããŠããæŒã®å®è¡ãäžè¶³åãè£å ããŸããæŒã倱æããã°å€ãè£å ãããã©ã®ã¿ã€ãã³ã°ã§äœåå®è¡ããŠãã1æ¥ã®çµããã«ã¯ç®æšæ¬æ°ã«åæããŸãã
ãã®èšèšããªããã°ãæã®å®è¡ã倱æãããã³ã«ã仿¥ã¯ãã¡ã ã£ããã§çµãããŸããåªçèšèšãããã°ãéšåçãªå€±æã¯ã·ã¹ãã ãèªåã§åžåããŸãã
`generate.sh`ã®äžã§ClaudeãåŒã³åºãæ žå¿éšåã¯æ¬¡ã®1è¡ã§ãïŒå®éã®ã¹ã¯ãªãã272è¡ç®ïŒã
RESP=$(timeout "$GEN_TIMEOUT" "$CLAUDE" -p "$PROMPT" --allowedTools WebSearch \
--model sonnet --permission-mode auto </dev/null 2>/dev/null)`</dev/null`ã§stdinãéãã`--permission-mode auto`ã§WebSearchäœ¿çšæã®èš±å¯ããã³ããããã€ãã¹ãã`timeout "$GEN_TIMEOUT"`ã§ãã³ã°æã«åŒ·å¶çµäºãããã³ã¡ã³ãã«ã¯ã`</dev/null` å¿ é : ç¡ããšclaude -p ãstdinã3ç§åŸ ã£ãŠããé²ãïŒlaunchdã§ã¯ttyãªãã§æ¯åçºçïŒããšæžããŠãããŸããlaunchdç°å¢ç¹æã®ãããã©ããã§ãã
`GEN_TIMEOUT`ã¯ç°å¢å€æ°ã§äžæžãã§ããŸãããããã©ã«ãã¯`1200`ç§ïŒ20åïŒã§ãã8,000ã10,000åã®èšäºïŒç«¶å3補åã®WebSearch蟌ã¿ã§çæãããããããããã確ä¿ããªããšé·æã®éäžã§killããã空å¿çâ0æ¬å ¬éãšããææªã®çµæã«ãªããŸããã³ã¹ãçã«ã¯é«ãèŠããŸããã1æ¬ãããã®èšäºåäŸ¡ãšæ¯ã¹ãã°äœãåé¡ãããŸããã
launchd (æã»æŒã»å€ã1æ¥è€æ°å)
â
âŒ
daily.sh
â NEED = TARGET - æ¬æ¥å
¬éæž - ãã©ããæ® â åªçèšç®
â NEED=0 ãªãçæã¹ããã
â
ââ [NEED > 0] generate.sh à NEED æ¬
â â
â ââ Claude Code claude -p (timeout 1200s, æå€§3åãªãã©ã€)
â â ââ WebSearch ã§è£œåèª¿æ» + ç«¶å3è£œåæ¯èŒ
â â
â ââ resp_is_valid() ããªããŒã·ã§ã³
â â ââ PRODUCT:è¡ãªã / ãšã©ãŒæå«ã / 400æåæªæº â å€±ææ±ã
â â
â ââ rakuten_affiliate_url() â æ¥œå€©APIã§ååãªã³ã¯ååŸ
â â ââ è³æ Œæ
å ±ãã â APIæ€çŽ¢ (resolve: æ«å°ŸèªåãæŠç¥)
â â â ââ åœäž + ãã©ã³ãäžèŽ â affiliateUrl ååŸ
â â â ââ 400 "keyword is not valid" â èªãåã£ãŠåææŠ
â â â ââ å
šæ»
â hgc æ€çŽ¢ãªã³ã¯fallback (å ±é
¬ä¹ã)
â â ââ è³æ Œæ
å ±ãªã â çŽ ã®æ€çŽ¢URL (å ±é
¬ãŒã泚æ)
â â
â ââ postprocess_body(): çŽ ãªã³ã¯ã»ãã¬ãŒã¹ãã«ã㌠â ã¢ãã£ãªãªã³ã¯å
šå·®æ¿
â â
â ââ ~/Desktop/ã¢ãã£ãªèšäº/<YYYYMMDD_HHMMSS>.md
â
ââ post-to-hatena.sh --publish --all
â ââ posted-hatena.log ã§ã¹ãããå€å® (åªç)
â ââ å£ãèšäº (äžæãªåå / Request timed out) ã¹ããã
â ââ blogsync post --title "$title" bokuwalily.hatenablog.com
â ââ å
¬éæžã¿ â published/ ã«ã¢ãŒã«ã€ãç§»å
â
ââ audit-heal.sh
ââ å£ãèšäºã Desktop ãã¥ãŒããåé€
ââ published/ ã®å
šèšäºã hb.afl.rakuten.co.jp 嫿ãã§ãã¯
ââ å
¬éæ° < TARGET â â æªéèŠå
ââ åé¡ãã â osascript macOSéç¥ + logs/audit-YYYY-MM-DD.log`daily.sh` ã®æ žå¿ã¯10è¡ã«æºããªã NEED èšç®ã§ããå®éã®ã³ãŒãïŒ16ã23è¡ç®ïŒãèŠãŠãã ããã
# 仿¥ãã§ã«å
¬éã§ããæ¬æ°ïŒpublished/ ã®æ¬æ¥ãã¬ãã£ãã¯ã¹ïŒ
PUB_TODAY=$(find "$ARCHIVE" -maxdepth 1 -name "${TODAY}_*.md" 2>/dev/null | wc -l | tr -d ' ')
# DesktopçŽäžã«æ®ã£ãŠããæªå
¬éãã©ããïŒæã¡è¶ãïŒå段ã§äœã£ããæªæçš¿ã®åïŒ
DRAFTS=$(find "$OUT" -maxdepth 1 -name '*.md' 2>/dev/null | wc -l | tr -d ' ')
# ç®æšå°éã«å¿
èŠãªæ°èŠçææ¬æ° = ç®æš â æ¬æ¥å
¬éæž â æå
ãã©ãã
NEED=$((TARGET - PUB_TODAY - DRAFTS))
[ "$NEED" -lt 0 ] && NEED=0
echo "[daily] $TODAY $(date '+%H:%M') æ¬æ¥å
¬éæž: ${PUB_TODAY}æ¬ / ãã©ãã: ${DRAFTS}æ¬ / ç®æš: ${TARGET}æ¬ â çæ: ${NEED}æ¬"`TARGET`ã¯ã¹ã¯ãªããäžéšã§`TARGET=5`ãšããŒãã³ãŒããããŠããŸãïŒ`audit-heal.sh` ã§ã¯ç°å¢å€æ° `${AFFILIATE_FACTORY_TARGET:-3}` ã§ããã©ã«ã3æ¬ãšããŠå€éšãã倿Žå¯èœãªèšèšã§ãïŒã
éèŠãªã®ã¯ããã©ããæ®ãã NEED èšç®ã«å«ããŠããç¹ã§ããååã®å®è¡ã§çæãŸã§å®äºãããæçš¿ã«å€±æããèšäºã Desktop ã«æ®ã£ãŠããã°ã次ã®å®è¡ã§ã¯çæãå¢ããããæçš¿ã ãå詊è¡ããŸããããã«ãããçæã³ã¹ãïŒClaude APIã®æ¶è²»ïŒããšãæçš¿ãªãã©ã€ããåé¢ã§ããŠããŸãã
`generate.sh` ã®çæã«ãŒãïŒ269ã276è¡ç®ïŒã¯ã1æ¬ã®èšäºçæã«æå€§3åã®ãªãã©ã€ãèšããŠããŸãã
for attempt in 1 2 3; do
RESP=$(timeout "$GEN_TIMEOUT" "$CLAUDE" -p "$PROMPT" --allowedTools WebSearch \
--model sonnet --permission-mode auto </dev/null 2>/dev/null)
if resp_is_valid "$RESP"; then break; fi
echo "[generate] çæå€±æ(詊è¡${attempt}/3)ãå詊è¡ããŸãâŠ" >&2
RESP=""
done`resp_is_valid()` ã®å€å®ããžãã¯ïŒ251ã257è¡ç®ïŒãå ·äœçã§ãã
resp_is_valid() {
local r="$1"
[ -z "$r" ] && return 1
# PRODUCTè¡ãç¡ãïŒã¿ã€ã ã¢ãŠãçã®ãšã©ãŒæïŒæ¥µç«¯ã«çãå¿çã¯å€±ææ±ã
printf '%s' "$r" | grep -q '^PRODUCT:' || return 1
printf '%s' "$r" | grep -qiE 'request timed out|error:|rate limit|usage limit' && return 1
[ "$(printf '%s' "$r" | wc -c | tr -d ' ')" -lt 400 ] && return 1
return 0
}3åãã¹ãŠå€±æããå Žåã`generate.sh` ã¯å£ãèšäºãæžããã« `exit 1` ã§çµäºããŸãïŒ280ã282è¡ç®ïŒãå£ããç¶æ ã§ãã¡ã€ã«ãæžãåºããšãã¥ãŒãæ±æãããåŸç¶ã® audit-heal.sh ã«æé€ã³ã¹ããçºçããŸãããã®ã³ã¹ããäºååé¿ããããã®å€å®ã§ãã
ããã³ããã®äžã§ã¯åååã®1è¡ç®åºåãã©ãŒãããã`PRODUCT: <æ£åŒååå>`ããå³åœããŠããŸãããã®1è¡ãããããšã§åååã®æœåºïŒ285è¡ç®ïŒã𿬿ã®åãåºãïŒ287ã289è¡ç®ïŒã確å®ã«è¡ããŸããã¢ãã«ã«ææ§ãªåºåãããããšåŸæ®µã®ããŒã¹ãå šéšåŽ©ãããããåºåãã©ãŒãããã®åŒ·å¶ã¯å¿ é ã§ãã
楜倩APIã®æå€§ã®ãããã©ããã¯ã`400 Bad Request: "keyword is not valid"` ãšã©ãŒã§ãã"Narwal Freo Z Ultra" ã®ãããªè£œååã«å«ãŸãã "Z" ã "Ultra" ãšãã£ãåç¬ããŒã¯ã³ããæ¥œå€©ã®æ€çŽ¢ãšã³ãžã³ã«åŒŸãããŸãã
ããã解決ããã®ã `resolve()` 颿°ïŒ152ã179è¡ç®ïŒã®ãæ«å°Ÿèªã1èªãã€åã£ãŠåææŠãããæŠç¥ã§ãã
def resolve():
words = product.split()
brand = words[0].lower() if words else ""
tried = set()
for n in range(len(words), 0, -1):
keyword = " ".join(words[:n]).strip()
if not keyword or keyword in tried:
continue
tried.add(keyword)
try:
result = fetch(keyword)
except Exception as exc:
print(f"[generate] 楜倩APIæ€çŽ¢ã«å€±æ({keyword}): {exc}", file=sys.stderr)
return None
if result:
if not brand or brand in (result["name"] + " " + result["url"]).lower():
return result["url"]
# ãã©ã³ãäžäžèŽ=å¥ååã«åããã
print(f"[generate] åè£ããã©ã³ãäžäžèŽ({keyword}â{result['name'][:30]})ãæ€çŽ¢ãªã³ã¯ãžã", file=sys.stderr)
return None
time.sleep(1.0)
return None"Narwal Freo Z Ultra" â "Narwal Freo Z"ïŒ400ïŒâ "Narwal Freo"ïŒåœäžïŒãšããæµãã§ãããã ãèªãåãããããšãNarwalãåç¬ã§å šç¶å¥ã®é«ã¬ãã¥ãŒååãåŒã£ãããå¯èœæ§ããããŸãããã®ãã ãã©ã³ãåïŒå é èªïŒãååŸåååãŸãã¯URLã«å«ãŸãããã©ãã ã `brand in (result["name"] + " " + result["url"]).lower()` ã§æ€èšŒããäžèŽããªããã°ãå¥ååã«åããããšå€å®ããŠãã以äžåãã®ããããŸãã
429ïŒã¬ãŒãå¶éïŒã¯ `time.sleep(1.5)` ãæãã§æå€§2åå詊è¡ããŸãïŒ130ã148è¡ç®ã® `fetch()` å ïŒã
åååå¥ãªã³ã¯ãã©ãããŠãåããªããšãã`resolve()` 㯠`None` ãè¿ããŸãããã®åŸã®åŠçïŒ182ã190è¡ç®ïŒãæ¬è³ªçãªãã©ãŒã«ããã¯ã§ãã
affiliate_url = resolve()
if not affiliate_url:
# åå¥ååãåããªãæã¯ãã¢ãã£ãªèšæž¬ä»ãæ€çŽ¢ãªã³ã¯(hgc)ã«ãã©ãŒã«ããã¯=å¿
ãå ±é
¬ãä¹ãã
search_url_enc = quote(search_url, safe="")
affiliate_url = (
f"https://hb.afl.rakuten.co.jp/hgc/{affiliate_id}/?pc={search_url_enc}&m={search_url_enc}"
)
print(f"[generate] åååå¥ãªã³ã¯ãååŸã§ããæ€çŽ¢ãªã³ã¯ã«ãã©ãŒã«ããã¯: {product}", file=sys.stderr)
print(affiliate_url)`hb.afl.rakuten.co.jp/hgc/` ã¯æ¥œå€©ã¢ãã£ãªãšã€ãã®ã¢ãã£ãªèšæž¬ä»ãæ€çŽ¢ãªã³ã¯ã§ããåå¥ååãžã®ãªã³ã¯ã§ã¯ãªãããã®åååã§æ¥œå€©åžå Žãæ€çŽ¢ããçµæããŒãžããžã®ãªã³ã¯ã«ãªããŸãããå ±é ¬ã¯ä¹ããŸãã
ç°å¢å€æ° `RAKUTEN_APPLICATION_ID` / `RAKUTEN_ACCESS_KEY` / `RAKUTEN_AFFILIATE_ID` ã®ããããã空ã®å ŽåãAPIåŒã³åºãèªäœãè¡ãããçŽ ã®æ€çŽ¢URLïŒ`https://search.rakuten.co.jp/search/mall/ã`ïŒã«ãã©ãŒã«ããã¯ããŸãïŒ83ã87è¡ç®ïŒããã®ç¶æ ã§ã¯å ±é ¬ããŒãã«ãªããŸãã`.env` ã®èšå®ãã¹ã§æ°ã¥ããã«éçšããŠããŸãã®ãã¢ãã£ãªåçãŒãã®å žåçãªåå ã§ãã
Claude Codeãçæããèšäºæ¬æã«ã¯ãçŽ ã®æ¥œå€©æ€çŽ¢URLãå«ãŸããããšããããŸããããã³ããã§ã¢ãã£ãªãªã³ã¯ãå ¥ããããæç€ºããŠããã¢ãã«ãéã¢ãã£ãªã®URLãæžããŠããå ŽåããããŸããããããã®ãŸãŸå ¬éãããšå ±é ¬ããŒãã«ãªããŸãã
`postprocess_body()`ïŒ201ã246è¡ç®ïŒã¯ãã®åé¡ãåŸåŠçã§æ ¹æ²»ããŸãã
# 2) claudeãæ¬æã«æžããå®ãªã³ã¯ [楜倩ã§ãâŠããæ¢ã](ä»»æURL) ããæ£ããã¢ãã£ãªãªã³ã¯ã«äžžããšå·®ãæ¿ããã
# ïŒããããããªããš claude ãæžããéã¢ãã£ãªã®æ€çŽ¢URLããã®ãŸãŸæ®ãïŒå ±é
¬ãŒãã«ãªãïŒ
rakuten_md_link_re = re.compile(r"\[楜倩ã§ã[^ã]*ããæ¢ã\]\([^)]*\)")
body = rakuten_md_link_re.sub(lambda m: link, body)
# 3) 念ã®ãããæ¥œå€©ãã¡ã€ã³ãæãçŽ ã®markdownãªã³ã¯ãå·®ãæ¿ãã
rakuten_any_re = re.compile(r"\[[^\]]+\]\((?:https?:)?//[^)]*rakuten\.co\.jp[^)]*\)")
body = rakuten_any_re.sub(lambda m: link, body)ããã«ãã¬ãŒã¹ãã«ããŒãžã®å¯Ÿå¿ããããŸãïŒ219ã224è¡ç®ïŒãClaudeãã`ïŒâŒæ¥œå€©ã§ãããããæ€çŽ¢ããŠãªã³ã¯ã貌ãïŒ`ãã®ãããªãã¬ãŒã¹ãã«ããŒãæžããŠããããšãããããããæ£èŠè¡šçŸã§æ€åºããŠçœ®æããŸãã
3段éã®å·®æ¿ããžãã¯ã«ãããã©ããªåœ¢ã§ãªã³ã¯ãèšè¿°ãããŠããŠãæçµçã«ã¯æ£ããã¢ãã£ãªãšã€ãURLã«åæããŸããããããåçåãªã³ã¯ã®ç¢ºå®ãªåã蟌ã¿ãã®å®äœã§ãã
`post-to-hatena.sh`ã®`--all`ã¢ãŒãã¯ã`posted-hatena.log` ã§ã¹ãããå€å®ãè¡ããŸãïŒ40ã61è¡ç®ïŒãäžåºŠæçš¿ãããã¡ã€ã«ã®ãã«ãã¹ããã°ã«èšé²ããæ¬¡ã®å®è¡æã«åããã¡ã€ã«ãæ®ã£ãŠããŠãäºéæçš¿ããŸããã
for f in "$OUT"/*.md; do
[ -e "$f" ] || continue
if /usr/bin/grep -qxF "$f" "$POSTED_LOG"; then continue; fi
# çæå€±æã®æ®éªžã¯æçš¿ããªã
if /usr/bin/grep -qE 'äžæãªåå|Request timed out' "$f"; then
echo "[hatena] ã¹ããã(çæå€±æã®æ®éªž): $f" >&2; continue
fi
if post_one "$f"; then
echo "$f" >> "$POSTED_LOG"; found=$((found+1))
[ -z "$DRAFT_FLAG" ] && mv "$f" "$ARCHIVE/" && echo "[hatena] ã¢ãŒã«ã€ããžç§»å: $(basename "$f")"
fi
done`--publish` ãã©ã°ä»ãã§å®è¡ããå Žåã®ã¿ãæçš¿æžã¿ãã¡ã€ã«ã `published/` ãã£ã¬ã¯ããªã«ç§»åããŸããDesktop ã®ãã¥ãŒããå€ãããšã§ããpublished/ ã«ããæ¬æ¥åã®ã«ãŠã³ãããå¢ããæ¬¡ã® `daily.sh` å®è¡æã« `PUB_TODAY` ãæ£ããã«ãŠã³ããããŸãããã®ã¢ãŒã«ã€ãç§»åãåªçèšèšã®æ¯è»ãšããŠæ©èœããŠããŸãã
`daily.sh` 㯠`post-to-hatena.sh --publish --all` ãšããŠåŒã³åºããŠããããïŒ36è¡ç®ïŒãå ¬éãšåæã«èªåã§ã¢ãŒã«ã€ãã«ç§»åããŸãã
æåŸã®ã¹ãããã `audit-heal.sh` ã§ãã3ã€ã®åœ¹å²ãé çªã«å®è¡ããŸãã
1. å£ãèšäºã®æé€ïŒ23ã29è¡ç®ïŒ
for f in "$OUT"/*.md; do
[ -e "$f" ] || continue
if /usr/bin/grep -qE 'äžæãªåå|Request timed out' "$f"; then
log " [æé€] å£ãèšäºãåé€: $(basename "$f")"
rm -f "$f"
fi
done`resp_is_valid()` ã§åŒŸããŠå£ãèšäºãæžããªãèšèšã«ã¯ãªã£ãŠããŸãããéå»ã®å®è¡ãæåä»å ¥ã§ãã¥ãŒã«æ··å ¥ããå Žåã®ã»ãŒããã£ãããã§ãã"äžæãªåå" ã "Request timed out" ãå«ããã¡ã€ã«ã¯åçç¡çšã§åé€ããŸãã
2. ã¢ãã£ãªãªã³ã¯å«æãã§ãã¯ïŒ36ã44è¡ç®ïŒ
for f in "$ARCHIVE/${TODAY}"_*.md; do
published=$((published+1))
title="$(sed -n 's/^# //p' "$f" | head -1 | cut -c1-30)"
if /usr/bin/grep -q 'hb.afl.rakuten.co.jp' "$f"; then
link="âã¢ãã£ãª"
else
link="âéã¢ãã£ãª"; bad_link=$((bad_link+1)); problems=$((problems+1))
fi
log " ${link} | ${title}"
doneå ¬éæžã¿ã®å šèšäºã« `hb.afl.rakuten.co.jp` ãå«ãŸããŠããããã§ãã¯ããŸãã`postprocess_body()` ãæ£åžžã«åäœããŠããã°å šä»¶ `âã¢ãã£ãª` ã«ãªããŸãããäœããã®çç±ã§å·®æ¿ã倱æããå Žåã«ããã§æ€åºã§ããŸãã
3. ç®æšéæãã§ãã¯ãšmacOSéç¥ïŒ52ã60è¡ç®ïŒ
if [ "$published" -lt "$TARGET" ]; then
log " â å
¬éãç®æšæªéïŒçæ or å
¬éã倱æããå¯èœæ§ïŒ"
problems=$((problems+1))
fi
if [ "$problems" -gt 0 ]; then
log " â ç£æ»NG: èŠç¢ºèª (${problems}ä»¶)"
notify "ç£æ»NG: å
¬é${published}/${TARGET}æ¬ã»éã¢ãã£ãª${bad_link}æ¬ãlogs/audit-${TODAY}.log ã確èª"
exit 1
else
log " â
ç£æ»OK: ${published}æ¬ãã¹ãŠã¢ãã£ãªãªã³ã¯ä»ãã§å
Ž"
exit 0
fi`notify()` 㯠`osascript -e "display notification..."` ã§macOSã®éç¥ã»ã³ã¿ãŒã«é£ã°ããŸãããã°ãã¡ã€ã«ã¯ `logs/audit-YYYY-MM-DD.log` ã«æ®ããããåé¡ã®çºçæ¥æãšå 容ãåŸãã远跡ã§ããŸãã
ãã®ç£æ»ã `exit 1` ã§çµãããšã`daily.sh` åŽã§ããâ ç£æ»NGããã³ã³ãœãŒã«ã«åºåããŸãïŒ40è¡ç®ïŒãlaunchdã®å®è¡ãã°ãèŠãã°ãã©ã®å®è¡ã§äœãèµ·ãããã远跡ã§ããŸãã
次åã¯ããã®ä»çµã¿ãå®éã«ç«ã¡äžãããšãã«ç§ãã¶ã€ãã£ã倱æïŒ`.env` æ¶æ» ã«ããå ±é ¬ãŒãåçºã»launchd plistç Žæã»èªå·±ä¿®åŸ©ãŠã©ããããã°ãèªåã§ãã¡ã€ã«ãå£ããäºæ ïŒãšãåãèœãèžãŸãªãããã®èšèšæéãæžããŸãã
èšäºå·¥å Žãæåã«çŽé¢ããã®ã¯ãéè€ã³ã³ãã³ãã§ããLaunchDãæ¯æ¥èµ°ããšããããšã¯ãæ¯æ¥ãããªããæ±ºãããžã£ã³ã«ã§æ°è£œåã1æ¬æžããŠããšClaudeã«æç€ºããããšã«ãªããŸãããªã«ãæãæããªããšãããããæé€æ©ã®èšäºã30æ¬äžŠãã§ãå šéšãRoborock S8 Pro Ultraããšããå°çã«ãªããŸãã
解決ããã®ã `posted-products.log`ïŒå®éã®ãã¹ã¯ `$AFFILIATE_FACTORY_LOG`ïŒãšããããããã³ããã«åã蟌ãä»çµã¿ã§ãã`generate.sh` ã®8ã19è¡ç®ããã®å®è£ ã§ãã
LOG="${AFFILIATE_FACTORY_LOG:-$DIR/posted-products.log}"
# ...
touch "$LOG"
# æ¢åºååïŒéè€åé¿çšïŒ
EXCL=$(paste -sd 'ã' "$LOG" 2>/dev/null)
[ -z "$EXCL" ] && EXCL="ïŒãŸã ç¡ãïŒ"ãã®`EXCL`倿°ãããã³ããã® `# é€å€ïŒãããã®è£œåã¯ä»åéžã°ãªãïŒ` ã»ã¯ã·ã§ã³ã« `$EXCL` ãšããŠå·®ã蟌ãŸããŸãã1æ¬çæãããã³ã«ããã¡ã€ã«æ«å°Ÿã«åååã1è¡è¿œèšãããã®ã§ïŒ298è¡ç®ïŒã100æ¬æºãŸãã°100補åãé€å€ãªã¹ãã«äžŠã³ãŸãã
[ "$PRODUCT" != "äžæãªåå" ] && echo "$PRODUCT" >> "$LOG"èã¯ãã¢ãã«ãžã®å¶çŽã¯ããã³ããã§äŒããã ãšããäžç¹ã§ããã³ãŒãã§éè€ãã§ãã¯ãããããšãããšãã¿ã€ãã«ã®è¡šèšããããã©ã³ãéããåžåããããã®æååãããã³ã°ãå¿ èŠã«ãªããäŸå€åŠçãèšãã¿ãŸãããéå»ã«éžãã åååäžèŠ§ãæž¡ããŠããããéžã°ãªãããæç€ºãããã ãã§ãClaudeããããªã«é¿ããŠãããŸããã¢ãã«ã«ä»»ããŠããä»äºã¯ç©æ¥µçã«ã¢ãã«ã«æããããšããææ³ãããã«åºãŠããŸãã
`generate.sh` ã®12è¡ç®ã«ã¯1è¡ã ããããªæžãæ¹ããããŸãã
[ -f "$DIR/.env" ] && set -a && . "$DIR/.env" && set +a`set -a` ã¯ã以éã«å®çŸ©ãã倿°ãèªåã§exportãããã¢ãŒãã`set +a` ã§è§£é€ããŸãã`.env` ã `source` ããã ãã ãšãbashã®æåã«ãã£ãŠã¯å€æ°ããµãããã»ã¹ã«åŒãç¶ãããªãããšããããŸããæ¥œå€©APIã®èªèšŒæ å ±ïŒ`RAKUTEN_APPLICATION_ID` ãªã©ïŒã¯Python3ãµãã·ã§ã«ã«æž¡ãå¿ èŠãããããã`set -a` ã§export蟌ã¿ããŒãããŠããŸãã
`.env` ãååšããªãå Žåã¯äœãèµ·ããŸãããgitã«ã¯ `.env` ã远å ãã `.env.example` ã ãã³ããããããšããæšæºçãªæ§æã§ãããã.env ããªããŠãã¹ã¯ãªãããèœã¡ãªããããšãæå€ãšéèŠã§ããlaunchdã¯ã·ã¹ãã èµ·åæã«ãçºç«ããããã`.env` ããªããã°ç°å¢å€æ°ã空ã®ãŸãŸå®è¡ãããŸãããã®å Žå㯠`RAKUTEN_APPLICATION_ID` ãæªå®çŸ©ã«ãªãã`generate.sh` ã®83ã86è¡ç®ã®æ¡ä»¶ã§ `rakuten_search_url()` ã«ãã©ãŒã«ããã¯ââã€ãŸããå ±é ¬ã®ã€ããªãçŽ ãªã³ã¯ãã§èšäºãæžãç¶ããŸããã¹ã¯ãªããã¯èœã¡ãªããåçããŒãããšããææªã®ãµã€ã¬ã³ã倱æã§ããåŸè¿°ãããç§ãè©°ãŸã£ã話ãã§ãããå®éã«ãããããŠããŸãã
`generate.sh` ã®260ã263è¡ç®ã®ã³ã¡ã³ãã«ãç§ã詊è¡é¯èª€ããè·¡ããã®ãŸãŸæ®ã£ãŠããŸãã
# ãã«èšäºçæã®æèŠæéãåŸæ¥(2500åã»æ€çŽ¢æ°å)ã§çŽ260sã ã£ãããæ¬æã8000ã10000åïŒ
# ç«¶å3補åã®è¿œå WebSearchã«å¢ãããããçæã䌞ã³ããäœè£ãæã£ãŠå®æž¬ã®æ°åã確ä¿ããã
# çããšé·æã®éäžã§killãã空å¿çâ0æ¬å
¬éã«ãªããããããã¯ã±ãããªãã
GEN_TIMEOUT="${AFFILIATE_FACTORY_GEN_TIMEOUT:-1200}"æå㯠`GEN_TIMEOUT=300` ã§åãããŠããŸãããåçŽãªçæãªãWebSearchæ°å蟌ã¿ã§4ã5åã§çµãããŸãããšããããç«¶å3補åãWebSearchããŠå®åšã¹ããã¯ã確èªããŠããæžãããšããæç€ºãè¶³ããé端ãå¹³åã12ã15åã«äŒžã³ãŸããã`timeout 300` ã¯300ç§ã§ããã»ã¹ãkillããã®ã§ãé·æçæã®éäžã§Claudeã匷å¶çµäºããã`RESP` ã空ã«ãªããŸãã`resp_is_valid()` ã空ã匟ããŠ3åãªãã©ã€ãå šæ» ã㊠`exit 1`ââãããã仿¥ã¯1æ¬ãçæãããªãã£ããã®æ£äœã§ããã
`1200`ç§ïŒ20åïŒã¯å®æž¬ã®çŽ1.5åã§ãã`AFFILIATE_FACTORY_GEN_TIMEOUT` ã§äžæžãã§ããããã«ããŠããã®ã¯ãå°æ¥ããã³ãããçãå€ãããšãã«ã³ãŒããè§Šããã«èª¿æŽã§ããããã«ããããã§ããç°å¢å€æ°ã§ããã©ã«ãå€ãæã¡ãå¿ èŠãªãå€ããäžæžããšãããã¿ãŒã³ã¯ãã¹ã¯ãªããå šäœã§çµ±äžããŠããŸãïŒ`AFFILIATE_FACTORY_OUT`ã»`AFFILIATE_FACTORY_LOG`ã»`AFFILIATE_FACTORY_TARGET` ãåãæ§é ã§ãïŒã
Claude APIã¯åŒã³åºããã³ã«èª²éãããŸããããžãã¯ã®å€æŽããã¹ããããã³ã«å®éã«APIãå©ãã®ã¯ãã³ã¹ããšããŠãæéãšããŠãéå¹çã§ãããã®ããã« `generate.sh` 265ã266è¡ç®ã«å·®ã蟌ãã ã®ã `AFFILIATE_FACTORY_TEST_RESPONSE` ã§ãã
if [ -n "${AFFILIATE_FACTORY_TEST_RESPONSE:-}" ]; then
RESP="$AFFILIATE_FACTORY_TEST_RESPONSE"
else
for attempt in 1 2 3; do
RESP=$(timeout "$GEN_TIMEOUT" "$CLAUDE" -p "$PROMPT" ...)ãã®ç°å¢å€æ°ã«ãããŒå¿çãæž¡ããŠã¹ã¯ãªãããåãããšãAPIãåŒã°ãã« `resp_is_valid()` â åååæœåº â `postprocess_body()` â ãã¡ã€ã«æžãåºããŸã§ãå šéšèµ°ãããããŸããããšãã°æ¬¡ã®ããã«äœ¿ããŸãã
export AFFILIATE_FACTORY_TEST_RESPONSE='PRODUCT: ãã¹ãæé€æ© X100
# ã2025幎ããã¹ãæé€æ© X100 å
šã¹ããã¯è§£èª¬
> â»æ¬èšäºã¯ã¢ãã£ãªãšã€ãããã°ã©ã ãå©çšããŠããŸãã
[:contents]
## ãã®èšäºã§ãããããš
ãã¹ãèšäºã§ãã'
bash generate.sh 1ããã§ã楜倩ãªã³ã¯ãæ£ããå·®ãæ¿ãã£ãŠããããããã¡ã€ã«åãã¿ã€ã ã¹ã¿ã³ãä»ãã§äœãããŠãããããposted-products.logã«åååã远èšãããŠãããããäžæ°ã«ç¢ºèªã§ããŸããæ¬çªç°å¢ã®ã³ãŒããã¹ããã®ãŸãŸéãã®ã§ããŠããããã¹ããšã¯éãå®éã®æåã®ç¢ºèªã«ãªããŸãã
`postprocess_body()` ã®æåŸã«ãæçµçãªåºåã®çµã¿ç«ãŠé ãããŒãã³ãŒããããŠããŸãïŒgenerate.sh 240è¡ç®ïŒã
out = [title, "", disclaimer, "", contents]`""` ã2ã€ããããšã«æ°ã¥ãã§ãããããã¿ã€ãã«ã®åŸãã«ç©ºè¡ã`disclaimer`ïŒå 責ã®blockquoteïŒã®åŸãã«ã空è¡ã眮ããŠãã `[:contents]`ïŒç®æ¬¡ïŒãå ¥ããŠããŸãã
æå㯠`[title, disclaimer, contents]` ãšè©°ããŠæžããŠããŸããããã®ãšããã¯ãŠãªããã°äžã§ãªããç®æ¬¡ã衚瀺ãããªãã£ãããç®æ¬¡ãå 責blockquoteå ã«åžã蟌ãŸããŠèŠãç®ã厩ãããããåé¡ãèµ·ããŸãããåå ã調ã¹ããšãã¯ãŠãªã®MarkdownåŠçç³»ã¯blockquoteïŒ`>` ã§å§ãŸãè¡ïŒã®çŽåŸã«ç©ºè¡ãªãã§`[:contents]`ãæ¥ããšãããããblockquoteã®ç¶ç¶ããšããŠåŠçããŠããŸãããšããããšãã仿§äžã®æåã§ããã
空è¡ã1è¡æãããšã§ãããŒãµãŒããblockquoteãããã§çµãã£ãããšå€æãã`[:contents]` ãç¬ç«ããç®æ¬¡æä»€ãšããŠè§£éãããŸããããã¯ã¯ãŠãªMarkdownç¹æã®ã¯ã»ã§ããæ®éã®Markdownã¬ã³ãã©ãŒãnoteãZennã§ã¯èµ·ããŸããã
`post-to-hatena.sh` ã«ã¯ã¹ã¯ãªããäžéšã«1ã€ã®ã¬ãŒãåŠçããããŸãïŒ19ã24è¡ç®ïŒã
CFG="$HOME/.config/blogsync/config.yaml"
if [ ! -f "$CFG" ] || /usr/bin/grep -q 'REPLACE_' "$CFG"; then
echo "[hatena] ã¹ããã: $CFG ãæªèšå®ã§ãïŒã¯ãŠãªID/APIããŒæªå
¥åïŒã" >&2
exit 0
fi`blogsync` ã®èšå®ãã¡ã€ã«ã« `REPLACE_` ãšããæååãæ®ã£ãŠããïŒïŒãã³ãã¬ã®ãŸãŸæªèšå®ïŒå Žåãéãã« `exit 0` ããŠäœãããŸããã`daily.sh` ããåŒã°ããŠããæçš¿0æ¬ããšããŠæ±ãããŸããããããªããšãèšå®å¿ãã®ãŸãŸã¹ã¯ãªãããèµ°ã£ãŠ `blogsync` ããšã©ãŒãåãã`daily.sh` å šäœãæ¢ãŸãå¯èœæ§ããããŸããã
ããã²ãšã€éèŠãªã®ã `post_one()` 颿°å ã®ã¿ã€ãã«åé¢ã§ãïŒ26ã38è¡ç®ïŒã
title="$(sed -n 's/^# //p' "$file" | head -1)"
[ -z "$title" ] && title="$(basename "$file" .md)"
body="$(awk 'NR==1 && /^# /{next} {print}' "$file")"Markdownãã¡ã€ã«ã®1è¡ç®ã® `# ã¿ã€ãã«` ãæãåºããŠblogsyncã® `--title` åŒæ°ã«æž¡ããæ¬æããã¯ãã®1è¡ç®ãé€ããŠæçš¿ããŸãããããããªããšããã°ã®èŠåºãããH1ã¿ã€ãã«ããšããŠèšäºã®äžã«äžžããšå ¥ããã¯ãŠãªããã°ã®èšäºã¿ã€ãã«ãšèšäºå H1ãäºéã«ãªããŸããSEOçã«ãèŠãç®çã«ãææªãªã®ã§ãæ¬æããH1ãå¥ãã㊠`--title` ã«æž¡ãã®ãæ£ããèšèšã§ãã
æåã«ãã®åé¡ã«æ°ã¥ããã®ã¯ã1é±éã¶ãã«æ¥œå€©ã¢ãã£ãªãšã€ãã®ç®¡çç»é¢ãéãããšãã§ããèšé²äžã¯æ¯æ¥5æ¬å ¬éãããŠããã®ã«ãå ±é ¬ã®ã°ã©ãããŸã£ããåããŠããŸããã§ããã
`audit-heal.sh` ã®ãã°ãé¡ããšã`â ç£æ»OK` ã䞊ãã§ããŸãã`hb.afl.rakuten.co.jp` ã嫿ãã§ãã¯ããŠããã¯ããªã®ã«ããªãOKãåºãã®ããpublished/ é äžã®èšäºãçŽæ¥ `grep hb.afl.rakuten.co.jp` ãããšã1ä»¶ã該åœãªãã§ããã
`generate.sh` ãæåã§èµ°ãããŠã¿ããšãã³ã³ãœãŒã«ã«æ¬¡ã®ãã°ãæµããŸããã
[generate] åååå¥ãªã³ã¯ãååŸã§ããæ€çŽ¢ãªã³ã¯ã«ãã©ãŒã«ããã¯: Panasonic NA-LX129Bãã®ãã°ã¯ãåå¥ååãåããªãã£ãããhgcãªã³ã¯ã«èœãšããã§ã¯ãªããæ¥œå€©APIãåŒã³åºãåã®ããã£ãšæåã®åå²ã§åºãŠããŸããã`RAKUTEN_APPLICATION_ID` ã空æåã®ãšããPythonã³ãŒãã«å ¥ãåã®83ã86è¡ç®ã§çŽ ã®æ€çŽ¢URLãè¿ããŸãã
if [ -z "${RAKUTEN_APPLICATION_ID:-}" ] || [ -z "${RAKUTEN_ACCESS_KEY:-}" ] || [ -z "${RAKUTEN_AFFILIATE_ID:-}" ]; then
rakuten_search_url "$product"
return
fi`rakuten_search_url()` 㯠`search.rakuten.co.jp`ïŒå ±é ¬ãŒãïŒãè¿ããŸãã`postprocess_body()` ã¯ãããã¢ãã£ãªãªã³ã¯ãšããŠåã蟌ãããããªã³ã¯èªäœã¯ååšããŸããããã `hb.afl.rakuten.co.jp` ã§ã¯ãªãã®ã§ãaudit-heal.sh ã®ãã§ãã¯ãããæããŠãéã¢ãã£ãªããšæ€åºãããªããŸãŸå ¬éãããã®ã§ãã
åå 㯠`.env` ã®æ¶æ» ã§ããããã®ã·ã¹ãã ãšã¯å¥ã®èªååã¹ã¯ãªããïŒèªå·±ä¿®åŸ©ãŠã©ããããã°ïŒãåããã£ã¬ã¯ããªã察象ã«åããŠãããåŸè¿°ããäºæ ã§ `affiliate-factory/.env` ãäžæžãæ¶å»ãããŠããŸãããgitignoreã§ç®¡çå€ãªã®ã§ã`git checkout`ã§ã埩å ã§ããŸããã
çŽãæ¹ã¯2段éã§ãã
â ç£æ»ã®æ€åºããžãã¯ãä¿®æ£ããïŒ`audit-heal.sh` ã®å«æãã§ãã¯ã `hb.afl.rakuten.co.jp` ã ãã§ãªã `search.rakuten.co.jp` ããéã¢ãã£ãªããšããŠæç€ºçã«åŒŸãããã«å€ããŸãããçŽ ã®æ€çŽ¢URLãæ··å ¥ããæç¹ã§ `âéã¢ãã£ãª` ãç«ãŠãã°ã`problems > 0` â macOSéç¥ã§å³åº§ã«æ°ã¥ããŸãã
â¡`.env` ãèªå·±ä¿®åŸ©ã®å°çšå€ã«çœ®ãïŒç§å¯å€ãå«ããã¡ã€ã«ã¯çµ¶å¯Ÿã«ã¹ã¯ãªãããæžãæããŠã¯ãããŸãããèªå·±ä¿®åŸ©ãŠã©ããããã°ã®ã¹ã³ãŒãããçæãã°ãšãã©ãããã¡ã€ã«ã®ã¿ãã«éå®ãã`.env` ãèšå®ãã¡ã€ã«çŸ€ãæç€ºçã«é€å€ããããã«ä¿®æ£ããŸããã
2åç®ã®ããããã ã£ãã®ã§ããããã«æããèªåã«åããŸãããç§å¯å€ãæã€ãã¡ã€ã«ã¯ãèªååã®æãå±ããªãèåããšããŠæåããèšèšã«å ¥ããã¹ãã§ããã
ããã¯ãæã話ãç³»ã®å€±æã§ãã
å¥ã®ãããžã§ã¯ãã§ãèªå·±ä¿®åŸ©ãŠã©ããããã°ããšåŒãã§ããã¹ã¯ãªãããäœã£ãŠããŸãããã¹ã¯ãªãããç°åžžçµäºãããåºåããããããªã£ãããããšããæ¢å®ã®ãã¡ã€ã«ã»ãããæžãçŽããŠä¿®åŸ©ãããšããä»çµã¿ã§ãã
ãã®ãŠã©ããããã°ã« `fd77c12 fix(self-repair)` ãšããã³ãããã§ä¿®æ£ãå ¥ããçŽåŸã`affiliate-factory` ãã£ã¬ã¯ããªãå£ãå§ããŸãããå ·äœçã«ã¯ïŒ
`.env` ã0ãã€ãã«äžæžãããã
`post-to-hatena.sh` ãå¥ã®å 容ïŒåããŒãžã§ã³ã®ã³ãŒãïŒã«çœ®ãæãã£ã
launchd plistïŒåŸè¿°ïŒãææ³ãšã©ãŒãå«ãå 容ã«ç Žæãã
ãã¹ãŠã®ãã¡ã€ã«ã® `mtime` ãåãæå»ã«ãªã£ãŠããããäœããäžæã«æžãçŽãããã®ã¯æããã§ããã
åå ã¯ããŠã©ããããã°ãåºåãæšæºåºåã«æžãåºããªããåæã«ãã¡ã€ã«ãæäœããåŠçã§ãã·ã§ã«å€æ°ã®å±éãšãªãã€ã¬ã¯ãã®é åºããã¿åãããã¿ãŒã²ãããã¡ã€ã«ã決ãŸãåã«ãªãã€ã¬ã¯ãå ãéãããŠå å®¹ãæ¶ãããšããã·ã§ã«ã®å žåçãªçœ ã«ã¯ãŸã£ãŠããŸãããçµæãšããŠãæå³ããŠããªããã¹ã®ãã¡ã€ã«ãäžæžããããŸããã
çŽãæ¹ã¯ããŠã©ããããã°ã®æžã蟌ã¿åŠçããã¹ãŠãäžæãã¡ã€ã«ã«æžããŠãã `mv` ã§ååçã«å·®ãæ¿ããããã¿ãŒã³ã«å€ããããšã§ããã
# NG: ãªãã€ã¬ã¯ãããã¡ã€ã«ãéããæç¹ã§TARGET_FILEã空ã«ãªãåŸã
some_command > "$TARGET_FILE"
# OK: tmpã«æžããŠããmvã§åå眮æ
some_command > "$TARGET_FILE.tmp" && mv "$TARGET_FILE.tmp" "$TARGET_FILE"ããã«ãèªå·±ä¿®åŸ©ã¹ã¯ãªãããæžãæããŠãããã¡ã€ã«ã¯äœãããæç€ºçã«ãã¯ã€ããªã¹ãåããŸããããã以å€ã®ãã¡ã€ã«ã«ã¯è§Šããªãããã«ã¬ãŒããå ¥ããŠããŸããèªå·±ä¿®åŸ©ãšãããåªããæ©èœãã¯ãã¹ã³ãŒãå¶éããªããã°æå¶ã®ç Žå£è ã«ãªããŸãã
äžèšã®äºæ ã§ launchd plist ãç Žæãããšããããã«ã¯æ°ã¥ããŸããã§ãããMacã¯Sleep/Wakeãç¹°ãè¿ããŠããã ãã§launchdãžã§ããåç»é²ãããªããããplistãå£ããŠããŠããã°ã«äœãåºãŸãããèšäºãçããŠããªããã§ãæåã§ `bash daily.sh` ãå©ãã°åãââãã®ç¶æ ã2é±éç¶ããŸããã
æ°ã¥ããã®ã¯ `launchctl list | grep affiliate` ãå©ãããšãããžã§ããäžèЧã«åºãªãã£ãããã§ãã
# ãžã§ããç»é²ãããŠããã確èª
launchctl list | grep affiliate
# â åºåãªãïŒç»é²ãããŠããªãïŒ
# plist ã®ææ³ãã§ãã¯
plutil ~/Library/LaunchAgents/com.affiliate-factory.daily.plist
# â com.affiliate-factory.daily.plist: Unexpected character < at line 3
# 修埩ïŒã¢ã³ããŒãããŠplistãçŽããŠãªããŒã
launchctl unload ~/Library/LaunchAgents/com.affiliate-factory.daily.plist 2>/dev/null || true
# plistãæ£ããå
å®¹ã«æžãçŽã
launchctl load ~/Library/LaunchAgents/com.affiliate-factory.daily.pliståçºé²æ¢ãšããŠã`audit-heal.sh` ã®æ«å°Ÿã«æ¬¡ã®ç¢ºèªã远å ããŸããã
# launchdãžã§ããçããŠããã確èªïŒåæ¢äžãªãèŠåã ãåºãïŒ
if ! launchctl list 2>/dev/null | grep -q 'affiliate-factory'; then
log " â launchdãžã§ããæªç»é²ãplistã確èªããŠãã ãã"
problems=$((problems+1))
fiæ¯æã®ç£æ»ã§ãžã§ãç»é²ç¶æ³ããã§ãã¯ããããšã§ããåããŠããªãç¶æ ããç¿æã«ã¯å¿ ãæ€åºã§ããããã«ãªããŸããã
launchd plistã«ã€ããŠ1ç¹ã ãè£è¶³ããŸããplistã«æžãã³ãã³ãã¯çµ¶å¯Ÿãã¹ã§ãªããã°ãªããŸããã`/bin/bash` ã¯è¯ãã§ããã`bash` ã¯äžå¯ã§ãããŸã `PATH` ç°å¢å€æ°ã¯ `/usr/bin:/bin` çšåºŠããéã£ãŠããªãããã`nvm` çµç±ã§ã€ã³ã¹ããŒã«ããã³ãã³ããã`~/.local/bin/` ã®ãã€ããªã¯ãã«ãã¹æå®ãå¿ é ã§ãããã®ã·ã¹ãã ã§ã¯ `generate.sh` å ã® `CLAUDE` 倿°ïŒ9è¡ç®ïŒã§claudeã®ãã«ãã¹ãæå®ããŠããã®ã¯ãã®ããã§ãã
CLAUDE="${CLAUDE:-~/.local/bin/claude}"ãã¹ãç°å¢å€æ°ã§äžæžãã§ããèšèšã«ããŠããããšã§ãclaudeã®ã€ã³ã¹ããŒã«å ãå€ãã£ããšããã³ãŒããè§Šããã«å¯Ÿå¿ã§ããŸãã
3ã€ã®å€±æã«å ±éããã®ã¯ãèªååãèªåèªèº«ãå£ãããšããæ§é ã§ããå£ããããšãåæã«èšèšããŠãã `audit-heal.sh` ããå£ããããšã«æ°ã¥ããªããŸãŸå£ããŠããã®ãæå€§ã®ç®èã§ãããæ¬¡ã®ã»ã¯ã·ã§ã³ã§ã¯ããããã®å€±æããå°ããèšèšæéãšããã®ã·ã¹ãã ã0ããç«ã¡äžããæçã«ãŒãããŸãšããŸãã
ååã®äžæ®µã§ã¯ `.env` æ¶æ» ã»èªå·±ä¿®åŸ©ã®æŽèµ°ã»launchd plist ç Žæãšãã3倧倱æãè§£åããŸããããã®çµæ®µã§ã¯ããã以å€ã«å®éçšã§èžãã 现ããªã€ãŸãããäžèЧã«ããããããåŒãåºããèšèšæéããã¹ããã©ã¯ãã£ã¹ãšããŠæŽçããŸãã
ãããŸã§è§£èª¬ãã3倧倱æã«å ããŠãã³ãŒããå®éã«åãããšå¿ ãäžåºŠã¯ã¶ã€ãããã€ã³ãããŸãšããŸããç®æ¡æžãã§äžŠã¹ãŠããŸãããã©ãããå®éã«ãããããããã®ããèšèšäžã®èœãšã穎ãšããŠåŸããæ°ã¥ããããã®ã§ãã
â TARGET ã®æ°å€ã daily.sh ãš audit-heal.sh ã§ãºã¬ãŠãã
`daily.sh` 10è¡ç®ã¯ `TARGET=5` ã®ããŒãã³ãŒããäžæ¹ `audit-heal.sh` 11è¡ç®ã¯ `TARGET="${AFFILIATE_FACTORY_TARGET:-3}"` ã§ãç°å¢å€æ°æªèšå®æã®ããã©ã«ãã3ã§ãããã®ç¶æ ã§äž¡ã¹ã¯ãªãããåãããšãdaily.sh ã¯æ¯æ¥5æ¬ãç®æããŠçæã»æçš¿ããŸãããaudit-heal.sh ã¯3æ¬å ¬éããOKããšå€å®ããŸãã5æ¬å ¬éãããŠããâ å ¬éãç®æšæªéããåºãã3æ¬ããå ¬éã§ããŠããªãæ¥ã§ãç£æ»OKãåºããšããé転çŸè±¡ãèµ·ããŸããdaily.sh åŽã `TARGET="${AFFILIATE_FACTORY_TARGET:-5}"` ãšç°å¢å€æ°çµç±ã«ãã¹ãã§ããã`.env` ã« `AFFILIATE_FACTORY_TARGET=5` ãš1è¡æžãã°å šã¹ã¯ãªãããçµ±äžãããŸãã
â¡ `posted-products.log` ã®è¥å€§åã§ããã³ãããèšãã
`generate.sh` 18è¡ç®ã® `EXCL=$(paste -sd 'ã' "$LOG")` ã¯ãã°å šä»¶ããããã€ãªãã«ããŠããã³ãããžåã蟌ã¿ãŸããæ¯æ¥5æ¬ã1幎ç¶ãããš1,825ãšã³ããªã§ããååå1ä»¶ãå¹³å20æåãšãããšå šäœã§çŽ3.7äžæåãClaude Sonnet ã®ã³ã³ããã¹ãã«ã¯åãŸããŸãããããã³ããå šäœã5ã6äžããŒã¯ã³ã«éãã1åã®çæã³ã¹ããå幎ç®ããæ®µéçã«äžããå§ããŸããååæã«äžåºŠã¯å€ããšã³ããªãã¢ãŒã«ã€ããã`tail -n 200 "$LOG" > "${LOG}.tmp" && mv "${LOG}.tmp" "$LOG"` ã§çŽè¿200ä»¶ã ãæ®ãææ¬¡ cron ãå ¥ããŠãã ããã
⢠`resp_is_valid` ãèšäºæ¬æã® "Error:" ã«åŒã£ããã
`generate.sh` 255è¡ç®ã®å€å®ã¯ `grep -qiE 'request timed out|error:|rate limit|usage limit'` ã§ããæ£åžžã«çæãããèšäºæ¬æã«ããã®ãšã©ãŒïŒError: E10ïŒã³ãŒãã衚瀺ããããå é»ã確èªããŠãã ãããã®ãããªæãå«ãŸãããšã`resp_is_valid` ã倱æãšå€å®ããŠ3åãªãã©ã€åŸã« `exit 1` ããŸããé«çŽå®¶é»ã®ã¬ãã¥ãŒã§ã¯ãšã©ãŒã³ãŒãã®èª¬æãå ¥ãããšãå€ããããããæé€æ©ããã©ã åŒæŽæ¿¯æ©ã®ã¬ãã¥ãŒã§å®éã«åŒã£ããããŸããã`grep -qiE '^(request timed out|error:|rate limit)' <(printf '%s\n' "$r" | head -5)` ã®ããã«å é æ°è¡ã ããå€å®å¯Ÿè±¡ã«ããããèªé ã¢ã³ã«ãŒã§æ¬æå ã®èªç¶ãª "Error:" ãé€å€ããæ¹ä¿®ãæšå¥šããŸãã
⣠launchd ã®å€éèµ·åã§çæã³ã¹ããäºéã«ãªã
`StartCalendarInterval` ã«æ5æã»æŒ12æã»å€20æã®3æå»ãèšå®ããŠããå Žåãæã®å®è¡ã20åããã£ãŠããæäžã«æŒã®ã€ã³ã¿ãŒãã«ãçºç«ãããš2ã€ã® `daily.sh` ã䞊åã§èµ°ããŸããçæ¹ã `NEED=3` ãšèšç®ããŠ3æ¬çæãå§ããããçæ¹ãåæã« `NEED=3` ãšèšç®ããŠ3æ¬èµ°ãããŸããæçµçã« `PUB_TODAY` ã6ã«ãªã£ãŠãåªçèšèšããè¶ éã¯0æ¬ããšåžåããããå ¬éæ°ã¯åé¡ãããŸãããããã Claude API ã®åŒã³åºãã³ã¹ãã¯äºéã«çºçããŸãã`daily.sh` ã®åé ã« `flock -n /tmp/affiliate-factory.lock -c "bash ${0}"` ã®ãããªããã¯ãå ¥ãããšå€éèµ·åãé²ããŸãã
†`--model sonnet` ãããŒãã³ãŒããããŠããŠã¢ãã«ãåãæ¿ããããªã
`generate.sh` 272è¡ç®ã¯ `--model sonnet` åºå®ã§ããã³ã¹ããæããããšãã« Haiku ã«åãæ¿ããããŠããã³ãŒããçŽæ¥ç·šéããŠã³ããããçŽãå¿ èŠããããŸãã`GEN_MODEL="${AFFILIATE_FACTORY_MODEL:-sonnet}"` ãšã㊠`--model "$GEN_MODEL"` ã«å€ããŠããã°ã`.env` ã«1è¡æžãã ãã§æ¬¡ã®å®è¡ããåãæ¿ãããŸãããžã£ã³ã«ã«ãã£ãŠæ·±ã調æ»ãå¿ èŠãªæ¥ã¯ Opus 4.8 ãžäžæçã«äžããéç£æã¯ sonnet ã«æ»ãããšãã£ãéçšãå¯èœã«ãªããŸãã
⥠`RAKUTEN_AFFILIATE_ID` ã ãã空ã®ãšãå±éºãªäžéåç«¯ç¶æ ã«ãªã
`generate.sh` 83ã86è¡ç®ã¯ã3ã€ã®ç°å¢å€æ°ã®ããããã空ãªãçŽ ã®æ€çŽ¢URLãè¿ããæ¡ä»¶åå²ã§ãã3倿°ãã¹ãŠèšå® or ãã¹ãŠæªèšå®ã§ããã°æåãäžè²«ããŸããã`RAKUTEN_APPLICATION_ID` ãš `RAKUTEN_ACCESS_KEY` ã¯èšå®æžã¿ã§ `RAKUTEN_AFFILIATE_ID` ã ãã空ã ãšãPython ã³ãŒãã«å ¥ã£ãŠAPIãå©ãã187è¡ç®ã® hgc ãã©ãŒã«ããã¯ãªã³ã¯ã«ç©ºã® `affiliate_id` ãå±éãããŸãïŒ`/hgc//` ãšããäºéã¹ã©ãã·ã¥ïŒããã®ãªã³ã¯ã¯æ¥œå€©ã®ã¢ãã£ãªèšæž¬ã«ã®ããå ±é ¬ãŒãã«ãªããŸããèµ·åæã«ã3倿°ã®ãã¡1ã€ã§ã空ãªãå³ exit 1 ããŠåæ¢ãããã¬ãŒããæåããå ¥ããŠããã¹ãã§ããã
⊠ãã©ã³ãäžèŽãã§ãã¯ããã«ã¿ã«ããã©ã³ããã«åŒ±ã
`generate.sh` 172è¡ç®ã® `brand in (result["name"] + " " + result["url"]).lower()` ã¯ãåååã®å é èªã®å°æåãAPIã¬ã¹ãã³ã¹ã®ã©ããã«å«ãŸãããã§äžèŽãå€å®ããŸãã"Dyson" â URL ã¹ã©ãã°ã `dyson` ã®ã¹ãã¢åã§äžèŽããã®ã§æŠãæ©èœããŸããããã "Balmuda" ã®ãããªè£œåã§æ¥œå€©ã®ã¹ãã¢ç»é²åãããã«ãã¥ãŒããã®ã«ã¿ã«ã衚èšã ãšãURL ã¹ã©ãã°ã« `balmuda` ãå ¥ããäžäžèŽãšèª€å€å®ããŸãããã®å Žåã¯åå¥ååãªã³ã¯ãåããã« hgc ãã©ãŒã«ããã¯ãžèœã¡ãŸããå ±é ¬ã¯ä¹ããŸãããåå¥ååãžã®çŽãªã³ã¯ãã CVR ãäœäžããŸããã«ã¿ã«ããã©ã³ãåãããŒãåã«å€æããããŒãã«ãããã©ã³ãããšã®äŸå€ãªã¹ããæã€å¯Ÿå¿ãå¿ èŠã§ãã
⧠楜倩 API ãšã³ããã€ã³ãã®ããŒãžã§ã³å€æŽãèŠéã
`generate.sh` 103è¡ç®ã® `API = "https://openapi.rakuten.co.jp/ichibams/api/IchibaItem/Search/20260401?"` ã¯2026幎4ææŽæ°çã®ãšã³ããã€ã³ãã§ããæ§ãšã³ããã€ã³ãïŒ`app.rakuten.co.jp`ïŒã䜿ã£ãŠããã³ãŒãã¯2026幎4æä»¥éã403ãè¿ããŸããæ¥œå€©ã¢ãã£ãªãšã€ãã®APIããŒãžã§ã³ã¢ããåç¥ãèŠèœãšããšãå šä»¶ã0ä»¶å¿çã«ãªãå šèšäºã hgc ãã©ãŒã«ããã¯ã§åºåããç¶ããŸãããèšäºã¯å ¬éãããŠãããåå¥ååãªã³ã¯ã1ä»¶ããªãããšããç¶æ ãæ°æ¥ç¶ããŠããæ°ã¥ããã¿ãŒã³ã§ããå幎ã«äžåºŠã楜倩ã®éçºè ããŒã¿ã«ã§çŸè¡ãšã³ããã€ã³ãã確èªããç¹æ€ã cron ãæäœæ¥ã§ã¹ã±ãžã¥ãŒã«ããŠãã ããã
âš `blogsync` ã®ãã¹ã launchd ç°å¢ã§éããªã
`post-to-hatena.sh` 13è¡ç®ã® `BLOGSYNC="${BLOGSYNC:-$HOME/.local/bin/blogsync}"` ã¯ç°å¢å€æ°æªèšå®ãªã `~/.local/bin/blogsync` ãæ¢ããŸããHomebrew ã§ã€ã³ã¹ããŒã«ããå Žå㯠`/opt/homebrew/bin/blogsync` ã«å ¥ããŸããlaunchd ã® PATH 㯠`/usr/bin:/bin` çšåºŠããéã£ãŠããªããããã©ã¡ãã®ãã¹ã解決ã§ããŸããã`blogsync: command not found` ãåºãŠæçš¿ãå šä»¶å€±æããŸããã`--all` ã«ãŒããç¶ããŠã¹ã¯ãªããèªäœã¯ `exit 0` ã§çµãããŸãããã°ã«ã¯ãæçš¿: 0æ¬ããšåºãã ãã§ããšã©ãŒãšæ°ã¥ãã«ããã§ããlaunchd plist ã® `EnvironmentVariables` ã« `BLOGSYNC=/opt/homebrew/bin/blogsync` ããã«ãã¹ã§æžããã`BLOGSYNC` ç°å¢å€æ°ã `.env` ã«æèšããŠãã ããã
â© `shopt -s nullglob` ãå¿ãããšããã¡ã€ã«ãŒããã§1ä»¶ã«ãŒããåã
`audit-heal.sh` 34è¡ç®ã¯ `shopt -s nullglob` ã§ã°ããã空ã®ãšãã«ç©ºé åãè¿ãããèšå®ããŠãã35è¡ç®ã® for ã«ãŒãã«å ¥ãã44è¡ç®ã§ `shopt -u nullglob` ã«æ»ããŸãããã® `nullglob` ãå¿ãããšã`published/` ã«åœæ¥ãã¡ã€ã«ã1ä»¶ããªãç¶æ ã§ãã·ã§ã«ã¯ãªãã©ã«æåå `"$ARCHIVE/2026-06-23_*.md"` ã§ã«ãŒãã1åèµ°ããã`[ -e "$f" ]` ã倱æããŠã¹ããããããŸããçµæãšã㊠`published=0` ã®ãŸãŸãâ å ¬éãç®æšæªéãã«æ£ããå°éããã®ã§ãããã«ãŒããèµ°ã£ãçè·¡ããã°ã«æ®ãæ··ä¹±ãæããŸããglob ã䜿ã for ã«ãŒãã«ã¯å¿ ããã¢ã§ `shopt` ãèšå®ããç¿æ £ãæã£ãŠãã ããã
⪠macOS ã®ãéäžã¢ãŒããã§ osascript éç¥ãå±ããªã
`audit-heal.sh` 16è¡ç®ã® `notify()` 㯠`osascript -e "display notification ..."` ã§macOSéç¥ãé£ã°ããŸããmacOS 15以éã®éäžã¢ãŒãïŒFocusïŒããç¡ç ãã«èšå®ãããŠãããšããã®éç¥ããããŒè¡šç€ºãããéç¥ã»ã³ã¿ãŒã«ã®ã¿èç©ãããŸããæ7æã«èµ·ããŠç¢ºèªããã€ããããéç¥ã»ã³ã¿ãŒãéããªããšæ°ã¥ããªãç¶æ ã«ãªã£ãŠããŸãããlaunchd ã® `StandardOutPath` / `StandardErrorPath` ã plist ã«èšå®ããŠãã°ããã¡ã€ã«ã«æžãåºãããSlack Webhook ã LINE Notify ãžã®å¯çµè·¯ãæã€ããšãæšå¥šããŸãã
â« macOS ã®ã¹ãªãŒãäžã« `StartCalendarInterval` ãã¹ãããããã
launchd ã® `StartCalendarInterval` 㯠macOS ãã¹ãªãŒãäžã®ãšããã¹ã±ãžã¥ãŒã«æå»ãééããŠããžã§ããçºç«ããŸãããèµ·åºåŸã®ãã°ã€ã³æã« missed ãªå®è¡ãçºç«ããã±ãŒã¹ããããŸãããã¿ã€ãã³ã°æ¬¡ç¬¬ã§ãããæ5æã«èµ·åããã¯ãã10æãŸã§å®è¡ãããªãã£ããæ¥ãæã«æ°åãããŸããåªçèšèšã§æŒã®å®è¡ãè£å®ããããèšäºã¯1æ¥3ã5æ¬ã«åæããŸãããã¿ã€ãã³ã°ããããŸããMacBook ãã¯ã©ã ã·ã§ã«ã¢ãŒãïŒèãéããŠãã£ã¹ãã¬ã€æ¥ç¶ïŒã§åžžæèµ·åããããMac miniã»VPS ãªã©åžžæèµ·åç°å¢ã«ç§»è¡ããã®ãæ ¹æ¬è§£æ±ºã§ãã
1幎éã®éçšãšè€æ°åã®äºæ ããåŒãåºããèšèšæéã§ããæ°ãã«ã·ã¹ãã ãçµãæ¹ã¯æåãããã®æéãæèããŠèšèšããŠãã ããã
1. ç§å¯å€ãã¡ã€ã«ã¯ãèªååã®å°çšå€ããšããŠæåããèååãã
`.env` ã `~/.config/blogsync/config.yaml` ã¯èªååã¹ã¯ãªãããæžãæããŠã¯ãããŸãããèªå·±ä¿®åŸ©ã»ãŠã©ããããã°ã»ããã¯ã¢ããã¹ã¯ãªãããæžãæããŠãããã¡ã€ã«ããã¯ã€ããªã¹ãã§æç€ºãããã以å€ã«ã¯è§Šããªãèšèšãæåã«å ¥ããŠãã ããããåããŠããã®ã«çªç¶å ±é ¬ããŒãã«ãªããã®åå ã¯ãã»ãŒå¿ ãç§å¯å€ã®æ¶å€±ã§ããgitignore ã§ç®¡çå€ã«ããŠããç§å¯å€ã¯ãã¹ã¯ãªããã®äºæ ã§æ¶ãããš `git checkout` ã§ãæ»ããŸããã
2. TARGET ã¯ç°å¢å€æ° 1ã€ã§å šã¹ã¯ãªãããå ±æãã
`.env` ã« `AFFILIATE_FACTORY_TARGET=5` ãšæžããå šã¹ã¯ãªããã `TARGET="${AFFILIATE_FACTORY_TARGET:-5}"` ã§èªãèšèšã«çµ±äžããŠãã ãããdaily.sh ã®ããŒãã³ãŒã `TARGET=5` ãš audit-heal.sh ã®ããã©ã«ã `3` ã®ãããªãºã¬ã¯ãç£æ»ã®èª€å€å®ãçã¿ç¶ããŸããæ°å€ã®å€æŽã `.env` 1è¡ã®ä¿®æ£ã§å šäœã«åæ ãããã®ãæ£ããç¶æ ã§ãã
3. ã¢ãã£ãªãªã³ã¯ã®ååšãæ¯æ `grep` ã§ç¢ºèªãããµã€ã¬ã³ãå ±é ¬ãŒããæ ¹çµ¶ãã
`audit-heal.sh` ã® `grep -q 'hb.afl.rakuten.co.jp' "$f"` ã«å ããŠã`grep -q 'search.rakuten.co.jp' "$f"` ã§çŽ URLããéã¢ãã£ãªããšããŠæç€ºæ€åºããããžãã¯ãå ¥ããŠãã ããã`hb.afl` ã®äžåšã ããæ€åºãããšã`.env` æ¶æ» æã« `search.rakuten.co.jp`ïŒå ±é ¬ãŒãïŒãæ··å ¥ããŠãOKãšå€å®ãããŸããçŽ URLãæ··å ¥ããç¬éã« macOS éç¥ãå±ãèšèšã«ããã°ã1æ¥ä»¥å ã«æ°ã¥ããŸãã
4. `GEN_TIMEOUT` ã¯å®æž¬å€ã®1.5å以äžã«èšå®ãããããã¯ã±ãããªã
8,000ã10,000åïŒç«¶å3補åWebSearchã®å¹³åçææéã¯12ã15åã§ãã`GEN_TIMEOUT=1200`ïŒ20åïŒã¯ãã®1.5åã®äœè£èŸŒã¿ã®èšå®ã§ããã¿ã€ã ã¢ãŠããçããããšãçæéäžã§killâ空å¿çâ3å倱æâ0æ¬ããšããé£éã確å®ããŸããçæã³ã¹ãããå ¬é0æ¬ã®æå€±ã®ã»ãã確å®ã«å€§ããã§ããæ°ããããã³ããèšèšã«å€ãããšãã¯æåã§æ°åå®è¡ããŠæèŠæéã宿ž¬ãçŽããŠãã ããã
5. åºåãã©ãŒããã匷å¶ïŒ`PRODUCT:` è¡ïŒãå€ããªã
`resp_is_valid()` ãšåååæœåºã®äž¡æ¹ã `^PRODUCT:` è¡ã«äŸåããŠããŸããããã³ãããæ¹å€ãããšãã«ãã®1è¡ã®åºå匷å¶ãå€ããšãããŒã¹åŠçãå šéšåŽ©ããŸããåºåãã©ãŒãããã®å€æŽã¯ã¹ã¯ãªããå šäœãžã®åœ±é¿ç¢ºèªãå¿ é ã§ãã倿Žããå Žåã¯æ¬¡ã«æãã `AFFILIATE_FACTORY_TEST_RESPONSE` ã§ãã©ã€ã©ã³ããŠããæ¬çªã«å ¥ããŠãã ããã
6. `AFFILIATE_FACTORY_TEST_RESPONSE` ã§ãã©ã€ã©ã³ãå¿ ãå ¥ãã
`generate.sh` 265ã266è¡ç®ã®ã¢ãã¯ã¬ã¹ãã³ã¹æ©æ§ã§ããããã³ãã倿Žã»`postprocess_body` æ¹ä¿®ã»ãªã³ã¯å·®æ¿ããžãã¯è¿œå ã®ãã³ã«ãAPIãŒãã³ã¹ãã§ãã«ãã¹ãéããŸãã
bash
export AFFILIATE_FACTORY_TEST_RESPONSE='PRODUCT: ãã¹ãæé€æ© X100
# ã2026幎ããã¹ãæé€æ© X100 å
šã¹ããã¯è§£èª¬ïœç«¶å3æ©ç𮿝èŒ
> â»æ¬èšäºã¯ã¢ãã£ãªãšã€ãããã°ã©ã ïŒæ¥œå€©ã¢ãã£ãªãšã€ãçïŒãå©çšããŠããŸãã
[:contents]
## ãã®èšäºã§ãããããš'
bash generate.sh 1äžèšã³ãã³ãã§æ¥œå€©ãªã³ã¯ã®å·®æ¿ã»ãã¡ã€ã«åºåã»`posted-products.log` ãžã®è¿œèšãŸã§å šãã¹ãéããŸããæ¬çª launchd ã§åããŠåãããŠãç¿æ0æ¬ãã確å®ãããã¹ãé²ããŸãã
7. èªå·±ä¿®åŸ©ã¹ã¯ãªããã®ã¹ã³ãŒãã¯ãã¯ã€ããªã¹ãå¶ã§å³æ Œã«éå®ããæžã蟌ã¿ã¯åå眮æã®ã¿
èªå·±ä¿®åŸ©ãæžãæããŠãããã¡ã€ã«ãæç€ºçã«åæããŠãã ãããæžã蟌ã¿åŠçã¯ãã¹ãŠãäžæãã¡ã€ã« â `mv` ã§åå眮æããã¿ãŒã³äžæã§ãã`some_command > "$TARGET_FILE"` ã¯çµ¶å¯Ÿã«äœ¿ããŸããã`some_command > "$TARGET_FILE.tmp" && mv "$TARGET_FILE.tmp" "$TARGET_FILE"` ã«ããŸããã·ã§ã«å€æ°ã®å±éãšãªãã€ã¬ã¯ãã®é åºããã¿åããªããšãæå³ããªããã¹ã®ãã¡ã€ã«ãäžæžããããŸãããããå®éã« `.env` ãš `post-to-hatena.sh` ãäžæã«ç Žå£ããåå ã§ããã
8. launchd ãžã§ãã®æ»æŽ»ç¢ºèªãç£æ»ã«çµã¿èŸŒã
# audit-heal.sh æ«å°Ÿã«è¿œå
if ! launchctl list 2>/dev/null | grep -q 'affiliate-factory'; then
log " â launchdãžã§ããæªç»é²ãplistã確èªããŠãã ãã"
problems=$((problems+1))
fiãã®ãã§ãã¯ã `audit-heal.sh` ã«è¿œå ãããšãplist ãç ŽæããŠãžã§ããæªç»é²ã«ãªã£ãç¿æã«å¿ ãæ€åºã§ããŸãã2é±éæ°ã¥ããªãã£ã倱æã®åçºãé²ãæäœéã®ã¬ãŒãã§ãã
9. launchd plist 㯠`plutil` ã§æ§æãã§ãã¯ããã³ããã管çãã
plutil ~/Library/LaunchAgents/com.affiliate-factory.daily.plist
# â com.affiliate-factory.daily.plist: OKplist 㯠XML ã§ããéãã¿ã°ã®æãã»`<key>` ååŸã®äœåãªæåã§å£ããŸãã`plutil` ã¯æ§æãšã©ãŒãè¡çªå·ä»ãã§åºåããŸããã³ãŒã倿Žã®ãã³ã« `plutil` ãéããŠãã `launchctl unload && launchctl load` ã§ãªããŒãããæé ãåºå®ããŠãã ãããplist 㯠`.gitignore` 察象ããå€ããŠããŒãžã§ã³ç®¡çããŸããç Žææã« git ãã埩å ã§ããã®ãšã§ããªãã®ãšã§ã¯ãçºèŠããã®åŸ©æ§æéã1æéå€ãããŸãã
10. `blogsync` ã®ãã¹ã¯ `BLOGSYNC` ç°å¢å€æ°ã§ãã«ãã¹æå®ããlaunchd plist ã«ãèšèŒãã
launchd ã® `EnvironmentVariables` ã« `BLOGSYNC=/opt/homebrew/bin/blogsync`ïŒãŸã㯠`~/.local/bin/blogsync`ïŒãæèšããŠãã ããã`which blogsync` ã®åºåããã®ãŸãŸ plist ã«æžããŸãã`PATH` ã®éãæ¹ã«äŸåãããšãmacOS ã®ããŒãžã§ã³ã¢ããã Homebrew ã®ãã¬ãã£ãã¯ã¹å€æŽïŒIntelâApple Siliconç§»è¡ïŒã§çªç¶ `command not found` ã«ãªããŸãã
11. `--model` ãç°å¢å€æ°åããŠåãããã€äžèŠã§ã¢ãã«åãæ¿ããå¯èœã«ãã
`generate.sh` 272è¡ç®ã® `--model sonnet` ã `--model "${AFFILIATE_FACTORY_MODEL:-sonnet}"` ã«å€ããŠãã ãããã³ã¹ããåããããšã㯠`.env` ã« `AFFILIATE_FACTORY_MODEL=claude-haiku-4-5-20251001` ãšæžãã ãã§æ¬¡ã®å®è¡ããåãæ¿ãããŸããç¹å®ãžã£ã³ã«ïŒé«åäŸ¡ã»æè¡ç³»ïŒã®èšäºã ãäžæçã« Opus 4.8 ãžäžããéçšããã³ãŒããè§Šããã«å®çŸã§ããŸãã
12. `posted-products.log` ã¯ææ¬¡ã§ããŒããŒã·ã§ã³ããŠããã³ããã³ã¹ãã管çãã
# ææ¬¡ cron ã«è¿œå
tail -n 200 "$LOG" > "${LOG}.tmp" && mv "${LOG}.tmp" "$LOG"çŽè¿200ä»¶ã ããæ®ãã°é€å€å¹æã¯ååã§ãããŒã¯ã³ã³ã¹ãã 1/9 以äžã«åæžã§ããŸããæåã§çšŒãããã«ããŒã¯ã³ã³ã¹ããäžããŠããã®ã¯æ¬æ«è»¢åã§ãããã°ã®ããŒããŒã·ã§ã³ã¯åçåã·ã¹ãã ã®ã³ã¹ã管çã®äžéšã§ãã
13. éç¥ã ãã«é Œãã `logs/audit-YYYY-MM-DD.log` ãé±1å確èªãã
`audit-heal.sh` ã® `AUDIT_LOG="$DIR/logs/audit-${TODAY}.log"` ã«æ¯æã®ç£æ»çµæãå šä»¶æ®ããŸããmacOS éç¥ãéäžã¢ãŒãã§ã¹ããããããæ¥ã§ãããã°ãèŠãã°äœæ¬å ¬éã§ããããããããŸããé±ã«äžåºŠ `grep 'â |â' logs/audit-*.log | tail -14` ãå®è¡ããŠçŽè¿2é±éã® OK/NG çã確èªããŠãã ãããé£ç¶ããŠNGãåºãŠããæ¥ãããã°ããã®æ¥ã®ãã°ã§åå ã远跡ã§ããŸãã
ååã®ç¬¬1åã§ã·ã¹ãã ã®éªšæ ŒïŒlaunchdâdaily.shâgenerate.shâpost-to-hatena.shâaudit-heal.shïŒã解説ããä»åã¯ãåçã確å®ã«å»ãåŸåã®ä»çµã¿ããäžéãå ¬éããŸããã
ãã®ã·ã¹ãã ãèšèšé¢ãšå€±æé¢ã®2軞ã§è©äŸ¡ãããšãããªããŸãã
èšèšé¢ã®æ žå¿ã¯ãåªçæ§ããšãèªå·±ä¿®åŸ©ãã®äºæ¬æ±ã§ãã äœåºŠå®è¡ããŠã1æ¥ TARGET æ¬ã«åæããåªçèšèšããããããæã®å€±æã¯æŒãèªåã§è£ããŸããaudit-heal.sh ãæ¯æèµ°ã£ãŠç°åžžãæ€åºããmacOS éç¥ã§äººéã«äŒãããããåé¡ã«æ°ã¥ããŸã§ã®ã¿ã€ã ã©ã°ã1æ¥ä»¥å ã«åãŸããŸããç§ã確èªã«äœ¿ãæéã¯æ¯æ3å以äžã§ãã
倱æé¢ã®æèšã¯ãèªååãèªåèªèº«ãå£ãããªã¹ã¯ãæåããèšèšã«çµã¿èŸŒãããšã§ãã `.env` æ¶å€±ã»èªå·±ä¿®åŸ©ã®æŽèµ°ã»launchd plist ç Žæââãããã¯ãã¹ãŠãåããŠããã¯ãã®èªååããæ°Žé¢äžã§éãã«å£ããŠããäŸã§ããããã°ãèŠãªããã°æ°ã¥ããªãç¶æ ãç¶ããæ°ã¥ãããšãã«ã¯å ±é ¬ããŒãã«ãªã£ãŠãããã2é±éåã®æçš¿æ©äŒã倱ãããŠãããããŸããå£ããããšãåæã«èšèšããå£ããããšãç¿æã«ã¯å¿ ãæ€åºããä»çµã¿ãæã€ããšââãããé·æéçšã®åœã§ãã
æå120äžã®å€§éšåã¯ç§ãå¯ãŠããéã«ç©ã¿äžãã£ãŠããŸãããã®ä»çµã¿ã¯ãã®äžæ žã®ã²ãšã€ã§ããããç°å¢ã建ãŠãããšããå幎éã®æè³ã®çµæã§ããèšäºãæžãæéããŒãã«ããã®ã§ã¯ãªããèªåã䟡å€ãçããªãæéåž¯ã«æ©æ¢°ãèªåã§åãç¶ããç¶æ ã建ãŠãããšããæéã売ã坿¥ãšã®æ±ºå®çãªéãã§ãã
次åã¯ããã®ã¢ãã£ãªå·¥å Žãå«ã Claude Code èªåŸç°å¢å šäœã®èšèšââè€æ°ã®å·¥å Žã䞊åã§èµ°ããããšãã®ãªãœãŒã¹ç®¡çã»ã¢ãã«ã«ãŒãã£ã³ã°ã»æåäœã®ã³ã¹ã管çââã«ã€ããŠæžããŸãã
ä»çµã¿ã®å
šäœåã»æ120äžã®å
èš³ã»30æ¥æé ã¯æænoteã«ãŸãšããŠããŸãã
ð Claude CodeèªåŸç°å¢ã§ãå®éã©ã皌ãã â ä»çµã¿ã»å®äŸã»å§ãæ¹ã»ãµããŒã
LilyïŒ@bokuwalilyïŒâ å人éçºè ãClaude Code ã§èªåååºç€ãçµã¿ãªãããiOSã¢ããªãWebãµãŒãã¹ãéç£ããŠããŸã
âŒïžäœã£ãã¢ããªã¯ ããŒããã©ãªãª ã«ãŸãšããŠããŸãð±
âŒïžæ°çã»éçºã®è£åŽã¯ X @bokuwalily ã§çºä¿¡ããŠããŸãðŠ
âŒïžOSS: github.com/bokuwalilyð
çããã® â€ïž ãã·ã§ã¢ãå±ã¿ã«ãªããŸãïŒ
ã³ã¡ã³ãã¯ãŸã ãããŸãã
èªã¿èŸŒã¿äž...