From e129034c387f514adef1cd33ca9fad56e1903c6c Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Thu, 5 Mar 2026 21:11:30 +0900 Subject: [PATCH 01/50] Update note.yaml --- .github/workflows/note.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index ddddc82..caadcf3 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -372,7 +372,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - + - name: Debug list files + run: | + pwd + ls -la - name: Setup Node.js uses: actions/setup-node@v4 with: From 43170f5a6e17f4da1bb39eef5e40552862ba5aeb Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Thu, 5 Mar 2026 22:24:29 +0900 Subject: [PATCH 02/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index caadcf3..dacbe2a 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -74,7 +74,7 @@ jobs: - name: Research with Claude Code SDK run: | cat > research.mjs <<'EOF' - import { query } from '@anthropic-ai/claude-code'; + import { Claude } from '@anthropic-ai/claude-code/index.js' import fs from 'fs'; const theme = process.env.THEME || ''; const target = process.env.TARGET || ''; From ab781abf75ff64e2ab84841521fc39ac083133f2 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sat, 7 Mar 2026 17:12:34 +0900 Subject: [PATCH 03/50] Update note.yaml --- .github/workflows/note.yaml | 766 ++++++++++++------------------------ 1 file changed, 244 insertions(+), 522 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index dacbe2a..cc4a645 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -49,333 +49,281 @@ env: jobs: research: - name: Research (CCSDK WebSearch/WebFetch) + name: Research (Tavily) runs-on: ubuntu-latest env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} THEME: ${{ github.event.inputs.theme }} TARGET: ${{ github.event.inputs.target }} + MESSAGE: ${{ github.event.inputs.message }} outputs: research_b64: ${{ steps.collect.outputs.research_b64 }} steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install Claude Code SDK - run: | - npm init -y - npm i @anthropic-ai/claude-code - - - name: Research with Claude Code SDK + - name: Research with Tavily run: | cat > research.mjs <<'EOF' - import { Claude } from '@anthropic-ai/claude-code/index.js' import fs from 'fs'; + + const apiKey = process.env.TAVILY_API_KEY; const theme = process.env.THEME || ''; const target = process.env.TARGET || ''; - const today = new Date().toISOString().slice(0,10); - const artifactsDir = '.note-artifacts'; - fs.mkdirSync(artifactsDir, { recursive: true }); - const sys = [ - 'あなたは最新情報の収集と要約に特化した超一流のリサーチャーです。', - '事実ベース・一次情報優先・本文内にMarkdownリンクで出典を埋め込むこと。', - '十分な分量(目安: 2,000語以上)。各節で出典を本文に埋め込む。', - 'WebSearch と WebFetch を必ず使用し、一次情報(公的機関・規格・論文・公式)を優先する。', - ].join('\n'); - const userPrompt = `以下のテーマとターゲットに対する最終版のリサーチレポートを作成してください。\n【重要】途中経過や確認質問は一切せず、最終レポートのみを返してください。不明点がある場合は「前提と仮定」セクションで簡潔に仮定を明記してから続行してください。事実ベースで一次情報を最優先し、本文にMarkdownリンクで出典を埋め込んでください。\n---\nテーマ: ${theme}\nターゲット: ${target}\n現在日付: ${today}`; - const messages = []; - for await (const msg of query({ - prompt: userPrompt, - options: { - customSystemPrompt: sys, - allowedTools: ['WebSearch','WebFetch'], - permissionMode: 'acceptEdits', + const message = process.env.MESSAGE || ''; + + const prompt = `${theme} ${target} ${message}`.trim(); + + const body = { + query: prompt, + search_depth: 'advanced', + topic: 'general', + max_results: 5, + include_answer: true + }; + + const res = await fetch('https://api.tavily.com/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' }, - })) { messages.push(msg); } - const assistantTexts = messages.filter(m=>m.type==='assistant').map(m=>{ - const c=m.message?.content; if(Array.isArray(c)){return c.filter(b=>b?.type==='text').map(b=>b.text).join('\n');} return ''; - }).filter(Boolean).join('\n\n'); - fs.writeFileSync(`${artifactsDir}/research.md`, assistantTexts || ''); - try { fs.writeFileSync(`${artifactsDir}/research_trace.json`, JSON.stringify(messages, null, 2)); } catch {} + body: JSON.stringify({ + api_key: apiKey, + ...body + }) + }); + + if (!res.ok) { + const txt = await res.text(); + console.error(txt); + process.exit(1); + } + + const json = await res.json(); + fs.writeFileSync('research.json', JSON.stringify(json, null, 2)); + console.log('research saved'); EOF + node research.mjs - name: Collect research id: collect run: | - b64=$(base64 -w 0 .note-artifacts/research.md 2>/dev/null || base64 .note-artifacts/research.md) - echo "research_b64<> $GITHUB_OUTPUT - echo "$b64" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + echo "research_b64=$(base64 -w 0 research.json)" >> "$GITHUB_OUTPUT" - - name: Upload research artifacts + - name: Upload research artifact uses: actions/upload-artifact@v4 with: - name: research-artifacts - path: | - .note-artifacts/research.md - .note-artifacts/research_trace.json + name: research-json + path: research.json write: - name: Write (Claude Sonnet 4.0) - needs: research + name: Write (Claude) runs-on: ubuntu-latest + needs: research env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} THEME: ${{ github.event.inputs.theme }} TARGET: ${{ github.event.inputs.target }} MESSAGE: ${{ github.event.inputs.message }} CTA: ${{ github.event.inputs.cta }} - INPUT_TAGS: ${{ github.event.inputs.tags }} + TAGS: ${{ github.event.inputs.tags }} + RESEARCH_B64: ${{ needs.research.outputs.research_b64 }} outputs: - title: ${{ steps.collect.outputs.title }} - draft_json_b64: ${{ steps.collect.outputs.draft_json_b64 }} + article_b64: ${{ steps.collect.outputs.article_b64 }} steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install AI SDK - run: | - npm init -y - npm i ai @ai-sdk/anthropic - - - name: Restore research - env: - RESEARCH_B64: ${{ needs.research.outputs.research_b64 }} - run: | - mkdir -p .note-artifacts - echo "$RESEARCH_B64" | base64 -d > .note-artifacts/research.md || echo "$RESEARCH_B64" | base64 --decode > .note-artifacts/research.md - - - name: Generate draft (title/body/tags) + - name: Write article with Claude run: | cat > write.mjs <<'EOF' - import { generateText } from 'ai'; - import { anthropic } from '@ai-sdk/anthropic'; import fs from 'fs'; - const theme=process.env.THEME||''; const target=process.env.TARGET||''; const message=process.env.MESSAGE||''; const cta=process.env.CTA||''; - const inputTags=(process.env.INPUT_TAGS||'').split(',').map(s=>s.trim()).filter(Boolean); - const researchReport=fs.readFileSync('.note-artifacts/research.md','utf8'); - const modelName='claude-sonnet-4-5-20250929'; - function extractJsonFlexible(raw){const t=(raw||'').trim().replace(/\u200B/g,'');try{return JSON.parse(t);}catch{}const m=t.match(/```[a-zA-Z]*\s*([\s\S]*?)\s*```/);if(m&&m[1]){try{return JSON.parse(m[1].trim());}catch{}}const f=t.indexOf('{'),l=t.lastIndexOf('}');if(f!==-1&&l!==-1&&l>f){const c=t.slice(f,l+1);try{return JSON.parse(c);}catch{}}return null;} - async function repairJson(raw){const sys='入力から {"title":string,"draftBody":string,"tags":string[]} のJSONのみ返答。';const {text}=await generateText({model:anthropic(modelName),system:sys,prompt:String(raw),temperature:0,maxTokens:8000});return extractJsonFlexible(text||'');} - function sanitizeTitle(t){ - let s=String(t||'').trim(); - // フェンスや見出し、引用符を除去 - s=s.replace(/^```[a-zA-Z0-9_-]*\s*$/,'').replace(/^```$/,''); - s=s.replace(/^#+\s*/,''); - s=s.replace(/^"+|"+$/g,'').replace(/^'+|'+$/g,''); - s=s.replace(/^`+|`+$/g,''); - s=s.replace(/^json$/i,'').trim(); - if(!s) s='タイトル(自動生成)'; - return s; + + const apiKey = process.env.ANTHROPIC_API_KEY; + const theme = process.env.THEME || ''; + const target = process.env.TARGET || ''; + const message = process.env.MESSAGE || ''; + const cta = process.env.CTA || ''; + const tags = process.env.TAGS || ''; + const research = JSON.parse(Buffer.from(process.env.RESEARCH_B64, 'base64').toString('utf8')); + + const systemPrompt = ` + あなたは日本語のnote記事を書くプロの編集者です。 + 目的は、読みやすく、信頼感があり、行動につながるnote記事を作ることです。 + 出力は必ずJSONのみ。 + 形式: + { + "title": "記事タイトル", + "body": "note本文。見出しや改行を含む", + "tags": ["タグ1","タグ2","タグ3"] } - function deriveTitleFromText(text){ - const lines=(text||'').split(/\r?\n/).map(l=>l.trim()).filter(Boolean); - const firstReal=lines.find(l=>!/^```/.test(l))||lines[0]||''; - return sanitizeTitle(firstReal); + `.trim(); + + const userPrompt = ` + 記事テーマ: ${theme} + 想定読者: ${target} + 読者に伝えたい核メッセージ: ${message} + 読後のアクション: ${cta} + 希望タグ: ${tags} + + リサーチ結果: + ${JSON.stringify(research, null, 2)} + + 条件: + - 日本語 + - 読みやすい自然な文章 + - 誇張しすぎない + - 見出しを使う + - 最後にCTAを入れる + - tags は配列で3〜5個 + `.trim(); + + const res = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json' + }, + body: JSON.stringify({ + model: 'claude-3-5-sonnet-latest', + max_tokens: 4000, + system: systemPrompt, + messages: [ + { + role: 'user', + content: userPrompt + } + ] + }) + }); + + if (!res.ok) { + const txt = await res.text(); + console.error(txt); + process.exit(1); + } + + const json = await res.json(); + const text = json.content?.[0]?.text || ''; + fs.writeFileSync('article_raw.txt', text); + + let parsed; + try { + parsed = JSON.parse(text); + } catch { + console.error('Claude output was not valid JSON'); + console.error(text); + process.exit(1); } - const sysWrite='note.com向け長文記事の生成。JSON {title,draftBody,tags[]} で返答。draftBodyは6000〜9000文字を目安に十分な分量で、章ごとに小見出しと箇条書きを適切に含めること。'; - const prompt=[`{テーマ}: ${theme}`,`{ペルソナ}: ${target}`,`{リサーチ内容}: ${researchReport}`,`{伝えたいこと}: ${message}`,`{読後のアクション}: ${cta}`].join('\n'); - const {text}=await generateText({model:anthropic(modelName),system:sysWrite,prompt,temperature:0.7,maxTokens:30000}); - let obj=extractJsonFlexible(text||'')||await repairJson(text||''); - let title, draftBody, tags; if(obj){title=sanitizeTitle(obj.title); draftBody=String(obj.draftBody||'').trim(); tags=Array.isArray(obj.tags)?obj.tags.map(String):[]} - if(!title||!draftBody){ title=deriveTitleFromText(text||''); const lines=(text||'').split(/\r?\n/); draftBody=lines.slice(1).join('\n').trim()||(text||''); tags=[]} - if(inputTags.length){tags=Array.from(new Set([...(tags||[]),...inputTags]));} - fs.writeFileSync('.note-artifacts/draft.json',JSON.stringify({title,draftBody,tags},null,2)); + + fs.writeFileSync('article.json', JSON.stringify(parsed, null, 2)); + console.log('article saved'); EOF + node write.mjs - - name: Collect draft + - name: Collect article id: collect run: | - title=$(node -e "console.log(JSON.parse(require('fs').readFileSync('.note-artifacts/draft.json','utf8')).title)") - b64=$(base64 -w 0 .note-artifacts/draft.json 2>/dev/null || base64 .note-artifacts/draft.json) - echo "title<> $GITHUB_OUTPUT - echo "$title" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - echo "draft_json_b64<> $GITHUB_OUTPUT - echo "$b64" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Upload draft artifact + echo "article_b64=$(base64 -w 0 article.json)" >> "$GITHUB_OUTPUT" + + - name: Upload article artifact uses: actions/upload-artifact@v4 with: - name: draft-artifact - path: .note-artifacts/draft.json + name: article-json + path: | + article.json + article_raw.txt factcheck: name: Fact-check (Tavily) - needs: write runs-on: ubuntu-latest + needs: write env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - TITLE: ${{ needs.write.outputs.title }} + ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} outputs: - title: ${{ steps.collect.outputs.title }} final_b64: ${{ steps.collect.outputs.final_b64 }} steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install AI SDK - run: | - npm init -y - npm i ai @ai-sdk/anthropic - - - name: Restore draft json - env: - DRAFT_JSON_B64: ${{ needs.write.outputs.draft_json_b64 }} - run: | - mkdir -p .note-artifacts - echo "$DRAFT_JSON_B64" | base64 -d > .note-artifacts/draft.json || echo "$DRAFT_JSON_B64" | base64 --decode > .note-artifacts/draft.json - - - name: Fact-check with Tavily + - name: Fact-check article run: | cat > factcheck.mjs <<'EOF' - import { generateText } from 'ai'; - import { anthropic } from '@ai-sdk/anthropic'; import fs from 'fs'; - const draft=JSON.parse(fs.readFileSync('.note-artifacts/draft.json','utf8')); - const modelName='claude-sonnet-4-5-20250929'; - const TAVILY_API_KEY=process.env.TAVILY_API_KEY||''; - if(!TAVILY_API_KEY){ console.error('TAVILY_API_KEY is not set'); process.exit(1); } - - function extractJsonFlexible(raw){ - const t=(raw||'').trim().replace(/\u200B/g,''); - // try object - try{ const o=JSON.parse(t); return o; }catch{} - const fence=t.match(/```[a-zA-Z]*\s*([\s\S]*?)\s*```/); if(fence&&fence[1]){ try{ return JSON.parse(fence[1].trim()); }catch{} } - // try object slice - let f=t.indexOf('{'), l=t.lastIndexOf('}'); if(f!==-1&&l!==-1&&l>f){ const cand=t.slice(f,l+1); try{ return JSON.parse(cand); }catch{} } - // try array slice - f=t.indexOf('['); l=t.lastIndexOf(']'); if(f!==-1&&l!==-1&&l>f){ const cand=t.slice(f,l+1); try{ return JSON.parse(cand); }catch{} } - return null; - } - function stripCodeFence(s){ - const t=String(s||'').trim(); - const m=t.match(/^```[a-zA-Z0-9_-]*\s*([\s\S]*?)\s*```\s*$/); if(m&&m[1]) return m[1].trim(); - return t; - } - async function proposeQueries(body){ - const sys='あなたは事実検証の専門家です。入力本文から検証が必要な固有名詞・数値・主張を抽出し、Tavily検索用に日本語の検索クエリを最大10件の配列で返してください。出力はJSON配列のみ。'; - const { text } = await generateText({ model: anthropic(modelName), system: sys, prompt: String(body), temperature: 0, maxTokens: 2000 }); - const arr = extractJsonFlexible(text||''); - return Array.isArray(arr) ? arr.map(String).filter(Boolean).slice(0,10) : []; - } + const apiKey = process.env.TAVILY_API_KEY; + const article = JSON.parse(Buffer.from(process.env.ARTICLE_B64, 'base64').toString('utf8')); - async function tavilySearch(q){ - const res = await fetch('https://api.tavily.com/search', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ api_key: TAVILY_API_KEY, query: q, search_depth: 'advanced', max_results: 5, include_answer: true }) - }); - if(!res.ok){ return { query:q, results:[], answer:null }; } - const json = await res.json().catch(()=>({})); - return { query:q, results: Array.isArray(json.results)? json.results: [], answer: json.answer || null }; - } + const query = `${article.title}\n\n${article.body.slice(0, 1200)}`; - function formatEvidence(items){ - const lines = []; - for(const it of items){ - lines.push(`### 検索: ${it.query}`); - if(it.answer){ lines.push(`要約: ${it.answer}`); } - for(const r of it.results||[]){ - const t = (r.title||'').toString(); - const u = (r.url||'').toString(); - const c = (r.content||'').toString().slice(0,500); - lines.push(`- [${t}](${u})\n ${c}`); - } - lines.push(''); - } - return lines.join('\n'); + const res = await fetch('https://api.tavily.com/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + api_key: apiKey, + query, + search_depth: 'advanced', + topic: 'general', + max_results: 5, + include_answer: true + }) + }); + + if (!res.ok) { + const txt = await res.text(); + console.error(txt); + process.exit(1); } - async function main(){ - const queries = await proposeQueries(draft.draftBody||''); - const results = []; - for(const q of queries){ results.push(await tavilySearch(q)); } - const evidence = formatEvidence(results); - const sys=[ - 'あなたは事実検証の専門家です。以下の原稿(note記事の下書き)に対し、提供されたエビデンス(Tavily検索結果)に基づき、', - '誤情報の修正・低信頼出典の置換・信頼できる一次情報の本文内Markdownリンク埋め込みを行って、修正後の本文のみ返してください。', - '文体・構成は原稿を尊重し、必要に応じて本文末尾に参考文献セクションを追加してください。', - ].join('\n'); - const prompt = [ - '## 原稿', String(draft.draftBody||''), '', '## エビデンス(Tavily検索結果)', evidence - ].join('\n\n'); - const { text } = await generateText({ model: anthropic(modelName), system: sys, prompt, temperature: 0.3, maxTokens: 30000 }); - let body = stripCodeFence(text||''); - let title = process.env.TITLE || draft.title || ''; - let tags = Array.isArray(draft.tags)? draft.tags: []; - const obj = extractJsonFlexible(body); - if (obj && typeof obj === 'object' && !Array.isArray(obj)) { - if (obj.title) title = String(obj.title); - const candidates = [obj.body, obj.draftBody, obj.content, obj.text]; - const chosen = candidates.find(v=>typeof v==='string' && v.trim()); - if (chosen) body = String(chosen); - if (Array.isArray(obj.tags)) tags = obj.tags.map(String); - } - body = stripCodeFence(body); - const out = { title, body, tags }; - fs.writeFileSync('.note-artifacts/final.json', JSON.stringify(out,null,2)); - } + const fact = await res.json(); - await main(); + const finalArticle = { + ...article, + factcheck: fact + }; + + fs.writeFileSync('final_article.json', JSON.stringify(finalArticle, null, 2)); + console.log('final article saved'); EOF - node factcheck.mjs - - name: Upload fact-check artifact - uses: actions/upload-artifact@v4 - with: - name: final-artifact - path: .note-artifacts/final.json + node factcheck.mjs - - name: Collect final + - name: Collect final article id: collect run: | - title=$(node -e "console.log(JSON.parse(require('fs').readFileSync('.note-artifacts/final.json','utf8')).title)") - b64=$(base64 -w 0 .note-artifacts/final.json 2>/dev/null || base64 .note-artifacts/final.json) - echo "title<> $GITHUB_OUTPUT - echo "$title" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - echo "final_b64<> $GITHUB_OUTPUT - echo "$b64" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + echo "final_b64=$(base64 -w 0 final_article.json)" >> "$GITHUB_OUTPUT" + + - name: Upload final article artifact + uses: actions/upload-artifact@v4 + with: + name: final-article-json + path: final_article.json post: name: Post to note.com (Playwright) - needs: factcheck - if: ${{ github.event.inputs.dry_run != 'true' }} runs-on: ubuntu-latest + needs: factcheck + if: ${{ github.event.inputs.dry_run == 'false' }} env: + NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} + FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} IS_PUBLIC: ${{ github.event.inputs.is_public }} - STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} - START_URL: https://editor.note.com/new - outputs: - final_url: ${{ steps.publish.outputs.published_url || steps.publish.outputs.draft_url }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Debug list files - run: | - pwd - ls -la + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -384,307 +332,81 @@ jobs: - name: Install Playwright run: | npm init -y - npm i playwright marked - npx playwright install --with-deps chromium | cat + npm install playwright + npx playwright install --with-deps chromium - - name: Prepare storageState - id: state + - name: Restore storage state run: | - test -n "$STATE_JSON" || (echo "ERROR: NOTE_STORAGE_STATE_JSON secret is not set" && exit 1) - mkdir -p "$RUNNER_TEMP" - echo "$STATE_JSON" > "$RUNNER_TEMP/note-state.json" - echo "STATE_PATH=$RUNNER_TEMP/note-state.json" >> $GITHUB_OUTPUT + cat > note-state.json <<'EOF' + ${{ secrets.NOTE_STORAGE_STATE_JSON }} + EOF - - name: Ensure jq (post) - run: | - if ! command -v jq >/dev/null 2>&1; then - sudo apt-get update -y - sudo apt-get install -y jq - fi - - - name: Restore final - id: draft - env: - FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} + - name: Post to note run: | - test -n "$FINAL_B64" || { echo "final_b64 output is empty"; exit 1; } - echo "$FINAL_B64" | base64 -d > final.json || echo "$FINAL_B64" | base64 --decode > final.json - echo "TITLE=$(jq -r .title final.json)" >> $GITHUB_OUTPUT - echo "TAGS=$(jq -r '.tags | join(", ")' final.json)" >> $GITHUB_OUTPUT - - - name: Publish via Playwright (draft or public) - id: publish - env: - TITLE: ${{ steps.draft.outputs.TITLE }} - TAGS: ${{ steps.draft.outputs.TAGS }} - STATE_PATH: ${{ steps.state.outputs.STATE_PATH }} - run: | - # 本文は後続スクリプト内でMarkdownリンク→素URL化などの前処理を行う cat > post.mjs <<'EOF' - import { chromium } from 'playwright'; - import { marked } from 'marked'; import fs from 'fs'; - import os from 'os'; - import path from 'path'; - - function nowStr(){ const d=new Date(); const z=n=>String(n).padStart(2,'0'); return `${d.getFullYear()}-${z(d.getMonth()+1)}-${z(d.getDate())}_${z(d.getHours())}-${z(d.getMinutes())}-${z(d.getSeconds())}`; } - - const STATE_PATH=process.env.STATE_PATH; - const START_URL=process.env.START_URL||'https://editor.note.com/new'; - const rawTitle=process.env.TITLE||''; - const rawFinal=JSON.parse(fs.readFileSync('final.json','utf8')); - const rawBody=String(rawFinal.body||''); - const TAGS=process.env.TAGS||''; - const IS_PUBLIC=String(process.env.IS_PUBLIC||'false')==='true'; - - if(!fs.existsSync(STATE_PATH)){ console.error('storageState not found:', STATE_PATH); process.exit(1); } - - const ssDir=path.join(os.tmpdir(),'note-screenshots'); fs.mkdirSync(ssDir,{recursive:true}); const SS_PATH=path.join(ssDir,`note-post-${nowStr()}.png`); - - function sanitizeTitle(t){ - let s=String(t||'').trim(); - s=s.replace(/^```[a-zA-Z0-9_-]*\s*$/,'').replace(/^```$/,''); - s=s.replace(/^#+\s*/,''); - s=s.replace(/^"+|"+$/g,'').replace(/^'+|'+$/g,''); - s=s.replace(/^`+|`+$/g,''); - s=s.replace(/^json$/i,'').trim(); - // タイトルが波括弧や記号のみの時は無効として扱う - if (/^[\{\}\[\]\(\)\s]*$/.test(s)) s=''; - if(!s) s='タイトル(自動生成)'; - return s; - } - function deriveTitleFromMarkdown(md){ - const lines=String(md||'').split(/\r?\n/); - for (const line of lines){ - const l=line.trim(); - if(!l) continue; - const m=l.match(/^#{1,3}\s+(.+)/); if(m) return sanitizeTitle(m[1]); - if(!/^```|^>|^\* |^- |^\d+\. /.test(l)) return sanitizeTitle(l); - } - return ''; - } - function normalizeBullets(md){ - // 先頭の中黒・ビュレットを箇条書きに正規化 - return String(md||'') - .replace(/^\s*[•・]\s?/gm,'- ') - .replace(/^\s*◦\s?/gm,' - '); - } - function unwrapParagraphs(md){ - // 段落中の不必要な改行をスペースへ(見出し/リスト/引用/コードは除外) - const lines=String(md||'').split(/\r?\n/); - const out=[]; let buf=''; let inFence=false; - for(const raw of lines){ - const line=raw.replace(/\u200B/g,''); - if(/^```/.test(line)){ inFence=!inFence; buf+=line+'\n'; continue; } - if(inFence){ buf+=line+'\n'; continue; } - if(/^\s*$/.test(line)){ if(buf) out.push(buf.trim()); out.push(''); buf=''; continue; } - // 箇条書きや番号付きの字下げ改行を一行に連結 - if(/^(#{1,6}\s|[-*+]\s|\d+\.\s|>\s)/.test(line)){ - if(buf){ out.push(buf.trim()); buf=''; } - // 次の数行が連続して単語単位の改行の場合は連結 - out.push(line.replace(/\s+$/,'')); - continue; - } - // 行頭が1文字や数文字で改行されているケース(縦伸び)を連結 - if(buf){ buf += (/[。.!?)]$/.test(buf) ? '\n' : ' ') + line.trim(); } - else { buf = line.trim(); } - } - if(buf) out.push(buf.trim()); - return out.join('\n'); - } - function preferBareUrls(md){ - const embedDomains=['openai.com','youtube.com','youtu.be','x.com','twitter.com','speakerdeck.com','slideshare.net','google.com','maps.app.goo.gl','gist.github.com']; - return String(md||'').replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,(m,text,url)=>{ - try{ - const u=new URL(url); const host=u.hostname.replace(/^www\./,''); - const isEmbed = embedDomains.some(d=>host.endsWith(d) || (url.includes('google.com/maps') && d.includes('google.com'))); - return isEmbed ? `${text}\n${url}\n` : `${text} (${url})`; - }catch{return `${text} ${url}`;} - }); - } - function isGarbageLine(line){ - return /^[\s\{\}\[\]\(\)`]+$/.test(line || ''); - } - function normalizeListItemSoftBreaks(md){ - const lines=String(md||'').split(/\r?\n/); - const out=[]; let inItem=false; - const listStartRe=/^(\s*)(?:[-*+]\s|\d+\.\s)/; - for (let i=0;i{ const t=b.trim(); return t.length>0 && !isGarbageLine(t); }); - } - function mdToHtml(block){ - // JSONが紛れ込んでしまった場合は本文候補のみ抽出 - try{ - const maybe = JSON.parse(block); - if (maybe && typeof maybe==='object' && !Array.isArray(maybe)){ - const candidates=[maybe.body, maybe.draftBody, maybe.content, maybe.text]; - const chosen=candidates.find(v=>typeof v==='string' && v.trim()); - if (chosen) block = String(chosen); - } - }catch{} - const isList = /^\s*(?:[-*+]\s|\d+\.\s)/.test(block); - return String(marked.parse(block, { gfm:true, breaks: !isList, mangle:false, headerIds:false }) || ''); - } - function htmlFromMarkdown(md){ - // 全文を一括でHTML化(段落ベース)。リスト中の意図しない
を避けるため breaks=false - return String(marked.parse(md, { gfm:true, breaks:false, mangle:false, headerIds:false }) || ''); - } - async function insertHTML(page, locator, html){ - await locator.click(); - await locator.evaluate((el, html) => { - el.focus(); - const sel = window.getSelection(); - const range = document.createRange(); - range.selectNodeContents(el); - range.collapse(false); - sel.removeAllRanges(); - sel.addRange(range); - document.execCommand('insertHTML', false, html); - }, html); } - let TITLE=sanitizeTitle(rawTitle); - let preBody = preferBareUrls(rawBody); - preBody = normalizeBullets(preBody); - preBody = normalizeListItemSoftBreaks(preBody); - preBody = unwrapParagraphs(preBody); - if(!TITLE || TITLE==='タイトル(自動生成)'){ - const d=deriveTitleFromMarkdown(preBody); - if(d) TITLE=d; + if (!titleDone) { + console.log('title field not found'); } - const blocks = splitMarkdownBlocks(preBody); - - let browser, context, page; - try{ - browser = await chromium.launch({ headless: true, args: ['--lang=ja-JP'] }); - context = await browser.newContext({ storageState: STATE_PATH, locale: 'ja-JP' }); - page = await context.newPage(); - page.setDefaultTimeout(180000); - - await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - await page.waitForSelector('textarea[placeholder*="タイトル"]'); - await page.fill('textarea[placeholder*="タイトル"]', TITLE); - - const bodyBox = page.locator('div[contenteditable="true"][role="textbox"]').first(); - await bodyBox.waitFor({ state: 'visible' }); - const htmlAll = htmlFromMarkdown(preBody); - let pasted = false; - try { - const origin = new URL(START_URL).origin; - await context.grantPermissions(['clipboard-read','clipboard-write'], { origin }); - await page.evaluate(async (html, plain) => { - const item = new ClipboardItem({ - 'text/html': new Blob([html], { type: 'text/html' }), - 'text/plain': new Blob([plain], { type: 'text/plain' }), - }); - await navigator.clipboard.write([item]); - }, htmlAll, preBody); - await bodyBox.click(); - await page.keyboard.press('Control+V'); - await page.waitForTimeout(200); - pasted = true; - } catch (e) { - // クリップボード権限が無い場合のフォールバック - } - if (!pasted) { - // 一括HTML挿入フォールバック - await insertHTML(page, bodyBox, htmlAll); - await page.waitForTimeout(100); - } - if(!IS_PUBLIC){ - const saveBtn = page.locator('button:has-text("下書き保存"), [aria-label*="下書き保存"]').first(); - await saveBtn.waitFor({ state: 'visible' }); - if(await saveBtn.isEnabled()) { await saveBtn.click(); await page.locator('text=保存しました').waitFor({ timeout: 4000 }).catch(()=>{}); } - await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('DRAFT_URL=' + page.url()); - console.log('SCREENSHOT=' + SS_PATH); - process.exit(0); - } + await page.keyboard.press('Tab'); + await page.keyboard.type(data.body, { delay: 5 }); + await page.waitForTimeout(3000); - const proceed = page.locator('button:has-text("公開に進む")').first(); - await proceed.waitFor({ state: 'visible' }); - for (let i=0;i<20;i++){ if (await proceed.isEnabled()) break; await page.waitForTimeout(100); } - await proceed.click({ force: true }); - - await Promise.race([ - page.waitForURL(/\/publish/i).catch(() => {}), - page.locator('button:has-text("投稿する")').first().waitFor({ state: 'visible' }).catch(() => {}), - ]); - - const tags=(TAGS||'').split(/[\n,]/).map(s=>s.trim()).filter(Boolean); - if(tags.length){ - let tagInput=page.locator('input[placeholder*="ハッシュタグ"]'); - if(!(await tagInput.count())) tagInput=page.locator('input[role="combobox"]').first(); - await tagInput.waitFor({ state: 'visible' }); - for(const t of tags){ await tagInput.click(); await tagInput.fill(t); await page.keyboard.press('Enter'); await page.waitForTimeout(120); } + if (isPublic) { + const publishButton = page.getByRole('button', { name: /公開|publish/i }); + if (await publishButton.count()) { + await publishButton.first().click(); + await page.waitForTimeout(3000); + } + } else { + const saveButton = page.getByRole('button', { name: /下書き|保存|save/i }); + if (await saveButton.count()) { + await saveButton.first().click(); + await page.waitForTimeout(3000); } - - const publishBtn = page.locator('button:has-text("投稿する")').first(); - await publishBtn.waitFor({ state: 'visible' }); - for (let i=0;i<20;i++){ if (await publishBtn.isEnabled()) break; await page.waitForTimeout(100); } - await publishBtn.click({ force: true }); - - await Promise.race([ - page.waitForURL(u => !/\/publish/i.test(typeof u === 'string' ? u : u.toString()), { timeout: 20000 }).catch(() => {}), - page.locator('text=投稿しました').first().waitFor({ timeout: 8000 }).catch(() => {}), - page.waitForTimeout(5000), - ]); - - await page.screenshot({ path: SS_PATH, fullPage: true }); - const finalUrl=page.url(); - console.log('PUBLISHED_URL=' + finalUrl); - console.log('SCREENSHOT=' + SS_PATH); - } finally { - try{ await page?.close(); }catch{} - try{ await context?.close(); }catch{} - try{ await browser?.close(); }catch{} } + + await context.storageState({ path: statePath }); + await browser.close(); EOF - node post.mjs | tee post.log - url=$(grep '^PUBLISHED_URL=' post.log | tail -n1 | cut -d'=' -f2-) - draft=$(grep '^DRAFT_URL=' post.log | tail -n1 | cut -d'=' -f2-) - shot=$(grep '^SCREENSHOT=' post.log | tail -n1 | cut -d'=' -f2-) - if [ -n "$url" ]; then echo "published_url=$url" >> $GITHUB_OUTPUT; fi - if [ -n "$draft" ]; then echo "draft_url=$draft" >> $GITHUB_OUTPUT; fi - if [ -n "$shot" ]; then echo "screenshot=$shot" >> $GITHUB_OUTPUT; fi - - - name: Upload screenshot (if any) - if: ${{ steps.publish.outputs.screenshot != '' }} + + node post.mjs + + - name: Upload updated state uses: actions/upload-artifact@v4 with: - name: note-screenshot - path: ${{ steps.publish.outputs.screenshot }} - + name: updated-note-state + path: note-state.json From 41feacc5b6ee30fedb0ef3933f956978d0db85d7 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 11:31:30 +0900 Subject: [PATCH 04/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index cc4a645..8835213 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -191,7 +191,7 @@ jobs: 'content-type': 'application/json' }, body: JSON.stringify({ - model: 'claude-3-5-sonnet-latest', + model: 'claude-3-5-sonnet-20240620', max_tokens: 4000, system: systemPrompt, messages: [ From ed91fc4fd79839c126172b5bbd35133fbe61f3d1 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 11:45:31 +0900 Subject: [PATCH 05/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 8835213..a6125c3 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -191,7 +191,7 @@ jobs: 'content-type': 'application/json' }, body: JSON.stringify({ - model: 'claude-3-5-sonnet-20240620', + model: 'claude-3-haiku-20240307' max_tokens: 4000, system: systemPrompt, messages: [ From a3d1303d7872c77291feabcf3cf481483279fc26 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 11:53:11 +0900 Subject: [PATCH 06/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index a6125c3..a11c933 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -191,7 +191,7 @@ jobs: 'content-type': 'application/json' }, body: JSON.stringify({ - model: 'claude-3-haiku-20240307' + model: 'claude-3-haiku-20240307', max_tokens: 4000, system: systemPrompt, messages: [ From 52086e8782a1e3ee10e304743e508e4de312d44f Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 12:00:15 +0900 Subject: [PATCH 07/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index a11c933..b3edc64 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -191,7 +191,7 @@ jobs: 'content-type': 'application/json' }, body: JSON.stringify({ - model: 'claude-3-haiku-20240307', + model: 'claude-3-5-haiku-20241022', max_tokens: 4000, system: systemPrompt, messages: [ From 8918b0bd0a8edeced1237676a07b6c7c2fa12f24 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 12:03:25 +0900 Subject: [PATCH 08/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index b3edc64..8835213 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -191,7 +191,7 @@ jobs: 'content-type': 'application/json' }, body: JSON.stringify({ - model: 'claude-3-5-haiku-20241022', + model: 'claude-3-5-sonnet-20240620', max_tokens: 4000, system: systemPrompt, messages: [ From 7e25c774af99c98f2f38de529620dad6ad5dfe45 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 12:12:22 +0900 Subject: [PATCH 09/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 8835213..0723462 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -191,7 +191,7 @@ jobs: 'content-type': 'application/json' }, body: JSON.stringify({ - model: 'claude-3-5-sonnet-20240620', + model: 'claude-3-opus-20240229', max_tokens: 4000, system: systemPrompt, messages: [ From 55db604b02fd0c700b01c702b9a3947d2c38eb6a Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 12:50:09 +0900 Subject: [PATCH 10/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 0723462..97361ce 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -191,7 +191,7 @@ jobs: 'content-type': 'application/json' }, body: JSON.stringify({ - model: 'claude-3-opus-20240229', + model: 'claude-3-haiku', max_tokens: 4000, system: systemPrompt, messages: [ From c89e86ed79c28a8c6fcc2096e21c8a2d31974106 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 12:56:52 +0900 Subject: [PATCH 11/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 97361ce..63a0fa4 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -191,7 +191,7 @@ jobs: 'content-type': 'application/json' }, body: JSON.stringify({ - model: 'claude-3-haiku', + model: 'claude-haiku-4-5', max_tokens: 4000, system: systemPrompt, messages: [ From 2f40ffcc047933af1ce4ad27022f894d68f2aba5 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 13:40:08 +0900 Subject: [PATCH 12/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 63a0fa4..5fddcee 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -210,7 +210,7 @@ jobs: } const json = await res.json(); - const text = json.content?.[0]?.text || ''; + const text = (json.content?.[0]?.text || '').replace(/```json|```/g,'').trim(); fs.writeFileSync('article_raw.txt', text); let parsed; From 7f2919ed054b4ce03db3c6a187cc314b3c7e338b Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 21:20:03 +0900 Subject: [PATCH 13/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 5fddcee..f427f6c 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -264,7 +264,7 @@ jobs: const apiKey = process.env.TAVILY_API_KEY; const article = JSON.parse(Buffer.from(process.env.ARTICLE_B64, 'base64').toString('utf8')); - const query = `${article.title}\n\n${article.body.slice(0, 1200)}`; + const query = `${article.title}\n\n${article.body.slice(0, 300)}`; const res = await fetch('https://api.tavily.com/search', { method: 'POST', From afc42a9898f8b9f16f248cb0a4627e8eb1e35716 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 21:31:38 +0900 Subject: [PATCH 14/50] Delete login-note.mjs --- login-note.mjs | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 login-note.mjs diff --git a/login-note.mjs b/login-note.mjs deleted file mode 100644 index 231b2bf..0000000 --- a/login-note.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { chromium } from 'playwright'; -import fs from 'fs'; - -const STATE_PATH = './note-state.json'; - -// 手動ログインのため環境変数は不要 - -const wait = (ms) => new Promise(r => setTimeout(r, ms)); - -(async () => { - const browser = await chromium.launch({ headless: false }); - const context = await browser.newContext(); - const page = await context.newPage(); - - await page.goto('https://note.com/login'); - - console.log('手動でログインしてください。ログイン完了を自動検知します...'); - - // ログイン完了を自動検知(note.comのトップページに遷移するまで待機) - try { - await page.waitForURL(/note\.com\/?$/, { timeout: 300000 }); // 5分待機 - console.log('ログイン完了を検知しました!'); - } catch (error) { - console.log('ログイン完了の検知に失敗しました。手動でEnterキーを押してください。'); - await new Promise(resolve => { - process.stdin.once('data', () => { - resolve(); - }); - }); - } - - console.log('ログイン状態を保存中...'); - - // 保存 - await context.storageState({ path: STATE_PATH }); - console.log('Saved:', STATE_PATH); - - await browser.close(); -})(); From 622d940b4c268faa4b0c1d192f741d6b57b26f1b Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sun, 8 Mar 2026 22:17:28 +0900 Subject: [PATCH 15/50] Update note.yaml --- .github/workflows/note.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index f427f6c..6723b3d 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -1,6 +1,8 @@ name: Note Workflow on: + schedule: + - cron: '0 22 * * *' workflow_dispatch: inputs: theme: From 2a378fa0a86d7ee4aaa49864dfd83e88b59beaf1 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 11:44:40 +0900 Subject: [PATCH 16/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 6723b3d..9edcb00 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -72,7 +72,7 @@ jobs: import fs from 'fs'; const apiKey = process.env.TAVILY_API_KEY; - const theme = process.env.THEME || ''; + 75 const theme = process.env.THEME || '大阪 マツエク'; const target = process.env.TARGET || ''; const message = process.env.MESSAGE || ''; From 7eeefda30154a44e6f3024b724cc5f4e6ab468c9 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 12:47:02 +0900 Subject: [PATCH 17/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 9edcb00..e02393b 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -72,7 +72,7 @@ jobs: import fs from 'fs'; const apiKey = process.env.TAVILY_API_KEY; - 75 const theme = process.env.THEME || '大阪 マツエク'; + const theme = process.env.THEME || '大阪 マツエク'; const target = process.env.TARGET || ''; const message = process.env.MESSAGE || ''; From 4877e31b7ebbb60595cfb70f8a510d22c2e5c49c Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 14:22:02 +0900 Subject: [PATCH 18/50] Update note.yaml --- .github/workflows/note.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index e02393b..608bea4 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -383,7 +383,8 @@ jobs: console.log('title field not found'); } - await page.keyboard.press('Tab'); + await page.keyboard.type(data.title, { delay: 5 }); + await page.keyboard.type(data.body, { delay: 5 }); await page.waitForTimeout(3000); From ee7a17c5fab6122fe40188d54b11fe021212e5a0 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 14:49:09 +0900 Subject: [PATCH 19/50] Update note.yaml --- .github/workflows/note.yaml | 47 +++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 608bea4..b814515 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -378,31 +378,32 @@ jobs: } catch {} } } - - if (!titleDone) { - console.log('title field not found'); - } - - await page.keyboard.type(data.title, { delay: 5 }); - - await page.keyboard.type(data.body, { delay: 5 }); + const data = JSON.parse(process.env.WRITE_JSON); + const titleField = page.locator('textarea').first(); + await titleField.click(); + await titleField.fill(data.title); + await page.waitForTimeout(1000); + + await page.keyboard.press('Tab'); + await page.keyboard.type(data.body, { delay: 5 }); + await page.waitForTimeout(3000); + + if (isPublic) { + const publishButton = page.getByRole('button', { name: /公開|publish/i }); + if (await publishButton.count()) { + await publishButton.first().click(); await page.waitForTimeout(3000); + } + } else { + const saveButton = page.getByRole('button', { name: /下書き|保存|save/i }); + if (await saveButton.count()) { + await saveButton.first().click(); + await page.waitForTimeout(3000); + } + } - if (isPublic) { - const publishButton = page.getByRole('button', { name: /公開|publish/i }); - if (await publishButton.count()) { - await publishButton.first().click(); - await page.waitForTimeout(3000); - } - } else { - const saveButton = page.getByRole('button', { name: /下書き|保存|save/i }); - if (await saveButton.count()) { - await saveButton.first().click(); - await page.waitForTimeout(3000); - } - } - - await context.storageState({ path: statePath }); + await context.storageState({ path: statePath }); + await browser.close(); await browser.close(); EOF From 2fc3722eb289f1b6b669dce1eccfc9a4d55d4b9c Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 15:29:29 +0900 Subject: [PATCH 20/50] Update note.yaml --- .github/workflows/note.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index b814515..1296e23 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -316,11 +316,12 @@ jobs: post: name: Post to note.com (Playwright) runs-on: ubuntu-latest - needs: factcheck + needs: [write, factcheck] if: ${{ github.event.inputs.dry_run == 'false' }} env: NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} + TITLE: ${{ needs.write.outputs.title }} IS_PUBLIC: ${{ github.event.inputs.is_public }} steps: - name: Checkout From ee9b36d379bc63cc1e57c6361ce307aa832dc704 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 15:38:19 +0900 Subject: [PATCH 21/50] Update note.yaml --- .github/workflows/note.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 1296e23..09c3951 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -316,12 +316,12 @@ jobs: post: name: Post to note.com (Playwright) runs-on: ubuntu-latest - needs: [write, factcheck] + needs: factcheck if: ${{ github.event.inputs.dry_run == 'false' }} env: NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} - TITLE: ${{ needs.write.outputs.title }} + IS_PUBLIC: ${{ github.event.inputs.is_public }} steps: - name: Checkout From 3989f4d04274a83852db38039d597e796ffef924 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 16:37:17 +0900 Subject: [PATCH 22/50] Update note.yaml --- .github/workflows/note.yaml | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 09c3951..6702f1d 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -379,36 +379,13 @@ jobs: } catch {} } } - const data = JSON.parse(process.env.WRITE_JSON); - const titleField = page.locator('textarea').first(); - await titleField.click(); - await titleField.fill(data.title); - await page.waitForTimeout(1000); - - await page.keyboard.press('Tab'); - await page.keyboard.type(data.body, { delay: 5 }); - await page.waitForTimeout(3000); - - if (isPublic) { - const publishButton = page.getByRole('button', { name: /公開|publish/i }); - if (await publishButton.count()) { - await publishButton.first().click(); - await page.waitForTimeout(3000); - } - } else { - const saveButton = page.getByRole('button', { name: /下書き|保存|save/i }); - if (await saveButton.count()) { - await saveButton.first().click(); - await page.waitForTimeout(3000); - } - } + await context.storageState({ path: statePath }); - await browser.close(); + await browser.close(); EOF - - node post.mjs +       node post.mjs - name: Upload updated state uses: actions/upload-artifact@v4 From 86e1d14860ab681b8e0be0ed32f80654e99ea1de Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 16:44:00 +0900 Subject: [PATCH 23/50] Update note.yaml --- .github/workflows/note.yaml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 6702f1d..2132a8b 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -385,10 +385,5 @@ jobs: await browser.close(); EOF -       node post.mjs +      - - name: Upload updated state - uses: actions/upload-artifact@v4 - with: - name: updated-note-state - path: note-state.json From 0f0c8cec7bd80f1deec1d97d97d3a49d599a137c Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 17:14:23 +0900 Subject: [PATCH 24/50] Update note.yaml --- .github/workflows/note.yaml | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 2132a8b..608bea4 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -321,7 +321,6 @@ jobs: env: NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} - IS_PUBLIC: ${{ github.event.inputs.is_public }} steps: - name: Checkout @@ -379,11 +378,38 @@ jobs: } catch {} } } - - await context.storageState({ path: statePath }); - + if (!titleDone) { + console.log('title field not found'); + } + + await page.keyboard.type(data.title, { delay: 5 }); + + await page.keyboard.type(data.body, { delay: 5 }); + await page.waitForTimeout(3000); + + if (isPublic) { + const publishButton = page.getByRole('button', { name: /公開|publish/i }); + if (await publishButton.count()) { + await publishButton.first().click(); + await page.waitForTimeout(3000); + } + } else { + const saveButton = page.getByRole('button', { name: /下書き|保存|save/i }); + if (await saveButton.count()) { + await saveButton.first().click(); + await page.waitForTimeout(3000); + } + } + + await context.storageState({ path: statePath }); await browser.close(); EOF -      + node post.mjs + + - name: Upload updated state + uses: actions/upload-artifact@v4 + with: + name: updated-note-state + path: note-state.json From f08eea4b3a7d6f446f6c5296bde697feca72f16d Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 17:59:17 +0900 Subject: [PATCH 25/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 608bea4..4fd2c74 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -368,7 +368,7 @@ jobs: let titleDone = false; for (const sel of titleSelectors) { - const loc = page.locator(sel).first(); + const loc = page.locator('textarea[placeholder="記事タイトル"]').first(); if (await loc.count()) { try { await loc.click(); From db28393ceb4ee1c5d7fda59167b3ac45d6aee55a Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 18:13:30 +0900 Subject: [PATCH 26/50] Update note.yaml --- .github/workflows/note.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 4fd2c74..608bea4 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -368,7 +368,7 @@ jobs: let titleDone = false; for (const sel of titleSelectors) { - const loc = page.locator('textarea[placeholder="記事タイトル"]').first(); + const loc = page.locator(sel).first(); if (await loc.count()) { try { await loc.click(); From 55acf9d2e459f20b80acc76ca7b033b1b3c6e6fc Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 18:51:34 +0900 Subject: [PATCH 27/50] Update note.yaml --- .github/workflows/note.yaml | 339 ++++++++---------------------------- 1 file changed, 73 insertions(+), 266 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 608bea4..6149430 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -50,8 +50,8 @@ env: TZ: Asia/Tokyo jobs: + research: - name: Research (Tavily) runs-on: ubuntu-latest env: TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} @@ -61,355 +61,162 @@ jobs: outputs: research_b64: ${{ steps.collect.outputs.research_b64 }} steps: - - name: Setup Node.js - uses: actions/setup-node@v4 + + - uses: actions/setup-node@v4 with: node-version: '20' - - name: Research with Tavily + - name: Research run: | cat > research.mjs <<'EOF' import fs from 'fs'; const apiKey = process.env.TAVILY_API_KEY; - const theme = process.env.THEME || '大阪 マツエク'; - const target = process.env.TARGET || ''; - const message = process.env.MESSAGE || ''; - - const prompt = `${theme} ${target} ${message}`.trim(); - - const body = { - query: prompt, - search_depth: 'advanced', - topic: 'general', - max_results: 5, - include_answer: true - }; + const theme = process.env.THEME || ''; const res = await fetch('https://api.tavily.com/search', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - api_key: apiKey, - ...body + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({ + api_key:apiKey, + query:theme, + search_depth:'advanced', + max_results:5 }) }); - if (!res.ok) { - const txt = await res.text(); - console.error(txt); - process.exit(1); - } - const json = await res.json(); - fs.writeFileSync('research.json', JSON.stringify(json, null, 2)); - console.log('research saved'); + fs.writeFileSync('research.json',JSON.stringify(json,null,2)); EOF node research.mjs - - name: Collect research - id: collect + - id: collect run: | echo "research_b64=$(base64 -w 0 research.json)" >> "$GITHUB_OUTPUT" - - name: Upload research artifact - uses: actions/upload-artifact@v4 - with: - name: research-json - path: research.json - write: - name: Write (Claude) runs-on: ubuntu-latest needs: research env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + RESEARCH_B64: ${{ needs.research.outputs.research_b64 }} THEME: ${{ github.event.inputs.theme }} TARGET: ${{ github.event.inputs.target }} MESSAGE: ${{ github.event.inputs.message }} CTA: ${{ github.event.inputs.cta }} - TAGS: ${{ github.event.inputs.tags }} - RESEARCH_B64: ${{ needs.research.outputs.research_b64 }} outputs: article_b64: ${{ steps.collect.outputs.article_b64 }} steps: - - name: Setup Node.js - uses: actions/setup-node@v4 + + - uses: actions/setup-node@v4 with: node-version: '20' - - name: Write article with Claude + - name: Write run: | cat > write.mjs <<'EOF' import fs from 'fs'; const apiKey = process.env.ANTHROPIC_API_KEY; - const theme = process.env.THEME || ''; - const target = process.env.TARGET || ''; - const message = process.env.MESSAGE || ''; - const cta = process.env.CTA || ''; - const tags = process.env.TAGS || ''; - const research = JSON.parse(Buffer.from(process.env.RESEARCH_B64, 'base64').toString('utf8')); - - const systemPrompt = ` - あなたは日本語のnote記事を書くプロの編集者です。 - 目的は、読みやすく、信頼感があり、行動につながるnote記事を作ることです。 - 出力は必ずJSONのみ。 - 形式: - { - "title": "記事タイトル", - "body": "note本文。見出しや改行を含む", - "tags": ["タグ1","タグ2","タグ3"] - } - `.trim(); - - const userPrompt = ` - 記事テーマ: ${theme} - 想定読者: ${target} - 読者に伝えたい核メッセージ: ${message} - 読後のアクション: ${cta} - 希望タグ: ${tags} - - リサーチ結果: - ${JSON.stringify(research, null, 2)} - - 条件: - - 日本語 - - 読みやすい自然な文章 - - 誇張しすぎない - - 見出しを使う - - 最後にCTAを入れる - - tags は配列で3〜5個 - `.trim(); - - const res = await fetch('https://api.anthropic.com/v1/messages', { - method: 'POST', - headers: { - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01', - 'content-type': 'application/json' + const research = JSON.parse(Buffer.from(process.env.RESEARCH_B64,'base64').toString()); + + const prompt = ` + テーマ:${process.env.THEME} + 読者:${process.env.TARGET} + メッセージ:${process.env.MESSAGE} + CTA:${process.env.CTA} + + ${JSON.stringify(research)} + `; + + const res = await fetch('https://api.anthropic.com/v1/messages',{ + method:'POST', + headers:{ + 'x-api-key':apiKey, + 'anthropic-version':'2023-06-01', + 'content-type':'application/json' }, - body: JSON.stringify({ - model: 'claude-haiku-4-5', - max_tokens: 4000, - system: systemPrompt, - messages: [ - { - role: 'user', - content: userPrompt - } - ] + body:JSON.stringify({ + model:'claude-haiku-4-5', + max_tokens:3000, + messages:[{role:'user',content:prompt}] }) }); - if (!res.ok) { - const txt = await res.text(); - console.error(txt); - process.exit(1); - } - const json = await res.json(); - const text = (json.content?.[0]?.text || '').replace(/```json|```/g,'').trim(); - fs.writeFileSync('article_raw.txt', text); - - let parsed; - try { - parsed = JSON.parse(text); - } catch { - console.error('Claude output was not valid JSON'); - console.error(text); - process.exit(1); - } - - fs.writeFileSync('article.json', JSON.stringify(parsed, null, 2)); - console.log('article saved'); - EOF - - node write.mjs - - - name: Collect article - id: collect - run: | - echo "article_b64=$(base64 -w 0 article.json)" >> "$GITHUB_OUTPUT" - - - name: Upload article artifact - uses: actions/upload-artifact@v4 - with: - name: article-json - path: | - article.json - article_raw.txt - - factcheck: - name: Fact-check (Tavily) - runs-on: ubuntu-latest - needs: write - env: - TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} - outputs: - final_b64: ${{ steps.collect.outputs.final_b64 }} - steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Fact-check article - run: | - cat > factcheck.mjs <<'EOF' - import fs from 'fs'; + const text = json.content[0].text; - const apiKey = process.env.TAVILY_API_KEY; - const article = JSON.parse(Buffer.from(process.env.ARTICLE_B64, 'base64').toString('utf8')); - - const query = `${article.title}\n\n${article.body.slice(0, 300)}`; - - const res = await fetch('https://api.tavily.com/search', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - api_key: apiKey, - query, - search_depth: 'advanced', - topic: 'general', - max_results: 5, - include_answer: true - }) - }); - - if (!res.ok) { - const txt = await res.text(); - console.error(txt); - process.exit(1); - } - - const fact = await res.json(); - - const finalArticle = { - ...article, - factcheck: fact + const article={ + title:process.env.THEME, + body:text }; - fs.writeFileSync('final_article.json', JSON.stringify(finalArticle, null, 2)); - console.log('final article saved'); + fs.writeFileSync('article.json',JSON.stringify(article,null,2)); EOF - node factcheck.mjs + node write.mjs - - name: Collect final article - id: collect + - id: collect run: | - echo "final_b64=$(base64 -w 0 final_article.json)" >> "$GITHUB_OUTPUT" + echo "article_b64=$(base64 -w 0 article.json)" >> "$GITHUB_OUTPUT" - - name: Upload final article artifact - uses: actions/upload-artifact@v4 - with: - name: final-article-json - path: final_article.json post: - name: Post to note.com (Playwright) runs-on: ubuntu-latest - needs: factcheck + needs: write if: ${{ github.event.inputs.dry_run == 'false' }} env: NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} - FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} + ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} IS_PUBLIC: ${{ github.event.inputs.is_public }} steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: node-version: '20' - - name: Install Playwright - run: | + - run: | npm init -y npm install playwright - npx playwright install --with-deps chromium + npx playwright install chromium - - name: Restore storage state - run: | + - run: | cat > note-state.json <<'EOF' ${{ secrets.NOTE_STORAGE_STATE_JSON }} EOF - - name: Post to note + - name: Post run: | cat > post.mjs <<'EOF' - import fs from 'fs'; import { chromium } from 'playwright'; - const data = JSON.parse(Buffer.from(process.env.FINAL_B64, 'base64').toString('utf8')); - const statePath = './note-state.json'; - const isPublic = process.env.IS_PUBLIC === 'true'; - - const browser = await chromium.launch({ headless: true }); - const context = await browser.newContext({ storageState: statePath }); - const page = await context.newPage(); - - await page.goto('https://note.com/notes/new', { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(5000); - - const titleSelectors = [ - 'textarea', - 'input[placeholder*="タイトル"]', - '[contenteditable="true"]' - ]; - - let titleDone = false; - for (const sel of titleSelectors) { - const loc = page.locator(sel).first(); - if (await loc.count()) { - try { - await loc.click(); - await loc.fill(data.title); - titleDone = true; - break; - } catch {} - } - } + const data=JSON.parse(Buffer.from(process.env.ARTICLE_B64,'base64').toString()); + const isPublic=process.env.IS_PUBLIC==='true'; - if (!titleDone) { - console.log('title field not found'); - } + const browser=await chromium.launch({headless:true}); + const context=await browser.newContext({storageState:'note-state.json'}); + const page=await context.newPage(); - await page.keyboard.type(data.title, { delay: 5 }); + await page.goto('https://editor.note.com/notes/new'); - await page.keyboard.type(data.body, { delay: 5 }); - await page.waitForTimeout(3000); + await page.waitForSelector('textarea'); + + await page.fill('textarea',data.title); + + await page.keyboard.press('Tab'); + + await page.keyboard.type(data.body,{delay:5}); - if (isPublic) { - const publishButton = page.getByRole('button', { name: /公開|publish/i }); - if (await publishButton.count()) { - await publishButton.first().click(); - await page.waitForTimeout(3000); - } - } else { - const saveButton = page.getByRole('button', { name: /下書き|保存|save/i }); - if (await saveButton.count()) { - await saveButton.first().click(); - await page.waitForTimeout(3000); - } + if(isPublic){ + await page.click('button:has-text("公開")'); + }else{ + await page.click('button:has-text("下書き")'); } - await context.storageState({ path: statePath }); + await page.waitForTimeout(3000); + await browser.close(); EOF node post.mjs - - - name: Upload updated state - uses: actions/upload-artifact@v4 - with: - name: updated-note-state - path: note-state.json From 3cc06326e332b9d7a2add1cc0e5e4b9d8e9d459d Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 19:00:58 +0900 Subject: [PATCH 28/50] Update note.yaml --- .github/workflows/note.yaml | 176 +++++++++--------------------------- 1 file changed, 44 insertions(+), 132 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 6149430..141de5b 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -1,95 +1,44 @@ name: Note Workflow on: - schedule: - - cron: '0 22 * * *' workflow_dispatch: inputs: theme: - description: '記事テーマ' + description: 記事テーマ required: true type: string target: - description: '想定読者(ペルソナ)' + description: 想定読者 required: true type: string message: - description: '読者に伝えたい核メッセージ' + description: 核メッセージ required: true type: string cta: - description: '読後のアクション(CTA)' + description: CTA required: true type: string - tags: - description: 'カンマ区切りタグ(任意)' - required: false - default: '' - type: string is_public: - description: '公開(true)/下書き(false)' - required: true - default: 'false' - type: choice - options: - - 'true' - - 'false' - dry_run: - description: '投稿をスキップ(生成のみ)' + description: 公開(true)/下書き(false) required: true - default: 'false' + default: "false" type: choice options: - - 'true' - - 'false' - -permissions: - contents: read - -env: - TZ: Asia/Tokyo + - "true" + - "false" jobs: research: runs-on: ubuntu-latest - env: - TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - THEME: ${{ github.event.inputs.theme }} - TARGET: ${{ github.event.inputs.target }} - MESSAGE: ${{ github.event.inputs.message }} - outputs: - research_b64: ${{ steps.collect.outputs.research_b64 }} steps: - - uses: actions/setup-node@v4 with: - node-version: '20' - - - name: Research - run: | - cat > research.mjs <<'EOF' - import fs from 'fs'; - - const apiKey = process.env.TAVILY_API_KEY; - const theme = process.env.THEME || ''; - - const res = await fetch('https://api.tavily.com/search', { - method:'POST', - headers:{'Content-Type':'application/json'}, - body:JSON.stringify({ - api_key:apiKey, - query:theme, - search_depth:'advanced', - max_results:5 - }) - }); + node-version: 20 - const json = await res.json(); - fs.writeFileSync('research.json',JSON.stringify(json,null,2)); - EOF - - node research.mjs + - run: | + echo '{"research":"ok"}' > research.json - id: collect run: | @@ -98,65 +47,22 @@ jobs: write: runs-on: ubuntu-latest needs: research - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - RESEARCH_B64: ${{ needs.research.outputs.research_b64 }} - THEME: ${{ github.event.inputs.theme }} - TARGET: ${{ github.event.inputs.target }} - MESSAGE: ${{ github.event.inputs.message }} - CTA: ${{ github.event.inputs.cta }} outputs: article_b64: ${{ steps.collect.outputs.article_b64 }} steps: - uses: actions/setup-node@v4 with: - node-version: '20' - - - name: Write - run: | - cat > write.mjs <<'EOF' - import fs from 'fs'; - - const apiKey = process.env.ANTHROPIC_API_KEY; - const research = JSON.parse(Buffer.from(process.env.RESEARCH_B64,'base64').toString()); - - const prompt = ` - テーマ:${process.env.THEME} - 読者:${process.env.TARGET} - メッセージ:${process.env.MESSAGE} - CTA:${process.env.CTA} - - ${JSON.stringify(research)} - `; - - const res = await fetch('https://api.anthropic.com/v1/messages',{ - method:'POST', - headers:{ - 'x-api-key':apiKey, - 'anthropic-version':'2023-06-01', - 'content-type':'application/json' - }, - body:JSON.stringify({ - model:'claude-haiku-4-5', - max_tokens:3000, - messages:[{role:'user',content:prompt}] - }) - }); + node-version: 20 - const json = await res.json(); - const text = json.content[0].text; - - const article={ - title:process.env.THEME, - body:text - }; - - fs.writeFileSync('article.json',JSON.stringify(article,null,2)); + - run: | + cat < article.json + { + "title": "テスト記事", + "body": "これは自動投稿テストです" + } EOF - node write.mjs - - id: collect run: | echo "article_b64=$(base64 -w 0 article.json)" >> "$GITHUB_OUTPUT" @@ -165,16 +71,16 @@ jobs: post: runs-on: ubuntu-latest needs: write - if: ${{ github.event.inputs.dry_run == 'false' }} env: NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} IS_PUBLIC: ${{ github.event.inputs.is_public }} + steps: - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: 20 - run: | npm init -y @@ -182,39 +88,45 @@ jobs: npx playwright install chromium - run: | - cat > note-state.json <<'EOF' + cat > note-state.json < post.mjs <<'EOF' - import { chromium } from 'playwright'; + - run: | + cat <<'EOF' > post.mjs + import { chromium } from "playwright"; + + const data = JSON.parse( + Buffer.from(process.env.ARTICLE_B64, "base64").toString() + ); + + const browser = await chromium.launch(); + const context = await browser.newContext({ + storageState: "note-state.json", + }); - const data=JSON.parse(Buffer.from(process.env.ARTICLE_B64,'base64').toString()); - const isPublic=process.env.IS_PUBLIC==='true'; + const page = await context.newPage(); - const browser=await chromium.launch({headless:true}); - const context=await browser.newContext({storageState:'note-state.json'}); - const page=await context.newPage(); + await page.goto("https://editor.note.com/notes/new"); - await page.goto('https://editor.note.com/notes/new'); + await page.waitForSelector('[contenteditable="true"]'); - await page.waitForSelector('textarea'); + const title = page.locator('[contenteditable="true"]').first(); - await page.fill('textarea',data.title); + await title.click(); + await title.type(data.title); - await page.keyboard.press('Tab'); + await page.keyboard.press("Tab"); - await page.keyboard.type(data.body,{delay:5}); + await page.keyboard.type(data.body); - if(isPublic){ + if (process.env.IS_PUBLIC === "true") { await page.click('button:has-text("公開")'); - }else{ + } else { await page.click('button:has-text("下書き")'); } - await page.waitForTimeout(3000); + await page.waitForTimeout(4000); await browser.close(); EOF From bf97974c44c654aa22c19f881f4007ea8a9b43d0 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 19:09:40 +0900 Subject: [PATCH 29/50] Update note.yaml --- .github/workflows/note.yaml | 73 +++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 141de5b..b08a40f 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -32,7 +32,9 @@ jobs: research: runs-on: ubuntu-latest + steps: + - uses: actions/setup-node@v4 with: node-version: 20 @@ -44,11 +46,15 @@ jobs: run: | echo "research_b64=$(base64 -w 0 research.json)" >> "$GITHUB_OUTPUT" + + write: runs-on: ubuntu-latest needs: research + outputs: article_b64: ${{ steps.collect.outputs.article_b64 }} + steps: - uses: actions/setup-node@v4 @@ -58,8 +64,8 @@ jobs: - run: | cat < article.json { - "title": "テスト記事", - "body": "これは自動投稿テストです" + "title": "note自動投稿テスト", + "body": "これはGitHub Actionsから自動投稿されたテスト記事です。" } EOF @@ -68,9 +74,11 @@ jobs: echo "article_b64=$(base64 -w 0 article.json)" >> "$GITHUB_OUTPUT" + post: runs-on: ubuntu-latest needs: write + env: NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} @@ -94,41 +102,84 @@ jobs: - run: | cat <<'EOF' > post.mjs + import { chromium } from "playwright"; const data = JSON.parse( Buffer.from(process.env.ARTICLE_B64, "base64").toString() ); - const browser = await chromium.launch(); + const browser = await chromium.launch({ + headless: true + }); + const context = await browser.newContext({ - storageState: "note-state.json", + storageState: "note-state.json" }); const page = await context.newPage(); - await page.goto("https://editor.note.com/notes/new"); + await page.goto("https://note.com/new"); + + await page.waitForTimeout(5000); + + const titleSelectors = [ + '[placeholder="記事タイトル"]', + '[data-placeholder="記事タイトル"]', + '[contenteditable="true"]', + 'textarea' + ]; + + let titleDone = false; + + for (const sel of titleSelectors) { + + const loc = page.locator(sel).first(); + + if (await loc.count()) { - await page.waitForSelector('[contenteditable="true"]'); + try { - const title = page.locator('[contenteditable="true"]').first(); + await loc.click(); - await title.click(); - await title.type(data.title); + await loc.fill(data.title); + + titleDone = true; + + break; + + } catch {} + + } + + } + + if (!titleDone) { + + throw new Error("title field not found"); + + } await page.keyboard.press("Tab"); - await page.keyboard.type(data.body); + await page.keyboard.type(data.body, { + delay: 5 + }); if (process.env.IS_PUBLIC === "true") { + await page.click('button:has-text("公開")'); + } else { + await page.click('button:has-text("下書き")'); + } - await page.waitForTimeout(4000); + await page.waitForTimeout(5000); await browser.close(); + EOF node post.mjs From 4074830d56057ea03cbc6bbb1ec19ebc19ea2f31 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 19:21:41 +0900 Subject: [PATCH 30/50] Update note.yaml --- .github/workflows/note.yaml | 293 +++++++++++++----------------------- 1 file changed, 108 insertions(+), 185 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index b08a40f..ad949b3 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -1,185 +1,108 @@ -name: Note Workflow - -on: - workflow_dispatch: - inputs: - theme: - description: 記事テーマ - required: true - type: string - target: - description: 想定読者 - required: true - type: string - message: - description: 核メッセージ - required: true - type: string - cta: - description: CTA - required: true - type: string - is_public: - description: 公開(true)/下書き(false) - required: true - default: "false" - type: choice - options: - - "true" - - "false" - -jobs: - - research: - runs-on: ubuntu-latest - - steps: - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - run: | - echo '{"research":"ok"}' > research.json - - - id: collect - run: | - echo "research_b64=$(base64 -w 0 research.json)" >> "$GITHUB_OUTPUT" - - - - write: - runs-on: ubuntu-latest - needs: research - - outputs: - article_b64: ${{ steps.collect.outputs.article_b64 }} - - steps: - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - run: | - cat < article.json - { - "title": "note自動投稿テスト", - "body": "これはGitHub Actionsから自動投稿されたテスト記事です。" - } - EOF - - - id: collect - run: | - echo "article_b64=$(base64 -w 0 article.json)" >> "$GITHUB_OUTPUT" - - - - post: - runs-on: ubuntu-latest - needs: write - - env: - NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} - ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} - IS_PUBLIC: ${{ github.event.inputs.is_public }} - - steps: - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - run: | - npm init -y - npm install playwright - npx playwright install chromium - - - run: | - cat > note-state.json < post.mjs - - import { chromium } from "playwright"; - - const data = JSON.parse( - Buffer.from(process.env.ARTICLE_B64, "base64").toString() - ); - - const browser = await chromium.launch({ - headless: true - }); - - const context = await browser.newContext({ - storageState: "note-state.json" - }); - - const page = await context.newPage(); - - await page.goto("https://note.com/new"); - - await page.waitForTimeout(5000); - - const titleSelectors = [ - '[placeholder="記事タイトル"]', - '[data-placeholder="記事タイトル"]', - '[contenteditable="true"]', - 'textarea' - ]; - - let titleDone = false; - - for (const sel of titleSelectors) { - - const loc = page.locator(sel).first(); - - if (await loc.count()) { - - try { - - await loc.click(); - - await loc.fill(data.title); - - titleDone = true; - - break; - - } catch {} - - } - - } - - if (!titleDone) { - - throw new Error("title field not found"); - - } - - await page.keyboard.press("Tab"); - - await page.keyboard.type(data.body, { - delay: 5 - }); - - if (process.env.IS_PUBLIC === "true") { - - await page.click('button:has-text("公開")'); - - } else { - - await page.click('button:has-text("下書き")'); - - } - - await page.waitForTimeout(5000); - - await browser.close(); - - EOF - - node post.mjs +import { chromium } from 'playwright'; +import fs from 'fs'; + +const article = JSON.parse(fs.readFileSync('article.json', 'utf-8')); + +(async () => { + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + storageState: 'auth.json' + }); + const page = await context.newPage(); + + try { + // note.comの記事作成ページに移動 + await page.goto('https://note.com/new', { + waitUntil: 'networkidle', + timeout: 60000 + }); + + // UIの完全レンダリングを待つ + await page.waitForTimeout(3000); + + // タイトル入力欄を複数の方法で探す + const titleSelectors = [ + 'input[placeholder*="タイトル"]', + 'textarea[placeholder*="タイトル"]', + '[data-testid="title-input"]', + 'div[contenteditable="true"]:first-of-type', + '[placeholder="記事タイトル"]', + '[data-placeholder="記事タイトル"]' + ]; + + let titleInput = null; + for (const selector of titleSelectors) { + try { + console.log(`Trying selector: ${selector}`); + titleInput = await page.waitForSelector(selector, { timeout: 5000 }); + if (titleInput) { + console.log(`Found title input with: ${selector}`); + break; + } + } catch (e) { + console.log(`Selector ${selector} not found, trying next...`); + continue; + } + } + + if (!titleInput) { + throw new Error('Title field not found after trying all selectors'); + } + + // タイトル入力 + await titleInput.click(); + await titleInput.fill(article.title); + console.log('Title entered successfully'); + + // 本文入力欄を探す + const bodySelectors = [ + 'div[contenteditable="true"].note-common-editor', + '[data-testid="body-editor"]', + 'div[contenteditable="true"]:not(:first-of-type)', + '.note-editor-body' + ]; + + let bodyInput = null; + for (const selector of bodySelectors) { + try { + console.log(`Trying body selector: ${selector}`); + bodyInput = await page.waitForSelector(selector, { timeout: 5000 }); + if (bodyInput) { + console.log(`Found body input with: ${selector}`); + break; + } + } catch (e) { + console.log(`Body selector ${selector} not found, trying next...`); + continue; + } + } + + if (!bodyInput) { + throw new Error('Body field not found'); + } + + // 本文入力 + await bodyInput.click(); + await bodyInput.fill(article.body); + console.log('Body entered successfully'); + + // スクリーンショット保存(デバッグ用) + await page.screenshot({ path: 'debug-before-save.png' }); + + // 下書き保存ボタンを探す + await page.waitForTimeout(1000); + const saveButton = await page.waitForSelector('button:has-text("下書き保存"), button:has-text("保存")'); + await saveButton.click(); + console.log('Draft saved successfully'); + + await page.waitForTimeout(2000); + await page.screenshot({ path: 'debug-after-save.png' }); + + } catch (error) { + console.error('Error:', error); + await page.screenshot({ path: 'error-screenshot.png' }); + throw error; + } finally { + await browser.close(); + } +})(); From 76ff231570ef27561a30231e2a377aac2ab57352 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 19:24:31 +0900 Subject: [PATCH 31/50] Update note.yaml --- .github/workflows/note.yaml | 293 +++++++++++++++++++++++------------- 1 file changed, 185 insertions(+), 108 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index ad949b3..b08a40f 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -1,108 +1,185 @@ -import { chromium } from 'playwright'; -import fs from 'fs'; - -const article = JSON.parse(fs.readFileSync('article.json', 'utf-8')); - -(async () => { - const browser = await chromium.launch({ headless: true }); - const context = await browser.newContext({ - storageState: 'auth.json' - }); - const page = await context.newPage(); - - try { - // note.comの記事作成ページに移動 - await page.goto('https://note.com/new', { - waitUntil: 'networkidle', - timeout: 60000 - }); - - // UIの完全レンダリングを待つ - await page.waitForTimeout(3000); - - // タイトル入力欄を複数の方法で探す - const titleSelectors = [ - 'input[placeholder*="タイトル"]', - 'textarea[placeholder*="タイトル"]', - '[data-testid="title-input"]', - 'div[contenteditable="true"]:first-of-type', - '[placeholder="記事タイトル"]', - '[data-placeholder="記事タイトル"]' - ]; - - let titleInput = null; - for (const selector of titleSelectors) { - try { - console.log(`Trying selector: ${selector}`); - titleInput = await page.waitForSelector(selector, { timeout: 5000 }); - if (titleInput) { - console.log(`Found title input with: ${selector}`); - break; - } - } catch (e) { - console.log(`Selector ${selector} not found, trying next...`); - continue; - } - } - - if (!titleInput) { - throw new Error('Title field not found after trying all selectors'); - } - - // タイトル入力 - await titleInput.click(); - await titleInput.fill(article.title); - console.log('Title entered successfully'); - - // 本文入力欄を探す - const bodySelectors = [ - 'div[contenteditable="true"].note-common-editor', - '[data-testid="body-editor"]', - 'div[contenteditable="true"]:not(:first-of-type)', - '.note-editor-body' - ]; - - let bodyInput = null; - for (const selector of bodySelectors) { - try { - console.log(`Trying body selector: ${selector}`); - bodyInput = await page.waitForSelector(selector, { timeout: 5000 }); - if (bodyInput) { - console.log(`Found body input with: ${selector}`); - break; - } - } catch (e) { - console.log(`Body selector ${selector} not found, trying next...`); - continue; - } - } - - if (!bodyInput) { - throw new Error('Body field not found'); - } - - // 本文入力 - await bodyInput.click(); - await bodyInput.fill(article.body); - console.log('Body entered successfully'); - - // スクリーンショット保存(デバッグ用) - await page.screenshot({ path: 'debug-before-save.png' }); - - // 下書き保存ボタンを探す - await page.waitForTimeout(1000); - const saveButton = await page.waitForSelector('button:has-text("下書き保存"), button:has-text("保存")'); - await saveButton.click(); - console.log('Draft saved successfully'); - - await page.waitForTimeout(2000); - await page.screenshot({ path: 'debug-after-save.png' }); - - } catch (error) { - console.error('Error:', error); - await page.screenshot({ path: 'error-screenshot.png' }); - throw error; - } finally { - await browser.close(); - } -})(); +name: Note Workflow + +on: + workflow_dispatch: + inputs: + theme: + description: 記事テーマ + required: true + type: string + target: + description: 想定読者 + required: true + type: string + message: + description: 核メッセージ + required: true + type: string + cta: + description: CTA + required: true + type: string + is_public: + description: 公開(true)/下書き(false) + required: true + default: "false" + type: choice + options: + - "true" + - "false" + +jobs: + + research: + runs-on: ubuntu-latest + + steps: + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - run: | + echo '{"research":"ok"}' > research.json + + - id: collect + run: | + echo "research_b64=$(base64 -w 0 research.json)" >> "$GITHUB_OUTPUT" + + + + write: + runs-on: ubuntu-latest + needs: research + + outputs: + article_b64: ${{ steps.collect.outputs.article_b64 }} + + steps: + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - run: | + cat < article.json + { + "title": "note自動投稿テスト", + "body": "これはGitHub Actionsから自動投稿されたテスト記事です。" + } + EOF + + - id: collect + run: | + echo "article_b64=$(base64 -w 0 article.json)" >> "$GITHUB_OUTPUT" + + + + post: + runs-on: ubuntu-latest + needs: write + + env: + NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} + ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} + IS_PUBLIC: ${{ github.event.inputs.is_public }} + + steps: + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - run: | + npm init -y + npm install playwright + npx playwright install chromium + + - run: | + cat > note-state.json < post.mjs + + import { chromium } from "playwright"; + + const data = JSON.parse( + Buffer.from(process.env.ARTICLE_B64, "base64").toString() + ); + + const browser = await chromium.launch({ + headless: true + }); + + const context = await browser.newContext({ + storageState: "note-state.json" + }); + + const page = await context.newPage(); + + await page.goto("https://note.com/new"); + + await page.waitForTimeout(5000); + + const titleSelectors = [ + '[placeholder="記事タイトル"]', + '[data-placeholder="記事タイトル"]', + '[contenteditable="true"]', + 'textarea' + ]; + + let titleDone = false; + + for (const sel of titleSelectors) { + + const loc = page.locator(sel).first(); + + if (await loc.count()) { + + try { + + await loc.click(); + + await loc.fill(data.title); + + titleDone = true; + + break; + + } catch {} + + } + + } + + if (!titleDone) { + + throw new Error("title field not found"); + + } + + await page.keyboard.press("Tab"); + + await page.keyboard.type(data.body, { + delay: 5 + }); + + if (process.env.IS_PUBLIC === "true") { + + await page.click('button:has-text("公開")'); + + } else { + + await page.click('button:has-text("下書き")'); + + } + + await page.waitForTimeout(5000); + + await browser.close(); + + EOF + + node post.mjs From c7890b190d52a2fda25511b6d5dadf70d2b86aaa Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 20:55:54 +0900 Subject: [PATCH 32/50] Update note.yaml --- .github/workflows/note.yaml | 51 +++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index b08a40f..858c999 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -28,13 +28,20 @@ on: - "true" - "false" +permissions: + contents: read + +env: + TZ: Asia/Tokyo + jobs: research: runs-on: ubuntu-latest + outputs: + research_b64: ${{ steps.collect.outputs.research_b64 }} steps: - - uses: actions/setup-node@v4 with: node-version: 20 @@ -65,7 +72,7 @@ jobs: cat < article.json { "title": "note自動投稿テスト", - "body": "これはGitHub Actionsから自動投稿されたテスト記事です。" + "body": "GitHub Actionsからの自動投稿テストです。" } EOF @@ -102,7 +109,6 @@ jobs: - run: | cat <<'EOF' > post.mjs - import { chromium } from "playwright"; const data = JSON.parse( @@ -124,10 +130,9 @@ jobs: await page.waitForTimeout(5000); const titleSelectors = [ - '[placeholder="記事タイトル"]', - '[data-placeholder="記事タイトル"]', - '[contenteditable="true"]', - 'textarea' + "textarea", + "input[placeholder*='タイトル']", + "[contenteditable='true']" ]; let titleDone = false; @@ -155,31 +160,39 @@ jobs: } if (!titleDone) { + console.log("title field not found"); + } - throw new Error("title field not found"); + await page.keyboard.type(data.title,{delay:5}); + await page.keyboard.type(data.body,{delay:5}); - } + if (process.env.IS_PUBLIC === "true") { - await page.keyboard.press("Tab"); + const publishButton = page.getByRole("button",{name:/公開|publish/i}); - await page.keyboard.type(data.body, { - delay: 5 - }); + if (await publishButton.count()) { - if (process.env.IS_PUBLIC === "true") { + await publishButton.first().click(); - await page.click('button:has-text("公開")'); + await page.waitForTimeout(3000); + + } } else { - await page.click('button:has-text("下書き")'); + const saveButton = page.getByRole("button",{name:/下書き|保存|save/i}); - } + if (await saveButton.count()) { - await page.waitForTimeout(5000); + await saveButton.first().click(); - await browser.close(); + await page.waitForTimeout(3000); + } + + } + + await browser.close(); EOF node post.mjs From 0013c3721a722d6e032d829bc7ce2d6d85038b48 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 9 Mar 2026 21:09:17 +0900 Subject: [PATCH 33/50] Update note.yaml --- .github/workflows/note.yaml | 383 ++++++++++++++++++++++++++++-------- 1 file changed, 299 insertions(+), 84 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 858c999..9c7beca 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -1,32 +1,47 @@ name: Note Workflow on: + schedule: + - cron: '0 22 * * *' workflow_dispatch: inputs: theme: - description: 記事テーマ + description: '記事テーマ' required: true type: string target: - description: 想定読者 + description: '想定読者(ペルソナ)' required: true type: string message: - description: 核メッセージ + description: '読者に伝えたい核メッセージ' required: true type: string cta: - description: CTA + description: '読後のアクション(CTA)' required: true type: string + tags: + description: 'カンマ区切りタグ(任意)' + required: false + default: '' + type: string is_public: - description: 公開(true)/下書き(false) + description: '公開(true)/下書き(false)' + required: true + default: 'false' + type: choice + options: + - 'true' + - 'false' + dry_run: + description: '投稿をスキップ(生成のみ)' required: true - default: "false" + default: 'false' type: choice options: - - "true" - - "false" + - 'true' + - 'false' permissions: contents: read @@ -35,164 +50,364 @@ env: TZ: Asia/Tokyo jobs: - research: + name: Research (Tavily) runs-on: ubuntu-latest + env: + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} + THEME: ${{ github.event.inputs.theme }} + TARGET: ${{ github.event.inputs.target }} + MESSAGE: ${{ github.event.inputs.message }} outputs: research_b64: ${{ steps.collect.outputs.research_b64 }} - steps: - - uses: actions/setup-node@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '20' + + - name: Research with Tavily + run: | + cat > research.mjs <<'EOF' + import fs from 'fs'; + + const apiKey = process.env.TAVILY_API_KEY; + const theme = process.env.THEME || '大阪 マツエク'; + const target = process.env.TARGET || ''; + const message = process.env.MESSAGE || ''; + + const prompt = `${theme} ${target} ${message}`.trim(); + + const body = { + query: prompt, + search_depth: 'advanced', + topic: 'general', + max_results: 5, + include_answer: true + }; + + const res = await fetch('https://api.tavily.com/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + api_key: apiKey, + ...body + }) + }); + + if (!res.ok) { + const txt = await res.text(); + console.error(txt); + process.exit(1); + } + + const json = await res.json(); + fs.writeFileSync('research.json', JSON.stringify(json, null, 2)); + console.log('research saved'); + EOF - - run: | - echo '{"research":"ok"}' > research.json + node research.mjs - - id: collect + - name: Collect research + id: collect run: | echo "research_b64=$(base64 -w 0 research.json)" >> "$GITHUB_OUTPUT" - + - name: Upload research artifact + uses: actions/upload-artifact@v4 + with: + name: research-json + path: research.json write: + name: Write (Claude) runs-on: ubuntu-latest needs: research - + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + THEME: ${{ github.event.inputs.theme }} + TARGET: ${{ github.event.inputs.target }} + MESSAGE: ${{ github.event.inputs.message }} + CTA: ${{ github.event.inputs.cta }} + TAGS: ${{ github.event.inputs.tags }} + RESEARCH_B64: ${{ needs.research.outputs.research_b64 }} outputs: article_b64: ${{ steps.collect.outputs.article_b64 }} - steps: - - - uses: actions/setup-node@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '20' - - run: | - cat < article.json + - name: Write article with Claude + run: | + cat > write.mjs <<'EOF' + import fs from 'fs'; + + const apiKey = process.env.ANTHROPIC_API_KEY; + const theme = process.env.THEME || ''; + const target = process.env.TARGET || ''; + const message = process.env.MESSAGE || ''; + const cta = process.env.CTA || ''; + const tags = process.env.TAGS || ''; + const research = JSON.parse(Buffer.from(process.env.RESEARCH_B64, 'base64').toString('utf8')); + + const systemPrompt = ` + あなたは日本語のnote記事を書くプロの編集者です。 + 目的は、読みやすく、信頼感があり、行動につながるnote記事を作ることです。 + 出力は必ずJSONのみ。 + 形式: { - "title": "note自動投稿テスト", - "body": "GitHub Actionsからの自動投稿テストです。" + "title": "記事タイトル", + "body": "note本文。見出しや改行を含む", + "tags": ["タグ1","タグ2","タグ3"] + } + `.trim(); + + const userPrompt = ` + 記事テーマ: ${theme} + 想定読者: ${target} + 読者に伝えたい核メッセージ: ${message} + 読後のアクション: ${cta} + 希望タグ: ${tags} + + リサーチ結果: + ${JSON.stringify(research, null, 2)} + + 条件: + - 日本語 + - 読みやすい自然な文章 + - 誇張しすぎない + - 見出しを使う + - 最後にCTAを入れる + - tags は配列で3〜5個 + `.trim(); + + const res = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json' + }, + body: JSON.stringify({ + model: 'claude-haiku-4-5', + max_tokens: 4000, + system: systemPrompt, + messages: [ + { + role: 'user', + content: userPrompt + } + ] + }) + }); + + if (!res.ok) { + const txt = await res.text(); + console.error(txt); + process.exit(1); + } + + const json = await res.json(); + const text = (json.content?.[0]?.text || '').replace(/```json|```/g, '').trim(); + fs.writeFileSync('article_raw.txt', text); + + let parsed; + try { + parsed = JSON.parse(text); + } catch { + console.error('Claude output was not valid JSON'); + console.error(text); + process.exit(1); } + + fs.writeFileSync('article.json', JSON.stringify(parsed, null, 2)); + console.log('article saved'); EOF - - id: collect + node write.mjs + + - name: Collect article + id: collect run: | echo "article_b64=$(base64 -w 0 article.json)" >> "$GITHUB_OUTPUT" + - name: Upload article artifact + uses: actions/upload-artifact@v4 + with: + name: article-json + path: | + article.json + article_raw.txt - - post: + factcheck: + name: Fact-check (Tavily) runs-on: ubuntu-latest needs: write + env: + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} + ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} + outputs: + final_b64: ${{ steps.collect.outputs.final_b64 }} + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Fact-check article + run: | + cat > factcheck.mjs <<'EOF' + import fs from 'fs'; + + const apiKey = process.env.TAVILY_API_KEY; + const article = JSON.parse(Buffer.from(process.env.ARTICLE_B64, 'base64').toString('utf8')); + + const query = `${article.title}\n\n${article.body.slice(0, 300)}`; + + const res = await fetch('https://api.tavily.com/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + api_key: apiKey, + query, + search_depth: 'advanced', + topic: 'general', + max_results: 5, + include_answer: true + }) + }); + if (!res.ok) { + const txt = await res.text(); + console.error(txt); + process.exit(1); + } + + const fact = await res.json(); + + const finalArticle = { + ...article, + factcheck: fact + }; + + fs.writeFileSync('final_article.json', JSON.stringify(finalArticle, null, 2)); + console.log('final article saved'); + EOF + + node factcheck.mjs + + - name: Collect final article + id: collect + run: | + echo "final_b64=$(base64 -w 0 final_article.json)" >> "$GITHUB_OUTPUT" + + - name: Upload final article artifact + uses: actions/upload-artifact@v4 + with: + name: final-article-json + path: final_article.json + + post: + name: Post to note.com (Playwright) + runs-on: ubuntu-latest + needs: factcheck + if: ${{ github.event.inputs.dry_run == 'false' }} env: NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} - ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} + FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} IS_PUBLIC: ${{ github.event.inputs.is_public }} - steps: + - name: Checkout + uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '20' - - run: | + - name: Install Playwright + run: | npm init -y npm install playwright - npx playwright install chromium + npx playwright install --with-deps chromium - - run: | - cat > note-state.json < note-state.json <<'EOF' ${{ secrets.NOTE_STORAGE_STATE_JSON }} EOF - - run: | - cat <<'EOF' > post.mjs - import { chromium } from "playwright"; - - const data = JSON.parse( - Buffer.from(process.env.ARTICLE_B64, "base64").toString() - ); - - const browser = await chromium.launch({ - headless: true - }); + - name: Post to note + run: | + cat > post.mjs <<'EOF' + import { chromium } from 'playwright'; - const context = await browser.newContext({ - storageState: "note-state.json" - }); + const data = JSON.parse(Buffer.from(process.env.FINAL_B64, 'base64').toString('utf8')); + const statePath = './note-state.json'; + const isPublic = process.env.IS_PUBLIC === 'true'; + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ storageState: statePath }); const page = await context.newPage(); - await page.goto("https://note.com/new"); - + await page.goto('https://note.com/new', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(5000); const titleSelectors = [ - "textarea", - "input[placeholder*='タイトル']", - "[contenteditable='true']" + 'textarea', + 'input[placeholder*="タイトル"]', + '[contenteditable="true"]' ]; let titleDone = false; - for (const sel of titleSelectors) { - const loc = page.locator(sel).first(); - if (await loc.count()) { - try { - await loc.click(); - await loc.fill(data.title); - titleDone = true; - break; - } catch {} - } - } if (!titleDone) { - console.log("title field not found"); + console.log('title field not found'); } - await page.keyboard.type(data.title,{delay:5}); - await page.keyboard.type(data.body,{delay:5}); - - if (process.env.IS_PUBLIC === "true") { - - const publishButton = page.getByRole("button",{name:/公開|publish/i}); + await page.keyboard.type(data.title, { delay: 5 }); + await page.keyboard.type(data.body, { delay: 5 }); + await page.waitForTimeout(3000); + if (isPublic) { + const publishButton = page.getByRole('button', { name: /公開|publish/i }); if (await publishButton.count()) { - await publishButton.first().click(); - await page.waitForTimeout(3000); - } - } else { - - const saveButton = page.getByRole("button",{name:/下書き|保存|save/i}); - + const saveButton = page.getByRole('button', { name: /下書き|保存|save/i }); if (await saveButton.count()) { - await saveButton.first().click(); - await page.waitForTimeout(3000); - } - } + await context.storageState({ path: statePath }); await browser.close(); EOF node post.mjs + + - name: Upload updated state + uses: actions/upload-artifact@v4 + with: + name: updated-note-state + path: note-state.json From 5b1b1d203da15765b4c300656392a4ad0332f663 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Wed, 11 Mar 2026 18:35:34 +0900 Subject: [PATCH 34/50] Update note.yaml --- .github/workflows/note.yaml | 145 ++++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 49 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 9c7beca..90fc113 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -348,64 +348,111 @@ jobs: cat > post.mjs <<'EOF' import { chromium } from 'playwright'; - const data = JSON.parse(Buffer.from(process.env.FINAL_B64, 'base64').toString('utf8')); - const statePath = './note-state.json'; - const isPublic = process.env.IS_PUBLIC === 'true'; - - const browser = await chromium.launch({ headless: true }); - const context = await browser.newContext({ storageState: statePath }); - const page = await context.newPage(); - - await page.goto('https://note.com/new', { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(5000); - - const titleSelectors = [ - 'textarea', - 'input[placeholder*="タイトル"]', - '[contenteditable="true"]' - ]; - - let titleDone = false; - for (const sel of titleSelectors) { - const loc = page.locator(sel).first(); - if (await loc.count()) { - try { - await loc.click(); - await loc.fill(data.title); - titleDone = true; - break; - } catch {} - } - } - - if (!titleDone) { - console.log('title field not found'); - } - - await page.keyboard.type(data.title, { delay: 5 }); - await page.keyboard.type(data.body, { delay: 5 }); - await page.waitForTimeout(3000); + async function main() { + const data = JSON.parse(Buffer.from(process.env.FINAL_B64, 'base64').toString('utf8')); + const statePath = './note-state.json'; + const isPublic = process.env.IS_PUBLIC === 'true'; + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ storageState: statePath }); + const page = await context.newPage(); + + let step = 0; + const shot = async (label) => { + await page.screenshot({ path: `debug_${String(++step).padStart(2,'0')}_${label}.png` }); + console.log(`[screenshot] ${label}`); + }; + + try { + console.log('Navigating to note.com/new...'); + await page.goto('https://note.com/new', { waitUntil: 'networkidle' }); + await shot('after_goto'); + + // ── ① タイトル入力 ── + console.log('Filling title...'); + const titleLocator = page.locator([ + 'div[data-placeholder*="タイトル"]', + 'div[data-placeholder*="title"]', + 'textarea[placeholder*="タイトル"]', + 'input[placeholder*="タイトル"]', + '.ProseMirror >> nth=0', + '[contenteditable="true"] >> nth=0' + ].join(', ')).first(); + + await titleLocator.waitFor({ state: 'visible', timeout: 30000 }); + await titleLocator.click(); + await page.keyboard.press('Meta+A'); + await page.keyboard.press('Control+A'); + await page.keyboard.type(data.title, { delay: 30 }); + await shot('after_title'); + + // ── ② 本文入力 ── + console.log('Filling body...'); + await page.keyboard.press('Tab'); + await page.waitForTimeout(500); + + const bodyLocator = page.locator([ + 'div[data-placeholder*="本文"]', + 'div[data-placeholder*="body"]', + '.ProseMirror >> nth=1', + '[contenteditable="true"] >> nth=1' + ].join(', ')).first(); + + const bodyFound = await bodyLocator.count(); + if (bodyFound > 0) { + await bodyLocator.click(); + } + + await page.keyboard.type(data.body, { delay: 10 }); + await shot('after_body'); + + await page.waitForTimeout(2000); + + // ── ③ 保存 / 公開 ── + if (isPublic) { + console.log('Publishing...'); + const publishBtn = page.getByRole('button', { name: /公開する|公開設定|publish/i }).first(); + await publishBtn.waitFor({ state: 'visible', timeout: 15000 }); + await publishBtn.click(); + await page.waitForTimeout(3000); + const confirmBtn = page.getByRole('button', { name: /公開する/i }).first(); + if (await confirmBtn.count()) { + await confirmBtn.click(); + } + await shot('after_publish'); + } else { + console.log('Saving draft...'); + const saveBtn = page.getByRole('button', { name: /下書き保存|下書き|保存/i }).first(); + await saveBtn.waitFor({ state: 'visible', timeout: 15000 }); + await saveBtn.click(); + await shot('after_save'); + } - if (isPublic) { - const publishButton = page.getByRole('button', { name: /公開|publish/i }); - if (await publishButton.count()) { - await publishButton.first().click(); - await page.waitForTimeout(3000); - } - } else { - const saveButton = page.getByRole('button', { name: /下書き|保存|save/i }); - if (await saveButton.count()) { - await saveButton.first().click(); await page.waitForTimeout(3000); + console.log('Done.'); + + } catch (err) { + await shot('error'); + console.error('Post failed:', err.message); + throw err; + } finally { + await context.storageState({ path: statePath }); + await browser.close(); } } - await context.storageState({ path: statePath }); - await browser.close(); + main(); EOF node post.mjs + - name: Upload debug screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: debug-screenshots + path: debug_*.png + - name: Upload updated state uses: actions/upload-artifact@v4 with: From 9f294bdb853ae5875d37e196deeb85b61e72c403 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Wed, 11 Mar 2026 20:32:28 +0900 Subject: [PATCH 35/50] Update note.yaml --- .github/workflows/note.yaml | 69 +++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 90fc113..c049161 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -359,48 +359,60 @@ jobs: let step = 0; const shot = async (label) => { - await page.screenshot({ path: `debug_${String(++step).padStart(2,'0')}_${label}.png` }); + await page.screenshot({ path: `debug_${String(++step).padStart(2,'0')}_${label}.png`, fullPage: true }); console.log(`[screenshot] ${label}`); }; + const dumpEditables = async () => { + const info = await page.evaluate(() => { + const els = document.querySelectorAll('[contenteditable]'); + return Array.from(els).map((el, i) => ({ + index: i, + tag: el.tagName, + contenteditable: el.getAttribute('contenteditable'), + placeholder: el.getAttribute('data-placeholder') || el.getAttribute('placeholder') || '', + class: el.className.slice(0, 80), + text: el.innerText.slice(0, 30) + })); + }); + console.log('[editables]', JSON.stringify(info, null, 2)); + }; + try { console.log('Navigating to note.com/new...'); await page.goto('https://note.com/new', { waitUntil: 'networkidle' }); + await page.waitForTimeout(5000); await shot('after_goto'); + await dumpEditables(); // ── ① タイトル入力 ── console.log('Filling title...'); - const titleLocator = page.locator([ - 'div[data-placeholder*="タイトル"]', - 'div[data-placeholder*="title"]', - 'textarea[placeholder*="タイトル"]', - 'input[placeholder*="タイトル"]', - '.ProseMirror >> nth=0', - '[contenteditable="true"] >> nth=0' - ].join(', ')).first(); - - await titleLocator.waitFor({ state: 'visible', timeout: 30000 }); - await titleLocator.click(); - await page.keyboard.press('Meta+A'); + const allEditables = page.locator('[contenteditable="true"]'); + const count = await allEditables.count(); + console.log(`Found ${count} contenteditable elements`); + + if (count === 0) { + throw new Error('No contenteditable elements found. Check debug screenshot.'); + } + + const titleEl = allEditables.nth(0); + await titleEl.click(); + await page.waitForTimeout(300); await page.keyboard.press('Control+A'); + await page.keyboard.press('Meta+A'); + await page.keyboard.press('Backspace'); await page.keyboard.type(data.title, { delay: 30 }); await shot('after_title'); // ── ② 本文入力 ── console.log('Filling body...'); - await page.keyboard.press('Tab'); - await page.waitForTimeout(500); - - const bodyLocator = page.locator([ - 'div[data-placeholder*="本文"]', - 'div[data-placeholder*="body"]', - '.ProseMirror >> nth=1', - '[contenteditable="true"] >> nth=1' - ].join(', ')).first(); - - const bodyFound = await bodyLocator.count(); - if (bodyFound > 0) { - await bodyLocator.click(); + if (count >= 2) { + const bodyEl = allEditables.nth(1); + await bodyEl.click(); + await page.waitForTimeout(300); + } else { + await page.keyboard.press('Tab'); + await page.waitForTimeout(500); } await page.keyboard.type(data.body, { delay: 10 }); @@ -409,6 +421,11 @@ jobs: await page.waitForTimeout(2000); // ── ③ 保存 / 公開 ── + const buttons = await page.evaluate(() => + Array.from(document.querySelectorAll('button')).map(b => b.innerText.trim()).filter(Boolean) + ); + console.log('[buttons]', buttons); + if (isPublic) { console.log('Publishing...'); const publishBtn = page.getByRole('button', { name: /公開する|公開設定|publish/i }).first(); From 1601dc47956383cdf2f979a1f32c70681b09edb6 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Wed, 11 Mar 2026 20:39:18 +0900 Subject: [PATCH 36/50] Update note.yaml --- .github/workflows/note.yaml | 59 +++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index c049161..f0b717a 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -363,38 +363,42 @@ jobs: console.log(`[screenshot] ${label}`); }; - const dumpEditables = async () => { - const info = await page.evaluate(() => { - const els = document.querySelectorAll('[contenteditable]'); - return Array.from(els).map((el, i) => ({ - index: i, - tag: el.tagName, - contenteditable: el.getAttribute('contenteditable'), - placeholder: el.getAttribute('data-placeholder') || el.getAttribute('placeholder') || '', - class: el.className.slice(0, 80), - text: el.innerText.slice(0, 30) - })); - }); - console.log('[editables]', JSON.stringify(info, null, 2)); - }; - try { console.log('Navigating to note.com/new...'); - await page.goto('https://note.com/new', { waitUntil: 'networkidle' }); - await page.waitForTimeout(5000); + await page.goto('https://note.com/new', { waitUntil: 'domcontentloaded' }); + + // エディタのJS描画を待つ(最大20秒) + console.log('Waiting for editor to load...'); + await page.waitForFunction(() => { + return document.querySelectorAll('[contenteditable]').length > 0; + }, { timeout: 20000 }).catch(() => { + console.log('waitForFunction timed out, continuing...'); + }); + + // 追加で3秒待機 + await page.waitForTimeout(3000); await shot('after_goto'); - await dumpEditables(); - // ── ① タイトル入力 ── - console.log('Filling title...'); - const allEditables = page.locator('[contenteditable="true"]'); - const count = await allEditables.count(); - console.log(`Found ${count} contenteditable elements`); + // 現在のURLとタイトルをログ + console.log('URL:', page.url()); + console.log('Title:', await page.title()); + + // contenteditable要素の数をログ + const editableCount = await page.evaluate(() => + document.querySelectorAll('[contenteditable]').length + ); + console.log(`Found ${editableCount} contenteditable elements`); - if (count === 0) { - throw new Error('No contenteditable elements found. Check debug screenshot.'); + // もし0個ならページのHTMLを一部出力して調査 + if (editableCount === 0) { + const bodySnippet = await page.evaluate(() => document.body.innerHTML.slice(0, 500)); + console.log('Body snippet:', bodySnippet); + throw new Error('No contenteditable elements found. Check debug screenshot and body snippet above.'); } + // ── ① タイトル入力 ── + console.log('Filling title...'); + const allEditables = page.locator('[contenteditable]'); const titleEl = allEditables.nth(0); await titleEl.click(); await page.waitForTimeout(300); @@ -406,9 +410,9 @@ jobs: // ── ② 本文入力 ── console.log('Filling body...'); + const count = await allEditables.count(); if (count >= 2) { - const bodyEl = allEditables.nth(1); - await bodyEl.click(); + await allEditables.nth(1).click(); await page.waitForTimeout(300); } else { await page.keyboard.press('Tab'); @@ -417,7 +421,6 @@ jobs: await page.keyboard.type(data.body, { delay: 10 }); await shot('after_body'); - await page.waitForTimeout(2000); // ── ③ 保存 / 公開 ── From 4a2c6dc377372952fffcdd3e07be2eee4b9c36d5 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Wed, 11 Mar 2026 20:45:54 +0900 Subject: [PATCH 37/50] Update note.yaml --- .github/workflows/note.yaml | 47 ++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index f0b717a..a50f604 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -354,7 +354,14 @@ jobs: const isPublic = process.env.IS_PUBLIC === 'true'; const browser = await chromium.launch({ headless: true }); - const context = await browser.newContext({ storageState: statePath }); + + // editor.note.com も同じセッションで扱えるよう設定 + const context = await browser.newContext({ + storageState: statePath, + extraHTTPHeaders: { + 'Accept-Language': 'ja-JP,ja;q=0.9' + } + }); const page = await context.newPage(); let step = 0; @@ -364,36 +371,33 @@ jobs: }; try { - console.log('Navigating to note.com/new...'); - await page.goto('https://note.com/new', { waitUntil: 'domcontentloaded' }); + // editor.note.com に直接アクセス + console.log('Navigating to editor.note.com/new...'); + await page.goto('https://editor.note.com/new', { waitUntil: 'domcontentloaded' }); + console.log('URL after goto:', page.url()); - // エディタのJS描画を待つ(最大20秒) - console.log('Waiting for editor to load...'); + // Reactのハイドレーション完了を待つ(最大30秒) + console.log('Waiting for React editor to hydrate...'); await page.waitForFunction(() => { - return document.querySelectorAll('[contenteditable]').length > 0; - }, { timeout: 20000 }).catch(() => { - console.log('waitForFunction timed out, continuing...'); + const els = document.querySelectorAll('[contenteditable]'); + return els.length > 0; + }, { timeout: 30000 }).catch(() => { + console.log('Still waiting...'); }); - // 追加で3秒待機 - await page.waitForTimeout(3000); + // さらに2秒待機 + await page.waitForTimeout(2000); await shot('after_goto'); - // 現在のURLとタイトルをログ - console.log('URL:', page.url()); - console.log('Title:', await page.title()); - - // contenteditable要素の数をログ const editableCount = await page.evaluate(() => document.querySelectorAll('[contenteditable]').length ); console.log(`Found ${editableCount} contenteditable elements`); - // もし0個ならページのHTMLを一部出力して調査 if (editableCount === 0) { - const bodySnippet = await page.evaluate(() => document.body.innerHTML.slice(0, 500)); - console.log('Body snippet:', bodySnippet); - throw new Error('No contenteditable elements found. Check debug screenshot and body snippet above.'); + const snippet = await page.evaluate(() => document.body.innerHTML.slice(0, 800)); + console.log('HTML snippet:', snippet); + throw new Error('Editor did not load. Check screenshot.'); } // ── ① タイトル入力 ── @@ -403,7 +407,6 @@ jobs: await titleEl.click(); await page.waitForTimeout(300); await page.keyboard.press('Control+A'); - await page.keyboard.press('Meta+A'); await page.keyboard.press('Backspace'); await page.keyboard.type(data.title, { delay: 30 }); await shot('after_title'); @@ -411,6 +414,8 @@ jobs: // ── ② 本文入力 ── console.log('Filling body...'); const count = await allEditables.count(); + console.log(`Total editables: ${count}`); + if (count >= 2) { await allEditables.nth(1).click(); await page.waitForTimeout(300); @@ -427,7 +432,7 @@ jobs: const buttons = await page.evaluate(() => Array.from(document.querySelectorAll('button')).map(b => b.innerText.trim()).filter(Boolean) ); - console.log('[buttons]', buttons); + console.log('[buttons]', JSON.stringify(buttons)); if (isPublic) { console.log('Publishing...'); From e1b244e7cf6f8cbb1c9f9355ea0d0b3c11e3a2b6 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Wed, 11 Mar 2026 20:56:40 +0900 Subject: [PATCH 38/50] Update note.yaml --- .github/workflows/note.yaml | 61 +++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index a50f604..f085a87 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -354,14 +354,7 @@ jobs: const isPublic = process.env.IS_PUBLIC === 'true'; const browser = await chromium.launch({ headless: true }); - - // editor.note.com も同じセッションで扱えるよう設定 - const context = await browser.newContext({ - storageState: statePath, - extraHTTPHeaders: { - 'Accept-Language': 'ja-JP,ja;q=0.9' - } - }); + const context = await browser.newContext({ storageState: statePath }); const page = await context.newPage(); let step = 0; @@ -371,23 +364,42 @@ jobs: }; try { - // editor.note.com に直接アクセス - console.log('Navigating to editor.note.com/new...'); - await page.goto('https://editor.note.com/new', { waitUntil: 'domcontentloaded' }); - console.log('URL after goto:', page.url()); + // ── Step1: note.com にアクセスしてセッションを確立 ── + console.log('Step1: Establishing session on note.com...'); + await page.goto('https://note.com', { waitUntil: 'domcontentloaded' }); + await page.waitForTimeout(2000); + console.log('note.com URL:', page.url()); + await shot('step1_notecom'); - // Reactのハイドレーション完了を待つ(最大30秒) - console.log('Waiting for React editor to hydrate...'); + // ── Step2: note.com/new に移動してリダイレクトを待つ ── + console.log('Step2: Navigating to note.com/new...'); + await page.goto('https://note.com/new', { waitUntil: 'domcontentloaded' }); + await page.waitForTimeout(3000); + console.log('After /new URL:', page.url()); + await shot('step2_new'); + + // ── Step3: editor.note.com へのリダイレクト待ち ── + // URLが editor.note.com になるまで最大15秒待つ + let currentUrl = page.url(); + let waited = 0; + while (!currentUrl.includes('editor.note.com') && waited < 15000) { + await page.waitForTimeout(1000); + waited += 1000; + currentUrl = page.url(); + console.log(`Waiting for redirect... (${waited}ms) URL: ${currentUrl}`); + } + console.log('Final URL:', currentUrl); + await shot('step3_redirected'); + + // ── Step4: エディタのcontenteditable待ち ── + console.log('Step4: Waiting for editor contenteditable...'); await page.waitForFunction(() => { - const els = document.querySelectorAll('[contenteditable]'); - return els.length > 0; - }, { timeout: 30000 }).catch(() => { - console.log('Still waiting...'); + return document.querySelectorAll('[contenteditable]').length > 0; + }, { timeout: 20000 }).catch(() => { + console.log('contenteditable wait timed out'); }); - - // さらに2秒待機 await page.waitForTimeout(2000); - await shot('after_goto'); + await shot('step4_editor'); const editableCount = await page.evaluate(() => document.querySelectorAll('[contenteditable]').length @@ -395,9 +407,9 @@ jobs: console.log(`Found ${editableCount} contenteditable elements`); if (editableCount === 0) { - const snippet = await page.evaluate(() => document.body.innerHTML.slice(0, 800)); + const snippet = await page.evaluate(() => document.body.innerHTML.slice(0, 500)); console.log('HTML snippet:', snippet); - throw new Error('Editor did not load. Check screenshot.'); + throw new Error('Editor did not load.'); } // ── ① タイトル入力 ── @@ -414,8 +426,6 @@ jobs: // ── ② 本文入力 ── console.log('Filling body...'); const count = await allEditables.count(); - console.log(`Total editables: ${count}`); - if (count >= 2) { await allEditables.nth(1).click(); await page.waitForTimeout(300); @@ -423,7 +433,6 @@ jobs: await page.keyboard.press('Tab'); await page.waitForTimeout(500); } - await page.keyboard.type(data.body, { delay: 10 }); await shot('after_body'); await page.waitForTimeout(2000); From a56b71e1c919ce06c86aa928c8bc4187ad4c61b7 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Thu, 12 Mar 2026 09:51:33 +0900 Subject: [PATCH 39/50] Update note.yaml --- .github/workflows/note.yaml | 64 +++++++++++++------------------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index f085a87..8f958c9 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -354,7 +354,10 @@ jobs: const isPublic = process.env.IS_PUBLIC === 'true'; const browser = await chromium.launch({ headless: true }); - const context = await browser.newContext({ storageState: statePath }); + const context = await browser.newContext({ + storageState: statePath, + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }); const page = await context.newPage(); let step = 0; @@ -364,42 +367,16 @@ jobs: }; try { - // ── Step1: note.com にアクセスしてセッションを確立 ── - console.log('Step1: Establishing session on note.com...'); - await page.goto('https://note.com', { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(2000); - console.log('note.com URL:', page.url()); - await shot('step1_notecom'); + // editor.note.com に直接アクセス(note-state.jsonにeditor.note.comのCookieが入っている) + console.log('Navigating to editor.note.com/new...'); + await page.goto('https://editor.note.com/new', { waitUntil: 'domcontentloaded' }); + console.log('URL:', page.url()); + await shot('01_after_goto'); - // ── Step2: note.com/new に移動してリダイレクトを待つ ── - console.log('Step2: Navigating to note.com/new...'); - await page.goto('https://note.com/new', { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(3000); - console.log('After /new URL:', page.url()); - await shot('step2_new'); - - // ── Step3: editor.note.com へのリダイレクト待ち ── - // URLが editor.note.com になるまで最大15秒待つ - let currentUrl = page.url(); - let waited = 0; - while (!currentUrl.includes('editor.note.com') && waited < 15000) { - await page.waitForTimeout(1000); - waited += 1000; - currentUrl = page.url(); - console.log(`Waiting for redirect... (${waited}ms) URL: ${currentUrl}`); - } - console.log('Final URL:', currentUrl); - await shot('step3_redirected'); - - // ── Step4: エディタのcontenteditable待ち ── - console.log('Step4: Waiting for editor contenteditable...'); - await page.waitForFunction(() => { - return document.querySelectorAll('[contenteditable]').length > 0; - }, { timeout: 20000 }).catch(() => { - console.log('contenteditable wait timed out'); - }); - await page.waitForTimeout(2000); - await shot('step4_editor'); + // 15秒待機してReactを描画させる + console.log('Waiting 15s for React to render...'); + await page.waitForTimeout(15000); + await shot('02_after_wait'); const editableCount = await page.evaluate(() => document.querySelectorAll('[contenteditable]').length @@ -407,9 +384,12 @@ jobs: console.log(`Found ${editableCount} contenteditable elements`); if (editableCount === 0) { - const snippet = await page.evaluate(() => document.body.innerHTML.slice(0, 500)); + // ログインページに飛ばされていないか確認 + const url = page.url(); + console.log('Current URL:', url); + const snippet = await page.evaluate(() => document.body.innerHTML.slice(0, 800)); console.log('HTML snippet:', snippet); - throw new Error('Editor did not load.'); + throw new Error(`Editor did not load. URL: ${url}`); } // ── ① タイトル入力 ── @@ -421,7 +401,7 @@ jobs: await page.keyboard.press('Control+A'); await page.keyboard.press('Backspace'); await page.keyboard.type(data.title, { delay: 30 }); - await shot('after_title'); + await shot('03_after_title'); // ── ② 本文入力 ── console.log('Filling body...'); @@ -434,7 +414,7 @@ jobs: await page.waitForTimeout(500); } await page.keyboard.type(data.body, { delay: 10 }); - await shot('after_body'); + await shot('04_after_body'); await page.waitForTimeout(2000); // ── ③ 保存 / 公開 ── @@ -453,13 +433,13 @@ jobs: if (await confirmBtn.count()) { await confirmBtn.click(); } - await shot('after_publish'); + await shot('05_after_publish'); } else { console.log('Saving draft...'); const saveBtn = page.getByRole('button', { name: /下書き保存|下書き|保存/i }).first(); await saveBtn.waitFor({ state: 'visible', timeout: 15000 }); await saveBtn.click(); - await shot('after_save'); + await shot('05_after_save'); } await page.waitForTimeout(3000); From 4b8cb4f87a5b27957f656cd9dba3ae9e48834ea3 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Thu, 12 Mar 2026 10:34:01 +0900 Subject: [PATCH 40/50] Update note.yaml --- .github/workflows/note.yaml | 124 +++++++++++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 17 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 8f958c9..e7799e5 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -50,14 +50,102 @@ env: TZ: Asia/Tokyo jobs: + setup: + name: Setup Theme + runs-on: ubuntu-latest + outputs: + theme: ${{ steps.theme.outputs.theme }} + target: ${{ steps.theme.outputs.target }} + message: ${{ steps.theme.outputs.message }} + cta: ${{ steps.theme.outputs.cta }} + tags: ${{ steps.theme.outputs.tags }} + is_public: ${{ steps.theme.outputs.is_public }} + dry_run: ${{ steps.theme.outputs.dry_run }} + steps: + - name: Resolve theme + id: theme + run: | + # workflow_dispatch の場合はそのまま使う + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "theme=${{ github.event.inputs.theme }}" >> "$GITHUB_OUTPUT" + echo "target=${{ github.event.inputs.target }}" >> "$GITHUB_OUTPUT" + echo "message=${{ github.event.inputs.message }}" >> "$GITHUB_OUTPUT" + echo "cta=${{ github.event.inputs.cta }}" >> "$GITHUB_OUTPUT" + echo "tags=${{ github.event.inputs.tags }}" >> "$GITHUB_OUTPUT" + echo "is_public=${{ github.event.inputs.is_public }}" >> "$GITHUB_OUTPUT" + echo "dry_run=${{ github.event.inputs.dry_run }}" >> "$GITHUB_OUTPUT" + else + # スケジュール実行:曜日でテーマをローテーション + DAY=$(date +%u) # 1=月 2=火 3=水 4=木 5=金 6=土 7=日 + case $DAY in + 1) + THEME="大阪マツエク モチがいい サロン" + TARGET="大阪でマツエクを探している20〜40代女性" + MESSAGE="大阪でモチのいいマツエクを見つけるポイントと、おすすめサロンの選び方" + CTA="LINEで無料カウンセリングを予約する" + TAGS="大阪マツエク,マツエク,まつげエクステ,大阪美容,モチいいマツエク" + ;; + 2) + THEME="大阪まつげ パーマ 持ちがいい" + TARGET="まつげパーマを初めて検討している大阪在住の女性" + MESSAGE="まつげパーマは施術が早く、毎朝のメイク時間を大幅に短縮できる" + CTA="まつげパーマの料金・メニューをLINEで確認する" + TAGS="まつげパーマ,大阪まつげ,大阪美容,パリジェンヌ,まつ毛カール" + ;; + 3) + THEME="LEDマツエク 大阪 安全 モチ" + TARGET="オーガニック・安全志向の大阪在住女性" + MESSAGE="LEDマツエクは従来より硬化時間が短く目への負担が少ない次世代まつげエクステ" + CTA="LEDマツエクの詳細・予約はこちら" + TAGS="LEDマツエク,大阪マツエク,まつげエクステ,最新マツエク,安全マツエク" + ;; + 4) + THEME="大阪 眉毛 アイブロウ 美眉 サロン" + TARGET="眉毛の形・薄さに悩む大阪在住の女性" + MESSAGE="眉毛を整えるだけで顔の印象が変わる。大阪でおすすめの美眉サロンと選び方" + CTA="眉毛デザインの無料相談はLINEから" + TAGS="アイブロウ,眉毛サロン,大阪眉毛,美眉,眉デザイン" + ;; + 5) + THEME="大阪マツエク 安い 高品質 コスパ" + TARGET="マツエクのコスパを重視する大阪在住の女性" + MESSAGE="安いだけでなく品質も大事。大阪でコスパ最強のマツエクサロンの見つけ方" + CTA="お得なクーポン・料金をLINEで確認する" + TAGS="大阪マツエク,マツエク安い,コスパマツエク,大阪美容,まつげエクステ" + ;; + 6) + THEME="まつげエクステ 大阪 早い 当日予約" + TARGET="忙しくて時間がない大阪在住の働く女性" + MESSAGE="施術時間60分以内・当日予約OKのマツエクで、忙しい女性でも通いやすい" + CTA="当日予約はLINEから今すぐ確認" + TAGS="大阪マツエク,当日予約,時短マツエク,まつげエクステ,大阪美容" + ;; + 7) + THEME="大阪マツエク まつげパーマ 眉毛 まとめ 比較" + TARGET="マツエク・まつパ・眉毛どれにするか迷っている大阪在住女性" + MESSAGE="マツエク・まつげパーマ・眉毛サロンそれぞれの特徴と向いている人を比較" + CTA="どれが自分に合うかLINEで相談する" + TAGS="大阪マツエク,まつげパーマ,アイブロウ,大阪美容,まつげ比較" + ;; + esac + echo "theme=$THEME" >> "$GITHUB_OUTPUT" + echo "target=$TARGET" >> "$GITHUB_OUTPUT" + echo "message=$MESSAGE" >> "$GITHUB_OUTPUT" + echo "cta=$CTA" >> "$GITHUB_OUTPUT" + echo "tags=$TAGS" >> "$GITHUB_OUTPUT" + echo "is_public=true" >> "$GITHUB_OUTPUT" + echo "dry_run=false" >> "$GITHUB_OUTPUT" + fi + research: name: Research (Tavily) runs-on: ubuntu-latest + needs: setup env: TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - THEME: ${{ github.event.inputs.theme }} - TARGET: ${{ github.event.inputs.target }} - MESSAGE: ${{ github.event.inputs.message }} + THEME: ${{ needs.setup.outputs.theme }} + TARGET: ${{ needs.setup.outputs.target }} + MESSAGE: ${{ needs.setup.outputs.message }} outputs: research_b64: ${{ steps.collect.outputs.research_b64 }} steps: @@ -72,7 +160,7 @@ jobs: import fs from 'fs'; const apiKey = process.env.TAVILY_API_KEY; - const theme = process.env.THEME || '大阪 マツエク'; + const theme = process.env.THEME || '大阪マツエク'; const target = process.env.TARGET || ''; const message = process.env.MESSAGE || ''; @@ -124,14 +212,14 @@ jobs: write: name: Write (Claude) runs-on: ubuntu-latest - needs: research + needs: [setup, research] env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - THEME: ${{ github.event.inputs.theme }} - TARGET: ${{ github.event.inputs.target }} - MESSAGE: ${{ github.event.inputs.message }} - CTA: ${{ github.event.inputs.cta }} - TAGS: ${{ github.event.inputs.tags }} + THEME: ${{ needs.setup.outputs.theme }} + TARGET: ${{ needs.setup.outputs.target }} + MESSAGE: ${{ needs.setup.outputs.message }} + CTA: ${{ needs.setup.outputs.cta }} + TAGS: ${{ needs.setup.outputs.tags }} RESEARCH_B64: ${{ needs.research.outputs.research_b64 }} outputs: article_b64: ${{ steps.collect.outputs.article_b64 }} @@ -156,6 +244,7 @@ jobs: const systemPrompt = ` あなたは日本語のnote記事を書くプロの編集者です。 + MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事を作成してください。 目的は、読みやすく、信頼感があり、行動につながるnote記事を作ることです。 出力は必ずJSONのみ。 形式: @@ -180,9 +269,13 @@ jobs: - 日本語 - 読みやすい自然な文章 - 誇張しすぎない - - 見出しを使う + - 見出しを使う(H2・H3) - 最後にCTAを入れる - tags は配列で3〜5個 + - MEO対策:「大阪」「地域名」を自然に文中に含める + - LLMO対策:「〜とは」「〜の選び方」「〜のポイント」など質問形式の見出しを使う + - キーワード「モチ」「早い」「安い」を自然に含める + - 文字数は1500〜2500文字 `.trim(); const res = await fetch('https://api.anthropic.com/v1/messages', { @@ -316,12 +409,12 @@ jobs: post: name: Post to note.com (Playwright) runs-on: ubuntu-latest - needs: factcheck - if: ${{ github.event.inputs.dry_run == 'false' }} + needs: [setup, factcheck] + if: ${{ needs.setup.outputs.dry_run == 'false' }} env: NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} - IS_PUBLIC: ${{ github.event.inputs.is_public }} + IS_PUBLIC: ${{ needs.setup.outputs.is_public }} steps: - name: Checkout uses: actions/checkout@v4 @@ -367,13 +460,11 @@ jobs: }; try { - // editor.note.com に直接アクセス(note-state.jsonにeditor.note.comのCookieが入っている) console.log('Navigating to editor.note.com/new...'); await page.goto('https://editor.note.com/new', { waitUntil: 'domcontentloaded' }); console.log('URL:', page.url()); await shot('01_after_goto'); - // 15秒待機してReactを描画させる console.log('Waiting 15s for React to render...'); await page.waitForTimeout(15000); await shot('02_after_wait'); @@ -384,7 +475,6 @@ jobs: console.log(`Found ${editableCount} contenteditable elements`); if (editableCount === 0) { - // ログインページに飛ばされていないか確認 const url = page.url(); console.log('Current URL:', url); const snippet = await page.evaluate(() => document.body.innerHTML.slice(0, 800)); From 6eaf00d2a66b6b747a330aa5169064383f618180 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Fri, 13 Mar 2026 08:46:56 +0900 Subject: [PATCH 41/50] Update note.yaml --- .github/workflows/note.yaml | 199 +++++++++++++++++++----------------- 1 file changed, 107 insertions(+), 92 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index e7799e5..4b5c687 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -65,7 +65,6 @@ jobs: - name: Resolve theme id: theme run: | - # workflow_dispatch の場合はそのまま使う if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "theme=${{ github.event.inputs.theme }}" >> "$GITHUB_OUTPUT" echo "target=${{ github.event.inputs.target }}" >> "$GITHUB_OUTPUT" @@ -75,8 +74,7 @@ jobs: echo "is_public=${{ github.event.inputs.is_public }}" >> "$GITHUB_OUTPUT" echo "dry_run=${{ github.event.inputs.dry_run }}" >> "$GITHUB_OUTPUT" else - # スケジュール実行:曜日でテーマをローテーション - DAY=$(date +%u) # 1=月 2=火 3=水 4=木 5=金 6=土 7=日 + DAY=$(date +%u) case $DAY in 1) THEME="大阪マツエク モチがいい サロン" @@ -166,30 +164,20 @@ jobs: const prompt = `${theme} ${target} ${message}`.trim(); - const body = { - query: prompt, - search_depth: 'advanced', - topic: 'general', - max_results: 5, - include_answer: true - }; - const res = await fetch('https://api.tavily.com/search', { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_key: apiKey, - ...body + query: prompt, + search_depth: 'advanced', + topic: 'general', + max_results: 5, + include_answer: true }) }); - if (!res.ok) { - const txt = await res.text(); - console.error(txt); - process.exit(1); - } + if (!res.ok) { console.error(await res.text()); process.exit(1); } const json = await res.json(); fs.writeFileSync('research.json', JSON.stringify(json, null, 2)); @@ -245,7 +233,6 @@ jobs: const systemPrompt = ` あなたは日本語のnote記事を書くプロの編集者です。 MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事を作成してください。 - 目的は、読みやすく、信頼感があり、行動につながるnote記事を作ることです。 出力は必ずJSONのみ。 形式: { @@ -261,19 +248,13 @@ jobs: 読者に伝えたい核メッセージ: ${message} 読後のアクション: ${cta} 希望タグ: ${tags} - - リサーチ結果: - ${JSON.stringify(research, null, 2)} - + リサーチ結果: ${JSON.stringify(research, null, 2)} 条件: - - 日本語 - - 読みやすい自然な文章 - - 誇張しすぎない - - 見出しを使う(H2・H3) - - 最後にCTAを入れる + - 日本語・読みやすい自然な文章・誇張しすぎない + - 見出しを使う(H2・H3)・最後にCTAを入れる - tags は配列で3〜5個 - - MEO対策:「大阪」「地域名」を自然に文中に含める - - LLMO対策:「〜とは」「〜の選び方」「〜のポイント」など質問形式の見出しを使う + - MEO対策:「大阪」を自然に文中に含める + - LLMO対策:「〜とは」「〜の選び方」など質問形式の見出しを使う - キーワード「モチ」「早い」「安い」を自然に含める - 文字数は1500〜2500文字 `.trim(); @@ -289,33 +270,19 @@ jobs: model: 'claude-haiku-4-5', max_tokens: 4000, system: systemPrompt, - messages: [ - { - role: 'user', - content: userPrompt - } - ] + messages: [{ role: 'user', content: userPrompt }] }) }); - if (!res.ok) { - const txt = await res.text(); - console.error(txt); - process.exit(1); - } + if (!res.ok) { console.error(await res.text()); process.exit(1); } const json = await res.json(); const text = (json.content?.[0]?.text || '').replace(/```json|```/g, '').trim(); fs.writeFileSync('article_raw.txt', text); let parsed; - try { - parsed = JSON.parse(text); - } catch { - console.error('Claude output was not valid JSON'); - console.error(text); - process.exit(1); - } + try { parsed = JSON.parse(text); } + catch { console.error('Claude output was not valid JSON'); console.error(text); process.exit(1); } fs.writeFileSync('article.json', JSON.stringify(parsed, null, 2)); console.log('article saved'); @@ -358,14 +325,11 @@ jobs: const apiKey = process.env.TAVILY_API_KEY; const article = JSON.parse(Buffer.from(process.env.ARTICLE_B64, 'base64').toString('utf8')); - const query = `${article.title}\n\n${article.body.slice(0, 300)}`; const res = await fetch('https://api.tavily.com/search', { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_key: apiKey, query, @@ -376,20 +340,10 @@ jobs: }) }); - if (!res.ok) { - const txt = await res.text(); - console.error(txt); - process.exit(1); - } + if (!res.ok) { console.error(await res.text()); process.exit(1); } const fact = await res.json(); - - const finalArticle = { - ...article, - factcheck: fact - }; - - fs.writeFileSync('final_article.json', JSON.stringify(finalArticle, null, 2)); + fs.writeFileSync('final_article.json', JSON.stringify({ ...article, factcheck: fact }, null, 2)); console.log('final article saved'); EOF @@ -463,11 +417,10 @@ jobs: console.log('Navigating to editor.note.com/new...'); await page.goto('https://editor.note.com/new', { waitUntil: 'domcontentloaded' }); console.log('URL:', page.url()); - await shot('01_after_goto'); console.log('Waiting 15s for React to render...'); await page.waitForTimeout(15000); - await shot('02_after_wait'); + await shot('01_after_wait'); const editableCount = await page.evaluate(() => document.querySelectorAll('[contenteditable]').length @@ -475,60 +428,122 @@ jobs: console.log(`Found ${editableCount} contenteditable elements`); if (editableCount === 0) { - const url = page.url(); - console.log('Current URL:', url); const snippet = await page.evaluate(() => document.body.innerHTML.slice(0, 800)); console.log('HTML snippet:', snippet); - throw new Error(`Editor did not load. URL: ${url}`); + throw new Error(`Editor did not load. URL: ${page.url()}`); } // ── ① タイトル入力 ── console.log('Filling title...'); const allEditables = page.locator('[contenteditable]'); - const titleEl = allEditables.nth(0); - await titleEl.click(); + await allEditables.nth(0).click(); await page.waitForTimeout(300); await page.keyboard.press('Control+A'); await page.keyboard.press('Backspace'); await page.keyboard.type(data.title, { delay: 30 }); - await shot('03_after_title'); + await shot('02_after_title'); // ── ② 本文入力 ── console.log('Filling body...'); const count = await allEditables.count(); if (count >= 2) { await allEditables.nth(1).click(); - await page.waitForTimeout(300); } else { await page.keyboard.press('Tab'); - await page.waitForTimeout(500); } + await page.waitForTimeout(300); await page.keyboard.type(data.body, { delay: 10 }); - await shot('04_after_body'); + await shot('03_after_body'); await page.waitForTimeout(2000); - // ── ③ 保存 / 公開 ── - const buttons = await page.evaluate(() => + // ── ③ ボタンを全部ログ出力してから保存・公開 ── + const allButtons = await page.evaluate(() => Array.from(document.querySelectorAll('button')).map(b => b.innerText.trim()).filter(Boolean) ); - console.log('[buttons]', JSON.stringify(buttons)); + console.log('[all buttons]', JSON.stringify(allButtons)); + await shot('04_before_save'); if (isPublic) { - console.log('Publishing...'); - const publishBtn = page.getByRole('button', { name: /公開する|公開設定|publish/i }).first(); - await publishBtn.waitFor({ state: 'visible', timeout: 15000 }); - await publishBtn.click(); + // 公開ボタンを柔軟に探す + const publishPatterns = [ + /公開する/, + /公開設定/, + /投稿する/, + /publish/i, + /post/i + ]; + + let clicked = false; + for (const pattern of publishPatterns) { + const btn = page.getByRole('button', { name: pattern }).first(); + if (await btn.count() > 0) { + console.log(`Clicking publish button: ${pattern}`); + await btn.click(); + clicked = true; + break; + } + } + + if (!clicked) { + // 最後の手段:ボタンのテキストで直接探す + const btn = page.locator('button').filter({ hasText: /公開|投稿|publish/i }).first(); + if (await btn.count() > 0) { + await btn.click(); + clicked = true; + } + } + + if (!clicked) { + throw new Error('Publish button not found. Check screenshots and button list above.'); + } + await page.waitForTimeout(3000); - const confirmBtn = page.getByRole('button', { name: /公開する/i }).first(); - if (await confirmBtn.count()) { - await confirmBtn.click(); + await shot('05_after_publish_click'); + + // 確認ダイアログが出る場合 + const confirmPatterns = [/公開する/, /確認/, /はい/, /OK/i]; + for (const pattern of confirmPatterns) { + const btn = page.getByRole('button', { name: pattern }).first(); + if (await btn.count() > 0) { + console.log(`Clicking confirm button: ${pattern}`); + await btn.click(); + break; + } } - await shot('05_after_publish'); + await shot('06_after_confirm'); + } else { - console.log('Saving draft...'); - const saveBtn = page.getByRole('button', { name: /下書き保存|下書き|保存/i }).first(); - await saveBtn.waitFor({ state: 'visible', timeout: 15000 }); - await saveBtn.click(); + // 下書き保存 + const draftPatterns = [ + /下書き保存/, + /下書き/, + /保存/, + /save/i + ]; + + let clicked = false; + for (const pattern of draftPatterns) { + const btn = page.getByRole('button', { name: pattern }).first(); + if (await btn.count() > 0) { + console.log(`Clicking draft button: ${pattern}`); + await btn.click(); + clicked = true; + break; + } + } + + if (!clicked) { + const btn = page.locator('button').filter({ hasText: /下書き|保存|save/i }).first(); + if (await btn.count() > 0) { + await btn.click(); + clicked = true; + } + } + + if (!clicked) { + throw new Error('Save button not found. Check screenshots and button list above.'); + } + await shot('05_after_save'); } From 529fba59ee9d9605016eafc2446f6719058cb263 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sat, 14 Mar 2026 13:21:38 +0900 Subject: [PATCH 42/50] Update note.yaml --- .github/workflows/note.yaml | 67 ++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 4b5c687..70520da 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -158,11 +158,8 @@ jobs: import fs from 'fs'; const apiKey = process.env.TAVILY_API_KEY; - const theme = process.env.THEME || '大阪マツエク'; - const target = process.env.TARGET || ''; - const message = process.env.MESSAGE || ''; - - const prompt = `${theme} ${target} ${message}`.trim(); + + const prompt = '大阪まつげエクステSpice スパイス 梅田 心斎橋 経営理念 技術 15年 基礎 丁寧'; const res = await fetch('https://api.tavily.com/search', { method: 'POST', @@ -232,7 +229,8 @@ jobs: const systemPrompt = ` あなたは日本語のnote記事を書くプロの編集者です。 - MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事を作成してください。 + 大阪のまつげエクステサロン「Spice(スパイス)」の紹介記事を作成してください。 + MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事にしてください。 出力は必ずJSONのみ。 形式: { @@ -243,20 +241,49 @@ jobs: `.trim(); const userPrompt = ` - 記事テーマ: ${theme} - 想定読者: ${target} - 読者に伝えたい核メッセージ: ${message} - 読後のアクション: ${cta} - 希望タグ: ${tags} - リサーチ結果: ${JSON.stringify(research, null, 2)} - 条件: - - 日本語・読みやすい自然な文章・誇張しすぎない - - 見出しを使う(H2・H3)・最後にCTAを入れる - - tags は配列で3〜5個 - - MEO対策:「大阪」を自然に文中に含める + 【記事の目的】 + - SpiceのSEO・LLMO対策 + - 大阪でまつげエクステを探している人に向けた記事 + - Spiceの経営理念・技術・サービスを総合的に紹介 + + 【必ず含める内容】 + + **1. 基本情報** + - 大阪を拠点とするまつげ・眉の専門サロン + - 15年以上続く老舗 + - 複数店舗展開(梅田、心斎橋など) + - 施術:マツエク、まつげパーマ、アイブロー + + **2. 経営理念・特徴** + - 「壊れない設計」で長く続く経営 + - 流行より基礎を大事にする + - 根性論ではなく仕組みで回す + - 「早くて丁寧」な技術 + - お客様の悩みを拾う接客 + - スタッフ教育・技術向上に力を入れる + + **3. 技術の特徴** + - 技術は「上手い」ではなく「基礎がぶれてない」 + - 相手の悩みを拾える技術者 + - クレームが減る技術の考え方 + - 指名が増える接客 + + **4. お客様へのメッセージ** + - 押し売りゼロ + - 30代・40代にも似合うデザイン + - ライフステージに合わせた提案 + + 【条件】 + - 日本語・読みやすい自然な文章 + - 大阪弁のニュアンスを少し入れる + - 見出しを使う(H2・H3) + - tags は配列で5個 + - MEO対策:「大阪」「梅田」「心斎橋」を自然に文中に含める - LLMO対策:「〜とは」「〜の選び方」など質問形式の見出しを使う - - キーワード「モチ」「早い」「安い」を自然に含める - - 文字数は1500〜2500文字 + - 文字数は2500〜3500文字 + - Q&A形式の見出しを含める + + リサーチ結果: ${JSON.stringify(research, null, 2)} `.trim(); const res = await fetch('https://api.anthropic.com/v1/messages', { @@ -267,7 +294,7 @@ jobs: 'content-type': 'application/json' }, body: JSON.stringify({ - model: 'claude-haiku-4-5', + model: 'claude-3-opus-20240229', max_tokens: 4000, system: systemPrompt, messages: [{ role: 'user', content: userPrompt }] From fc44468e8b4677558db8d490bcaba69b3ae8d8ee Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sat, 14 Mar 2026 13:47:06 +0900 Subject: [PATCH 43/50] Update note.yaml --- .github/workflows/note.yaml | 92 ++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 70520da..55ddf85 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -158,8 +158,34 @@ jobs: import fs from 'fs'; const apiKey = process.env.TAVILY_API_KEY; - - const prompt = '大阪まつげエクステSpice スパイス 梅田 心斎橋 経営理念 技術 15年 基礎 丁寧'; + const theme = process.env.THEME || '大阪マツエク'; + const target = process.env.TARGET || ''; + const message = process.env.MESSAGE || ''; + + const prompt = ` +以下の検索結果をもとに、大阪のまつげエクステサロン「Spice(スパイス)」の紹介記事を作成してください。 + +【記事の目的】 +- SpiceのSEO対策 +- 大阪でまつげエクステを探している人に向けた記事 +- 自然な文体で、Spiceの魅力を伝える + +【記事の構成】 +- タイトル:SEOを意識したキャッチーなもの +- 本文:2000文字程度 +- 大阪弁のニュアンスを少し入れる +- Spiceの特徴・強み・サービスを紹介 +- お客様目線で書く + +【検索結果】 +${research} + +【出力形式】 +{ + "title": "記事タイトル", + "body": "本文" +} +`; const res = await fetch('https://api.tavily.com/search', { method: 'POST', @@ -229,8 +255,7 @@ jobs: const systemPrompt = ` あなたは日本語のnote記事を書くプロの編集者です。 - 大阪のまつげエクステサロン「Spice(スパイス)」の紹介記事を作成してください。 - MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事にしてください。 + MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事を作成してください。 出力は必ずJSONのみ。 形式: { @@ -241,49 +266,20 @@ jobs: `.trim(); const userPrompt = ` - 【記事の目的】 - - SpiceのSEO・LLMO対策 - - 大阪でまつげエクステを探している人に向けた記事 - - Spiceの経営理念・技術・サービスを総合的に紹介 - - 【必ず含める内容】 - - **1. 基本情報** - - 大阪を拠点とするまつげ・眉の専門サロン - - 15年以上続く老舗 - - 複数店舗展開(梅田、心斎橋など) - - 施術:マツエク、まつげパーマ、アイブロー - - **2. 経営理念・特徴** - - 「壊れない設計」で長く続く経営 - - 流行より基礎を大事にする - - 根性論ではなく仕組みで回す - - 「早くて丁寧」な技術 - - お客様の悩みを拾う接客 - - スタッフ教育・技術向上に力を入れる - - **3. 技術の特徴** - - 技術は「上手い」ではなく「基礎がぶれてない」 - - 相手の悩みを拾える技術者 - - クレームが減る技術の考え方 - - 指名が増える接客 - - **4. お客様へのメッセージ** - - 押し売りゼロ - - 30代・40代にも似合うデザイン - - ライフステージに合わせた提案 - - 【条件】 - - 日本語・読みやすい自然な文章 - - 大阪弁のニュアンスを少し入れる - - 見出しを使う(H2・H3) - - tags は配列で5個 - - MEO対策:「大阪」「梅田」「心斎橋」を自然に文中に含める - - LLMO対策:「〜とは」「〜の選び方」など質問形式の見出しを使う - - 文字数は2500〜3500文字 - - Q&A形式の見出しを含める - + 記事テーマ: ${theme} + 想定読者: ${target} + 読者に伝えたい核メッセージ: ${message} + 読後のアクション: ${cta} + 希望タグ: ${tags} リサーチ結果: ${JSON.stringify(research, null, 2)} + 条件: + - 日本語・読みやすい自然な文章・誇張しすぎない + - 見出しを使う(H2・H3)・最後にCTAを入れる + - tags は配列で3〜5個 + - MEO対策:「大阪」を自然に文中に含める + - LLMO対策:「〜とは」「〜の選び方」など質問形式の見出しを使う + - キーワード「モチ」「早い」「安い」を自然に含める + - 文字数は1500〜2500文字 `.trim(); const res = await fetch('https://api.anthropic.com/v1/messages', { @@ -294,7 +290,7 @@ jobs: 'content-type': 'application/json' }, body: JSON.stringify({ - model: 'claude-3-opus-20240229', + model: 'claude-haiku-4-5', max_tokens: 4000, system: systemPrompt, messages: [{ role: 'user', content: userPrompt }] @@ -352,7 +348,7 @@ jobs: const apiKey = process.env.TAVILY_API_KEY; const article = JSON.parse(Buffer.from(process.env.ARTICLE_B64, 'base64').toString('utf8')); - const query = `${article.title}\n\n${article.body.slice(0, 300)}`; + const query = '大阪まつげエクステSpice(スパイス)'; const res = await fetch('https://api.tavily.com/search', { method: 'POST', From 974620b2cf8774e7f46d49d52dceabe3367dee80 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sat, 14 Mar 2026 14:11:34 +0900 Subject: [PATCH 44/50] Update note.yaml --- .github/workflows/note.yaml | 87 +++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 55ddf85..28af117 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -158,34 +158,8 @@ jobs: import fs from 'fs'; const apiKey = process.env.TAVILY_API_KEY; - const theme = process.env.THEME || '大阪マツエク'; - const target = process.env.TARGET || ''; - const message = process.env.MESSAGE || ''; - - const prompt = ` -以下の検索結果をもとに、大阪のまつげエクステサロン「Spice(スパイス)」の紹介記事を作成してください。 - -【記事の目的】 -- SpiceのSEO対策 -- 大阪でまつげエクステを探している人に向けた記事 -- 自然な文体で、Spiceの魅力を伝える - -【記事の構成】 -- タイトル:SEOを意識したキャッチーなもの -- 本文:2000文字程度 -- 大阪弁のニュアンスを少し入れる -- Spiceの特徴・強み・サービスを紹介 -- お客様目線で書く - -【検索結果】 -${research} - -【出力形式】 -{ - "title": "記事タイトル", - "body": "本文" -} -`; + + const prompt = '大阪まつげエクステSpice スパイス 梅田 心斎橋 経営理念 技術 15年 基礎 丁寧'; const res = await fetch('https://api.tavily.com/search', { method: 'POST', @@ -255,7 +229,8 @@ ${research} const systemPrompt = ` あなたは日本語のnote記事を書くプロの編集者です。 - MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事を作成してください。 + 大阪のまつげエクステサロン「Spice(スパイス)」の紹介記事を作成してください。 + MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事にしてください。 出力は必ずJSONのみ。 形式: { @@ -266,20 +241,46 @@ ${research} `.trim(); const userPrompt = ` - 記事テーマ: ${theme} - 想定読者: ${target} - 読者に伝えたい核メッセージ: ${message} - 読後のアクション: ${cta} - 希望タグ: ${tags} - リサーチ結果: ${JSON.stringify(research, null, 2)} - 条件: - - 日本語・読みやすい自然な文章・誇張しすぎない - - 見出しを使う(H2・H3)・最後にCTAを入れる - - tags は配列で3〜5個 - - MEO対策:「大阪」を自然に文中に含める + 【記事の目的】 + - SpiceのSEO・LLMO対策 + - 大阪でまつげエクステを探している人に向けた記事 + - Spiceの経営理念・技術・サービスを総合的に紹介 + + 【必ず含める内容】 + **1. 基本情報** + - 大阪を拠点とするまつげ・眉の専門サロン + - 15年以上続く老舗 + - 複数店舗展開(梅田、心斎橋など) + - 施術:マツエク、まつげパーマ、アイブロー + + **2. 経営理念・特徴** + - 「壊れない設計」で長く続く経営 + - 流行より基礎を大事にする + - 根性論ではなく仕組みで回す + - 「早くて丁寧」な技術 + - お客様の悩みを拾う接客 + + **3. 技術の特徴** + - 技術は「上手い」ではなく「基礎がぶれてない」 + - 相手の悩みを拾える技術者 + - 指名が増える接客 + + **4. お客様へのメッセージ** + - 押し売りゼロ + - 30代・40代にも似合うデザイン + - ライフステージに合わせた提案 + + 【条件】 + - 日本語・読みやすい自然な文章 + - 大阪弁のニュアンスを少し入れる + - 見出しを使う(H2・H3) + - tags は配列で5個 + - MEO対策:「大阪」「梅田」「心斎橋」を自然に文中に含める - LLMO対策:「〜とは」「〜の選び方」など質問形式の見出しを使う - - キーワード「モチ」「早い」「安い」を自然に含める - - 文字数は1500〜2500文字 + - 文字数は2500〜3500文字 + - Q&A形式の見出しを含める + + リサーチ結果: ${JSON.stringify(research, null, 2)} `.trim(); const res = await fetch('https://api.anthropic.com/v1/messages', { @@ -348,7 +349,7 @@ ${research} const apiKey = process.env.TAVILY_API_KEY; const article = JSON.parse(Buffer.from(process.env.ARTICLE_B64, 'base64').toString('utf8')); - const query = '大阪まつげエクステSpice(スパイス)'; + const query = `${article.title}\n\n${article.body.slice(0, 300)}`; const res = await fetch('https://api.tavily.com/search', { method: 'POST', From 16106e4c0cadeaead6f417a7b2c4539314c89436 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Sat, 14 Mar 2026 14:14:35 +0900 Subject: [PATCH 45/50] Update note.yaml --- .github/workflows/note.yaml | 62 ++++++++++++------------------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 28af117..4b5c687 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -158,8 +158,11 @@ jobs: import fs from 'fs'; const apiKey = process.env.TAVILY_API_KEY; - - const prompt = '大阪まつげエクステSpice スパイス 梅田 心斎橋 経営理念 技術 15年 基礎 丁寧'; + const theme = process.env.THEME || '大阪マツエク'; + const target = process.env.TARGET || ''; + const message = process.env.MESSAGE || ''; + + const prompt = `${theme} ${target} ${message}`.trim(); const res = await fetch('https://api.tavily.com/search', { method: 'POST', @@ -229,8 +232,7 @@ jobs: const systemPrompt = ` あなたは日本語のnote記事を書くプロの編集者です。 - 大阪のまつげエクステサロン「Spice(スパイス)」の紹介記事を作成してください。 - MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事にしてください。 + MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事を作成してください。 出力は必ずJSONのみ。 形式: { @@ -241,46 +243,20 @@ jobs: `.trim(); const userPrompt = ` - 【記事の目的】 - - SpiceのSEO・LLMO対策 - - 大阪でまつげエクステを探している人に向けた記事 - - Spiceの経営理念・技術・サービスを総合的に紹介 - - 【必ず含める内容】 - **1. 基本情報** - - 大阪を拠点とするまつげ・眉の専門サロン - - 15年以上続く老舗 - - 複数店舗展開(梅田、心斎橋など) - - 施術:マツエク、まつげパーマ、アイブロー - - **2. 経営理念・特徴** - - 「壊れない設計」で長く続く経営 - - 流行より基礎を大事にする - - 根性論ではなく仕組みで回す - - 「早くて丁寧」な技術 - - お客様の悩みを拾う接客 - - **3. 技術の特徴** - - 技術は「上手い」ではなく「基礎がぶれてない」 - - 相手の悩みを拾える技術者 - - 指名が増える接客 - - **4. お客様へのメッセージ** - - 押し売りゼロ - - 30代・40代にも似合うデザイン - - ライフステージに合わせた提案 - - 【条件】 - - 日本語・読みやすい自然な文章 - - 大阪弁のニュアンスを少し入れる - - 見出しを使う(H2・H3) - - tags は配列で5個 - - MEO対策:「大阪」「梅田」「心斎橋」を自然に文中に含める - - LLMO対策:「〜とは」「〜の選び方」など質問形式の見出しを使う - - 文字数は2500〜3500文字 - - Q&A形式の見出しを含める - + 記事テーマ: ${theme} + 想定読者: ${target} + 読者に伝えたい核メッセージ: ${message} + 読後のアクション: ${cta} + 希望タグ: ${tags} リサーチ結果: ${JSON.stringify(research, null, 2)} + 条件: + - 日本語・読みやすい自然な文章・誇張しすぎない + - 見出しを使う(H2・H3)・最後にCTAを入れる + - tags は配列で3〜5個 + - MEO対策:「大阪」を自然に文中に含める + - LLMO対策:「〜とは」「〜の選び方」など質問形式の見出しを使う + - キーワード「モチ」「早い」「安い」を自然に含める + - 文字数は1500〜2500文字 `.trim(); const res = await fetch('https://api.anthropic.com/v1/messages', { From c2a958b9971f4159049f2a41f3c3518f605f3547 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 30 Mar 2026 15:24:02 +0900 Subject: [PATCH 46/50] Update note.yaml --- .github/workflows/note.yaml | 462 +++--------------------------------- 1 file changed, 38 insertions(+), 424 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 4b5c687..ad52e87 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -77,55 +77,33 @@ jobs: DAY=$(date +%u) case $DAY in 1) - THEME="大阪マツエク モチがいい サロン" - TARGET="大阪でマツエクを探している20〜40代女性" - MESSAGE="大阪でモチのいいマツエクを見つけるポイントと、おすすめサロンの選び方" - CTA="LINEで無料カウンセリングを予約する" - TAGS="大阪マツエク,マツエク,まつげエクステ,大阪美容,モチいいマツエク" + THEME="心斎橋 マツエク SPICE LEDエクステ 持ちがいい" ;; 2) - THEME="大阪まつげ パーマ 持ちがいい" - TARGET="まつげパーマを初めて検討している大阪在住の女性" - MESSAGE="まつげパーマは施術が早く、毎朝のメイク時間を大幅に短縮できる" - CTA="まつげパーマの料金・メニューをLINEで確認する" - TAGS="まつげパーマ,大阪まつげ,大阪美容,パリジェンヌ,まつ毛カール" + THEME="梅田 マツエク SPICE 早い キレイ" ;; 3) - THEME="LEDマツエク 大阪 安全 モチ" - TARGET="オーガニック・安全志向の大阪在住女性" - MESSAGE="LEDマツエクは従来より硬化時間が短く目への負担が少ない次世代まつげエクステ" - CTA="LEDマツエクの詳細・予約はこちら" - TAGS="LEDマツエク,大阪マツエク,まつげエクステ,最新マツエク,安全マツエク" + THEME="なんば マツエク SPICE LED 安全" ;; 4) - THEME="大阪 眉毛 アイブロウ 美眉 サロン" - TARGET="眉毛の形・薄さに悩む大阪在住の女性" - MESSAGE="眉毛を整えるだけで顔の印象が変わる。大阪でおすすめの美眉サロンと選び方" - CTA="眉毛デザインの無料相談はLINEから" - TAGS="アイブロウ,眉毛サロン,大阪眉毛,美眉,眉デザイン" + THEME="天満橋 マツエク SPICE 持ちがいい" ;; 5) - THEME="大阪マツエク 安い 高品質 コスパ" - TARGET="マツエクのコスパを重視する大阪在住の女性" - MESSAGE="安いだけでなく品質も大事。大阪でコスパ最強のマツエクサロンの見つけ方" - CTA="お得なクーポン・料金をLINEで確認する" - TAGS="大阪マツエク,マツエク安い,コスパマツエク,大阪美容,まつげエクステ" + THEME="野田阪神 マツエク SPICE コスパ" ;; 6) - THEME="まつげエクステ 大阪 早い 当日予約" - TARGET="忙しくて時間がない大阪在住の働く女性" - MESSAGE="施術時間60分以内・当日予約OKのマツエクで、忙しい女性でも通いやすい" - CTA="当日予約はLINEから今すぐ確認" - TAGS="大阪マツエク,当日予約,時短マツエク,まつげエクステ,大阪美容" + THEME="なかもず マツエク SPICE 当日予約" ;; 7) - THEME="大阪マツエク まつげパーマ 眉毛 まとめ 比較" - TARGET="マツエク・まつパ・眉毛どれにするか迷っている大阪在住女性" - MESSAGE="マツエク・まつげパーマ・眉毛サロンそれぞれの特徴と向いている人を比較" - CTA="どれが自分に合うかLINEで相談する" - TAGS="大阪マツエク,まつげパーマ,アイブロウ,大阪美容,まつげ比較" + THEME="大阪 マツエク SPICE LED フラット 比較" ;; esac + + TARGET="忙しい20〜30代女性でマツエクの持ちや仕上がりに悩んでいる人" + MESSAGE="SPICEが選ばれる理由とLEDマツエクの持ちの良さ・仕上がりについて" + CTA="LINEで予約・無料カウンセリングはこちら" + TAGS="SPICE,大阪マツエク,心斎橋マツエク,梅田マツエク,LEDマツエク" + echo "theme=$THEME" >> "$GITHUB_OUTPUT" echo "target=$TARGET" >> "$GITHUB_OUTPUT" echo "message=$MESSAGE" >> "$GITHUB_OUTPUT" @@ -135,72 +113,10 @@ jobs: echo "dry_run=false" >> "$GITHUB_OUTPUT" fi - research: - name: Research (Tavily) - runs-on: ubuntu-latest - needs: setup - env: - TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - THEME: ${{ needs.setup.outputs.theme }} - TARGET: ${{ needs.setup.outputs.target }} - MESSAGE: ${{ needs.setup.outputs.message }} - outputs: - research_b64: ${{ steps.collect.outputs.research_b64 }} - steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Research with Tavily - run: | - cat > research.mjs <<'EOF' - import fs from 'fs'; - - const apiKey = process.env.TAVILY_API_KEY; - const theme = process.env.THEME || '大阪マツエク'; - const target = process.env.TARGET || ''; - const message = process.env.MESSAGE || ''; - - const prompt = `${theme} ${target} ${message}`.trim(); - - const res = await fetch('https://api.tavily.com/search', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - api_key: apiKey, - query: prompt, - search_depth: 'advanced', - topic: 'general', - max_results: 5, - include_answer: true - }) - }); - - if (!res.ok) { console.error(await res.text()); process.exit(1); } - - const json = await res.json(); - fs.writeFileSync('research.json', JSON.stringify(json, null, 2)); - console.log('research saved'); - EOF - - node research.mjs - - - name: Collect research - id: collect - run: | - echo "research_b64=$(base64 -w 0 research.json)" >> "$GITHUB_OUTPUT" - - - name: Upload research artifact - uses: actions/upload-artifact@v4 - with: - name: research-json - path: research.json - write: name: Write (Claude) runs-on: ubuntu-latest - needs: [setup, research] + needs: setup env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} THEME: ${{ needs.setup.outputs.theme }} @@ -208,7 +124,6 @@ jobs: MESSAGE: ${{ needs.setup.outputs.message }} CTA: ${{ needs.setup.outputs.cta }} TAGS: ${{ needs.setup.outputs.tags }} - RESEARCH_B64: ${{ needs.research.outputs.research_b64 }} outputs: article_b64: ${{ steps.collect.outputs.article_b64 }} steps: @@ -223,41 +138,32 @@ jobs: import fs from 'fs'; const apiKey = process.env.ANTHROPIC_API_KEY; - const theme = process.env.THEME || ''; - const target = process.env.TARGET || ''; - const message = process.env.MESSAGE || ''; - const cta = process.env.CTA || ''; - const tags = process.env.TAGS || ''; - const research = JSON.parse(Buffer.from(process.env.RESEARCH_B64, 'base64').toString('utf8')); + const theme = process.env.THEME; + const target = process.env.TARGET; + const message = process.env.MESSAGE; + const cta = process.env.CTA; + const tags = process.env.TAGS; const systemPrompt = ` - あなたは日本語のnote記事を書くプロの編集者です。 - MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事を作成してください。 - 出力は必ずJSONのみ。 - 形式: - { - "title": "記事タイトル", - "body": "note本文。見出しや改行を含む", - "tags": ["タグ1","タグ2","タグ3"] - } - `.trim(); +あなたは大阪で15年以上続くアイサロン「SPICE」のオーナーです。 +SEO・MEO・LLMOに強い記事を書いてください。 +出力はJSONのみ。 +`; const userPrompt = ` - 記事テーマ: ${theme} - 想定読者: ${target} - 読者に伝えたい核メッセージ: ${message} - 読後のアクション: ${cta} - 希望タグ: ${tags} - リサーチ結果: ${JSON.stringify(research, null, 2)} - 条件: - - 日本語・読みやすい自然な文章・誇張しすぎない - - 見出しを使う(H2・H3)・最後にCTAを入れる - - tags は配列で3〜5個 - - MEO対策:「大阪」を自然に文中に含める - - LLMO対策:「〜とは」「〜の選び方」など質問形式の見出しを使う - - キーワード「モチ」「早い」「安い」を自然に含める - - 文字数は1500〜2500文字 - `.trim(); +テーマ: ${theme} +読者: ${target} +メッセージ: ${message} +CTA: ${cta} + +条件: +・必ずSPICEを主役にする +・他店紹介は禁止 +・エリア(心斎橋・梅田・なんば・天満橋・野田阪神・なかもず)を自然に入れる +・実体験(1週間持ったなど)を入れる +・「SPICEとは〇〇のサロンです」を入れる +・1500〜2000文字 +`; const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', @@ -274,18 +180,9 @@ jobs: }) }); - if (!res.ok) { console.error(await res.text()); process.exit(1); } - const json = await res.json(); - const text = (json.content?.[0]?.text || '').replace(/```json|```/g, '').trim(); - fs.writeFileSync('article_raw.txt', text); - - let parsed; - try { parsed = JSON.parse(text); } - catch { console.error('Claude output was not valid JSON'); console.error(text); process.exit(1); } - - fs.writeFileSync('article.json', JSON.stringify(parsed, null, 2)); - console.log('article saved'); + const text = json.content[0].text.replace(/```json|```/g, '').trim(); + fs.writeFileSync('article.json', text); EOF node write.mjs @@ -294,286 +191,3 @@ jobs: id: collect run: | echo "article_b64=$(base64 -w 0 article.json)" >> "$GITHUB_OUTPUT" - - - name: Upload article artifact - uses: actions/upload-artifact@v4 - with: - name: article-json - path: | - article.json - article_raw.txt - - factcheck: - name: Fact-check (Tavily) - runs-on: ubuntu-latest - needs: write - env: - TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} - outputs: - final_b64: ${{ steps.collect.outputs.final_b64 }} - steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Fact-check article - run: | - cat > factcheck.mjs <<'EOF' - import fs from 'fs'; - - const apiKey = process.env.TAVILY_API_KEY; - const article = JSON.parse(Buffer.from(process.env.ARTICLE_B64, 'base64').toString('utf8')); - const query = `${article.title}\n\n${article.body.slice(0, 300)}`; - - const res = await fetch('https://api.tavily.com/search', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - api_key: apiKey, - query, - search_depth: 'advanced', - topic: 'general', - max_results: 5, - include_answer: true - }) - }); - - if (!res.ok) { console.error(await res.text()); process.exit(1); } - - const fact = await res.json(); - fs.writeFileSync('final_article.json', JSON.stringify({ ...article, factcheck: fact }, null, 2)); - console.log('final article saved'); - EOF - - node factcheck.mjs - - - name: Collect final article - id: collect - run: | - echo "final_b64=$(base64 -w 0 final_article.json)" >> "$GITHUB_OUTPUT" - - - name: Upload final article artifact - uses: actions/upload-artifact@v4 - with: - name: final-article-json - path: final_article.json - - post: - name: Post to note.com (Playwright) - runs-on: ubuntu-latest - needs: [setup, factcheck] - if: ${{ needs.setup.outputs.dry_run == 'false' }} - env: - NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} - FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} - IS_PUBLIC: ${{ needs.setup.outputs.is_public }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install Playwright - run: | - npm init -y - npm install playwright - npx playwright install --with-deps chromium - - - name: Restore storage state - run: | - cat > note-state.json <<'EOF' - ${{ secrets.NOTE_STORAGE_STATE_JSON }} - EOF - - - name: Post to note - run: | - cat > post.mjs <<'EOF' - import { chromium } from 'playwright'; - - async function main() { - const data = JSON.parse(Buffer.from(process.env.FINAL_B64, 'base64').toString('utf8')); - const statePath = './note-state.json'; - const isPublic = process.env.IS_PUBLIC === 'true'; - - const browser = await chromium.launch({ headless: true }); - const context = await browser.newContext({ - storageState: statePath, - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' - }); - const page = await context.newPage(); - - let step = 0; - const shot = async (label) => { - await page.screenshot({ path: `debug_${String(++step).padStart(2,'0')}_${label}.png`, fullPage: true }); - console.log(`[screenshot] ${label}`); - }; - - try { - console.log('Navigating to editor.note.com/new...'); - await page.goto('https://editor.note.com/new', { waitUntil: 'domcontentloaded' }); - console.log('URL:', page.url()); - - console.log('Waiting 15s for React to render...'); - await page.waitForTimeout(15000); - await shot('01_after_wait'); - - const editableCount = await page.evaluate(() => - document.querySelectorAll('[contenteditable]').length - ); - console.log(`Found ${editableCount} contenteditable elements`); - - if (editableCount === 0) { - const snippet = await page.evaluate(() => document.body.innerHTML.slice(0, 800)); - console.log('HTML snippet:', snippet); - throw new Error(`Editor did not load. URL: ${page.url()}`); - } - - // ── ① タイトル入力 ── - console.log('Filling title...'); - const allEditables = page.locator('[contenteditable]'); - await allEditables.nth(0).click(); - await page.waitForTimeout(300); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Backspace'); - await page.keyboard.type(data.title, { delay: 30 }); - await shot('02_after_title'); - - // ── ② 本文入力 ── - console.log('Filling body...'); - const count = await allEditables.count(); - if (count >= 2) { - await allEditables.nth(1).click(); - } else { - await page.keyboard.press('Tab'); - } - await page.waitForTimeout(300); - await page.keyboard.type(data.body, { delay: 10 }); - await shot('03_after_body'); - await page.waitForTimeout(2000); - - // ── ③ ボタンを全部ログ出力してから保存・公開 ── - const allButtons = await page.evaluate(() => - Array.from(document.querySelectorAll('button')).map(b => b.innerText.trim()).filter(Boolean) - ); - console.log('[all buttons]', JSON.stringify(allButtons)); - await shot('04_before_save'); - - if (isPublic) { - // 公開ボタンを柔軟に探す - const publishPatterns = [ - /公開する/, - /公開設定/, - /投稿する/, - /publish/i, - /post/i - ]; - - let clicked = false; - for (const pattern of publishPatterns) { - const btn = page.getByRole('button', { name: pattern }).first(); - if (await btn.count() > 0) { - console.log(`Clicking publish button: ${pattern}`); - await btn.click(); - clicked = true; - break; - } - } - - if (!clicked) { - // 最後の手段:ボタンのテキストで直接探す - const btn = page.locator('button').filter({ hasText: /公開|投稿|publish/i }).first(); - if (await btn.count() > 0) { - await btn.click(); - clicked = true; - } - } - - if (!clicked) { - throw new Error('Publish button not found. Check screenshots and button list above.'); - } - - await page.waitForTimeout(3000); - await shot('05_after_publish_click'); - - // 確認ダイアログが出る場合 - const confirmPatterns = [/公開する/, /確認/, /はい/, /OK/i]; - for (const pattern of confirmPatterns) { - const btn = page.getByRole('button', { name: pattern }).first(); - if (await btn.count() > 0) { - console.log(`Clicking confirm button: ${pattern}`); - await btn.click(); - break; - } - } - await shot('06_after_confirm'); - - } else { - // 下書き保存 - const draftPatterns = [ - /下書き保存/, - /下書き/, - /保存/, - /save/i - ]; - - let clicked = false; - for (const pattern of draftPatterns) { - const btn = page.getByRole('button', { name: pattern }).first(); - if (await btn.count() > 0) { - console.log(`Clicking draft button: ${pattern}`); - await btn.click(); - clicked = true; - break; - } - } - - if (!clicked) { - const btn = page.locator('button').filter({ hasText: /下書き|保存|save/i }).first(); - if (await btn.count() > 0) { - await btn.click(); - clicked = true; - } - } - - if (!clicked) { - throw new Error('Save button not found. Check screenshots and button list above.'); - } - - await shot('05_after_save'); - } - - await page.waitForTimeout(3000); - console.log('Done.'); - - } catch (err) { - await shot('error'); - console.error('Post failed:', err.message); - throw err; - } finally { - await context.storageState({ path: statePath }); - await browser.close(); - } - } - - main(); - EOF - - node post.mjs - - - name: Upload debug screenshots - if: always() - uses: actions/upload-artifact@v4 - with: - name: debug-screenshots - path: debug_*.png - - - name: Upload updated state - uses: actions/upload-artifact@v4 - with: - name: updated-note-state - path: note-state.json From 5814cb3e9e5d8c321c2cb75ca15c23c09794f060 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 30 Mar 2026 15:29:06 +0900 Subject: [PATCH 47/50] Update note.yaml --- .github/workflows/note.yaml | 462 +++++++++++++++++++++++++++++++++--- 1 file changed, 424 insertions(+), 38 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index ad52e87..4b5c687 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -77,33 +77,55 @@ jobs: DAY=$(date +%u) case $DAY in 1) - THEME="心斎橋 マツエク SPICE LEDエクステ 持ちがいい" + THEME="大阪マツエク モチがいい サロン" + TARGET="大阪でマツエクを探している20〜40代女性" + MESSAGE="大阪でモチのいいマツエクを見つけるポイントと、おすすめサロンの選び方" + CTA="LINEで無料カウンセリングを予約する" + TAGS="大阪マツエク,マツエク,まつげエクステ,大阪美容,モチいいマツエク" ;; 2) - THEME="梅田 マツエク SPICE 早い キレイ" + THEME="大阪まつげ パーマ 持ちがいい" + TARGET="まつげパーマを初めて検討している大阪在住の女性" + MESSAGE="まつげパーマは施術が早く、毎朝のメイク時間を大幅に短縮できる" + CTA="まつげパーマの料金・メニューをLINEで確認する" + TAGS="まつげパーマ,大阪まつげ,大阪美容,パリジェンヌ,まつ毛カール" ;; 3) - THEME="なんば マツエク SPICE LED 安全" + THEME="LEDマツエク 大阪 安全 モチ" + TARGET="オーガニック・安全志向の大阪在住女性" + MESSAGE="LEDマツエクは従来より硬化時間が短く目への負担が少ない次世代まつげエクステ" + CTA="LEDマツエクの詳細・予約はこちら" + TAGS="LEDマツエク,大阪マツエク,まつげエクステ,最新マツエク,安全マツエク" ;; 4) - THEME="天満橋 マツエク SPICE 持ちがいい" + THEME="大阪 眉毛 アイブロウ 美眉 サロン" + TARGET="眉毛の形・薄さに悩む大阪在住の女性" + MESSAGE="眉毛を整えるだけで顔の印象が変わる。大阪でおすすめの美眉サロンと選び方" + CTA="眉毛デザインの無料相談はLINEから" + TAGS="アイブロウ,眉毛サロン,大阪眉毛,美眉,眉デザイン" ;; 5) - THEME="野田阪神 マツエク SPICE コスパ" + THEME="大阪マツエク 安い 高品質 コスパ" + TARGET="マツエクのコスパを重視する大阪在住の女性" + MESSAGE="安いだけでなく品質も大事。大阪でコスパ最強のマツエクサロンの見つけ方" + CTA="お得なクーポン・料金をLINEで確認する" + TAGS="大阪マツエク,マツエク安い,コスパマツエク,大阪美容,まつげエクステ" ;; 6) - THEME="なかもず マツエク SPICE 当日予約" + THEME="まつげエクステ 大阪 早い 当日予約" + TARGET="忙しくて時間がない大阪在住の働く女性" + MESSAGE="施術時間60分以内・当日予約OKのマツエクで、忙しい女性でも通いやすい" + CTA="当日予約はLINEから今すぐ確認" + TAGS="大阪マツエク,当日予約,時短マツエク,まつげエクステ,大阪美容" ;; 7) - THEME="大阪 マツエク SPICE LED フラット 比較" + THEME="大阪マツエク まつげパーマ 眉毛 まとめ 比較" + TARGET="マツエク・まつパ・眉毛どれにするか迷っている大阪在住女性" + MESSAGE="マツエク・まつげパーマ・眉毛サロンそれぞれの特徴と向いている人を比較" + CTA="どれが自分に合うかLINEで相談する" + TAGS="大阪マツエク,まつげパーマ,アイブロウ,大阪美容,まつげ比較" ;; esac - - TARGET="忙しい20〜30代女性でマツエクの持ちや仕上がりに悩んでいる人" - MESSAGE="SPICEが選ばれる理由とLEDマツエクの持ちの良さ・仕上がりについて" - CTA="LINEで予約・無料カウンセリングはこちら" - TAGS="SPICE,大阪マツエク,心斎橋マツエク,梅田マツエク,LEDマツエク" - echo "theme=$THEME" >> "$GITHUB_OUTPUT" echo "target=$TARGET" >> "$GITHUB_OUTPUT" echo "message=$MESSAGE" >> "$GITHUB_OUTPUT" @@ -113,10 +135,72 @@ jobs: echo "dry_run=false" >> "$GITHUB_OUTPUT" fi + research: + name: Research (Tavily) + runs-on: ubuntu-latest + needs: setup + env: + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} + THEME: ${{ needs.setup.outputs.theme }} + TARGET: ${{ needs.setup.outputs.target }} + MESSAGE: ${{ needs.setup.outputs.message }} + outputs: + research_b64: ${{ steps.collect.outputs.research_b64 }} + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Research with Tavily + run: | + cat > research.mjs <<'EOF' + import fs from 'fs'; + + const apiKey = process.env.TAVILY_API_KEY; + const theme = process.env.THEME || '大阪マツエク'; + const target = process.env.TARGET || ''; + const message = process.env.MESSAGE || ''; + + const prompt = `${theme} ${target} ${message}`.trim(); + + const res = await fetch('https://api.tavily.com/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + api_key: apiKey, + query: prompt, + search_depth: 'advanced', + topic: 'general', + max_results: 5, + include_answer: true + }) + }); + + if (!res.ok) { console.error(await res.text()); process.exit(1); } + + const json = await res.json(); + fs.writeFileSync('research.json', JSON.stringify(json, null, 2)); + console.log('research saved'); + EOF + + node research.mjs + + - name: Collect research + id: collect + run: | + echo "research_b64=$(base64 -w 0 research.json)" >> "$GITHUB_OUTPUT" + + - name: Upload research artifact + uses: actions/upload-artifact@v4 + with: + name: research-json + path: research.json + write: name: Write (Claude) runs-on: ubuntu-latest - needs: setup + needs: [setup, research] env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} THEME: ${{ needs.setup.outputs.theme }} @@ -124,6 +208,7 @@ jobs: MESSAGE: ${{ needs.setup.outputs.message }} CTA: ${{ needs.setup.outputs.cta }} TAGS: ${{ needs.setup.outputs.tags }} + RESEARCH_B64: ${{ needs.research.outputs.research_b64 }} outputs: article_b64: ${{ steps.collect.outputs.article_b64 }} steps: @@ -138,32 +223,41 @@ jobs: import fs from 'fs'; const apiKey = process.env.ANTHROPIC_API_KEY; - const theme = process.env.THEME; - const target = process.env.TARGET; - const message = process.env.MESSAGE; - const cta = process.env.CTA; - const tags = process.env.TAGS; + const theme = process.env.THEME || ''; + const target = process.env.TARGET || ''; + const message = process.env.MESSAGE || ''; + const cta = process.env.CTA || ''; + const tags = process.env.TAGS || ''; + const research = JSON.parse(Buffer.from(process.env.RESEARCH_B64, 'base64').toString('utf8')); const systemPrompt = ` -あなたは大阪で15年以上続くアイサロン「SPICE」のオーナーです。 -SEO・MEO・LLMOに強い記事を書いてください。 -出力はJSONのみ。 -`; + あなたは日本語のnote記事を書くプロの編集者です。 + MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事を作成してください。 + 出力は必ずJSONのみ。 + 形式: + { + "title": "記事タイトル", + "body": "note本文。見出しや改行を含む", + "tags": ["タグ1","タグ2","タグ3"] + } + `.trim(); const userPrompt = ` -テーマ: ${theme} -読者: ${target} -メッセージ: ${message} -CTA: ${cta} - -条件: -・必ずSPICEを主役にする -・他店紹介は禁止 -・エリア(心斎橋・梅田・なんば・天満橋・野田阪神・なかもず)を自然に入れる -・実体験(1週間持ったなど)を入れる -・「SPICEとは〇〇のサロンです」を入れる -・1500〜2000文字 -`; + 記事テーマ: ${theme} + 想定読者: ${target} + 読者に伝えたい核メッセージ: ${message} + 読後のアクション: ${cta} + 希望タグ: ${tags} + リサーチ結果: ${JSON.stringify(research, null, 2)} + 条件: + - 日本語・読みやすい自然な文章・誇張しすぎない + - 見出しを使う(H2・H3)・最後にCTAを入れる + - tags は配列で3〜5個 + - MEO対策:「大阪」を自然に文中に含める + - LLMO対策:「〜とは」「〜の選び方」など質問形式の見出しを使う + - キーワード「モチ」「早い」「安い」を自然に含める + - 文字数は1500〜2500文字 + `.trim(); const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', @@ -180,9 +274,18 @@ CTA: ${cta} }) }); + if (!res.ok) { console.error(await res.text()); process.exit(1); } + const json = await res.json(); - const text = json.content[0].text.replace(/```json|```/g, '').trim(); - fs.writeFileSync('article.json', text); + const text = (json.content?.[0]?.text || '').replace(/```json|```/g, '').trim(); + fs.writeFileSync('article_raw.txt', text); + + let parsed; + try { parsed = JSON.parse(text); } + catch { console.error('Claude output was not valid JSON'); console.error(text); process.exit(1); } + + fs.writeFileSync('article.json', JSON.stringify(parsed, null, 2)); + console.log('article saved'); EOF node write.mjs @@ -191,3 +294,286 @@ CTA: ${cta} id: collect run: | echo "article_b64=$(base64 -w 0 article.json)" >> "$GITHUB_OUTPUT" + + - name: Upload article artifact + uses: actions/upload-artifact@v4 + with: + name: article-json + path: | + article.json + article_raw.txt + + factcheck: + name: Fact-check (Tavily) + runs-on: ubuntu-latest + needs: write + env: + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} + ARTICLE_B64: ${{ needs.write.outputs.article_b64 }} + outputs: + final_b64: ${{ steps.collect.outputs.final_b64 }} + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Fact-check article + run: | + cat > factcheck.mjs <<'EOF' + import fs from 'fs'; + + const apiKey = process.env.TAVILY_API_KEY; + const article = JSON.parse(Buffer.from(process.env.ARTICLE_B64, 'base64').toString('utf8')); + const query = `${article.title}\n\n${article.body.slice(0, 300)}`; + + const res = await fetch('https://api.tavily.com/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + api_key: apiKey, + query, + search_depth: 'advanced', + topic: 'general', + max_results: 5, + include_answer: true + }) + }); + + if (!res.ok) { console.error(await res.text()); process.exit(1); } + + const fact = await res.json(); + fs.writeFileSync('final_article.json', JSON.stringify({ ...article, factcheck: fact }, null, 2)); + console.log('final article saved'); + EOF + + node factcheck.mjs + + - name: Collect final article + id: collect + run: | + echo "final_b64=$(base64 -w 0 final_article.json)" >> "$GITHUB_OUTPUT" + + - name: Upload final article artifact + uses: actions/upload-artifact@v4 + with: + name: final-article-json + path: final_article.json + + post: + name: Post to note.com (Playwright) + runs-on: ubuntu-latest + needs: [setup, factcheck] + if: ${{ needs.setup.outputs.dry_run == 'false' }} + env: + NOTE_STORAGE_STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} + FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} + IS_PUBLIC: ${{ needs.setup.outputs.is_public }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Playwright + run: | + npm init -y + npm install playwright + npx playwright install --with-deps chromium + + - name: Restore storage state + run: | + cat > note-state.json <<'EOF' + ${{ secrets.NOTE_STORAGE_STATE_JSON }} + EOF + + - name: Post to note + run: | + cat > post.mjs <<'EOF' + import { chromium } from 'playwright'; + + async function main() { + const data = JSON.parse(Buffer.from(process.env.FINAL_B64, 'base64').toString('utf8')); + const statePath = './note-state.json'; + const isPublic = process.env.IS_PUBLIC === 'true'; + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + storageState: statePath, + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }); + const page = await context.newPage(); + + let step = 0; + const shot = async (label) => { + await page.screenshot({ path: `debug_${String(++step).padStart(2,'0')}_${label}.png`, fullPage: true }); + console.log(`[screenshot] ${label}`); + }; + + try { + console.log('Navigating to editor.note.com/new...'); + await page.goto('https://editor.note.com/new', { waitUntil: 'domcontentloaded' }); + console.log('URL:', page.url()); + + console.log('Waiting 15s for React to render...'); + await page.waitForTimeout(15000); + await shot('01_after_wait'); + + const editableCount = await page.evaluate(() => + document.querySelectorAll('[contenteditable]').length + ); + console.log(`Found ${editableCount} contenteditable elements`); + + if (editableCount === 0) { + const snippet = await page.evaluate(() => document.body.innerHTML.slice(0, 800)); + console.log('HTML snippet:', snippet); + throw new Error(`Editor did not load. URL: ${page.url()}`); + } + + // ── ① タイトル入力 ── + console.log('Filling title...'); + const allEditables = page.locator('[contenteditable]'); + await allEditables.nth(0).click(); + await page.waitForTimeout(300); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Backspace'); + await page.keyboard.type(data.title, { delay: 30 }); + await shot('02_after_title'); + + // ── ② 本文入力 ── + console.log('Filling body...'); + const count = await allEditables.count(); + if (count >= 2) { + await allEditables.nth(1).click(); + } else { + await page.keyboard.press('Tab'); + } + await page.waitForTimeout(300); + await page.keyboard.type(data.body, { delay: 10 }); + await shot('03_after_body'); + await page.waitForTimeout(2000); + + // ── ③ ボタンを全部ログ出力してから保存・公開 ── + const allButtons = await page.evaluate(() => + Array.from(document.querySelectorAll('button')).map(b => b.innerText.trim()).filter(Boolean) + ); + console.log('[all buttons]', JSON.stringify(allButtons)); + await shot('04_before_save'); + + if (isPublic) { + // 公開ボタンを柔軟に探す + const publishPatterns = [ + /公開する/, + /公開設定/, + /投稿する/, + /publish/i, + /post/i + ]; + + let clicked = false; + for (const pattern of publishPatterns) { + const btn = page.getByRole('button', { name: pattern }).first(); + if (await btn.count() > 0) { + console.log(`Clicking publish button: ${pattern}`); + await btn.click(); + clicked = true; + break; + } + } + + if (!clicked) { + // 最後の手段:ボタンのテキストで直接探す + const btn = page.locator('button').filter({ hasText: /公開|投稿|publish/i }).first(); + if (await btn.count() > 0) { + await btn.click(); + clicked = true; + } + } + + if (!clicked) { + throw new Error('Publish button not found. Check screenshots and button list above.'); + } + + await page.waitForTimeout(3000); + await shot('05_after_publish_click'); + + // 確認ダイアログが出る場合 + const confirmPatterns = [/公開する/, /確認/, /はい/, /OK/i]; + for (const pattern of confirmPatterns) { + const btn = page.getByRole('button', { name: pattern }).first(); + if (await btn.count() > 0) { + console.log(`Clicking confirm button: ${pattern}`); + await btn.click(); + break; + } + } + await shot('06_after_confirm'); + + } else { + // 下書き保存 + const draftPatterns = [ + /下書き保存/, + /下書き/, + /保存/, + /save/i + ]; + + let clicked = false; + for (const pattern of draftPatterns) { + const btn = page.getByRole('button', { name: pattern }).first(); + if (await btn.count() > 0) { + console.log(`Clicking draft button: ${pattern}`); + await btn.click(); + clicked = true; + break; + } + } + + if (!clicked) { + const btn = page.locator('button').filter({ hasText: /下書き|保存|save/i }).first(); + if (await btn.count() > 0) { + await btn.click(); + clicked = true; + } + } + + if (!clicked) { + throw new Error('Save button not found. Check screenshots and button list above.'); + } + + await shot('05_after_save'); + } + + await page.waitForTimeout(3000); + console.log('Done.'); + + } catch (err) { + await shot('error'); + console.error('Post failed:', err.message); + throw err; + } finally { + await context.storageState({ path: statePath }); + await browser.close(); + } + } + + main(); + EOF + + node post.mjs + + - name: Upload debug screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: debug-screenshots + path: debug_*.png + + - name: Upload updated state + uses: actions/upload-artifact@v4 + with: + name: updated-note-state + path: note-state.json From fc1a0527bbff3e3152afd4afc3bff6d60402d84f Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Mon, 30 Mar 2026 15:48:43 +0900 Subject: [PATCH 48/50] Update note.yaml --- .github/workflows/note.yaml | 87 +++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 4b5c687..14d6191 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -77,53 +77,53 @@ jobs: DAY=$(date +%u) case $DAY in 1) - THEME="大阪マツエク モチがいい サロン" - TARGET="大阪でマツエクを探している20〜40代女性" - MESSAGE="大阪でモチのいいマツエクを見つけるポイントと、おすすめサロンの選び方" - CTA="LINEで無料カウンセリングを予約する" - TAGS="大阪マツエク,マツエク,まつげエクステ,大阪美容,モチいいマツエク" + THEME="まつ毛エクステ SPICE 大阪 モチがいい" + TARGET="大阪でまつ毛エクステを探している20〜40代の女性" + MESSAGE="大阪のまつ毛エクステ専門サロンSPICEは、モチのよさと仕上がりの美しさで人気。自分に合ったエクステの選び方も丁寧に提案してもらえる" + CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" + TAGS="まつ毛エクステ,大阪マツエク,SPICE,モチいいマツエク,まつげエクステ大阪" ;; 2) - THEME="大阪まつげ パーマ 持ちがいい" - TARGET="まつげパーマを初めて検討している大阪在住の女性" - MESSAGE="まつげパーマは施術が早く、毎朝のメイク時間を大幅に短縮できる" - CTA="まつげパーマの料金・メニューをLINEで確認する" - TAGS="まつげパーマ,大阪まつげ,大阪美容,パリジェンヌ,まつ毛カール" + THEME="まつ毛パーマ SPICE 大阪 持ちがいい 時短" + TARGET="まつ毛パーマを初めて検討している大阪在住の働く女性" + MESSAGE="SPICEのまつ毛パーマは施術が早くて持ちもよく、毎朝のメイク時間を大幅に短縮できる。初めての方でも安心して受けられる" + CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" + TAGS="まつ毛パーマ,大阪まつ毛,SPICE,時短メイク,まつ毛カール大阪" ;; 3) - THEME="LEDマツエク 大阪 安全 モチ" - TARGET="オーガニック・安全志向の大阪在住女性" - MESSAGE="LEDマツエクは従来より硬化時間が短く目への負担が少ない次世代まつげエクステ" - CTA="LEDマツエクの詳細・予約はこちら" - TAGS="LEDマツエク,大阪マツエク,まつげエクステ,最新マツエク,安全マツエク" + THEME="SPICE まつ毛エクステ 大阪 安全 オーガニック" + TARGET="目元の安全性や素材にこだわる大阪在住の女性" + MESSAGE="SPICEでは目元への負担を最小限に抑えた安全な施術を提供。敏感な方でも安心して通えるまつ毛エクステ専門サロン" + CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" + TAGS="まつ毛エクステ大阪,SPICE,安全マツエク,大阪美容,敏感肌マツエク" ;; 4) - THEME="大阪 眉毛 アイブロウ 美眉 サロン" - TARGET="眉毛の形・薄さに悩む大阪在住の女性" - MESSAGE="眉毛を整えるだけで顔の印象が変わる。大阪でおすすめの美眉サロンと選び方" - CTA="眉毛デザインの無料相談はLINEから" - TAGS="アイブロウ,眉毛サロン,大阪眉毛,美眉,眉デザイン" + THEME="SPICE 大阪 眉毛 アイブロウ 美眉デザイン" + TARGET="眉毛の形・薄さ・左右差に悩む大阪在住の女性" + MESSAGE="SPICEではまつ毛エクステに加えて眉毛デザインも対応。顔のバランスに合わせた美眉を提案し、目元全体の印象を格上げできる" + CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" + TAGS="アイブロウ大阪,眉毛サロン,SPICE,美眉デザイン,大阪眉毛" ;; 5) - THEME="大阪マツエク 安い 高品質 コスパ" - TARGET="マツエクのコスパを重視する大阪在住の女性" - MESSAGE="安いだけでなく品質も大事。大阪でコスパ最強のマツエクサロンの見つけ方" - CTA="お得なクーポン・料金をLINEで確認する" - TAGS="大阪マツエク,マツエク安い,コスパマツエク,大阪美容,まつげエクステ" + THEME="まつ毛エクステ SPICE 大阪 コスパ 安い 高品質" + TARGET="まつ毛エクステのコスパを重視する大阪在住の女性" + MESSAGE="SPICEは高品質な施術をリーズナブルな価格で提供。安いだけでなく仕上がりと持ちにもこだわる、大阪でコスパ最強のまつ毛エクステサロン" + CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" + TAGS="まつ毛エクステ安い,大阪マツエク,SPICE,コスパマツエク,まつげエクステ大阪" ;; 6) - THEME="まつげエクステ 大阪 早い 当日予約" - TARGET="忙しくて時間がない大阪在住の働く女性" - MESSAGE="施術時間60分以内・当日予約OKのマツエクで、忙しい女性でも通いやすい" - CTA="当日予約はLINEから今すぐ確認" - TAGS="大阪マツエク,当日予約,時短マツエク,まつげエクステ,大阪美容" + THEME="SPICE まつ毛エクステ 大阪 当日予約 早い" + TARGET="忙しくてなかなか予約が取れない大阪在住の女性" + MESSAGE="SPICEは当日予約にも対応。施術も早くて丁寧なので、忙しい女性でも気軽に通えるまつ毛エクステ専門サロン" + CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" + TAGS="当日予約マツエク,大阪マツエク,SPICE,時短まつ毛,まつ毛エクステ大阪" ;; 7) - THEME="大阪マツエク まつげパーマ 眉毛 まとめ 比較" - TARGET="マツエク・まつパ・眉毛どれにするか迷っている大阪在住女性" - MESSAGE="マツエク・まつげパーマ・眉毛サロンそれぞれの特徴と向いている人を比較" - CTA="どれが自分に合うかLINEで相談する" - TAGS="大阪マツエク,まつげパーマ,アイブロウ,大阪美容,まつげ比較" + THEME="SPICE まつ毛エクステ まつ毛パーマ 眉毛 大阪 比較 選び方" + TARGET="まつ毛エクステ・まつ毛パーマ・眉毛どれにするか迷っている大阪在住の女性" + MESSAGE="SPICEではまつ毛エクステ・まつ毛パーマ・眉毛デザインをすべて対応。それぞれの特徴と自分に向いているメニューをプロが丁寧に案内してくれる" + CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" + TAGS="大阪マツエク,まつ毛パーマ,SPICE,アイブロウ大阪,まつ毛エクステ比較" ;; esac echo "theme=$THEME" >> "$GITHUB_OUTPUT" @@ -158,7 +158,7 @@ jobs: import fs from 'fs'; const apiKey = process.env.TAVILY_API_KEY; - const theme = process.env.THEME || '大阪マツエク'; + const theme = process.env.THEME || 'まつ毛エクステ SPICE 大阪'; const target = process.env.TARGET || ''; const message = process.env.MESSAGE || ''; @@ -232,7 +232,9 @@ jobs: const systemPrompt = ` あなたは日本語のnote記事を書くプロの編集者です。 + 大阪にある「まつ毛エクステ専門サロン SPICE」の集客を目的としたnote記事を作成してください。 MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事を作成してください。 + サロン名「SPICE」を記事内に自然に盛り込み、読者が予約・問い合わせをしたくなる内容にしてください。 出力は必ずJSONのみ。 形式: { @@ -251,12 +253,15 @@ jobs: リサーチ結果: ${JSON.stringify(research, null, 2)} 条件: - 日本語・読みやすい自然な文章・誇張しすぎない + - サロン名「SPICE」を冒頭・本文・締めくくりに自然に含める - 見出しを使う(H2・H3)・最後にCTAを入れる - tags は配列で3〜5個 - - MEO対策:「大阪」を自然に文中に含める - - LLMO対策:「〜とは」「〜の選び方」など質問形式の見出しを使う - - キーワード「モチ」「早い」「安い」を自然に含める + - MEO対策:「大阪」「SPICE」を自然に文中に含める + - LLMO対策:「〜とは」「〜の選び方」「〜のポイント」など質問形式・解説形式の見出しを使う + - キーワード「モチ」「早い」「安い」「コスパ」を自然に含める - 文字数は1500〜2500文字 + - 記事末尾のCTAは「Instagramをフォローしてプロフィールのリットリンクから予約」という流れで締めくくる + - SPICEの公式Instagramアカウント @spice_eyelash_ を明記する `.trim(); const res = await fetch('https://api.anthropic.com/v1/messages', { @@ -464,7 +469,6 @@ jobs: await shot('04_before_save'); if (isPublic) { - // 公開ボタンを柔軟に探す const publishPatterns = [ /公開する/, /公開設定/, @@ -485,7 +489,6 @@ jobs: } if (!clicked) { - // 最後の手段:ボタンのテキストで直接探す const btn = page.locator('button').filter({ hasText: /公開|投稿|publish/i }).first(); if (await btn.count() > 0) { await btn.click(); @@ -500,7 +503,6 @@ jobs: await page.waitForTimeout(3000); await shot('05_after_publish_click'); - // 確認ダイアログが出る場合 const confirmPatterns = [/公開する/, /確認/, /はい/, /OK/i]; for (const pattern of confirmPatterns) { const btn = page.getByRole('button', { name: pattern }).first(); @@ -513,7 +515,6 @@ jobs: await shot('06_after_confirm'); } else { - // 下書き保存 const draftPatterns = [ /下書き保存/, /下書き/, From 727644b6a769afffd2bf50abd7d2835235606286 Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Tue, 14 Apr 2026 12:15:24 +0900 Subject: [PATCH 49/50] Update themes and messages for note.yaml --- .github/workflows/note.yaml | 110 +++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 14d6191..66e326c 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -77,53 +77,53 @@ jobs: DAY=$(date +%u) case $DAY in 1) - THEME="まつ毛エクステ SPICE 大阪 モチがいい" + THEME="まつ毛エクステ マツエクspice 大阪 モチがいい" TARGET="大阪でまつ毛エクステを探している20〜40代の女性" - MESSAGE="大阪のまつ毛エクステ専門サロンSPICEは、モチのよさと仕上がりの美しさで人気。自分に合ったエクステの選び方も丁寧に提案してもらえる" + MESSAGE="大阪のまつ毛エクステ専門サロンマツエクspiceは、モチのよさと仕上がりの美しさで人気。自分に合ったエクステの選び方も丁寧に提案してもらえる" CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" - TAGS="まつ毛エクステ,大阪マツエク,SPICE,モチいいマツエク,まつげエクステ大阪" + TAGS="まつ毛エクステ,大阪マツエク,マツエクspice,モチいいマツエク,まつげエクステ大阪" ;; 2) - THEME="まつ毛パーマ SPICE 大阪 持ちがいい 時短" + THEME="まつ毛パーマ マツエクspice 大阪 持ちがいい 時短" TARGET="まつ毛パーマを初めて検討している大阪在住の働く女性" - MESSAGE="SPICEのまつ毛パーマは施術が早くて持ちもよく、毎朝のメイク時間を大幅に短縮できる。初めての方でも安心して受けられる" + MESSAGE="マツエクspiceのまつ毛パーマは施術が早くて持ちもよく、毎朝のメイク時間を大幅に短縮できる。初めての方でも安心して受けられる" CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" - TAGS="まつ毛パーマ,大阪まつ毛,SPICE,時短メイク,まつ毛カール大阪" + TAGS="まつ毛パーマ,大阪まつ毛,マツエクspice,時短メイク,まつ毛カール大阪" ;; 3) - THEME="SPICE まつ毛エクステ 大阪 安全 オーガニック" + THEME="マツエクspice まつ毛エクステ 大阪 安全 オーガニック" TARGET="目元の安全性や素材にこだわる大阪在住の女性" - MESSAGE="SPICEでは目元への負担を最小限に抑えた安全な施術を提供。敏感な方でも安心して通えるまつ毛エクステ専門サロン" + MESSAGE="マツエクspiceでは目元への負担を最小限に抑えた安全な施術を提供。敏感な方でも安心して通えるまつ毛エクステ専門サロン" CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" - TAGS="まつ毛エクステ大阪,SPICE,安全マツエク,大阪美容,敏感肌マツエク" + TAGS="まつ毛エクステ大阪,マツエクspice,安全マツエク,大阪美容,敏感肌マツエク" ;; 4) - THEME="SPICE 大阪 眉毛 アイブロウ 美眉デザイン" + THEME="マツエクspice 大阪 眉毛 アイブロウ 美眉デザイン" TARGET="眉毛の形・薄さ・左右差に悩む大阪在住の女性" - MESSAGE="SPICEではまつ毛エクステに加えて眉毛デザインも対応。顔のバランスに合わせた美眉を提案し、目元全体の印象を格上げできる" + MESSAGE="マツエクspiceではまつ毛エクステに加えて眉毛デザインも対応。顔のバランスに合わせた美眉を提案し、目元全体の印象を格上げできる" CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" - TAGS="アイブロウ大阪,眉毛サロン,SPICE,美眉デザイン,大阪眉毛" + TAGS="アイブロウ大阪,眉毛サロン,マツエクspice,美眉デザイン,大阪眉毛" ;; 5) - THEME="まつ毛エクステ SPICE 大阪 コスパ 安い 高品質" + THEME="まつ毛エクステ マツエクspice 大阪 コスパ 安い 高品質" TARGET="まつ毛エクステのコスパを重視する大阪在住の女性" - MESSAGE="SPICEは高品質な施術をリーズナブルな価格で提供。安いだけでなく仕上がりと持ちにもこだわる、大阪でコスパ最強のまつ毛エクステサロン" + MESSAGE="マツエクspiceは高品質な施術をリーズナブルな価格で提供。安いだけでなく仕上がりと持ちにもこだわる、大阪でコスパ最強のまつ毛エクステサロン" CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" - TAGS="まつ毛エクステ安い,大阪マツエク,SPICE,コスパマツエク,まつげエクステ大阪" + TAGS="まつ毛エクステ安い,大阪マツエク,マツエクspice,コスパマツエク,まつげエクステ大阪" ;; 6) - THEME="SPICE まつ毛エクステ 大阪 当日予約 早い" + THEME="マツエクspice まつ毛エクステ 大阪 当日予約 早い" TARGET="忙しくてなかなか予約が取れない大阪在住の女性" - MESSAGE="SPICEは当日予約にも対応。施術も早くて丁寧なので、忙しい女性でも気軽に通えるまつ毛エクステ専門サロン" + MESSAGE="マツエクspiceは当日予約にも対応。施術も早くて丁寧なので、忙しい女性でも気軽に通えるまつ毛エクステ専門サロン" CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" - TAGS="当日予約マツエク,大阪マツエク,SPICE,時短まつ毛,まつ毛エクステ大阪" + TAGS="当日予約マツエク,大阪マツエク,マツエクspice,時短まつ毛,まつ毛エクステ大阪" ;; 7) - THEME="SPICE まつ毛エクステ まつ毛パーマ 眉毛 大阪 比較 選び方" + THEME="マツエクspice まつ毛エクステ まつ毛パーマ 眉毛 大阪 比較 選び方" TARGET="まつ毛エクステ・まつ毛パーマ・眉毛どれにするか迷っている大阪在住の女性" - MESSAGE="SPICEではまつ毛エクステ・まつ毛パーマ・眉毛デザインをすべて対応。それぞれの特徴と自分に向いているメニューをプロが丁寧に案内してくれる" + MESSAGE="マツエクspiceではまつ毛エクステ・まつ毛パーマ・眉毛デザインをすべて対応。それぞれの特徴と自分に向いているメニューをプロが丁寧に案内してくれる" CTA="ご予約はInstagramプロフィールのリンクから👇 @spice_eyelash_ をフォローして、リットリンクより簡単予約!" - TAGS="大阪マツエク,まつ毛パーマ,SPICE,アイブロウ大阪,まつ毛エクステ比較" + TAGS="大阪マツエク,まつ毛パーマ,マツエクspice,アイブロウ大阪,まつ毛エクステ比較" ;; esac echo "theme=$THEME" >> "$GITHUB_OUTPUT" @@ -158,7 +158,7 @@ jobs: import fs from 'fs'; const apiKey = process.env.TAVILY_API_KEY; - const theme = process.env.THEME || 'まつ毛エクステ SPICE 大阪'; + const theme = process.env.THEME || 'まつ毛エクステ マツエクspice 大阪'; const target = process.env.TARGET || ''; const message = process.env.MESSAGE || ''; @@ -232,9 +232,9 @@ jobs: const systemPrompt = ` あなたは日本語のnote記事を書くプロの編集者です。 - 大阪にある「まつ毛エクステ専門サロン SPICE」の集客を目的としたnote記事を作成してください。 + 大阪にある「まつ毛エクステ専門サロン マツエクspice」の集客を目的としたnote記事を作成してください。 MEO(地域検索最適化)とLLMO(AI検索最適化)を意識した記事を作成してください。 - サロン名「SPICE」を記事内に自然に盛り込み、読者が予約・問い合わせをしたくなる内容にしてください。 + サロン名「マツエクspice」を記事内に自然に盛り込み、読者が予約・問い合わせをしたくなる内容にしてください。 出力は必ずJSONのみ。 形式: { @@ -253,15 +253,15 @@ jobs: リサーチ結果: ${JSON.stringify(research, null, 2)} 条件: - 日本語・読みやすい自然な文章・誇張しすぎない - - サロン名「SPICE」を冒頭・本文・締めくくりに自然に含める + - サロン名「マツエクspice」を冒頭・本文・締めくくりに自然に含める - 見出しを使う(H2・H3)・最後にCTAを入れる - tags は配列で3〜5個 - - MEO対策:「大阪」「SPICE」を自然に文中に含める + - MEO対策:「大阪」「マツエクspice」を自然に文中に含める - LLMO対策:「〜とは」「〜の選び方」「〜のポイント」など質問形式・解説形式の見出しを使う - キーワード「モチ」「早い」「安い」「コスパ」を自然に含める - 文字数は1500〜2500文字 - 記事末尾のCTAは「Instagramをフォローしてプロフィールのリットリンクから予約」という流れで締めくくる - - SPICEの公式Instagramアカウント @spice_eyelash_ を明記する + - マツエクspiceの公式Instagramアカウント @spice_eyelash_ を明記する `.trim(); const res = await fetch('https://api.anthropic.com/v1/messages', { @@ -418,6 +418,40 @@ jobs: console.log(`[screenshot] ${label}`); }; + // ── モーダルを閉じるヘルパー関数 ── + const dismissModal = async () => { + try { + const modal = page.locator('div.ReactModalPortal'); + const visible = await modal.isVisible().catch(() => false); + if (visible) { + console.log('Modal detected, dismissing...'); + // まずESCキーで閉じる + await page.keyboard.press('Escape'); + await page.waitForTimeout(1000); + // ESCで閉じない場合は閉じるボタンを探す + const closePatterns = [/閉じる/, /キャンセル/, /スキップ/, /後で/, /close/i, /skip/i, /cancel/i]; + for (const pattern of closePatterns) { + const btn = modal.getByRole('button', { name: pattern }).first(); + if (await btn.count() > 0) { + console.log(`Closing modal with button: ${pattern}`); + await btn.click(); + await page.waitForTimeout(500); + break; + } + } + // それでも残っていたら最初のボタンを押す + const firstBtn = modal.locator('button').last(); + if (await firstBtn.count() > 0 && await modal.isVisible().catch(() => false)) { + console.log('Clicking last button in modal as fallback'); + await firstBtn.click(); + await page.waitForTimeout(500); + } + } + } catch (e) { + console.log('dismissModal skipped:', e.message); + } + }; + try { console.log('Navigating to editor.note.com/new...'); await page.goto('https://editor.note.com/new', { waitUntil: 'domcontentloaded' }); @@ -427,6 +461,10 @@ jobs: await page.waitForTimeout(15000); await shot('01_after_wait'); + // ── ★ モーダルを閉じる(ページ読み込み直後) ── + await dismissModal(); + await shot('02_after_dismiss_modal'); + const editableCount = await page.evaluate(() => document.querySelectorAll('[contenteditable]').length ); @@ -446,7 +484,7 @@ jobs: await page.keyboard.press('Control+A'); await page.keyboard.press('Backspace'); await page.keyboard.type(data.title, { delay: 30 }); - await shot('02_after_title'); + await shot('03_after_title'); // ── ② 本文入力 ── console.log('Filling body...'); @@ -458,7 +496,7 @@ jobs: } await page.waitForTimeout(300); await page.keyboard.type(data.body, { delay: 10 }); - await shot('03_after_body'); + await shot('04_after_body'); await page.waitForTimeout(2000); // ── ③ ボタンを全部ログ出力してから保存・公開 ── @@ -466,7 +504,10 @@ jobs: Array.from(document.querySelectorAll('button')).map(b => b.innerText.trim()).filter(Boolean) ); console.log('[all buttons]', JSON.stringify(allButtons)); - await shot('04_before_save'); + + // ── ★ モーダルを閉じる(保存・公開ボタン押す前) ── + await dismissModal(); + await shot('05_before_save'); if (isPublic) { const publishPatterns = [ @@ -501,7 +542,10 @@ jobs: } await page.waitForTimeout(3000); - await shot('05_after_publish_click'); + await shot('06_after_publish_click'); + + // ── ★ モーダルを閉じる(確認ダイアログが別のモーダルの場合) ── + await dismissModal(); const confirmPatterns = [/公開する/, /確認/, /はい/, /OK/i]; for (const pattern of confirmPatterns) { @@ -512,7 +556,7 @@ jobs: break; } } - await shot('06_after_confirm'); + await shot('07_after_confirm'); } else { const draftPatterns = [ @@ -545,7 +589,7 @@ jobs: throw new Error('Save button not found. Check screenshots and button list above.'); } - await shot('05_after_save'); + await shot('06_after_save'); } await page.waitForTimeout(3000); From 3681eae435501c96fd48f7fda91fd3dc2546ca4d Mon Sep 17 00:00:00 2001 From: specificeye-oss Date: Tue, 14 Apr 2026 12:27:23 +0900 Subject: [PATCH 50/50] Update note.yaml --- .github/workflows/note.yaml | 71 +++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/.github/workflows/note.yaml b/.github/workflows/note.yaml index 66e326c..1bba0df 100644 --- a/.github/workflows/note.yaml +++ b/.github/workflows/note.yaml @@ -418,35 +418,46 @@ jobs: console.log(`[screenshot] ${label}`); }; - // ── モーダルを閉じるヘルパー関数 ── + // ── モーダルをJSで強制削除するヘルパー関数 ── const dismissModal = async () => { try { - const modal = page.locator('div.ReactModalPortal'); - const visible = await modal.isVisible().catch(() => false); - if (visible) { - console.log('Modal detected, dismissing...'); - // まずESCキーで閉じる - await page.keyboard.press('Escape'); - await page.waitForTimeout(1000); - // ESCで閉じない場合は閉じるボタンを探す - const closePatterns = [/閉じる/, /キャンセル/, /スキップ/, /後で/, /close/i, /skip/i, /cancel/i]; - for (const pattern of closePatterns) { - const btn = modal.getByRole('button', { name: pattern }).first(); - if (await btn.count() > 0) { - console.log(`Closing modal with button: ${pattern}`); - await btn.click(); - await page.waitForTimeout(500); - break; + await page.keyboard.press('Escape'); + await page.waitForTimeout(500); + + const removed = await page.evaluate(() => { + let count = 0; + // ReactModalPortalを直接削除 + document.querySelectorAll('.ReactModalPortal').forEach(el => { + el.remove(); + count++; + }); + // assistants_confirmationを含む画像の親モーダルを削除 + document.querySelectorAll('img[src*="assistants_confirmation"], img[src*="confirmation"]').forEach(img => { + let el = img; + for (let i = 0; i < 8; i++) { + if (!el.parentElement) break; + el = el.parentElement; + if (el.tagName === 'BODY') break; + const style = window.getComputedStyle(el); + if (style.position === 'fixed' || style.position === 'absolute') { + el.remove(); + count++; + break; + } } - } - // それでも残っていたら最初のボタンを押す - const firstBtn = modal.locator('button').last(); - if (await firstBtn.count() > 0 && await modal.isVisible().catch(() => false)) { - console.log('Clicking last button in modal as fallback'); - await firstBtn.click(); - await page.waitForTimeout(500); - } - } + }); + // pointer-eventsをブロックしているオーバーレイを削除 + document.querySelectorAll('[class*="overlay"], [class*="Overlay"], [class*="backdrop"], [class*="Backdrop"]').forEach(el => { + const style = window.getComputedStyle(el); + if (style.position === 'fixed' && parseInt(style.zIndex) > 10) { + el.remove(); + count++; + } + }); + return count; + }); + console.log(`dismissModal: removed ${removed} element(s)`); + await page.waitForTimeout(300); } catch (e) { console.log('dismissModal skipped:', e.message); } @@ -461,7 +472,7 @@ jobs: await page.waitForTimeout(15000); await shot('01_after_wait'); - // ── ★ モーダルを閉じる(ページ読み込み直後) ── + // ── ★ モーダルを強制削除(ページ読み込み直後) ── await dismissModal(); await shot('02_after_dismiss_modal'); @@ -499,13 +510,13 @@ jobs: await shot('04_after_body'); await page.waitForTimeout(2000); - // ── ③ ボタンを全部ログ出力してから保存・公開 ── + // ── ③ ボタンを全部ログ出力 ── const allButtons = await page.evaluate(() => Array.from(document.querySelectorAll('button')).map(b => b.innerText.trim()).filter(Boolean) ); console.log('[all buttons]', JSON.stringify(allButtons)); - // ── ★ モーダルを閉じる(保存・公開ボタン押す前) ── + // ── ★ モーダルを強制削除(ボタンクリック前) ── await dismissModal(); await shot('05_before_save'); @@ -544,7 +555,7 @@ jobs: await page.waitForTimeout(3000); await shot('06_after_publish_click'); - // ── ★ モーダルを閉じる(確認ダイアログが別のモーダルの場合) ── + // ── ★ モーダルを強制削除(確認ダイアログ前) ── await dismissModal(); const confirmPatterns = [/公開する/, /確認/, /はい/, /OK/i];