æ¯æ3æ¬ã®ã¢ãã£ãªèšäºãå®å šèªåã§å ¬éããä»çµã¿ ïŒå š2åã®ç¬¬2åïŒïŒåŸç·š â åçåãªã³ã¯ã»äŸå€åŠçã»1æ¥3æ¬ã«åæãããèªå·±å埩
LaunchDãæ5æã«èµ·åãããã³ã«ãç§ãå¯ãŠããéã«åçåèšäºãçããŠããŸããã
ååïŒç¬¬1åïŒã§ã¯ã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æ¥ã®çµããã«ã¯ç®æšæ¬æ°ã«åæããŸãã
ãã®èšèšããªããã°ãæã®å®è¡ã倱æãããã³ã«ã仿¥ã¯ãã¡ã ã£ããã§çµãããŸããåªçèšèšãããã°ãéšåçãªå€±æã¯ã·ã¹ãã ãèªåã§åžåããŸãã
Claude Codeããéå ·ããšããŠäœ¿ããšã¯ã©ãããããšã
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 â åªçèšç®ã®å®è£
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 â ClaudeåŒã³åºããš3åãªãã©ã€
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ãšã©ãŒãšæ«å°ŸèªåãæŠç¥
楜倩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() å
ïŒã
APIãå®å šã«åããªããšãã® hgc ãã©ãŒã«ããã¯
åååå¥ãªã³ã¯ãã©ãããŠãåããªããšãã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 ã®èšå®ãã¹ã§æ°ã¥ããã«éçšããŠããŸãã®ãã¢ãã£ãªåçãŒãã®å
žåçãªåå ã§ãã
postprocess_body â çŽ ãªã³ã¯ãã¢ãã£ãªãªã³ã¯ãžå šå·®æ¿
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 â èªå·±ä¿®åŸ©ã®å®è£
æåŸã®ã¹ãããã 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ããããªã«é¿ããŠãããŸããã¢ãã«ã«ä»»ããŠããä»äºã¯ç©æ¥µçã«ã¢ãã«ã«æããããšããææ³ãããã«åºãŠããŸãã
.env ã®èªåããŒããšã¹ã³ãŒã
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() ã«ãã©ãŒã«ããã¯ââã€ãŸããå ±é
¬ã®ã€ããªãçŽ ãªã³ã¯ãã§èšäºãæžãç¶ããŸããã¹ã¯ãªããã¯èœã¡ãªããåçããŒãããšããææªã®ãµã€ã¬ã³ã倱æã§ããåŸè¿°ãããç§ãè©°ãŸã£ã話ãã§ãããå®éã«ãããããŠããŸãã
GEN_TIMEOUT ã®ãã±ãããªããå²åŠ
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 ãåãæ§é ã§ãïŒã
AFFILIATE_FACTORY_TEST_RESPONSE ã§ã¢ãã¯ãã¹ã
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ã«åååã远èšãããŠãããããäžæ°ã«ç¢ºèªã§ããŸããæ¬çªç°å¢ã®ã³ãŒããã¹ããã®ãŸãŸéãã®ã§ããŠããããã¹ããšã¯éãå®éã®æåã®ç¢ºèªã«ãªããŸãã
ã¯ãŠãªMarkdownç¹æã®çœ ïŒ[:contents] ãšå 責 blockquote ã®ç©ºè¡
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 ã® config ã¬ãŒããš H1 åé¢
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.ã.env æ¶æ» ãã§å ±é ¬ãŒãåçºââ2åç®ã®ãããã
æåã«ãã®åé¡ã«æ°ã¥ããã®ã¯ã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åç®ã®ããããã ã£ãã®ã§ããããã«æããèªåã«åããŸãããç§å¯å€ãæã€ãã¡ã€ã«ã¯ãèªååã®æãå±ããªãèåããšããŠæåããèšèšã«å ¥ããã¹ãã§ããã
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"
ããã«ãèªå·±ä¿®åŸ©ã¹ã¯ãªãããæžãæããŠãããã¡ã€ã«ã¯äœãããæç€ºçã«ãã¯ã€ããªã¹ãåããŸããããã以å€ã®ãã¡ã€ã«ã«ã¯è§Šããªãããã«ã¬ãŒããå ¥ããŠããŸããèªå·±ä¿®åŸ©ãšãããåªããæ©èœãã¯ãã¹ã³ãŒãå¶éããªããã°æå¶ã®ç Žå£è ã«ãªããŸãã
3. launchd plist ãå£ããŠ2é±éæ°ã¥ããªãã£ã
äžèšã®äºæ
ã§ 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ãŒãã³ã¹ãã§ãã«ãã¹ãéããŸãã
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: OK
plist 㯠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 ð
çããã® â€ïž ãã·ã§ã¢ãå±ã¿ã«ãªããŸãïŒ