From 67df308947dce5f27652a6cac8bbb50362712a1b Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 10:47:02 +0900 Subject: [PATCH 01/32] Refactor note workflow for SEO URL and job updates Updated the GitHub Actions workflow to change input parameters and modify job steps for fetching and posting articles. --- .github/workflows/note-perplexity.yaml | 736 +++++-------------------- 1 file changed, 153 insertions(+), 583 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index ebdcaea..12f37fe 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -1,22 +1,10 @@ -name: Note Workflow (Perplexity) +name: Note Workflow (SEO URL -> note) 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)' + seo_url: + description: '投稿元のSEO記事URL' required: true type: string tags: @@ -48,417 +36,107 @@ env: TZ: Asia/Tokyo jobs: - research: - name: Research (Perplexity Search API) + fetch: + name: Fetch SEO article (Extract title/body) runs-on: ubuntu-latest env: - PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - THEME: ${{ github.event.inputs.theme }} - TARGET: ${{ github.event.inputs.target }} - 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 dependencies - run: | - npm init -y - npm i @perplexity-ai/perplexity_ai ai @ai-sdk/anthropic - - - name: Research with Perplexity API - run: | - cat > research.mjs <<'EOF' - import Perplexity from '@perplexity-ai/perplexity_ai'; - 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 today = new Date().toISOString().slice(0,10); - const artifactsDir = '.note-artifacts'; - fs.mkdirSync(artifactsDir, { recursive: true }); - - // Perplexity APIクライアント初期化 - const perplexity = new Perplexity({ - apiKey: process.env.PERPLEXITY_API_KEY - }); - - async function main() { - try { - // 複数の検索クエリを実行 - const queries = [ - `${theme} 最新情報 ${today}`, - `${theme} トレンド 動向`, - `${theme} 事例 実践`, - `${theme} 課題 解決策`, - `${theme} 将来 展望` - ]; - - console.log('Perplexity検索を開始...'); - const searchResults = []; - - for (const query of queries) { - console.log(`検索中: ${query}`); - const search = await perplexity.search.create({ - query: query, - maxResults: 5, - maxTokensPerPage: 2048, - country: 'JP' - }); - searchResults.push({ query, results: search.results }); - } - - // 検索結果をMarkdown形式でフォーマット - let researchMd = `# リサーチレポート: ${theme}\n\n`; - researchMd += `**作成日**: ${today}\n`; - researchMd += `**対象読者**: ${target}\n\n`; - researchMd += `---\n\n`; - - for (const { query, results } of searchResults) { - researchMd += `## ${query}\n\n`; - - if (Array.isArray(results) && results.length > 0) { - for (const result of results) { - researchMd += `### ${result.title}\n\n`; - researchMd += `**URL**: [${result.url}](${result.url})\n\n`; - if (result.date) { - researchMd += `**日付**: ${result.date}\n\n`; - } - if (result.snippet) { - researchMd += `${result.snippet}\n\n`; - } - researchMd += `---\n\n`; - } - } else { - researchMd += `検索結果なし\n\n`; - } - } - - // Claudeで検索結果を要約・構造化 - console.log('Claudeで検索結果を分析・構造化中...'); - const modelName = 'claude-sonnet-4-20250514'; - const systemPrompt = [ - 'あなたは最新情報の分析と構造化に特化した超一流のリサーチャーです。', - '提供された検索結果を基に、事実ベースで信頼性の高いリサーチレポートを作成してください。', - '出典は本文内にMarkdownリンクで必ず埋め込むこと。', - '十分な分量(目安: 2,000語以上)で、各節に適切な見出しと構造を持たせてください。', - '一次情報(公的機関・規格・論文・公式発表)を優先し、情報源を明記してください。' - ].join('\n'); - - const userPrompt = [ - `以下の検索結果を基に、テーマ「${theme}」に関する包括的なリサーチレポートを作成してください。`, - ``, - `**対象読者**: ${target}`, - `**現在日付**: ${today}`, - ``, - `## 検索結果`, - ``, - researchMd, - ``, - `## 要件`, - ``, - `1. 検索結果から重要な情報を抽出し、論理的に構造化する`, - `2. 各主張には必ず出典のMarkdownリンクを埋め込む`, - `3. 最新のトレンド、具体的な事例、課題と解決策、将来展望を含める`, - `4. 対象読者(${target})に分かりやすく説明する`, - `5. 情報の信頼性を評価し、一次情報を優先する`, - ``, - `**重要**: 途中経過や確認質問は一切せず、最終レポートのみを返してください。` - ].join('\n'); - - const { text } = await generateText({ - model: anthropic(modelName), - system: systemPrompt, - prompt: userPrompt, - temperature: 0.3, - maxTokens: 30000 - }); - - const finalReport = text || researchMd; - - // 保存 - fs.writeFileSync(`${artifactsDir}/research.md`, finalReport); - - // トレース用に生データも保存 - const traceData = { - theme, - target, - date: today, - queries, - searchResults, - finalReport: finalReport.substring(0, 500) + '...' - }; - fs.writeFileSync(`${artifactsDir}/research_trace.json`, JSON.stringify(traceData, null, 2)); - - console.log('リサーチ完了!'); - - } catch (error) { - console.error('エラー:', error); - // エラー時は検索結果のみを保存 - const fallbackReport = `# リサーチレポート: ${theme}\n\n**エラーが発生しました**: ${error.message}\n\n検索は部分的に完了している可能性があります。`; - fs.writeFileSync(`${artifactsDir}/research.md`, fallbackReport); - throw error; - } - } - - await main(); - 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 - - - name: Upload research artifacts - uses: actions/upload-artifact@v4 - with: - name: research-artifacts - path: | - .note-artifacts/research.md - .note-artifacts/research_trace.json - - write: - name: Write (Claude Sonnet 4.0) - needs: research - runs-on: ubuntu-latest - 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 }} + SEO_URL: ${{ github.event.inputs.seo_url }} INPUT_TAGS: ${{ github.event.inputs.tags }} outputs: - title: ${{ steps.collect.outputs.title }} - draft_json_b64: ${{ steps.collect.outputs.draft_json_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) - 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-20250514'; - 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; - } - 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); - } - 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)); - EOF - node write.mjs - - - name: Collect draft - 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 - uses: actions/upload-artifact@v4 - with: - name: draft-artifact - path: .note-artifacts/draft.json - - factcheck: - name: Fact-check (Tavily) - needs: write - runs-on: ubuntu-latest - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - TITLE: ${{ needs.write.outputs.title }} - outputs: - title: ${{ steps.collect.outputs.title }} final_b64: ${{ steps.collect.outputs.final_b64 }} + title: ${{ steps.collect.outputs.title }} steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install AI SDK + - name: Install dependencies 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 + npm i jsdom @mozilla/readability - - name: Fact-check with Tavily + - name: Extract article run: | - cat > factcheck.mjs <<'EOF' - import { generateText } from 'ai'; - import { anthropic } from '@ai-sdk/anthropic'; + cat > fetch.mjs <<'EOF' + import { JSDOM } from 'jsdom'; + import { Readability } from '@mozilla/readability'; import fs from 'fs'; - const draft=JSON.parse(fs.readFileSync('.note-artifacts/draft.json','utf8')); - const modelName='claude-sonnet-4-20250514'; - 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) : []; + + function normalizeUrl(raw) { + const s = String(raw || '').trim(); + if (!s) return ''; + // 全角記号を半角へ + const half = s.replace(/?/g, '?').replace(/#/g, '#').replace(/&/g, '&'); + // URL全体を安全にエンコード + const encoded = encodeURI(half); + return new URL(encoded).toString(); } - - 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 raw = process.env.SEO_URL || ''; + const url = normalizeUrl(raw); + if (!url) { + console.error('SEO_URL is empty'); + process.exit(1); } - - 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(''); + + const res = await fetch(url, { + redirect: 'follow', + headers: { + 'User-Agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml' } - return lines.join('\n'); + }); + + if (!res.ok) { + console.error('fetch failed:', res.status, res.statusText); + 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 html = await res.text(); + + const dom = new JSDOM(html, { url }); + const reader = new Readability(dom.window.document); + const article = reader.parse(); + + if (!article || !article.content) { + console.error('Readability failed to extract content'); + process.exit(1); } - - await main(); + + const inputTags = (process.env.INPUT_TAGS || '') + .split(',') + .map(s => s.trim()) + .filter(Boolean); + + // 出典表記を先頭に付ける(任意だが推奨) + const attributionHtml = + `

出典: ${url}


`; + + const out = { + title: (article.title || '').trim() || '(タイトル取得失敗)', + body_html: attributionHtml + article.content, + tags: inputTags, + source_url: url + }; + + fs.writeFileSync('final.json', JSON.stringify(out, null, 2)); + console.log('Extracted:', out.title); EOF - node factcheck.mjs + node fetch.mjs - - name: Upload fact-check artifact + - name: Upload extracted artifact uses: actions/upload-artifact@v4 with: - name: final-artifact - path: .note-artifacts/final.json + name: extracted-article + path: final.json - name: Collect final 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) + title=$(node -e "console.log(JSON.parse(require('fs').readFileSync('final.json','utf8')).title)") + b64=$(base64 -w 0 final.json 2>/dev/null || base64 final.json) echo "title<> $GITHUB_OUTPUT echo "$title" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT @@ -468,7 +146,7 @@ jobs: post: name: Post to note.com (Playwright) - needs: factcheck + needs: fetch if: ${{ github.event.inputs.dry_run != 'true' }} runs-on: ubuntu-latest env: @@ -489,7 +167,7 @@ jobs: - name: Install Playwright run: | npm init -y - npm i playwright marked + npm i playwright npx playwright install --with-deps chromium | cat - name: Prepare storageState @@ -510,7 +188,7 @@ jobs: - name: Restore final id: draft env: - FINAL_B64: ${{ needs.factcheck.outputs.final_b64 }} + FINAL_B64: ${{ needs.fetch.outputs.final_b64 }} 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 @@ -524,149 +202,48 @@ jobs: 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 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())}`; } - 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); }); + + 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 rawBodyHtml = String(rawFinal.body_html || ''); + 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); } - 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 }) || ''); + if (!rawBodyHtml.trim()) { + console.error('body_html is empty in final.json'); + process.exit(1); } - function htmlFromMarkdown(md){ - // 全文を一括でHTML化(段落ベース)。リスト中の意図しない
を避けるため breaks=false - return String(marked.parse(md, { gfm:true, breaks:false, mangle:false, headerIds:false }) || ''); + + 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(/^#+\s*/, ''); + s = s.replace(/^"+|"+$/g, '').replace(/^'+|'+$/g, ''); + if (!s) s = 'タイトル(自動生成)'; + return s; } - async function insertHTML(page, locator, html){ + + async function insertHTML(page, locator, html) { await locator.click(); await locator.evaluate((el, html) => { el.focus(); @@ -680,59 +257,40 @@ jobs: }, 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; - } - const blocks = splitMarkdownBlocks(preBody); + let TITLE = sanitizeTitle(rawTitle); let browser, context, page; - try{ + 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); + // 本文(contenteditable) 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){ + // 既存内容を消してからHTML挿入 + await bodyBox.click(); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Backspace'); + + await insertHTML(page, bodyBox, rawBodyHtml); + await page.waitForTimeout(200); + + 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(()=>{}); } + 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); @@ -741,41 +299,53 @@ jobs: 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); } + 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(() => {}), + 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(); + // タグ + 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); } + for (const t of tags) { + await tagInput.click(); + await tagInput.fill(t); + await page.keyboard.press('Enter'); + await page.waitForTimeout(120); + } } 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); } + 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), + page.waitForTimeout(5000) ]); await page.screenshot({ path: SS_PATH, fullPage: true }); - const finalUrl=page.url(); + 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{} + try { await page?.close(); } catch {} + try { await context?.close(); } catch {} + try { await browser?.close(); } catch {} } EOF node post.mjs | tee post.log From 5964e6547582098ffdd4455e05cd6a074b47d41c Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 14:26:45 +0900 Subject: [PATCH 02/32] Replace STATE_JSON with STATE_B64 in workflow --- .github/workflows/note-perplexity.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 12f37fe..859b79a 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -151,7 +151,7 @@ jobs: runs-on: ubuntu-latest env: IS_PUBLIC: ${{ github.event.inputs.is_public }} - STATE_JSON: ${{ secrets.NOTE_STORAGE_STATE_JSON }} + STATE_B64: ${{ secrets.NOTE_STORAGE_STATE_B64 }} START_URL: https://editor.note.com/new outputs: final_url: ${{ steps.publish.outputs.published_url || steps.publish.outputs.draft_url }} @@ -169,13 +169,14 @@ jobs: npm init -y npm i playwright npx playwright install --with-deps chromium | cat - + - name: Prepare storageState id: state run: | - test -n "$STATE_JSON" || (echo "ERROR: NOTE_STORAGE_STATE_JSON secret is not set" && exit 1) + test -n "$STATE_B64" || (echo "ERROR: NOTE_STORAGE_STATE_B64 secret is not set" && exit 1) mkdir -p "$RUNNER_TEMP" - echo "$STATE_JSON" > "$RUNNER_TEMP/note-state.json" + echo "$STATE_B64" | base64 -d > "$RUNNER_TEMP/note-state.json" + node -e "JSON.parse(require('fs').readFileSync('$RUNNER_TEMP/note-state.json','utf8')); console.log('storageState JSON OK')" echo "STATE_PATH=$RUNNER_TEMP/note-state.json" >> $GITHUB_OUTPUT - name: Ensure jq (post) From 83ee95f2ba54f22b5f8aa26080362512ab2c00e3 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 17:34:51 +0900 Subject: [PATCH 03/32] Enhance debugging for title selector in workflow Added debug logging for URL and screenshot capture when title selector is not found. --- .github/workflows/note-perplexity.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 859b79a..bf14d06 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -268,6 +268,9 @@ jobs: page.setDefaultTimeout(180000); await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); + console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('SCREENSHOT=' + SS_PATH); // タイトル await page.waitForSelector('textarea[placeholder*="タイトル"]'); @@ -298,6 +301,17 @@ jobs: process.exit(0); } + try { + await page.waitForSelector('textarea[placeholder*="タイトル"]', { timeout: 180000 }); + } catch (e) { + console.log('DEBUG_TITLE_NOT_FOUND_URL=' + page.url()); + fs.writeFileSync('debug.html', await page.content()); + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('SCREENSHOT=' + SS_PATH); + throw e; + } + + const proceed = page.locator('button:has-text("公開に進む")').first(); await proceed.waitFor({ state: 'visible' }); for (let i = 0; i < 20; i++) { From 15fb5b5020b2adaf26e1ab83f44aebd9862d7a17 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 17:36:30 +0900 Subject: [PATCH 04/32] Add debug.html to upload artifacts workflow --- .github/workflows/debug.html | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/workflows/debug.html diff --git a/.github/workflows/debug.html b/.github/workflows/debug.html new file mode 100644 index 0000000..0842d44 --- /dev/null +++ b/.github/workflows/debug.html @@ -0,0 +1,8 @@ +- name: Upload debug artifacts + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: note-debug + path: | + post.log + debug.html From 8ca70157cb63a0c6a237cb441d5beaf43b555fb9 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 17:50:28 +0900 Subject: [PATCH 05/32] Enhance title input selection for robustness --- .github/workflows/note-perplexity.yaml | 62 ++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index bf14d06..b815722 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -273,12 +273,66 @@ jobs: console.log('SCREENSHOT=' + SS_PATH); // タイトル - await page.waitForSelector('textarea[placeholder*="タイトル"]'); - await page.fill('textarea[placeholder*="タイトル"]', TITLE); + // タイトル(UI変更に強い探索) + const titleCandidates = [ + // 旧UI + 'textarea[placeholder*="タイトル"]', + 'textarea[aria-label*="タイトル"]', + 'input[placeholder*="タイトル"]', + 'input[aria-label*="タイトル"]', + + // contenteditable になっているケース + '[contenteditable="true"][role="textbox"][aria-label*="タイトル"]', + '[contenteditable="true"][role="textbox"][data-testid*="title"]', + '[contenteditable="true"][data-testid*="title"]', + + // かなり広め(最終手段) + '[contenteditable="true"][role="textbox"]' + ]; + + // SPAで描画が遅いことがあるので少し待つ + await page.waitForLoadState('networkidle').catch(() => {}); + await page.waitForTimeout(500); + + let titleLocator = null; + for (const sel of titleCandidates) { + const loc = page.locator(sel).first(); + if (await loc.count()) { titleLocator = loc; break; } + } + if (!titleLocator) { + // もう一回だけ待って再探索 + await page.waitForTimeout(1500); + for (const sel of titleCandidates) { + const loc = page.locator(sel).first(); + if (await loc.count()) { titleLocator = loc; break; } + } + } + if (!titleLocator) { + console.log('DEBUG_TITLE_NOT_FOUND_URL=' + page.url()); + fs.writeFileSync('debug.html', await page.content()); + throw new Error('Title input not found'); + } + + await titleLocator.waitFor({ state: 'visible', timeout: 180000 }); + + // textarea/input なら fill、contenteditable ならキーボード入力 + const tagName = await titleLocator.evaluate(el => el.tagName.toLowerCase()); + if (tagName === 'textarea' || tagName === 'input') { + await titleLocator.fill(TITLE); + } else { + await titleLocator.click({ force: true }); + await page.keyboard.press('Control+A'); + await page.keyboard.type(TITLE, { delay: 5 }); + } + // 本文(contenteditable) - const bodyBox = page.locator('div[contenteditable="true"][role="textbox"]').first(); - await bodyBox.waitFor({ state: 'visible' }); + // 本文(タイトルの次にある textbox を狙う) + const textboxes = page.locator('[contenteditable="true"][role="textbox"]'); + await textboxes.first().waitFor({ state: 'visible' }); + const bodyBox = textboxes.nth(1); // 0:タイトル、1:本文 になっているケースが多い + await bodyBox.waitFor({ state: 'visible', timeout: 180000 }); + // 既存内容を消してからHTML挿入 await bodyBox.click(); From 2607bb65a634fbfe5d9887345e4009229fa38410 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 18:03:26 +0900 Subject: [PATCH 06/32] Update note-perplexity.yaml --- .github/workflows/note-perplexity.yaml | 35 +++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index b815722..adab415 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -275,20 +275,24 @@ jobs: // タイトル // タイトル(UI変更に強い探索) const titleCandidates = [ - // 旧UI - 'textarea[placeholder*="タイトル"]', - 'textarea[aria-label*="タイトル"]', - 'input[placeholder*="タイトル"]', - 'input[aria-label*="タイトル"]', - - // contenteditable になっているケース - '[contenteditable="true"][role="textbox"][aria-label*="タイトル"]', - '[contenteditable="true"][role="textbox"][data-testid*="title"]', - '[contenteditable="true"][data-testid*="title"]', - - // かなり広め(最終手段) - '[contenteditable="true"][role="textbox"]' - ]; + // note現行で強い(最優先) + '[contenteditable="true"][data-placeholder*="記事タイトル"]', + '[contenteditable="true"][data-placeholder*="タイトル"]', + '[contenteditable="true"][aria-placeholder*="記事タイトル"]', + '[contenteditable="true"][aria-placeholder*="タイトル"]', + + // 念のため role/aria-label 系 + '[contenteditable="true"][role="textbox"][aria-label*="記事タイトル"]', + '[contenteditable="true"][role="textbox"][aria-label*="タイトル"]', + '[contenteditable="true"][role="textbox"][data-placeholder]', + '[contenteditable="true"][role="textbox"][aria-placeholder]', + + // 旧UI(残してOK) + 'textarea[placeholder*="タイトル"]', + 'textarea[aria-label*="タイトル"]', + 'input[placeholder*="タイトル"]', + 'input[aria-label*="タイトル"]', + ]; // SPAで描画が遅いことがあるので少し待つ await page.waitForLoadState('networkidle').catch(() => {}); @@ -297,8 +301,9 @@ jobs: let titleLocator = null; for (const sel of titleCandidates) { const loc = page.locator(sel).first(); - if (await loc.count()) { titleLocator = loc; break; } + if (await loc.isVisible().catch(() => false)) { titleLocator = loc; break; } } + if (!titleLocator) { // もう一回だけ待って再探索 await page.waitForTimeout(1500); From bb9e45f8fb9c9a2b5f85af54b402731b2b916c08 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 18:04:17 +0900 Subject: [PATCH 07/32] Update debug.html --- .github/workflows/debug.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/debug.html b/.github/workflows/debug.html index 0842d44..407ccac 100644 --- a/.github/workflows/debug.html +++ b/.github/workflows/debug.html @@ -2,7 +2,9 @@ if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: note-debug + name: note-debug-${{ github.run_id }} path: | post.log debug.html + debug.png + trace.zip From ce226157c69481ddc4e73db83999f7cae4d3389e Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 18:10:17 +0900 Subject: [PATCH 08/32] Update note-perplexity.yaml --- .github/workflows/note-perplexity.yaml | 30 ++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index adab415..9f33da8 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -268,9 +268,37 @@ jobs: page.setDefaultTimeout(180000); await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); + // “描画された”判定に使える候補(note側のUI変化に備えて複数) + const editorReadySelectors = [ + '[contenteditable="true"][data-placeholder*="記事タイトル"]', + '[contenteditable="true"][data-placeholder*="タイトル"]', + // もしヘッダーやメニューがあるならそれも + 'text=下書き', // 例(実際に出る文言に合わせて) + ]; + + const loginOrBlockSelectors = [ + 'text=ログイン', + 'text=メールアドレス', + 'text=パスワード', + 'text=アクセスが制限されています', + ]; + + await Promise.race([ + page.waitForSelector(editorReadySelectors.join(','), { timeout: 60000 }), + page.waitForSelector(loginOrBlockSelectors.join(','), { timeout: 60000 }), + ]).catch(() => {}); console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('SCREENSHOT=' + SS_PATH); + const isEditorReady = await page.locator(editorReadySelectors.join(',')).first().isVisible().catch(() => false); + + if (!isEditorReady) { + console.log('DEBUG_NOT_EDITOR_READY_URL=' + page.url()); + fs.writeFileSync('debug.html', await page.content()); + await page.screenshot({ path: 'debug.png', fullPage: true }); + throw new Error('Editor not ready (login required or blocked)'); + } + // タイトル // タイトル(UI変更に強い探索) @@ -284,8 +312,6 @@ jobs: // 念のため role/aria-label 系 '[contenteditable="true"][role="textbox"][aria-label*="記事タイトル"]', '[contenteditable="true"][role="textbox"][aria-label*="タイトル"]', - '[contenteditable="true"][role="textbox"][data-placeholder]', - '[contenteditable="true"][role="textbox"][aria-placeholder]', // 旧UI(残してOK) 'textarea[placeholder*="タイトル"]', From 6e7283ede8966d132d6754e62a288051f0e57fc4 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 18:24:28 +0900 Subject: [PATCH 09/32] Enhance note-perplexity workflow with debugging features Added debugging and tracing features to the note-perplexity workflow, including error handling and logging for various states. --- .github/workflows/note-perplexity.yaml | 74 ++++++++++++++++++++------ 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 9f33da8..658c4ff 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -265,8 +265,28 @@ jobs: 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 context.tracing.start({ screenshots: true, snapshots: true, sources: true }); + page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); + page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); + page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); + + page.on('response', res => { + const status = res.status(); + const url = res.url(); + if (status >= 400 && /note\.com/.test(url)) { + console.log('HTTP_ERROR:', status, url); + } + }); + page.setDefaultTimeout(180000); + const rawState = fs.readFileSync(STATE_PATH, 'utf8'); + console.log('DEBUG_STATE_BYTES=', rawState.length); + + const cookies0 = await context.cookies(); + const noteCookies0 = cookies0.filter(c => (c.domain || '').includes('note.com')); + console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); + console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); + await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); // “描画された”判定に使える候補(note側のUI変化に備えて複数) const editorReadySelectors = [ @@ -290,16 +310,29 @@ jobs: console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('SCREENSHOT=' + SS_PATH); - const isEditorReady = await page.locator(editorReadySelectors.join(',')).first().isVisible().catch(() => false); - + const editorReady = page.locator(editorReadySelectors.join(',')).first(); + const loginLike = page.locator(loginOrBlockSelectors.join(',')).first(); + + const isEditorReady = await editorReady.isVisible().catch(() => false); + const isLoginLike = await loginLike.isVisible().catch(() => false); + + console.log('DEBUG_IS_EDITOR_READY=', isEditorReady); + console.log('DEBUG_IS_LOGIN_LIKE=', isLoginLike); + if (!isEditorReady) { - console.log('DEBUG_NOT_EDITOR_READY_URL=' + page.url()); fs.writeFileSync('debug.html', await page.content()); - await page.screenshot({ path: 'debug.png', fullPage: true }); - throw new Error('Editor not ready (login required or blocked)'); + + if (isLoginLike) { + await page.screenshot({ path: 'debug-login.png', fullPage: true }); + throw new Error('Not logged in (login UI detected)'); + } else { + await page.screenshot({ path: 'debug-not-ready.png', fullPage: true }); + throw new Error('Editor not ready (JS init failed / blocked / network)'); + } } + // タイトル // タイトル(UI変更に強い探索) const titleCandidates = [ @@ -386,16 +419,6 @@ jobs: process.exit(0); } - try { - await page.waitForSelector('textarea[placeholder*="タイトル"]', { timeout: 180000 }); - } catch (e) { - console.log('DEBUG_TITLE_NOT_FOUND_URL=' + page.url()); - fs.writeFileSync('debug.html', await page.content()); - await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('SCREENSHOT=' + SS_PATH); - throw e; - } - const proceed = page.locator('button:has-text("公開に進む")').first(); await proceed.waitFor({ state: 'visible' }); @@ -442,7 +465,8 @@ jobs: const finalUrl = page.url(); console.log('PUBLISHED_URL=' + finalUrl); console.log('SCREENSHOT=' + SS_PATH); - } finally { + } { + try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} try { await page?.close(); } catch {} try { await context?.close(); } catch {} try { await browser?.close(); } catch {} @@ -456,6 +480,22 @@ jobs: if [ -n "$draft" ]; then echo "draft_url=$draft" >> $GITHUB_OUTPUT; fi if [ -n "$shot" ]; then echo "screenshot=$shot" >> $GITHUB_OUTPUT; fi + - name: Upload debug artifacts + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: note-debug-${{ github.run_id }} + if-no-files-found: warn + path: | + post.log + debug.html + debug.png + debug-login.png + debug-not-ready.png + trace.zip + /tmp/note-screenshots/** + + - name: Upload screenshot (if any) if: ${{ steps.publish.outputs.screenshot != '' }} uses: actions/upload-artifact@v4 From 9dbce705b18371b12e6651518f374b946d3310ac Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 18:27:53 +0900 Subject: [PATCH 10/32] Add Promise.race to wait for selectors --- .github/workflows/note-perplexity.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 658c4ff..730a0ed 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -303,6 +303,7 @@ jobs: 'text=アクセスが制限されています', ]; + await Promise.race([ page.waitForSelector(editorReadySelectors.join(','), { timeout: 60000 }), page.waitForSelector(loginOrBlockSelectors.join(','), { timeout: 60000 }), From 6496ced28d27c74e9a9920687ed6f99b452545c6 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 22:39:52 +0900 Subject: [PATCH 11/32] Refactor note-perplexity workflow for clarity and efficiency --- .github/workflows/note-perplexity.yaml | 160 ++++++++++--------------- 1 file changed, 65 insertions(+), 95 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 730a0ed..6fdc896 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -62,24 +62,22 @@ jobs: import { JSDOM } from 'jsdom'; import { Readability } from '@mozilla/readability'; import fs from 'fs'; - + function normalizeUrl(raw) { const s = String(raw || '').trim(); if (!s) return ''; - // 全角記号を半角へ const half = s.replace(/?/g, '?').replace(/#/g, '#').replace(/&/g, '&'); - // URL全体を安全にエンコード const encoded = encodeURI(half); return new URL(encoded).toString(); } - + const raw = process.env.SEO_URL || ''; const url = normalizeUrl(raw); if (!url) { console.error('SEO_URL is empty'); process.exit(1); } - + const res = await fetch(url, { redirect: 'follow', headers: { @@ -88,39 +86,38 @@ jobs: 'Accept': 'text/html,application/xhtml+xml' } }); - + if (!res.ok) { console.error('fetch failed:', res.status, res.statusText); process.exit(1); } - + const html = await res.text(); - + const dom = new JSDOM(html, { url }); const reader = new Readability(dom.window.document); const article = reader.parse(); - + if (!article || !article.content) { console.error('Readability failed to extract content'); process.exit(1); } - + const inputTags = (process.env.INPUT_TAGS || '') .split(',') .map(s => s.trim()) .filter(Boolean); - - // 出典表記を先頭に付ける(任意だが推奨) + const attributionHtml = `

出典: ${url}


`; - + const out = { title: (article.title || '').trim() || '(タイトル取得失敗)', body_html: attributionHtml + article.content, tags: inputTags, source_url: url }; - + fs.writeFileSync('final.json', JSON.stringify(out, null, 2)); console.log('Extracted:', out.title); EOF @@ -169,7 +166,7 @@ jobs: npm init -y npm i playwright npx playwright install --with-deps chromium | cat - + - name: Prepare storageState id: state run: | @@ -258,71 +255,68 @@ jobs: }, html); } - let TITLE = sanitizeTitle(rawTitle); + const TITLE = sanitizeTitle(rawTitle); 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(); await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); + + page = await context.newPage(); + page.setDefaultTimeout(180000); + page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); - page.on('response', res => { const status = res.status(); const url = res.url(); - if (status >= 400 && /note\.com/.test(url)) { - console.log('HTTP_ERROR:', status, url); - } + if (status >= 400 && /note\.com/.test(url)) console.log('HTTP_ERROR:', status, url); }); - page.setDefaultTimeout(180000); const rawState = fs.readFileSync(STATE_PATH, 'utf8'); console.log('DEBUG_STATE_BYTES=', rawState.length); - + const cookies0 = await context.cookies(); const noteCookies0 = cookies0.filter(c => (c.domain || '').includes('note.com')); console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); - + await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - // “描画された”判定に使える候補(note側のUI変化に備えて複数) + const editorReadySelectors = [ '[contenteditable="true"][data-placeholder*="記事タイトル"]', '[contenteditable="true"][data-placeholder*="タイトル"]', - // もしヘッダーやメニューがあるならそれも - 'text=下書き', // 例(実際に出る文言に合わせて) + 'text=下書き', ]; - const loginOrBlockSelectors = [ 'text=ログイン', 'text=メールアドレス', 'text=パスワード', 'text=アクセスが制限されています', ]; - - + await Promise.race([ page.waitForSelector(editorReadySelectors.join(','), { timeout: 60000 }), page.waitForSelector(loginOrBlockSelectors.join(','), { timeout: 60000 }), ]).catch(() => {}); + console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('SCREENSHOT=' + SS_PATH); + const editorReady = page.locator(editorReadySelectors.join(',')).first(); const loginLike = page.locator(loginOrBlockSelectors.join(',')).first(); - + const isEditorReady = await editorReady.isVisible().catch(() => false); const isLoginLike = await loginLike.isVisible().catch(() => false); - + console.log('DEBUG_IS_EDITOR_READY=', isEditorReady); console.log('DEBUG_IS_LOGIN_LIKE=', isLoginLike); - + if (!isEditorReady) { fs.writeFileSync('debug.html', await page.content()); - if (isLoginLike) { await page.screenshot({ path: 'debug-login.png', fullPage: true }); throw new Error('Not logged in (login UI detected)'); @@ -332,55 +326,43 @@ jobs: } } - - - // タイトル - // タイトル(UI変更に強い探索) const titleCandidates = [ - // note現行で強い(最優先) - '[contenteditable="true"][data-placeholder*="記事タイトル"]', - '[contenteditable="true"][data-placeholder*="タイトル"]', - '[contenteditable="true"][aria-placeholder*="記事タイトル"]', - '[contenteditable="true"][aria-placeholder*="タイトル"]', - - // 念のため role/aria-label 系 - '[contenteditable="true"][role="textbox"][aria-label*="記事タイトル"]', - '[contenteditable="true"][role="textbox"][aria-label*="タイトル"]', - - // 旧UI(残してOK) - 'textarea[placeholder*="タイトル"]', - 'textarea[aria-label*="タイトル"]', - 'input[placeholder*="タイトル"]', - 'input[aria-label*="タイトル"]', - ]; - - // SPAで描画が遅いことがあるので少し待つ + '[contenteditable="true"][data-placeholder*="記事タイトル"]', + '[contenteditable="true"][data-placeholder*="タイトル"]', + '[contenteditable="true"][aria-placeholder*="記事タイトル"]', + '[contenteditable="true"][aria-placeholder*="タイトル"]', + '[contenteditable="true"][role="textbox"][aria-label*="記事タイトル"]', + '[contenteditable="true"][role="textbox"][aria-label*="タイトル"]', + 'textarea[placeholder*="タイトル"]', + 'textarea[aria-label*="タイトル"]', + 'input[placeholder*="タイトル"]', + 'input[aria-label*="タイトル"]', + ]; + await page.waitForLoadState('networkidle').catch(() => {}); await page.waitForTimeout(500); - + let titleLocator = null; for (const sel of titleCandidates) { const loc = page.locator(sel).first(); if (await loc.isVisible().catch(() => false)) { titleLocator = loc; break; } } - if (!titleLocator) { - // もう一回だけ待って再探索 await page.waitForTimeout(1500); for (const sel of titleCandidates) { const loc = page.locator(sel).first(); - if (await loc.count()) { titleLocator = loc; break; } + if (await loc.isVisible().catch(() => false)) { titleLocator = loc; break; } } } if (!titleLocator) { console.log('DEBUG_TITLE_NOT_FOUND_URL=' + page.url()); fs.writeFileSync('debug.html', await page.content()); + await page.screenshot({ path: 'debug.png', fullPage: true }); throw new Error('Title input not found'); } - + await titleLocator.waitFor({ state: 'visible', timeout: 180000 }); - - // textarea/input なら fill、contenteditable ならキーボード入力 + const tagName = await titleLocator.evaluate(el => el.tagName.toLowerCase()); if (tagName === 'textarea' || tagName === 'input') { await titleLocator.fill(TITLE); @@ -390,16 +372,12 @@ jobs: await page.keyboard.type(TITLE, { delay: 5 }); } - - // 本文(contenteditable) - // 本文(タイトルの次にある textbox を狙う) const textboxes = page.locator('[contenteditable="true"][role="textbox"]'); await textboxes.first().waitFor({ state: 'visible' }); - const bodyBox = textboxes.nth(1); // 0:タイトル、1:本文 になっているケースが多い - await bodyBox.waitFor({ state: 'visible', timeout: 180000 }); + const bodyBox = textboxes.nth(1); + await bodyBox.waitFor({ state: 'visible', timeout: 180000 }); - // 既存内容を消してからHTML挿入 await bodyBox.click(); await page.keyboard.press('Control+A'); await page.keyboard.press('Backspace'); @@ -417,10 +395,9 @@ jobs: await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('DRAFT_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); - process.exit(0); + return; } - const proceed = page.locator('button:has-text("公開に進む")').first(); await proceed.waitFor({ state: 'visible' }); for (let i = 0; i < 20; i++) { @@ -434,7 +411,6 @@ jobs: 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*="ハッシュタグ"]'); @@ -463,10 +439,10 @@ jobs: ]); await page.screenshot({ path: SS_PATH, fullPage: true }); - const finalUrl = page.url(); - console.log('PUBLISHED_URL=' + finalUrl); + console.log('PUBLISHED_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); - } { + + } finally { try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} try { await page?.close(); } catch {} try { await context?.close(); } catch {} @@ -474,32 +450,26 @@ jobs: } 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 debug artifacts - if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: note-debug-${{ github.run_id }} - if-no-files-found: warn - path: | - post.log - debug.html - debug.png - debug-login.png - debug-not-ready.png - trace.zip - /tmp/note-screenshots/** - - - - name: Upload screenshot (if any) - if: ${{ steps.publish.outputs.screenshot != '' }} + - name: Upload debug artifacts + if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: note-screenshot - path: ${{ steps.publish.outputs.screenshot }} + name: note-debug-${{ github.run_id }} + if-no-files-found: warn + path: | + post.log + debug.html + debug.png + debug-login.png + debug-not-ready.png + trace.zip + /tmp/note-screenshots/** From 3d6b51830f9d218105d37c3476a859561a2c573b Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 22:49:10 +0900 Subject: [PATCH 12/32] Replace return with process.exit(0) in workflow Change return statement to process.exit(0) for proper termination. --- .github/workflows/note-perplexity.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 6fdc896..32f1019 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -395,7 +395,7 @@ jobs: await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('DRAFT_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); - return; + process.exit(0); } const proceed = page.locator('button:has-text("公開に進む")').first(); From 8e2da2f2c4b5f64adf50923db6858e2233172fd0 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 23:00:11 +0900 Subject: [PATCH 13/32] Add function to find visible elements in any frame --- .github/workflows/note-perplexity.yaml | 45 +++++++++++++++++++------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 32f1019..8099af7 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -285,17 +285,40 @@ jobs: await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - const editorReadySelectors = [ - '[contenteditable="true"][data-placeholder*="記事タイトル"]', - '[contenteditable="true"][data-placeholder*="タイトル"]', - 'text=下書き', - ]; - const loginOrBlockSelectors = [ - 'text=ログイン', - 'text=メールアドレス', - 'text=パスワード', - 'text=アクセスが制限されています', - ]; + async function findVisibleInAnyFrame(page, selectorCsv) { + for (const fr of page.frames()) { + try { + const loc = fr.locator(selectorCsv).first(); + if (await loc.isVisible({ timeout: 500 }).catch(() => false)) { + return { frame: fr, locator: loc }; + } + } catch {} + } + return null; + } + + const editorSel = editorReadySelectors.join(','); + const loginSel = loginOrBlockSelectors.join(','); + + await Promise.race([ + page.waitForTimeout(60000), + page.waitForSelector('iframe', { timeout: 60000 }).catch(() => {}), + ]).catch(() => {}); + + const foundEditor = await findVisibleInAnyFrame(page, editorSel); + const foundLogin = await findVisibleInAnyFrame(page, loginSel); + + console.log('DEBUG_FRAME_COUNT=', page.frames().length); + console.log('DEBUG_FRAMES=', page.frames().map(f => f.url()).join(' | ')); + console.log('DEBUG_FOUND_EDITOR_IN_FRAME=', !!foundEditor); + console.log('DEBUG_FOUND_LOGIN_IN_FRAME=', !!foundLogin); + + if (!foundEditor) { + fs.writeFileSync('debug.html', await page.content()); + await page.screenshot({ path: 'debug-not-ready.png', fullPage: true }); + throw new Error('Editor not ready (no editor selector in any frame)'); + } + await Promise.race([ page.waitForSelector(editorReadySelectors.join(','), { timeout: 60000 }), From 37773c5d9eca146bc59a82d52f65f0b8c86a83a7 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 23:08:09 +0900 Subject: [PATCH 14/32] Refactor selector handling in note-perplexity workflow --- .github/workflows/note-perplexity.yaml | 42 ++++++++++++++++++++------ 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 8099af7..c403d96 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -285,7 +285,28 @@ jobs: await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - async function findVisibleInAnyFrame(page, selectorCsv) { + await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); + + // ① まずセレクタ配列を定義(←ここが先) + const editorReadySelectors = [ + '[contenteditable="true"][data-placeholder*="記事タイトル"]', + '[contenteditable="true"][data-placeholder*="タイトル"]', + 'text=下書き', + ]; + + const loginOrBlockSelectors = [ + 'text=ログイン', + 'text=メールアドレス', + 'text=パスワード', + 'text=アクセスが制限されています', + ]; + + // ② その後に join して使う + const editorSel = editorReadySelectors.join(','); + const loginSel = loginOrBlockSelectors.join(','); + + // ③ フレーム含めて探す関数 + async function findVisibleInAnyFrame(page, selectorCsv) { for (const fr of page.frames()) { try { const loc = fr.locator(selectorCsv).first(); @@ -297,21 +318,23 @@ jobs: return null; } - const editorSel = editorReadySelectors.join(','); - const loginSel = loginOrBlockSelectors.join(','); + // ④ SPAなので少し待ってからフレーム走査 + await page.waitForTimeout(1500); - await Promise.race([ - page.waitForTimeout(60000), - page.waitForSelector('iframe', { timeout: 60000 }).catch(() => {}), - ]).catch(() => {}); + console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('SCREENSHOT=' + SS_PATH); + + console.log('DEBUG_FRAME_COUNT=', page.frames().length); + console.log('DEBUG_FRAMES=', page.frames().map(f => f.url()).join(' | ')); const foundEditor = await findVisibleInAnyFrame(page, editorSel); const foundLogin = await findVisibleInAnyFrame(page, loginSel); - console.log('DEBUG_FRAME_COUNT=', page.frames().length); - console.log('DEBUG_FRAMES=', page.frames().map(f => f.url()).join(' | ')); console.log('DEBUG_FOUND_EDITOR_IN_FRAME=', !!foundEditor); + if (foundEditor) console.log('DEBUG_EDITOR_FRAME_URL=', foundEditor.frame.url()); console.log('DEBUG_FOUND_LOGIN_IN_FRAME=', !!foundLogin); + if (foundLogin) console.log('DEBUG_LOGIN_FRAME_URL=', foundLogin.frame.url()); if (!foundEditor) { fs.writeFileSync('debug.html', await page.content()); @@ -320,6 +343,7 @@ jobs: } + await Promise.race([ page.waitForSelector(editorReadySelectors.join(','), { timeout: 60000 }), page.waitForSelector(loginOrBlockSelectors.join(','), { timeout: 60000 }), From 79a77f763d09179335c947ca608bf902d79d1f50 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 23:22:48 +0900 Subject: [PATCH 15/32] Refactor selector definitions and frame search logic --- .github/workflows/note-perplexity.yaml | 114 +++++++++++++------------ 1 file changed, 59 insertions(+), 55 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index c403d96..33edb8d 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -283,64 +283,68 @@ jobs: console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); + const resp = await context.request.get('https://note.com/api/v2/current_user'); + console.log('DEBUG_current_user_status=', resp.status()); + const txt = await resp.text(); + console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); + await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); + - await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - - // ① まずセレクタ配列を定義(←ここが先) - const editorReadySelectors = [ - '[contenteditable="true"][data-placeholder*="記事タイトル"]', - '[contenteditable="true"][data-placeholder*="タイトル"]', - 'text=下書き', - ]; - - const loginOrBlockSelectors = [ - 'text=ログイン', - 'text=メールアドレス', - 'text=パスワード', - 'text=アクセスが制限されています', - ]; - - // ② その後に join して使う - const editorSel = editorReadySelectors.join(','); - const loginSel = loginOrBlockSelectors.join(','); - - // ③ フレーム含めて探す関数 - async function findVisibleInAnyFrame(page, selectorCsv) { - for (const fr of page.frames()) { - try { - const loc = fr.locator(selectorCsv).first(); - if (await loc.isVisible({ timeout: 500 }).catch(() => false)) { - return { frame: fr, locator: loc }; - } - } catch {} + // ① まずセレクタ配列を定義(←ここが先) + const editorReadySelectors = [ + '[contenteditable="true"][data-placeholder*="記事タイトル"]', + '[contenteditable="true"][data-placeholder*="タイトル"]', + 'text=下書き', + ]; + + const loginOrBlockSelectors = [ + 'text=ログイン', + 'text=メールアドレス', + 'text=パスワード', + 'text=アクセスが制限されています', + ]; + + // ② その後に join して使う + const editorSel = editorReadySelectors.join(','); + const loginSel = loginOrBlockSelectors.join(','); + + // ③ フレーム含めて探す関数 + async function findVisibleInAnyFrame(page, selectorCsv) { + for (const fr of page.frames()) { + try { + const loc = fr.locator(selectorCsv).first(); + if (await loc.isVisible({ timeout: 500 }).catch(() => false)) { + return { frame: fr, locator: loc }; + } + } catch {} + } + return null; + } + + // ④ SPAなので少し待ってからフレーム走査 + await page.waitForTimeout(1500); + + console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('SCREENSHOT=' + SS_PATH); + + console.log('DEBUG_FRAME_COUNT=', page.frames().length); + console.log('DEBUG_FRAMES=', page.frames().map(f => f.url()).join(' | ')); + + const foundEditor = await findVisibleInAnyFrame(page, editorSel); + const foundLogin = await findVisibleInAnyFrame(page, loginSel); + + console.log('DEBUG_FOUND_EDITOR_IN_FRAME=', !!foundEditor); + if (foundEditor) console.log('DEBUG_EDITOR_FRAME_URL=', foundEditor.frame.url()); + console.log('DEBUG_FOUND_LOGIN_IN_FRAME=', !!foundLogin); + if (foundLogin) console.log('DEBUG_LOGIN_FRAME_URL=', foundLogin.frame.url()); + + if (!foundEditor) { + fs.writeFileSync('debug.html', await page.content()); + await page.screenshot({ path: 'debug-not-ready.png', fullPage: true }); + throw new Error('Editor not ready (no editor selector in any frame)'); } - return null; - } - - // ④ SPAなので少し待ってからフレーム走査 - await page.waitForTimeout(1500); - - console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); - await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('SCREENSHOT=' + SS_PATH); - - console.log('DEBUG_FRAME_COUNT=', page.frames().length); - console.log('DEBUG_FRAMES=', page.frames().map(f => f.url()).join(' | ')); - - const foundEditor = await findVisibleInAnyFrame(page, editorSel); - const foundLogin = await findVisibleInAnyFrame(page, loginSel); - - console.log('DEBUG_FOUND_EDITOR_IN_FRAME=', !!foundEditor); - if (foundEditor) console.log('DEBUG_EDITOR_FRAME_URL=', foundEditor.frame.url()); - console.log('DEBUG_FOUND_LOGIN_IN_FRAME=', !!foundLogin); - if (foundLogin) console.log('DEBUG_LOGIN_FRAME_URL=', foundLogin.frame.url()); - - if (!foundEditor) { - fs.writeFileSync('debug.html', await page.content()); - await page.screenshot({ path: 'debug-not-ready.png', fullPage: true }); - throw new Error('Editor not ready (no editor selector in any frame)'); - } From ed798703994cbda2379fb6f190d5ed33757047f2 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 23:33:13 +0900 Subject: [PATCH 16/32] Refactor browser context setup and logging Updated browser launch options and added user agent and viewport settings. Enhanced request and response logging for debugging. --- .github/workflows/note-perplexity.yaml | 49 +++++++++++++++++++++----- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 33edb8d..6545b4c 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -259,8 +259,27 @@ jobs: 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' }); + browser = await chromium.launch({ + headless: true, + args: [ + '--lang=ja-JP', + '--disable-blink-features=AutomationControlled', + '--no-sandbox', + ], + }); + + context = await browser.newContext({ + storageState: STATE_PATH, + locale: 'ja-JP', + viewport: { width: 1365, height: 900 }, + userAgent: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }); + + await context.addInitScript(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); + }); + await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); page = await context.newPage(); @@ -268,12 +287,21 @@ jobs: page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); - page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); - page.on('response', res => { - const status = res.status(); - const url = res.url(); - if (status >= 400 && /note\.com/.test(url)) console.log('HTTP_ERROR:', status, url); + page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); + page.on('request', req => { + const u = req.url(); + if (u.includes('note.com/api/v1/text_notes')) { + console.log('DEBUG_REQ:', req.method(), u); + } }); + + page.on('response', async res => { + const u = res.url(); + if (u.includes('note.com/api/v1/text_notes')) { + console.log('DEBUG_RES:', res.status(), res.request().method(), u); + } + }); + const rawState = fs.readFileSync(STATE_PATH, 'utf8'); console.log('DEBUG_STATE_BYTES=', rawState.length); @@ -289,7 +317,12 @@ jobs: console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - + + await page.waitForTimeout(3000); + await context.storageState({ path: 'runtime-state.json' }); + console.log('DEBUG_SAVED_RUNTIME_STATE=runtime-state.json'); + + // ① まずセレクタ配列を定義(←ここが先) const editorReadySelectors = [ From 7d009daa4cd615ca2ead84625d692beb5170cd60 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Wed, 11 Feb 2026 23:50:50 +0900 Subject: [PATCH 17/32] Update logging and screenshot paths in workflow --- .github/workflows/note-perplexity.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 6545b4c..7ecb58f 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -199,7 +199,9 @@ jobs: TITLE: ${{ steps.draft.outputs.TITLE }} TAGS: ${{ steps.draft.outputs.TAGS }} STATE_PATH: ${{ steps.state.outputs.STATE_PATH }} + run: | + : > post.log cat > post.mjs <<'EOF' import { chromium } from 'playwright'; import fs from 'fs'; @@ -479,7 +481,7 @@ jobs: await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('DRAFT_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); - process.exit(0); + return; } const proceed = page.locator('button:has-text("公開に進む")').first(); @@ -533,7 +535,7 @@ jobs: try { await browser?.close(); } catch {} } EOF - node post.mjs | tee post.log + node post.mjs 2>&1 | tee -a post.log url=$(grep '^PUBLISHED_URL=' post.log | tail -n1 | cut -d'=' -f2-) draft=$(grep '^DRAFT_URL=' post.log | tail -n1 | cut -d'=' -f2-) @@ -556,4 +558,5 @@ jobs: debug-login.png debug-not-ready.png trace.zip - /tmp/note-screenshots/** + note-screenshots/** + From 5d108766701c47fef7c9b90fc56eb1881c1b3a66 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 00:08:27 +0900 Subject: [PATCH 18/32] Refactor tracing and request handling in workflow --- .github/workflows/note-perplexity.yaml | 55 +++++++++++++++----------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 7ecb58f..d2ee48c 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -282,20 +282,20 @@ jobs: Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); }); - await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); - - page = await context.newPage(); - page.setDefaultTimeout(180000); - - page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); - page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); - page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); - page.on('request', req => { - const u = req.url(); - if (u.includes('note.com/api/v1/text_notes')) { - console.log('DEBUG_REQ:', req.method(), u); - } - }); + await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); + + page = await context.newPage(); + page.setDefaultTimeout(180000); + + page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); + page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); + page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); + page.on('request', req => { + const u = req.url(); + if (u.includes('note.com/api/v1/text_notes')) { + console.log('DEBUG_REQ:', req.method(), u); + } + }); page.on('response', async res => { const u = res.url(); @@ -472,17 +472,20 @@ jobs: await page.waitForTimeout(200); 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); - return; + 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); + + // ★トップレベルreturn禁止なので、正常終了用の例外で抜ける(finallyは必ず走る) + throw new Error('__DONE_DRAFT__'); + } + const proceed = page.locator('button:has-text("公開に進む")').first(); await proceed.waitFor({ state: 'visible' }); @@ -537,6 +540,10 @@ jobs: EOF node post.mjs 2>&1 | tee -a post.log + mkdir -p note-screenshots || true + cp -r /tmp/note-screenshots/* note-screenshots/ 2>/dev/null || true + + 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-) From 4904e2b53525a6471b1b2d36167fb52a7e7cb5c2 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 00:32:37 +0900 Subject: [PATCH 19/32] Update note-perplexity.yaml --- .github/workflows/note-perplexity.yaml | 92 ++++++++++++++++---------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index d2ee48c..0606745 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -328,10 +328,17 @@ jobs: // ① まずセレクタ配列を定義(←ここが先) const editorReadySelectors = [ - '[contenteditable="true"][data-placeholder*="記事タイトル"]', - '[contenteditable="true"][data-placeholder*="タイトル"]', - 'text=下書き', - ]; + // 最も汎用:contenteditable の存在(タイトル/本文どちらでも) + '[contenteditable="true"]', + + // ありがちな:textbox role + '[contenteditable="true"][role="textbox"]', + + // 念のため:見出しっぽい要素(React/Nextのエディタで使われがち) + '[data-testid*="editor"]', + '[data-testid*="title"]', + ]; + const loginOrBlockSelectors = [ 'text=ログイン', @@ -361,28 +368,45 @@ jobs: await page.waitForTimeout(1500); console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); + const urlNow = page.url(); + const isEditUrl = /\/notes\/[^/]+\/edit\/?/i.test(urlNow); + console.log('DEBUG_IS_EDIT_URL=', isEditUrl); + + if (!isEditUrl) { + // まだ /new のまま等なら、ここで待つ(最大60秒) + await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); + console.log('DEBUG_URL_AFTER_WAIT_EDIT=' + page.url()); + } + await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('SCREENSHOT=' + SS_PATH); console.log('DEBUG_FRAME_COUNT=', page.frames().length); console.log('DEBUG_FRAMES=', page.frames().map(f => f.url()).join(' | ')); + + // ---- ここが超重要:エディタのDOMが揃うまで待つ ---- + await page.waitForFunction(() => { + const els = Array.from(document.querySelectorAll('[contenteditable="true"]')); + const visible = els.filter(el => { + const r = el.getBoundingClientRect(); + return r.width > 30 && r.height > 10; + }); + return visible.length >= 2; // タイトル+本文を期待 + }, { timeout: 60000 }).catch(() => {}); + + const editable = page.locator('[contenteditable="true"]'); + const editableCount = await editable.count(); + console.log('DEBUG_EDITABLE_COUNT=', editableCount); - const foundEditor = await findVisibleInAnyFrame(page, editorSel); - const foundLogin = await findVisibleInAnyFrame(page, loginSel); - - console.log('DEBUG_FOUND_EDITOR_IN_FRAME=', !!foundEditor); - if (foundEditor) console.log('DEBUG_EDITOR_FRAME_URL=', foundEditor.frame.url()); - console.log('DEBUG_FOUND_LOGIN_IN_FRAME=', !!foundLogin); - if (foundLogin) console.log('DEBUG_LOGIN_FRAME_URL=', foundLogin.frame.url()); - - if (!foundEditor) { + if (editableCount < 1) { fs.writeFileSync('debug.html', await page.content()); await page.screenshot({ path: 'debug-not-ready.png', fullPage: true }); - throw new Error('Editor not ready (no editor selector in any frame)'); + throw new Error('Editor not ready (no contenteditable found)'); } + await Promise.race([ page.waitForSelector(editorReadySelectors.join(','), { timeout: 60000 }), page.waitForSelector(loginOrBlockSelectors.join(','), { timeout: 60000 }), @@ -412,27 +436,27 @@ jobs: } } - const titleCandidates = [ - '[contenteditable="true"][data-placeholder*="記事タイトル"]', - '[contenteditable="true"][data-placeholder*="タイトル"]', - '[contenteditable="true"][aria-placeholder*="記事タイトル"]', - '[contenteditable="true"][aria-placeholder*="タイトル"]', - '[contenteditable="true"][role="textbox"][aria-label*="記事タイトル"]', - '[contenteditable="true"][role="textbox"][aria-label*="タイトル"]', - 'textarea[placeholder*="タイトル"]', - 'textarea[aria-label*="タイトル"]', - 'input[placeholder*="タイトル"]', - 'input[aria-label*="タイトル"]', - ]; - - await page.waitForLoadState('networkidle').catch(() => {}); - await page.waitForTimeout(500); + // ---- タイトル/本文を「visible contenteditableの順序」で取る ---- + const allEditable = page.locator('[contenteditable="true"]'); + + // 画面上で見えてるものに絞る(width/heightで判定) + const visibleEditable = page.locator('[contenteditable="true"]').filter({ + has: page.locator(':scope') // ダミー(Playwrightのfilter構文用) + }); + + // Playwrightだけでサイズ判定しにくいので、indexベースでまず当てる + const titleBox = allEditable.nth(0); + const bodyBox = allEditable.nth(1); + + await titleBox.waitFor({ state: 'visible', timeout: 60000 }); + await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); + + // タイトル入力 + await titleBox.click({ force: true }); + await page.keyboard.press('Control+A'); + await page.keyboard.type(TITLE, { delay: 5 }); - let titleLocator = null; - for (const sel of titleCandidates) { - const loc = page.locator(sel).first(); - if (await loc.isVisible().catch(() => false)) { titleLocator = loc; break; } - } + if (!titleLocator) { await page.waitForTimeout(1500); for (const sel of titleCandidates) { From ec29abb3bb452ee258724b1e268dbd5d03507201 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 00:51:59 +0900 Subject: [PATCH 20/32] Update note-perplexity.yaml --- .github/workflows/note-perplexity.yaml | 55 ++++++-------------------- 1 file changed, 13 insertions(+), 42 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 0606745..5d1528e 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -437,16 +437,18 @@ jobs: } // ---- タイトル/本文を「visible contenteditableの順序」で取る ---- - const allEditable = page.locator('[contenteditable="true"]'); + const editables = page.locator('[contenteditable="true"]'); + const editableCount2 = await editables.count(); + console.log('DEBUG_EDITABLE_COUNT2=', editableCount2); - // 画面上で見えてるものに絞る(width/heightで判定) - const visibleEditable = page.locator('[contenteditable="true"]').filter({ - has: page.locator(':scope') // ダミー(Playwrightのfilter構文用) - }); + if (editableCount2 < 2) { + fs.writeFileSync('debug.html', await page.content()); + await page.screenshot({ path: 'debug-not-ready.png', fullPage: true }); + throw new Error('Editor not ready (need >=2 contenteditable)'); + } - // Playwrightだけでサイズ判定しにくいので、indexベースでまず当てる - const titleBox = allEditable.nth(0); - const bodyBox = allEditable.nth(1); + const titleBox = editables.nth(0); + const bodyBox = editables.nth(1); await titleBox.waitFor({ state: 'visible', timeout: 60000 }); await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); @@ -456,42 +458,11 @@ jobs: await page.keyboard.press('Control+A'); await page.keyboard.type(TITLE, { delay: 5 }); - - if (!titleLocator) { - await page.waitForTimeout(1500); - for (const sel of titleCandidates) { - const loc = page.locator(sel).first(); - if (await loc.isVisible().catch(() => false)) { titleLocator = loc; break; } - } - } - if (!titleLocator) { - console.log('DEBUG_TITLE_NOT_FOUND_URL=' + page.url()); - fs.writeFileSync('debug.html', await page.content()); - await page.screenshot({ path: 'debug.png', fullPage: true }); - throw new Error('Title input not found'); - } - - await titleLocator.waitFor({ state: 'visible', timeout: 180000 }); - - const tagName = await titleLocator.evaluate(el => el.tagName.toLowerCase()); - if (tagName === 'textarea' || tagName === 'input') { - await titleLocator.fill(TITLE); - } else { - await titleLocator.click({ force: true }); - await page.keyboard.press('Control+A'); - await page.keyboard.type(TITLE, { delay: 5 }); - } - - const textboxes = page.locator('[contenteditable="true"][role="textbox"]'); - await textboxes.first().waitFor({ state: 'visible' }); - - const bodyBox = textboxes.nth(1); - await bodyBox.waitFor({ state: 'visible', timeout: 180000 }); - - await bodyBox.click(); + // 本文入力(HTML挿入) + await bodyBox.click({ force: true }); await page.keyboard.press('Control+A'); await page.keyboard.press('Backspace'); - + await insertHTML(page, bodyBox, rawBodyHtml); await page.waitForTimeout(200); From 65e17dbfe053982a9fc7a132911f48b6e9dd058e Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 01:01:52 +0900 Subject: [PATCH 21/32] Refactor title and body input handling in workflow --- .github/workflows/note-perplexity.yaml | 70 +++++++++++++++++++------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 5d1528e..cd4059c 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -128,6 +128,9 @@ jobs: with: name: extracted-article path: final.json + debug-title.html + debug-title.png + - name: Collect final id: collect @@ -436,33 +439,64 @@ jobs: } } - // ---- タイトル/本文を「visible contenteditableの順序」で取る ---- - const editables = page.locator('[contenteditable="true"]'); - const editableCount2 = await editables.count(); - console.log('DEBUG_EDITABLE_COUNT2=', editableCount2); + // ===== タイトルは別要素、本文は contenteditable(0) を使う ===== + + // 本文(唯一のcontenteditable) + const bodyBox = page.locator('[contenteditable="true"]').first(); + await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); - if (editableCount2 < 2) { - fs.writeFileSync('debug.html', await page.content()); - await page.screenshot({ path: 'debug-not-ready.png', fullPage: true }); - throw new Error('Editor not ready (need >=2 contenteditable)'); + // タイトル(候補を広めに) + const titleCandidates = [ + // input/textarea 系(titleっぽい placeholder / name) + 'input[placeholder*="タイトル"]', + 'textarea[placeholder*="タイトル"]', + 'input[name*="title" i]', + 'textarea[name*="title" i]', + + // label / aria + 'input[aria-label*="タイトル"]', + 'textarea[aria-label*="タイトル"]', + + // data-testid / testid + '[data-testid*="title" i] input', + '[data-testid*="title" i] textarea', + '[data-testid*="title" i]', + + // role=textbox がタイトル側に付くケース + '[role="textbox"][aria-label*="タイトル"]', + '[role="textbox"][data-testid*="title" i]', + + // 最終手段:見出し入力っぽい1行textbox(複数ある場合は後で調整) + 'input[type="text"]', + ]; + + let titleEl = null; + for (const sel of titleCandidates) { + const loc = page.locator(sel).first(); + if (await loc.isVisible().catch(() => false)) { titleEl = loc; break; } } - const titleBox = editables.nth(0); - const bodyBox = editables.nth(1); + if (!titleEl) { + // DOM確認用にHTML/スクショ残して落とす + fs.writeFileSync('debug-title.html', await page.content()); + await page.screenshot({ path: 'debug-title.png', fullPage: true }); + throw new Error('Title element not found (candidates exhausted)'); + } - await titleBox.waitFor({ state: 'visible', timeout: 60000 }); - await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); + // タイトル入力(input/textareaならfill、それ以外はキーボード) + const tagName = await titleEl.evaluate(el => el.tagName.toLowerCase()); + if (tagName === 'input' || tagName === 'textarea') { + await titleEl.fill(TITLE); + } else { + await titleEl.click({ force: true }); + await page.keyboard.press('Control+A'); + await page.keyboard.type(TITLE, { delay: 5 }); + } - // タイトル入力 - await titleBox.click({ force: true }); - await page.keyboard.press('Control+A'); - await page.keyboard.type(TITLE, { delay: 5 }); - // 本文入力(HTML挿入) await bodyBox.click({ force: true }); await page.keyboard.press('Control+A'); await page.keyboard.press('Backspace'); - await insertHTML(page, bodyBox, rawBodyHtml); await page.waitForTimeout(200); From fa10fbf2e542aed4fcc4a9d0bf5fd4ed2a5a05dd Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 01:22:07 +0900 Subject: [PATCH 22/32] Refactor note-perplexity workflow for clarity and efficiency Refactor main function and streamline variable usage for clarity. Removed redundant checks and improved error handling. --- .github/workflows/note-perplexity.yaml | 511 +++++++++---------------- 1 file changed, 174 insertions(+), 337 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index cd4059c..66368cb 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -210,34 +210,13 @@ jobs: 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 rawBodyHtml = String(rawFinal.body_html || ''); - 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); - } - if (!rawBodyHtml.trim()) { - console.error('body_html is empty in final.json'); - 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(/^#+\s*/, ''); @@ -246,8 +225,8 @@ jobs: return s; } - async function insertHTML(page, locator, html) { - await locator.click(); + async function insertHTML(locator, html) { + await locator.click({ force: true }); await locator.evaluate((el, html) => { el.focus(); const sel = window.getSelection(); @@ -260,339 +239,197 @@ jobs: }, html); } - const TITLE = sanitizeTitle(rawTitle); - - let browser, context, page; - try { - browser = await chromium.launch({ - headless: true, - args: [ - '--lang=ja-JP', - '--disable-blink-features=AutomationControlled', - '--no-sandbox', - ], - }); - - context = await browser.newContext({ - storageState: STATE_PATH, - locale: 'ja-JP', - viewport: { width: 1365, height: 900 }, - userAgent: - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - }); - - await context.addInitScript(() => { - Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); - }); - - await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); - - page = await context.newPage(); - page.setDefaultTimeout(180000); - - page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); - page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); - page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); - page.on('request', req => { - const u = req.url(); - if (u.includes('note.com/api/v1/text_notes')) { - console.log('DEBUG_REQ:', req.method(), u); + async function main() { + 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 rawBodyHtml = String(rawFinal.body_html || ''); + 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); } - }); - - page.on('response', async res => { - const u = res.url(); - if (u.includes('note.com/api/v1/text_notes')) { - console.log('DEBUG_RES:', res.status(), res.request().method(), u); - } - }); - - - const rawState = fs.readFileSync(STATE_PATH, 'utf8'); - console.log('DEBUG_STATE_BYTES=', rawState.length); - - const cookies0 = await context.cookies(); - const noteCookies0 = cookies0.filter(c => (c.domain || '').includes('note.com')); - console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); - console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); - - const resp = await context.request.get('https://note.com/api/v2/current_user'); - console.log('DEBUG_current_user_status=', resp.status()); - const txt = await resp.text(); - console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); - - await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - - await page.waitForTimeout(3000); - await context.storageState({ path: 'runtime-state.json' }); - console.log('DEBUG_SAVED_RUNTIME_STATE=runtime-state.json'); - - - - // ① まずセレクタ配列を定義(←ここが先) - const editorReadySelectors = [ - // 最も汎用:contenteditable の存在(タイトル/本文どちらでも) - '[contenteditable="true"]', - - // ありがちな:textbox role - '[contenteditable="true"][role="textbox"]', - - // 念のため:見出しっぽい要素(React/Nextのエディタで使われがち) - '[data-testid*="editor"]', - '[data-testid*="title"]', - ]; - - - const loginOrBlockSelectors = [ - 'text=ログイン', - 'text=メールアドレス', - 'text=パスワード', - 'text=アクセスが制限されています', - ]; - - // ② その後に join して使う - const editorSel = editorReadySelectors.join(','); - const loginSel = loginOrBlockSelectors.join(','); - - // ③ フレーム含めて探す関数 - async function findVisibleInAnyFrame(page, selectorCsv) { - for (const fr of page.frames()) { - try { - const loc = fr.locator(selectorCsv).first(); - if (await loc.isVisible({ timeout: 500 }).catch(() => false)) { - return { frame: fr, locator: loc }; - } - } catch {} - } - return null; - } - - // ④ SPAなので少し待ってからフレーム走査 - await page.waitForTimeout(1500); - - console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); - const urlNow = page.url(); - const isEditUrl = /\/notes\/[^/]+\/edit\/?/i.test(urlNow); - console.log('DEBUG_IS_EDIT_URL=', isEditUrl); - - if (!isEditUrl) { - // まだ /new のまま等なら、ここで待つ(最大60秒) - await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); - console.log('DEBUG_URL_AFTER_WAIT_EDIT=' + page.url()); + if (!rawBodyHtml.trim()) { + console.error('body_html is empty in final.json'); + process.exit(1); } - - await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('SCREENSHOT=' + SS_PATH); - - console.log('DEBUG_FRAME_COUNT=', page.frames().length); - console.log('DEBUG_FRAMES=', page.frames().map(f => f.url()).join(' | ')); - - // ---- ここが超重要:エディタのDOMが揃うまで待つ ---- - await page.waitForFunction(() => { - const els = Array.from(document.querySelectorAll('[contenteditable="true"]')); - const visible = els.filter(el => { - const r = el.getBoundingClientRect(); - return r.width > 30 && r.height > 10; + + const ssDir = path.join(os.tmpdir(), 'note-screenshots'); + fs.mkdirSync(ssDir, { recursive: true }); + const SS_PATH = path.join(ssDir, `note-post-${nowStr()}.png`); + + const TITLE = sanitizeTitle(rawTitle); + + let browser, context, page; + try { + browser = await chromium.launch({ + headless: true, + args: [ + '--lang=ja-JP', + '--disable-blink-features=AutomationControlled', + '--no-sandbox', + ], }); - return visible.length >= 2; // タイトル+本文を期待 - }, { timeout: 60000 }).catch(() => {}); - - const editable = page.locator('[contenteditable="true"]'); - const editableCount = await editable.count(); - console.log('DEBUG_EDITABLE_COUNT=', editableCount); - - if (editableCount < 1) { - fs.writeFileSync('debug.html', await page.content()); - await page.screenshot({ path: 'debug-not-ready.png', fullPage: true }); - throw new Error('Editor not ready (no contenteditable found)'); - } - - - - - await Promise.race([ - page.waitForSelector(editorReadySelectors.join(','), { timeout: 60000 }), - page.waitForSelector(loginOrBlockSelectors.join(','), { timeout: 60000 }), - ]).catch(() => {}); - - console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); - await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('SCREENSHOT=' + SS_PATH); - const editorReady = page.locator(editorReadySelectors.join(',')).first(); - const loginLike = page.locator(loginOrBlockSelectors.join(',')).first(); - - const isEditorReady = await editorReady.isVisible().catch(() => false); - const isLoginLike = await loginLike.isVisible().catch(() => false); - - console.log('DEBUG_IS_EDITOR_READY=', isEditorReady); - console.log('DEBUG_IS_LOGIN_LIKE=', isLoginLike); - - if (!isEditorReady) { - fs.writeFileSync('debug.html', await page.content()); - if (isLoginLike) { - await page.screenshot({ path: 'debug-login.png', fullPage: true }); - throw new Error('Not logged in (login UI detected)'); - } else { - await page.screenshot({ path: 'debug-not-ready.png', fullPage: true }); - throw new Error('Editor not ready (JS init failed / blocked / network)'); - } - } + context = await browser.newContext({ + storageState: STATE_PATH, + locale: 'ja-JP', + viewport: { width: 1365, height: 900 }, + userAgent: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }); - // ===== タイトルは別要素、本文は contenteditable(0) を使う ===== - - // 本文(唯一のcontenteditable) - const bodyBox = page.locator('[contenteditable="true"]').first(); - await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); - - // タイトル(候補を広めに) - const titleCandidates = [ - // input/textarea 系(titleっぽい placeholder / name) - 'input[placeholder*="タイトル"]', - 'textarea[placeholder*="タイトル"]', - 'input[name*="title" i]', - 'textarea[name*="title" i]', - - // label / aria - 'input[aria-label*="タイトル"]', - 'textarea[aria-label*="タイトル"]', - - // data-testid / testid - '[data-testid*="title" i] input', - '[data-testid*="title" i] textarea', - '[data-testid*="title" i]', - - // role=textbox がタイトル側に付くケース - '[role="textbox"][aria-label*="タイトル"]', - '[role="textbox"][data-testid*="title" i]', - - // 最終手段:見出し入力っぽい1行textbox(複数ある場合は後で調整) - 'input[type="text"]', - ]; - - let titleEl = null; - for (const sel of titleCandidates) { - const loc = page.locator(sel).first(); - if (await loc.isVisible().catch(() => false)) { titleEl = loc; break; } - } - - if (!titleEl) { - // DOM確認用にHTML/スクショ残して落とす - fs.writeFileSync('debug-title.html', await page.content()); - await page.screenshot({ path: 'debug-title.png', fullPage: true }); - throw new Error('Title element not found (candidates exhausted)'); - } - - // タイトル入力(input/textareaならfill、それ以外はキーボード) - const tagName = await titleEl.evaluate(el => el.tagName.toLowerCase()); - if (tagName === 'input' || tagName === 'textarea') { - await titleEl.fill(TITLE); - } else { - await titleEl.click({ force: true }); + await context.addInitScript(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); + }); + + await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); + + page = await context.newPage(); + page.setDefaultTimeout(180000); + + page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); + page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); + page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); + page.on('request', req => { + const u = req.url(); + if (u.includes('note.com/api/v1/text_notes')) console.log('DEBUG_REQ:', req.method(), u); + }); + page.on('response', res => { + const u = res.url(); + if (u.includes('note.com/api/v1/text_notes')) console.log('DEBUG_RES:', res.status(), res.request().method(), u); + }); + + const rawState = fs.readFileSync(STATE_PATH, 'utf8'); + console.log('DEBUG_STATE_BYTES=', rawState.length); + + const cookies0 = await context.cookies(); + const noteCookies0 = cookies0.filter(c => (c.domain || '').includes('note.com')); + console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); + console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); + + const resp = await context.request.get('https://note.com/api/v2/current_user'); + console.log('DEBUG_current_user_status=', resp.status()); + const txt = await resp.text(); + console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); + + await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); + await page.waitForTimeout(3000); + + await context.storageState({ path: 'runtime-state.json' }); + console.log('DEBUG_SAVED_RUNTIME_STATE=runtime-state.json'); + + // /new → /notes/.../edit/ に遷移するのを待つ + console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); + await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); + console.log('DEBUG_URL_AFTER_WAIT_EDIT=' + page.url()); + + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('SCREENSHOT=' + SS_PATH); + + // エディタ準備:contenteditable が見えるまで待つ(今のUIは1個) + await page.waitForSelector('[contenteditable="true"]', { timeout: 60000 }); + const editor = page.locator('[contenteditable="true"]').first(); + + const editableCount = await page.locator('[contenteditable="true"]').count(); + console.log('DEBUG_EDITABLE_COUNT=', editableCount); + + // タイトル+本文を同一エディタに投入(タイトル→改行→本文HTML) + await editor.click({ force: true }); await page.keyboard.press('Control+A'); + await page.keyboard.press('Backspace'); + + // まずタイトルを入力して改行(ProseMirrorは Enter が段落になる) await page.keyboard.type(TITLE, { delay: 5 }); - } - - // 本文入力(HTML挿入) - await bodyBox.click({ force: true }); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Backspace'); - await insertHTML(page, bodyBox, rawBodyHtml); - await page.waitForTimeout(200); - - 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); - - // ★トップレベルreturn禁止なので、正常終了用の例外で抜ける(finallyは必ず走る) - throw new Error('__DONE_DRAFT__'); - } - - - 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); + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); // 見た目余白(好みで1回にしてOK) + + // HTML本文を挿入(末尾へ) + await insertHTML(editor, rawBodyHtml); + await page.waitForTimeout(300); + + if (!IS_PUBLIC) { + const saveBtn = page.locator('button:has-text("下書き保存"), [aria-label*="下書き保存"]').first(); + await saveBtn.waitFor({ state: 'visible', timeout: 60000 }); + await saveBtn.click({ force: true }); + await page.locator('text=保存しました').waitFor({ timeout: 10000 }).catch(() => {}); + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('DRAFT_URL=' + page.url()); + console.log('SCREENSHOT=' + SS_PATH); + throw new Error('__DONE_DRAFT__'); } + + // 公開フロー(必要ならここを詰める) + const proceed = page.locator('button:has-text("公開に進む")').first(); + await proceed.waitFor({ state: 'visible', timeout: 60000 }); + 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', timeout: 60000 }); + for (const t of tags) { + await tagInput.click({ force: true }); + await tagInput.fill(t); + await page.keyboard.press('Enter'); + await page.waitForTimeout(120); + } + } + + const publishBtn = page.locator('button:has-text("投稿する")').first(); + await publishBtn.waitFor({ state: 'visible', timeout: 60000 }); + 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: 10000 }).catch(() => {}), + page.waitForTimeout(5000) + ]); + + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('PUBLISHED_URL=' + page.url()); + console.log('SCREENSHOT=' + SS_PATH); + + } finally { + try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} + try { await page?.close(); } catch {} + try { await context?.close(); } catch {} + try { await browser?.close(); } catch {} } + } - 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); + main().catch((e) => { + // 下書き保存は成功終了にする + if (e && e.message === '__DONE_DRAFT__') { + console.log('DEBUG: done draft -> exit 0'); + process.exit(0); } - 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 }); - console.log('PUBLISHED_URL=' + page.url()); - console.log('SCREENSHOT=' + SS_PATH); - - } finally { - try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} - try { await page?.close(); } catch {} - try { await context?.close(); } catch {} - try { await browser?.close(); } catch {} - } + console.error(e); + process.exit(1); + }); EOF + node post.mjs 2>&1 | tee -a post.log - + mkdir -p note-screenshots || true cp -r /tmp/note-screenshots/* note-screenshots/ 2>/dev/null || true - - + 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 debug artifacts - if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: note-debug-${{ github.run_id }} - if-no-files-found: warn - path: | - post.log - debug.html - debug.png - debug-login.png - debug-not-ready.png - trace.zip - note-screenshots/** From 775d527b6c7763e32ef88000026a5de5d2dcb0fd Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 01:36:25 +0900 Subject: [PATCH 23/32] Update note-perplexity.yaml --- .github/workflows/note-perplexity.yaml | 395 +++++++++++++------------ 1 file changed, 212 insertions(+), 183 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 66368cb..3066b10 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -123,13 +123,19 @@ jobs: EOF node fetch.mjs + - name: Verify extracted files + run: | + ls -la + test -f final.json && echo "final.json exists" || (echo "final.json missing" && exit 1) + - name: Upload extracted artifact uses: actions/upload-artifact@v4 with: name: extracted-article - path: final.json - debug-title.html - debug-title.png + if-no-files-found: error + path: | + final.json + - name: Collect final @@ -216,7 +222,28 @@ jobs: 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 rawBodyHtml = String(rawFinal.body_html || ''); + 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); + } + if (!rawBodyHtml.trim()) { + console.error('body_html is empty in final.json'); + 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(/^#+\s*/, ''); @@ -224,199 +251,202 @@ jobs: if (!s) s = 'タイトル(自動生成)'; return s; } - - async function insertHTML(locator, html) { - await locator.click({ force: true }); - 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); + + // HTML -> テキスト(最低限。まずは本文が確実に入るのを優先) + function htmlToText(html) { + return String(html || '') + .replace(//gi, '') + .replace(//gi, '') + .replace(//gi, '\n') + .replace(/<\/p>/gi, '\n\n') + .replace(/<\/h[1-6]>/gi, '\n\n') + .replace(/
  • /gi, '・') + .replace(/<\/li>/gi, '\n') + .replace(/<[^>]+>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\n{3,}/g, '\n\n') + .trim(); } - - async function main() { - 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 rawBodyHtml = String(rawFinal.body_html || ''); - 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); - } - if (!rawBodyHtml.trim()) { - console.error('body_html is empty in final.json'); - 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`); - - const TITLE = sanitizeTitle(rawTitle); - - let browser, context, page; - try { - browser = await chromium.launch({ - headless: true, - args: [ - '--lang=ja-JP', - '--disable-blink-features=AutomationControlled', - '--no-sandbox', - ], - }); - - context = await browser.newContext({ - storageState: STATE_PATH, - locale: 'ja-JP', - viewport: { width: 1365, height: 900 }, - userAgent: - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - }); - - await context.addInitScript(() => { - Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); - }); - - await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); - - page = await context.newPage(); - page.setDefaultTimeout(180000); - - page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); - page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); - page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); - page.on('request', req => { - const u = req.url(); - if (u.includes('note.com/api/v1/text_notes')) console.log('DEBUG_REQ:', req.method(), u); - }); - page.on('response', res => { - const u = res.url(); - if (u.includes('note.com/api/v1/text_notes')) console.log('DEBUG_RES:', res.status(), res.request().method(), u); - }); - - const rawState = fs.readFileSync(STATE_PATH, 'utf8'); - console.log('DEBUG_STATE_BYTES=', rawState.length); - - const cookies0 = await context.cookies(); - const noteCookies0 = cookies0.filter(c => (c.domain || '').includes('note.com')); - console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); - console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); - - const resp = await context.request.get('https://note.com/api/v2/current_user'); - console.log('DEBUG_current_user_status=', resp.status()); - const txt = await resp.text(); - console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); - - await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(3000); - - await context.storageState({ path: 'runtime-state.json' }); - console.log('DEBUG_SAVED_RUNTIME_STATE=runtime-state.json'); - - // /new → /notes/.../edit/ に遷移するのを待つ - console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); - await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); - console.log('DEBUG_URL_AFTER_WAIT_EDIT=' + page.url()); + // ProseMirrorに対して「貼り付け(paste)」で流し込む + async function pasteText(page, locator, text) { + await locator.click({ force: true }); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Backspace'); - await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('SCREENSHOT=' + SS_PATH); + // クリップボード経由が最も安定 + await page.evaluate(async (t) => { + await navigator.clipboard.writeText(t); + }, text); - // エディタ準備:contenteditable が見えるまで待つ(今のUIは1個) - await page.waitForSelector('[contenteditable="true"]', { timeout: 60000 }); - const editor = page.locator('[contenteditable="true"]').first(); + await page.keyboard.press('Control+V'); + } - const editableCount = await page.locator('[contenteditable="true"]').count(); - console.log('DEBUG_EDITABLE_COUNT=', editableCount); + const TITLE = sanitizeTitle(rawTitle); + const BODY_TEXT = htmlToText(rawBodyHtml); + + let browser, context, page; + try { + browser = await chromium.launch({ + headless: true, + args: [ + '--lang=ja-JP', + '--disable-blink-features=AutomationControlled', + '--no-sandbox', + ], + }); + + context = await browser.newContext({ + storageState: STATE_PATH, + locale: 'ja-JP', + viewport: { width: 1365, height: 900 }, + userAgent: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }); + + await context.addInitScript(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); + }); + + await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); + + page = await context.newPage(); + page.setDefaultTimeout(180000); + + page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); + page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); + page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); + + const rawState = fs.readFileSync(STATE_PATH, 'utf8'); + console.log('DEBUG_STATE_BYTES=', rawState.length); + + const cookies0 = await context.cookies(); + const noteCookies0 = cookies0.filter(c => (c.domain || '').includes('note.com')); + console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); + console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); + + const resp = await context.request.get('https://note.com/api/v2/current_user'); + console.log('DEBUG_current_user_status=', resp.status()); + const txt = await resp.text(); + console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); + + await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); + await page.waitForTimeout(3000); + + // /new -> /notes/.../edit に遷移するのを待つ + await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); + console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); + + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('SCREENSHOT=' + SS_PATH); + + // 本文: contenteditable は 1つだけ(あなたのログ/スクショ通り) + const bodyBox = page.locator('[contenteditable="true"]').first(); + await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); + + // タイトル: input/textarea を広めに探索 + const titleCandidates = [ + 'input[placeholder*="タイトル"]', + 'textarea[placeholder*="タイトル"]', + 'input[name*="title" i]', + 'textarea[name*="title" i]', + 'input[aria-label*="タイトル"]', + 'textarea[aria-label*="タイトル"]', + '[data-testid*="title" i] input', + '[data-testid*="title" i] textarea', + '[data-testid*="title" i]', + 'input[type="text"]', + ]; + + let titleEl = null; + for (const sel of titleCandidates) { + const loc = page.locator(sel).first(); + if (await loc.isVisible().catch(() => false)) { titleEl = loc; break; } + } - // タイトル+本文を同一エディタに投入(タイトル→改行→本文HTML) - await editor.click({ force: true }); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Backspace'); + if (!titleEl) { + fs.writeFileSync('debug-title.html', await page.content()); + await page.screenshot({ path: 'debug-title.png', fullPage: true }); + throw new Error('Title element not found (candidates exhausted)'); + } - // まずタイトルを入力して改行(ProseMirrorは Enter が段落になる) + const tagName = await titleEl.evaluate(el => el.tagName.toLowerCase()); + if (tagName === 'input' || tagName === 'textarea') { + await titleEl.fill(TITLE); + } else { + await titleEl.click({ force: true }); + await page.keyboard.press('Control+A'); await page.keyboard.type(TITLE, { delay: 5 }); - await page.keyboard.press('Enter'); - await page.keyboard.press('Enter'); // 見た目余白(好みで1回にしてOK) - - // HTML本文を挿入(末尾へ) - await insertHTML(editor, rawBodyHtml); - await page.waitForTimeout(300); - - if (!IS_PUBLIC) { - const saveBtn = page.locator('button:has-text("下書き保存"), [aria-label*="下書き保存"]').first(); - await saveBtn.waitFor({ state: 'visible', timeout: 60000 }); - await saveBtn.click({ force: true }); - await page.locator('text=保存しました').waitFor({ timeout: 10000 }).catch(() => {}); - await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('DRAFT_URL=' + page.url()); - console.log('SCREENSHOT=' + SS_PATH); - throw new Error('__DONE_DRAFT__'); - } - - // 公開フロー(必要ならここを詰める) - const proceed = page.locator('button:has-text("公開に進む")').first(); - await proceed.waitFor({ state: 'visible', timeout: 60000 }); - 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', timeout: 60000 }); - for (const t of tags) { - await tagInput.click({ force: true }); - await tagInput.fill(t); - await page.keyboard.press('Enter'); - await page.waitForTimeout(120); - } - } - - const publishBtn = page.locator('button:has-text("投稿する")').first(); - await publishBtn.waitFor({ state: 'visible', timeout: 60000 }); - 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: 10000 }).catch(() => {}), - page.waitForTimeout(5000) - ]); + // 本文(確実に入る方法:paste) + await pasteText(page, bodyBox, BODY_TEXT); + await page.waitForTimeout(500); + 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: 8000 }).catch(() => {}); + } await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('PUBLISHED_URL=' + page.url()); + console.log('DRAFT_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); + throw new Error('__DONE_DRAFT__'); + } - } finally { - try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} - try { await page?.close(); } catch {} - try { await context?.close(); } catch {} - try { await browser?.close(); } catch {} + // 公開フロー(必要ならここも後で調整) + const proceed = page.locator('button:has-text("公開に進む")').first(); + await proceed.waitFor({ state: 'visible' }); + 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); + } } - } - - main().catch((e) => { - // 下書き保存は成功終了にする - if (e && e.message === '__DONE_DRAFT__') { - console.log('DEBUG: done draft -> exit 0'); - process.exit(0); + + const publishBtn = page.locator('button:has-text("投稿する")').first(); + await publishBtn.waitFor({ state: 'visible' }); + await publishBtn.click({ force: true }); + + await page.waitForTimeout(5000); + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('PUBLISHED_URL=' + page.url()); + console.log('SCREENSHOT=' + SS_PATH); + + } catch (e) { + // 下書き成功は「成功扱い」にする + if (String(e?.message || e).includes('__DONE_DRAFT__')) { + process.exitCode = 0; + } else { + console.error(e); + process.exitCode = 1; } - console.error(e); - process.exit(1); - }); + } finally { + try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} + try { await page?.close(); } catch {} + try { await context?.close(); } catch {} + try { await browser?.close(); } catch {} + } EOF node post.mjs 2>&1 | tee -a post.log @@ -432,4 +462,3 @@ jobs: if [ -n "$draft" ]; then echo "draft_url=$draft" >> $GITHUB_OUTPUT; fi if [ -n "$shot" ]; then echo "screenshot=$shot" >> $GITHUB_OUTPUT; fi - From cef484ac48ef9019d1d6c0e3265089ded4390736 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 01:48:26 +0900 Subject: [PATCH 24/32] Refactor HTML handling and improve insertion logic --- .github/workflows/note-perplexity.yaml | 360 +++++++++++++------------ 1 file changed, 181 insertions(+), 179 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 3066b10..004ed98 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -251,202 +251,205 @@ jobs: if (!s) s = 'タイトル(自動生成)'; return s; } + const TITLE = sanitizeTitle(rawTitle); - // HTML -> テキスト(最低限。まずは本文が確実に入るのを優先) - function htmlToText(html) { - return String(html || '') - .replace(//gi, '') - .replace(//gi, '') - .replace(//gi, '\n') - .replace(/<\/p>/gi, '\n\n') - .replace(/<\/h[1-6]>/gi, '\n\n') - .replace(/
  • /gi, '・') - .replace(/<\/li>/gi, '\n') - .replace(/<[^>]+>/g, '') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/\n{3,}/g, '\n\n') - .trim(); + // Clipboard禁止。本文は execCommand(insertHTML) で入れる + async function insertHTML(locator, html) { + await locator.evaluate((el, html) => { + el.focus(); + // 既存を消してからHTML挿入 + const sel = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(el); + sel.removeAllRanges(); + sel.addRange(range); + document.execCommand('delete', false, null); + document.execCommand('insertHTML', false, html); + }, html); } - // ProseMirrorに対して「貼り付け(paste)」で流し込む - async function pasteText(page, locator, text) { - await locator.click({ force: true }); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Backspace'); + async function main() { + let browser, context, page; + + try { + browser = await chromium.launch({ + headless: true, + args: [ + '--lang=ja-JP', + '--disable-blink-features=AutomationControlled', + '--no-sandbox', + ], + }); + + context = await browser.newContext({ + storageState: STATE_PATH, + locale: 'ja-JP', + viewport: { width: 1365, height: 900 }, + userAgent: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }); - // クリップボード経由が最も安定 - await page.evaluate(async (t) => { - await navigator.clipboard.writeText(t); - }, text); + await context.addInitScript(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); + }); - await page.keyboard.press('Control+V'); - } + await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); - const TITLE = sanitizeTitle(rawTitle); - const BODY_TEXT = htmlToText(rawBodyHtml); - - let browser, context, page; - try { - browser = await chromium.launch({ - headless: true, - args: [ - '--lang=ja-JP', - '--disable-blink-features=AutomationControlled', - '--no-sandbox', - ], - }); - - context = await browser.newContext({ - storageState: STATE_PATH, - locale: 'ja-JP', - viewport: { width: 1365, height: 900 }, - userAgent: - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - }); - - await context.addInitScript(() => { - Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); - }); - - await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); - - page = await context.newPage(); - page.setDefaultTimeout(180000); - - page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); - page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); - page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); - - const rawState = fs.readFileSync(STATE_PATH, 'utf8'); - console.log('DEBUG_STATE_BYTES=', rawState.length); - - const cookies0 = await context.cookies(); - const noteCookies0 = cookies0.filter(c => (c.domain || '').includes('note.com')); - console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); - console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); - - const resp = await context.request.get('https://note.com/api/v2/current_user'); - console.log('DEBUG_current_user_status=', resp.status()); - const txt = await resp.text(); - console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); - - await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(3000); - - // /new -> /notes/.../edit に遷移するのを待つ - await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); - console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); - - await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('SCREENSHOT=' + SS_PATH); - - // 本文: contenteditable は 1つだけ(あなたのログ/スクショ通り) - const bodyBox = page.locator('[contenteditable="true"]').first(); - await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); - - // タイトル: input/textarea を広めに探索 - const titleCandidates = [ - 'input[placeholder*="タイトル"]', - 'textarea[placeholder*="タイトル"]', - 'input[name*="title" i]', - 'textarea[name*="title" i]', - 'input[aria-label*="タイトル"]', - 'textarea[aria-label*="タイトル"]', - '[data-testid*="title" i] input', - '[data-testid*="title" i] textarea', - '[data-testid*="title" i]', - 'input[type="text"]', - ]; - - let titleEl = null; - for (const sel of titleCandidates) { - const loc = page.locator(sel).first(); - if (await loc.isVisible().catch(() => false)) { titleEl = loc; break; } - } + page = await context.newPage(); + page.setDefaultTimeout(180000); - if (!titleEl) { - fs.writeFileSync('debug-title.html', await page.content()); - await page.screenshot({ path: 'debug-title.png', fullPage: true }); - throw new Error('Title element not found (candidates exhausted)'); - } + page.on('console', msg => console.log('BROWSER_CONSOLE:', msg.type(), msg.text())); + page.on('pageerror', err => console.log('BROWSER_PAGEERROR:', err.message)); + page.on('requestfailed', req => console.log('REQ_FAILED:', req.url(), req.failure()?.errorText)); - const tagName = await titleEl.evaluate(el => el.tagName.toLowerCase()); - if (tagName === 'input' || tagName === 'textarea') { - await titleEl.fill(TITLE); - } else { - await titleEl.click({ force: true }); - await page.keyboard.press('Control+A'); - await page.keyboard.type(TITLE, { delay: 5 }); - } + const rawState = fs.readFileSync(STATE_PATH, 'utf8'); + console.log('DEBUG_STATE_BYTES=', rawState.length); - // 本文(確実に入る方法:paste) - await pasteText(page, bodyBox, BODY_TEXT); - await page.waitForTimeout(500); + const cookies0 = await context.cookies(); + const noteCookies0 = cookies0.filter(c => (c.domain || '').includes('note.com')); + console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); + console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); - 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: 8000 }).catch(() => {}); + const resp = await context.request.get('https://note.com/api/v2/current_user'); + console.log('DEBUG_current_user_status=', resp.status()); + const txt = await resp.text(); + console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); + + await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); + await page.waitForTimeout(3000); + + await context.storageState({ path: 'runtime-state.json' }); + console.log('DEBUG_SAVED_RUNTIME_STATE=runtime-state.json'); + + const urlNow = page.url(); + console.log('DEBUG_URL_AFTER_GOTO=' + urlNow); + + // /new から edit URL に遷移することがあるので待つ + if (!/\/notes\/[^/]+\/edit\/?/i.test(urlNow)) { + await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); + console.log('DEBUG_URL_AFTER_WAIT_EDIT=' + page.url()); } + await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('DRAFT_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); - throw new Error('__DONE_DRAFT__'); - } - // 公開フロー(必要ならここも後で調整) - const proceed = page.locator('button:has-text("公開に進む")').first(); - await proceed.waitFor({ state: 'visible' }); - 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); + // ここが肝:あなたのスクショ状況だと contenteditable が「本文の1つだけ」 + const bodyBox = page.locator('[contenteditable="true"]').first(); + await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); + + // タイトルは UI 側で既に入っている/別管理のことがあるので + // 「見つかったら入れる、無ければスキップ」にする(失敗で止めない) + const titleCandidates = [ + 'input[placeholder*="タイトル"]', + 'textarea[placeholder*="タイトル"]', + 'input[name*="title" i]', + 'textarea[name*="title" i]', + 'input[aria-label*="タイトル"]', + 'textarea[aria-label*="タイトル"]', + '[data-testid*="title" i] input', + '[data-testid*="title" i] textarea', + '[data-testid*="title" i]', + ]; + + let titleEl = null; + for (const sel of titleCandidates) { + const loc = page.locator(sel).first(); + if (await loc.isVisible().catch(() => false)) { titleEl = loc; break; } } - } - const publishBtn = page.locator('button:has-text("投稿する")').first(); - await publishBtn.waitFor({ state: 'visible' }); - await publishBtn.click({ force: true }); - - await page.waitForTimeout(5000); - await page.screenshot({ path: SS_PATH, fullPage: true }); - console.log('PUBLISHED_URL=' + page.url()); - console.log('SCREENSHOT=' + SS_PATH); - - } catch (e) { - // 下書き成功は「成功扱い」にする - if (String(e?.message || e).includes('__DONE_DRAFT__')) { - process.exitCode = 0; - } else { - console.error(e); - process.exitCode = 1; + if (titleEl) { + const tagName = await titleEl.evaluate(el => el.tagName.toLowerCase()); + if (tagName === 'input' || tagName === 'textarea') { + await titleEl.fill(TITLE); + } else { + await titleEl.click({ force: true }); + await page.keyboard.press('Control+A'); + await page.keyboard.type(TITLE, { delay: 5 }); + } + console.log('DEBUG_TITLE_SET=1'); + } else { + // デバッグ用に残す(タイトル入力がDOMに無いパターン) + fs.writeFileSync('debug-title.html', await page.content()); + await page.screenshot({ path: 'debug-title.png', fullPage: true }); + console.log('DEBUG_TITLE_SET=0 (no title element found)'); + } + + // 本文挿入(Clipboard不使用) + await bodyBox.click({ force: true }); + await insertHTML(bodyBox, rawBodyHtml); + await page.waitForTimeout(300); + + if (!IS_PUBLIC) { + const saveBtn = page.locator('button:has-text("下書き保存"), [aria-label*="下書き保存"]').first(); + await saveBtn.waitFor({ state: 'visible', timeout: 60000 }); + if (await saveBtn.isEnabled().catch(() => false)) { + await saveBtn.click(); + await page.locator('text=保存しました').waitFor({ timeout: 8000 }).catch(() => {}); + } + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('DRAFT_URL=' + page.url()); + console.log('SCREENSHOT=' + SS_PATH); + return; // main() なら return OK + } + + const proceed = page.locator('button:has-text("公開に進む")').first(); + await proceed.waitFor({ state: 'visible', timeout: 60000 }); + for (let i = 0; i < 20; i++) { + if (await proceed.isEnabled().catch(() => false)) 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', timeout: 60000 }); + for (const t of tags) { + await tagInput.click(); + await tagInput.fill(t); + await page.keyboard.press('Enter'); + await page.waitForTimeout(120); + } + } + + const publishBtn = page.locator('button:has-text("投稿する")').first(); + await publishBtn.waitFor({ state: 'visible', timeout: 60000 }); + for (let i = 0; i < 20; i++) { + if (await publishBtn.isEnabled().catch(() => false)) 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: 30000 }).catch(() => {}), + page.locator('text=投稿しました').first().waitFor({ timeout: 12000 }).catch(() => {}), + page.waitForTimeout(5000) + ]); + + await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('PUBLISHED_URL=' + page.url()); + console.log('SCREENSHOT=' + SS_PATH); + + } finally { + try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} + try { await page?.close(); } catch {} + try { await context?.close(); } catch {} + try { await browser?.close(); } catch {} } - } finally { - try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} - try { await page?.close(); } catch {} - try { await context?.close(); } catch {} - try { await browser?.close(); } catch {} } + + // main()でreturnできるようにしたので、エラーで落とさない + main().catch(e => { + console.error(e); + process.exit(1); + }); EOF node post.mjs 2>&1 | tee -a post.log @@ -461,4 +464,3 @@ jobs: 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 - From 01b1ca3098371a234c6a930ba79ff67eb8c8e265 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 01:56:06 +0900 Subject: [PATCH 25/32] Refactor insertHTML function and improve error handling --- .github/workflows/note-perplexity.yaml | 124 +++++++++++++------------ 1 file changed, 65 insertions(+), 59 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 004ed98..6a44e3f 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -224,9 +224,9 @@ jobs: } 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 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 rawBodyHtml = String(rawFinal.body_html || ''); const TAGS = process.env.TAGS || ''; const IS_PUBLIC = String(process.env.IS_PUBLIC || 'false') === 'true'; @@ -253,24 +253,46 @@ jobs: } const TITLE = sanitizeTitle(rawTitle); - // Clipboard禁止。本文は execCommand(insertHTML) で入れる - async function insertHTML(locator, html) { - await locator.evaluate((el, html) => { + // ProseMirror向け:clipboard不使用で HTML を挿入 + async function insertHTMLWithoutClipboard(page, targetLocator, html) { + await targetLocator.click({ force: true }); + // 全選択→削除 + await page.keyboard.press('Control+A'); + await page.keyboard.press('Backspace'); + + // execCommandでHTMLを挿入(clipboard不要) + await page.evaluate(({ selector, html }) => { + const el = document.querySelector(selector); + if (!el) throw new Error('insert target not found: ' + selector); + el.focus(); - // 既存を消してからHTML挿入 - const sel = window.getSelection(); + + // 末尾にカーソル(念のため) const range = document.createRange(); range.selectNodeContents(el); + range.collapse(false); + const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); - document.execCommand('delete', false, null); - document.execCommand('insertHTML', false, html); - }, html); + + // HTML挿入(失敗する環境もあるのでフォールバックも用意) + const ok = document.execCommand('insertHTML', false, html); + if (!ok) { + // 最終手段:HTMLタグを剥がしてテキストとして挿入 + const tmp = document.createElement('div'); + tmp.innerHTML = html; + const text = tmp.innerText || tmp.textContent || ''; + document.execCommand('insertText', false, text); + } + }, { selector: await targetLocator.evaluate(el => { + // その要素を再取得できるように一時属性を付与してセレクタ化 + el.setAttribute('data-auto-target', '1'); + return '[data-auto-target="1"]'; + }), html }); } async function main() { let browser, context, page; - try { browser = await chromium.launch({ headless: true, @@ -280,7 +302,7 @@ jobs: '--no-sandbox', ], }); - + context = await browser.newContext({ storageState: STATE_PATH, locale: 'ja-JP', @@ -316,39 +338,37 @@ jobs: console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(3000); + await page.waitForTimeout(2500); - await context.storageState({ path: 'runtime-state.json' }); - console.log('DEBUG_SAVED_RUNTIME_STATE=runtime-state.json'); - - const urlNow = page.url(); - console.log('DEBUG_URL_AFTER_GOTO=' + urlNow); - - // /new から edit URL に遷移することがあるので待つ - if (!/\/notes\/[^/]+\/edit\/?/i.test(urlNow)) { - await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); - console.log('DEBUG_URL_AFTER_WAIT_EDIT=' + page.url()); - } + // /new → 自動で /notes/.../edit/ に遷移するので待つ + await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); + console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('SCREENSHOT=' + SS_PATH); - // ここが肝:あなたのスクショ状況だと contenteditable が「本文の1つだけ」 - const bodyBox = page.locator('[contenteditable="true"]').first(); - await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); + // ---- 本文(ProseMirror)を最優先で掴む ---- + const bodyBox = page.locator('div.ProseMirror[contenteditable="true"]').first(); + if (!(await bodyBox.count())) { + // フォールバック + console.log('DEBUG_BODY_FALLBACK=using [contenteditable=true]'); + } + const body = (await bodyBox.count()) + ? bodyBox + : page.locator('[contenteditable="true"]').first(); + + await body.waitFor({ state: 'visible', timeout: 60000 }); - // タイトルは UI 側で既に入っている/別管理のことがあるので - // 「見つかったら入れる、無ければスキップ」にする(失敗で止めない) + // ---- タイトルは「見つかったら入れる」程度にする(既に入ってることが多い)---- const titleCandidates = [ 'input[placeholder*="タイトル"]', 'textarea[placeholder*="タイトル"]', - 'input[name*="title" i]', - 'textarea[name*="title" i]', 'input[aria-label*="タイトル"]', 'textarea[aria-label*="タイトル"]', '[data-testid*="title" i] input', '[data-testid*="title" i] textarea', '[data-testid*="title" i]', + 'h1', // もうタイトルが表示されてるだけのケース ]; let titleEl = null; @@ -362,23 +382,20 @@ jobs: if (tagName === 'input' || tagName === 'textarea') { await titleEl.fill(TITLE); } else { - await titleEl.click({ force: true }); - await page.keyboard.press('Control+A'); - await page.keyboard.type(TITLE, { delay: 5 }); + // h1等で編集できない場合があるので、無理に落とさない + await titleEl.click({ force: true }).catch(() => {}); + await page.keyboard.press('Control+A').catch(() => {}); + await page.keyboard.type(TITLE, { delay: 3 }).catch(() => {}); } - console.log('DEBUG_TITLE_SET=1'); } else { - // デバッグ用に残す(タイトル入力がDOMに無いパターン) - fs.writeFileSync('debug-title.html', await page.content()); - await page.screenshot({ path: 'debug-title.png', fullPage: true }); - console.log('DEBUG_TITLE_SET=0 (no title element found)'); + console.log('DEBUG_TITLE_SKIP=title element not found (but ok)'); } - // 本文挿入(Clipboard不使用) - await bodyBox.click({ force: true }); - await insertHTML(bodyBox, rawBodyHtml); + // ---- 本文を挿入(clipboardは使わない)---- + await insertHTMLWithoutClipboard(page, body, rawBodyHtml); await page.waitForTimeout(300); + // ---- 下書き保存 or 公開 ---- if (!IS_PUBLIC) { const saveBtn = page.locator('button:has-text("下書き保存"), [aria-label*="下書き保存"]').first(); await saveBtn.waitFor({ state: 'visible', timeout: 60000 }); @@ -389,15 +406,11 @@ jobs: await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('DRAFT_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); - return; // main() なら return OK + return; // async main の return はOK } const proceed = page.locator('button:has-text("公開に進む")').first(); await proceed.waitFor({ state: 'visible', timeout: 60000 }); - for (let i = 0; i < 20; i++) { - if (await proceed.isEnabled().catch(() => false)) break; - await page.waitForTimeout(100); - } await proceed.click({ force: true }); await Promise.race([ @@ -405,10 +418,9 @@ jobs: 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*="ハッシュタグ"]'); + let tagInput = page.locator('input[placeholder*="ハッシュタグ"]').first(); if (!(await tagInput.count())) tagInput = page.locator('input[role="combobox"]').first(); await tagInput.waitFor({ state: 'visible', timeout: 60000 }); for (const t of tags) { @@ -421,22 +433,17 @@ jobs: const publishBtn = page.locator('button:has-text("投稿する")').first(); await publishBtn.waitFor({ state: 'visible', timeout: 60000 }); - for (let i = 0; i < 20; i++) { - if (await publishBtn.isEnabled().catch(() => false)) 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: 30000 }).catch(() => {}), - page.locator('text=投稿しました').first().waitFor({ timeout: 12000 }).catch(() => {}), + page.locator('text=投稿しました').first().waitFor({ timeout: 15000 }).catch(() => {}), page.waitForTimeout(5000) ]); await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('PUBLISHED_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); - } finally { try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} try { await page?.close(); } catch {} @@ -444,10 +451,9 @@ jobs: try { await browser?.close(); } catch {} } } - - // main()でreturnできるようにしたので、エラーで落とさない + main().catch(e => { - console.error(e); + console.error(String(e?.stack || e)); process.exit(1); }); EOF From 49973dc81f72f40353c5158200e9b6e54978f3a8 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 02:06:41 +0900 Subject: [PATCH 26/32] Improve HTML paste handling and title input logic Refactor HTML insertion and title handling in workflow. --- .github/workflows/note-perplexity.yaml | 173 ++++++++++++++----------- 1 file changed, 97 insertions(+), 76 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 6a44e3f..fc0409f 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -224,9 +224,9 @@ jobs: } 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 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 rawBodyHtml = String(rawFinal.body_html || ''); const TAGS = process.env.TAGS || ''; const IS_PUBLIC = String(process.env.IS_PUBLIC || 'false') === 'true'; @@ -251,46 +251,56 @@ jobs: if (!s) s = 'タイトル(自動生成)'; return s; } - const TITLE = sanitizeTitle(rawTitle); - // ProseMirror向け:clipboard不使用で HTML を挿入 - async function insertHTMLWithoutClipboard(page, targetLocator, html) { - await targetLocator.click({ force: true }); - // 全選択→削除 + // HTMLから雑にプレーンテキストも作る(pasteイベントのfallback用) + function htmlToText(html) { + return String(html || '') + .replace(//gi, '\n') + .replace(/<\/p>/gi, '\n\n') + .replace(/<[^>]+>/g, '') + .replace(/\n{3,}/g, '\n\n') + .trim(); + } + + // ProseMirrorに効く:pasteイベントを擬似的に発火させてHTMLを挿入 + async function pasteHtml(page, locator, html) { + const text = htmlToText(html); + + await locator.click({ force: true }); await page.keyboard.press('Control+A'); await page.keyboard.press('Backspace'); - // execCommandでHTMLを挿入(clipboard不要) - await page.evaluate(({ selector, html }) => { - const el = document.querySelector(selector); - if (!el) throw new Error('insert target not found: ' + selector); - - el.focus(); - - // 末尾にカーソル(念のため) - const range = document.createRange(); - range.selectNodeContents(el); - range.collapse(false); - const sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - - // HTML挿入(失敗する環境もあるのでフォールバックも用意) - const ok = document.execCommand('insertHTML', false, html); - if (!ok) { - // 最終手段:HTMLタグを剥がしてテキストとして挿入 - const tmp = document.createElement('div'); - tmp.innerHTML = html; - const text = tmp.innerText || tmp.textContent || ''; - document.execCommand('insertText', false, text); + const ok = await locator.evaluate((el, payload) => { + try { + el.focus(); + + const dt = new DataTransfer(); + dt.setData('text/html', payload.html); + dt.setData('text/plain', payload.text); + + const ev = new ClipboardEvent('paste', { + clipboardData: dt, + bubbles: true, + cancelable: true + }); + + return el.dispatchEvent(ev); + } catch (e) { + return false; } - }, { selector: await targetLocator.evaluate(el => { - // その要素を再取得できるように一時属性を付与してセレクタ化 - el.setAttribute('data-auto-target', '1'); - return '[data-auto-target="1"]'; - }), html }); + }, { html, text }); + + // fallback: execCommand(効かない環境もあるが保険) + if (!ok) { + await locator.evaluate((el, payload) => { + el.focus(); + document.execCommand('insertHTML', false, payload.html); + }, { html }); + } } + const TITLE = sanitizeTitle(rawTitle); + async function main() { let browser, context, page; try { @@ -332,35 +342,32 @@ jobs: console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); + // ログイン確認(200ならOK) const resp = await context.request.get('https://note.com/api/v2/current_user'); console.log('DEBUG_current_user_status=', resp.status()); const txt = await resp.text(); console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(2500); - // /new → 自動で /notes/.../edit/ に遷移するので待つ + // noteは /new から自動で /notes//edit/ に遷移することがある + await page.waitForTimeout(2000); await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('SCREENSHOT=' + SS_PATH); - // ---- 本文(ProseMirror)を最優先で掴む ---- - const bodyBox = page.locator('div.ProseMirror[contenteditable="true"]').first(); - if (!(await bodyBox.count())) { - // フォールバック - console.log('DEBUG_BODY_FALLBACK=using [contenteditable=true]'); - } - const body = (await bodyBox.count()) - ? bodyBox - : page.locator('[contenteditable="true"]').first(); + // 本文は contenteditable が1個(あなたのスクショ通り) + const bodyBox = page.locator('[contenteditable="true"]').first(); + await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); - await body.waitFor({ state: 'visible', timeout: 60000 }); + const editableCount = await page.locator('[contenteditable="true"]').count(); + console.log('DEBUG_EDITABLE_COUNT=', editableCount); - // ---- タイトルは「見つかったら入れる」程度にする(既に入ってることが多い)---- + // タイトル:h1(クリックで編集)を最優先、ダメなら従来候補 const titleCandidates = [ + 'h1', // 現行UIでタイトルがh1表示されてる 'input[placeholder*="タイトル"]', 'textarea[placeholder*="タイトル"]', 'input[aria-label*="タイトル"]', @@ -368,7 +375,7 @@ jobs: '[data-testid*="title" i] input', '[data-testid*="title" i] textarea', '[data-testid*="title" i]', - 'h1', // もうタイトルが表示されてるだけのケース + 'input[type="text"]', ]; let titleEl = null; @@ -377,40 +384,46 @@ jobs: if (await loc.isVisible().catch(() => false)) { titleEl = loc; break; } } - if (titleEl) { - const tagName = await titleEl.evaluate(el => el.tagName.toLowerCase()); - if (tagName === 'input' || tagName === 'textarea') { - await titleEl.fill(TITLE); - } else { - // h1等で編集できない場合があるので、無理に落とさない - await titleEl.click({ force: true }).catch(() => {}); - await page.keyboard.press('Control+A').catch(() => {}); - await page.keyboard.type(TITLE, { delay: 3 }).catch(() => {}); - } + if (!titleEl) { + fs.writeFileSync('debug-title.html', await page.content()); + await page.screenshot({ path: 'debug-title.png', fullPage: true }); + throw new Error('Title element not found (candidates exhausted)'); + } + + // タイトル入力:h1の場合はクリック→キーボード + const tagName = await titleEl.evaluate(el => el.tagName.toLowerCase()); + if (tagName === 'input' || tagName === 'textarea') { + await titleEl.fill(TITLE); } else { - console.log('DEBUG_TITLE_SKIP=title element not found (but ok)'); + await titleEl.click({ force: true }); + await page.keyboard.press('Control+A'); + await page.keyboard.type(TITLE, { delay: 5 }); } - // ---- 本文を挿入(clipboardは使わない)---- - await insertHTMLWithoutClipboard(page, body, rawBodyHtml); + // ✅ 本文挿入(pasteイベント) + await pasteHtml(page, bodyBox, rawBodyHtml); await page.waitForTimeout(300); - // ---- 下書き保存 or 公開 ---- if (!IS_PUBLIC) { const saveBtn = page.locator('button:has-text("下書き保存"), [aria-label*="下書き保存"]').first(); - await saveBtn.waitFor({ state: 'visible', timeout: 60000 }); - if (await saveBtn.isEnabled().catch(() => false)) { + await saveBtn.waitFor({ state: 'visible' }); + if (await saveBtn.isEnabled()) { await saveBtn.click(); await page.locator('text=保存しました').waitFor({ timeout: 8000 }).catch(() => {}); } await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('DRAFT_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); - return; // async main の return はOK + throw new Error('__DONE_DRAFT__'); // finallyは走らせつつ正常終了させる } + // 公開フロー const proceed = page.locator('button:has-text("公開に進む")').first(); - await proceed.waitFor({ state: 'visible', timeout: 60000 }); + await proceed.waitFor({ state: 'visible' }); + for (let i = 0; i < 30; i++) { + if (await proceed.isEnabled()) break; + await page.waitForTimeout(200); + } await proceed.click({ force: true }); await Promise.race([ @@ -418,32 +431,38 @@ jobs: 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*="ハッシュタグ"]').first(); + let tagInput = page.locator('input[placeholder*="ハッシュタグ"]'); if (!(await tagInput.count())) tagInput = page.locator('input[role="combobox"]').first(); - await tagInput.waitFor({ state: 'visible', timeout: 60000 }); + 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); + await page.waitForTimeout(150); } } const publishBtn = page.locator('button:has-text("投稿する")').first(); - await publishBtn.waitFor({ state: 'visible', timeout: 60000 }); + await publishBtn.waitFor({ state: 'visible' }); + for (let i = 0; i < 30; i++) { + if (await publishBtn.isEnabled()) break; + await page.waitForTimeout(200); + } await publishBtn.click({ force: true }); await Promise.race([ page.waitForURL(u => !/\/publish/i.test(typeof u === 'string' ? u : u.toString()), { timeout: 30000 }).catch(() => {}), - page.locator('text=投稿しました').first().waitFor({ timeout: 15000 }).catch(() => {}), + page.locator('text=投稿しました').first().waitFor({ timeout: 12000 }).catch(() => {}), page.waitForTimeout(5000) ]); await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('PUBLISHED_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); + } finally { try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} try { await page?.close(); } catch {} @@ -452,9 +471,11 @@ jobs: } } - main().catch(e => { - console.error(String(e?.stack || e)); - process.exit(1); + main().catch((e) => { + if (String(e?.message || '') === '__DONE_DRAFT__') { + process.exit(0); + } + throw e; }); EOF From b5d96184a497936093f455adfb36791f0e5257b3 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 02:13:02 +0900 Subject: [PATCH 27/32] Refactor note-perplexity workflow for improved HTML handling --- .github/workflows/note-perplexity.yaml | 227 +++++++++++++------------ 1 file changed, 121 insertions(+), 106 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index fc0409f..f9e1e41 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -223,27 +223,6 @@ jobs: 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 rawBodyHtml = String(rawFinal.body_html || ''); - 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); - } - if (!rawBodyHtml.trim()) { - console.error('body_html is empty in final.json'); - 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(/^#+\s*/, ''); @@ -252,57 +231,83 @@ jobs: return s; } - // HTMLから雑にプレーンテキストも作る(pasteイベントのfallback用) + // HTMLを「本文として安全に入るテキスト」に変換(最小限) function htmlToText(html) { - return String(html || '') - .replace(//gi, '\n') - .replace(/<\/p>/gi, '\n\n') - .replace(/<[^>]+>/g, '') - .replace(/\n{3,}/g, '\n\n') - .trim(); + let s = String(html || ''); + // br / p / li / hr を改行に寄せる + s = s.replace(/<\s*br\s*\/?\s*>/gi, '\n'); + s = s.replace(/<\s*\/p\s*>/gi, '\n\n'); + s = s.replace(/<\s*p(\s+[^>]*)?>/gi, ''); + s = s.replace(/<\s*\/li\s*>/gi, '\n'); + s = s.replace(/<\s*li(\s+[^>]*)?>/gi, '・'); + s = s.replace(/<\s*hr\s*\/?\s*>/gi, '\n\n---\n\n'); + + // aタグは「テキスト (URL)」にする(軽い) + s = s.replace(/]*href="([^"]+)"[^>]*>(.*?)<\/a>/gi, (_, href, text) => { + const t = String(text || '').replace(/<[^>]+>/g, '').trim(); + const u = String(href || '').trim(); + if (!t) return u; + return `${t} (${u})`; + }); + + // それ以外のタグを落とす + s = s.replace(/<[^>]+>/g, ''); + + // HTMLエンティティ軽く戻す + s = s + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'"); + + // 連続空白/改行を整形 + s = s.replace(/\r/g, ''); + s = s.replace(/[ \t]+\n/g, '\n'); + s = s.replace(/\n{3,}/g, '\n\n'); + return s.trim() + '\n'; } - // ProseMirrorに効く:pasteイベントを擬似的に発火させてHTMLを挿入 - async function pasteHtml(page, locator, html) { - const text = htmlToText(html); - - await locator.click({ force: true }); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Backspace'); - - const ok = await locator.evaluate((el, payload) => { - try { - el.focus(); - - const dt = new DataTransfer(); - dt.setData('text/html', payload.html); - dt.setData('text/plain', payload.text); + async function typeLargeText(page, text) { + // ProseMirrorに大量投入すると落ちることがあるので分割 + const chunk = 800; + for (let i = 0; i < text.length; i += chunk) { + const part = text.slice(i, i + chunk); + await page.keyboard.insertText(part); + // 少しだけ譲る(429/不安定対策) + if (i % (chunk * 20) === 0) await page.waitForTimeout(50); + } + } - const ev = new ClipboardEvent('paste', { - clipboardData: dt, - bubbles: true, - cancelable: true - }); + async function main() { + 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 TAGS = process.env.TAGS || ''; + const IS_PUBLIC = String(process.env.IS_PUBLIC || 'false') === 'true'; + + if (!STATE_PATH || !fs.existsSync(STATE_PATH)) { + console.error('storageState not found:', STATE_PATH); + process.exit(1); + } - return el.dispatchEvent(ev); - } catch (e) { - return false; - } - }, { html, text }); - - // fallback: execCommand(効かない環境もあるが保険) - if (!ok) { - await locator.evaluate((el, payload) => { - el.focus(); - document.execCommand('insertHTML', false, payload.html); - }, { html }); + const rawFinal = JSON.parse(fs.readFileSync('final.json', 'utf8')); + const rawBodyHtml = String(rawFinal.body_html || ''); + if (!rawBodyHtml.trim()) { + console.error('body_html is empty in final.json'); + process.exit(1); } - } - const TITLE = sanitizeTitle(rawTitle); + const TITLE = sanitizeTitle(rawTitle); + const BODY_TEXT = htmlToText(rawBodyHtml); + + const ssDir = path.join(os.tmpdir(), 'note-screenshots'); + fs.mkdirSync(ssDir, { recursive: true }); + const SS_PATH = path.join(ssDir, `note-post-${nowStr()}.png`); - async function main() { let browser, context, page; + try { browser = await chromium.launch({ headless: true, @@ -342,39 +347,57 @@ jobs: console.log('DEBUG_NOTE_COOKIE_COUNT(before goto)=', noteCookies0.length); console.log('DEBUG_NOTE_COOKIE_NAMES(before goto)=', noteCookies0.map(c => c.name).slice(0, 30).join(',')); - // ログイン確認(200ならOK) + // 認証確認(200が返るならログインは通ってる) const resp = await context.request.get('https://note.com/api/v2/current_user'); console.log('DEBUG_current_user_status=', resp.status()); const txt = await resp.text(); console.log('DEBUG_current_user_body_head=', txt.slice(0, 200)); await page.goto(START_URL, { waitUntil: 'domcontentloaded' }); + await page.waitForTimeout(3000); - // noteは /new から自動で /notes//edit/ に遷移することがある - await page.waitForTimeout(2000); - await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); console.log('DEBUG_URL_AFTER_GOTO=' + page.url()); + // /new から /notes/.../edit へ遷移するのを待つ + const urlNow = page.url(); + const isEditUrl = /\/notes\/[^/]+\/edit\/?/i.test(urlNow); + if (!isEditUrl) { + await page.waitForURL(/\/notes\/[^/]+\/edit\/?/i, { timeout: 60000 }).catch(() => {}); + } + console.log('DEBUG_URL_AFTER_WAIT_EDIT=' + page.url()); + await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('SCREENSHOT=' + SS_PATH); - // 本文は contenteditable が1個(あなたのスクショ通り) - const bodyBox = page.locator('[contenteditable="true"]').first(); + // ===== 本文エディタ(ProseMirror)を特定 ===== + // まず ProseMirror を最優先、なければ contenteditable + let bodyBox = page.locator('.ProseMirror[contenteditable="true"]').first(); + if (!(await bodyBox.count())) { + bodyBox = page.locator('[contenteditable="true"]').first(); + } await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); const editableCount = await page.locator('[contenteditable="true"]').count(); console.log('DEBUG_EDITABLE_COUNT=', editableCount); - // タイトル:h1(クリックで編集)を最優先、ダメなら従来候補 + // ===== タイトルを特定(あなたのスクショでは本文とは別要素)===== const titleCandidates = [ - 'h1', // 現行UIでタイトルがh1表示されてる + // input/textarea 系 'input[placeholder*="タイトル"]', 'textarea[placeholder*="タイトル"]', + 'input[name*="title" i]', + 'textarea[name*="title" i]', + + // aria 'input[aria-label*="タイトル"]', 'textarea[aria-label*="タイトル"]', + + // data-testid 系(noteが変えても拾いやすい) '[data-testid*="title" i] input', '[data-testid*="title" i] textarea', '[data-testid*="title" i]', + + // 最終手段:画面上部の見出しっぽい入力 'input[type="text"]', ]; @@ -390,9 +413,8 @@ jobs: throw new Error('Title element not found (candidates exhausted)'); } - // タイトル入力:h1の場合はクリック→キーボード - const tagName = await titleEl.evaluate(el => el.tagName.toLowerCase()); - if (tagName === 'input' || tagName === 'textarea') { + const titleTag = await titleEl.evaluate(el => el.tagName.toLowerCase()); + if (titleTag === 'input' || titleTag === 'textarea') { await titleEl.fill(TITLE); } else { await titleEl.click({ force: true }); @@ -400,30 +422,34 @@ jobs: await page.keyboard.type(TITLE, { delay: 5 }); } - // ✅ 本文挿入(pasteイベント) - await pasteHtml(page, bodyBox, rawBodyHtml); - await page.waitForTimeout(300); + // ===== 本文入力(HTMLではなくテキストで入れる:最重要)===== + await bodyBox.click({ force: true }); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Backspace'); + + // ProseMirrorは insertHTML が不安定なので insertText で流し込む + await typeLargeText(page, BODY_TEXT); + + // 少し待って自動保存の発火を待つ + await page.waitForTimeout(1500); 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: 8000 }).catch(() => {}); + // 下書き保存(ボタンが見えない場合があるので複数候補) + const saveBtn = page.locator('button:has-text("下書き保存"), button:has-text("下書きを保存"), [aria-label*="下書き保存"]').first(); + if (await saveBtn.count()) { + await saveBtn.click({ force: true }).catch(() => {}); } + await page.waitForTimeout(2000); + await page.screenshot({ path: SS_PATH, fullPage: true }); console.log('DRAFT_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); - throw new Error('__DONE_DRAFT__'); // finallyは走らせつつ正常終了させる + return; // main() 内なので合法 } - // 公開フロー + // ===== 公開フロー(必要なら後で調整)===== const proceed = page.locator('button:has-text("公開に進む")').first(); await proceed.waitFor({ state: 'visible' }); - for (let i = 0; i < 30; i++) { - if (await proceed.isEnabled()) break; - await page.waitForTimeout(200); - } await proceed.click({ force: true }); await Promise.race([ @@ -431,7 +457,6 @@ jobs: 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*="ハッシュタグ"]'); @@ -441,28 +466,19 @@ jobs: await tagInput.click(); await tagInput.fill(t); await page.keyboard.press('Enter'); - await page.waitForTimeout(150); + await page.waitForTimeout(120); } } const publishBtn = page.locator('button:has-text("投稿する")').first(); await publishBtn.waitFor({ state: 'visible' }); - for (let i = 0; i < 30; i++) { - if (await publishBtn.isEnabled()) break; - await page.waitForTimeout(200); - } await publishBtn.click({ force: true }); - await Promise.race([ - page.waitForURL(u => !/\/publish/i.test(typeof u === 'string' ? u : u.toString()), { timeout: 30000 }).catch(() => {}), - page.locator('text=投稿しました').first().waitFor({ timeout: 12000 }).catch(() => {}), - page.waitForTimeout(5000) - ]); - + await page.waitForTimeout(5000); await page.screenshot({ path: SS_PATH, fullPage: true }); + console.log('PUBLISHED_URL=' + page.url()); console.log('SCREENSHOT=' + SS_PATH); - } finally { try { await context?.tracing?.stop({ path: 'trace.zip' }); } catch {} try { await page?.close(); } catch {} @@ -471,11 +487,10 @@ jobs: } } - main().catch((e) => { - if (String(e?.message || '') === '__DONE_DRAFT__') { - process.exit(0); - } - throw e; + // ここで実行(returnもOK) + main().catch(e => { + console.error(e); + process.exit(1); }); EOF From e3692823f8589cd3d0b301ac24aaf3ab50380334 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 02:23:46 +0900 Subject: [PATCH 28/32] Enhance debugging in note-perplexity workflow Added debug logs for body text length and content. --- .github/workflows/note-perplexity.yaml | 78 +++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index f9e1e41..ce0fa5e 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -301,6 +301,9 @@ jobs: const TITLE = sanitizeTitle(rawTitle); const BODY_TEXT = htmlToText(rawBodyHtml); + console.log('DEBUG_BODY_TEXT_LEN=', BODY_TEXT.length); + console.log('DEBUG_BODY_TEXT_HEAD=', BODY_TEXT.slice(0, 200)); + const ssDir = path.join(os.tmpdir(), 'note-screenshots'); fs.mkdirSync(ssDir, { recursive: true }); @@ -370,15 +373,53 @@ jobs: console.log('SCREENSHOT=' + SS_PATH); // ===== 本文エディタ(ProseMirror)を特定 ===== - // まず ProseMirror を最優先、なければ contenteditable - let bodyBox = page.locator('.ProseMirror[contenteditable="true"]').first(); - if (!(await bodyBox.count())) { - bodyBox = page.locator('[contenteditable="true"]').first(); + const bodyCandidates = [ + // data-testid 系(noteが持ってることが多い) + '[data-testid*="editor" i] .ProseMirror[contenteditable="true"]', + '[data-testid*="body" i] .ProseMirror[contenteditable="true"]', + + // よくある構造 + 'main .ProseMirror[contenteditable="true"]', + 'article .ProseMirror[contenteditable="true"]', + + // 最終手段:ProseMirror複数なら「2つ目以降」を本文とみなす + '.ProseMirror[contenteditable="true"]', + ]; + + let bodyBox = null; + + // 1) セレクタ候補を上から試す + for (const sel of bodyCandidates) { + const loc = page.locator(sel); + const n = await loc.count().catch(() => 0); + if (n === 0) continue; + + // selが広すぎる場合を考慮して「画面内で大きい要素」を本文とみなす + let best = null; + let bestArea = 0; + for (let i = 0; i < n; i++) { + const el = loc.nth(i); + if (!(await el.isVisible().catch(() => false))) continue; + const box = await el.boundingBox().catch(() => null); + if (!box) continue; + const area = box.width * box.height; + if (area > bestArea) { + bestArea = area; + best = el; + } + } + if (best) { bodyBox = best; break; } + } + + if (!bodyBox) { + fs.writeFileSync('debug-body.html', await page.content()); + await page.screenshot({ path: 'debug-body.png', fullPage: true }); + throw new Error('Body editor not found'); } + await bodyBox.waitFor({ state: 'visible', timeout: 60000 }); - - const editableCount = await page.locator('[contenteditable="true"]').count(); - console.log('DEBUG_EDITABLE_COUNT=', editableCount); + console.log('DEBUG_BODY_BOX_FOUND=', await bodyBox.evaluate(el => el.className)); + // ===== タイトルを特定(あなたのスクショでは本文とは別要素)===== const titleCandidates = [ @@ -428,7 +469,20 @@ jobs: await page.keyboard.press('Backspace'); // ProseMirrorは insertHTML が不安定なので insertText で流し込む - await typeLargeText(page, BODY_TEXT); + // ===== 本文入力 ===== + await bodyBox.click({ force: true }); + + // 全消し(Linux runner) + await page.keyboard.press('Control+A'); + await page.keyboard.press('Backspace'); + + // クリップボード経由で貼り付け(ProseMirrorが反応しやすい) + await page.evaluate(async (t) => { + await navigator.clipboard.writeText(t); + }, BODY_TEXT); + + await page.keyboard.press('Control+V'); + // 少し待って自動保存の発火を待つ await page.waitForTimeout(1500); @@ -446,6 +500,14 @@ jobs: console.log('SCREENSHOT=' + SS_PATH); return; // main() 内なので合法 } + + const bodyTextNow = await bodyBox.evaluate(el => el.innerText || ''); + console.log('DEBUG_BODY_AFTER_LEN=', bodyTextNow.trim().length); + if (bodyTextNow.trim().length < 20) { + await page.screenshot({ path: 'debug-body-after.png', fullPage: true }); + throw new Error('Body seems not inserted (too short). Check selectors.'); + } + // ===== 公開フロー(必要なら後で調整)===== const proceed = page.locator('button:has-text("公開に進む")').first(); From 3354539fe5db1e3892d9fceadcfd9f981f723ae3 Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 02:30:36 +0900 Subject: [PATCH 29/32] Update text insertion logic in note-perplexity workflow Refactor text insertion method for ProseMirror compatibility. --- .github/workflows/note-perplexity.yaml | 29 ++++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index ce0fa5e..164b7b6 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -472,19 +472,30 @@ jobs: // ===== 本文入力 ===== await bodyBox.click({ force: true }); - // 全消し(Linux runner) + // 全消し await page.keyboard.press('Control+A'); await page.keyboard.press('Backspace'); - // クリップボード経由で貼り付け(ProseMirrorが反応しやすい) - await page.evaluate(async (t) => { - await navigator.clipboard.writeText(t); - }, BODY_TEXT); + // execCommand(insertText)で分割投入(ProseMirrorが拾いやすい) + async function insertByExecCommand(text) { + const chunk = 800; + for (let i = 0; i < text.length; i += chunk) { + const part = text.slice(i, i + chunk); + + await page.evaluate((t) => { + // activeElement に対して insertText + document.execCommand('insertText', false, t); + }, part); + + if (i % (chunk * 20) === 0) await page.waitForTimeout(50); + } + } + + // フォーカスを強制(念のため) + await bodyBox.evaluate(el => el.focus()); + + await insertByExecCommand(BODY_TEXT); - await page.keyboard.press('Control+V'); - - - // 少し待って自動保存の発火を待つ await page.waitForTimeout(1500); if (!IS_PUBLIC) { From 0a154ac4c8747d41b3a9f71a9c34335daf5de48a Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 02:39:56 +0900 Subject: [PATCH 30/32] Update note-perplexity.yaml --- .github/workflows/note-perplexity.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index 164b7b6..ac33f2d 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -85,6 +85,8 @@ jobs: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml' } + const finalUrl = res.url; // ★これが超重要(redirect後のURL) + console.log('DEBUG_FETCH_FINAL_URL=', finalUrl) }); if (!res.ok) { @@ -94,7 +96,7 @@ jobs: const html = await res.text(); - const dom = new JSDOM(html, { url }); + const dom = new JSDOM(html, { url: finalUrl }); const reader = new Readability(dom.window.document); const article = reader.parse(); @@ -109,7 +111,7 @@ jobs: .filter(Boolean); const attributionHtml = - `

    出典: ${url}


    `; + `

    出典: ${finalUrl}


    `; const out = { title: (article.title || '').trim() || '(タイトル取得失敗)', From 076d80e2091115b239e807cdc1bbae0e98ed5eea Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 02:42:32 +0900 Subject: [PATCH 31/32] Fix indentation and duplicate line in YAML workflow --- .github/workflows/note-perplexity.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index ac33f2d..e35e3b5 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -85,10 +85,11 @@ jobs: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml' } - const finalUrl = res.url; // ★これが超重要(redirect後のURL) - console.log('DEBUG_FETCH_FINAL_URL=', finalUrl) }); + const finalUrl = res.url; // ★これが超重要(redirect後のURL) + console.log('DEBUG_FETCH_FINAL_URL=', finalUrl) + if (!res.ok) { console.error('fetch failed:', res.status, res.statusText); process.exit(1); From d8e975d158d743ecc36cc5c65d4cc70c7af3b2fc Mon Sep 17 00:00:00 2001 From: ushindr16 Date: Thu, 12 Feb 2026 03:27:41 +0900 Subject: [PATCH 32/32] Update note-perplexity.yaml --- .github/workflows/note-perplexity.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/note-perplexity.yaml b/.github/workflows/note-perplexity.yaml index e35e3b5..2549653 100644 --- a/.github/workflows/note-perplexity.yaml +++ b/.github/workflows/note-perplexity.yaml @@ -3,7 +3,7 @@ name: Note Workflow (SEO URL -> note) on: workflow_dispatch: inputs: - seo_url: + article_url: description: '投稿元のSEO記事URL' required: true type: string @@ -40,7 +40,7 @@ jobs: name: Fetch SEO article (Extract title/body) runs-on: ubuntu-latest env: - SEO_URL: ${{ github.event.inputs.seo_url }} + article_url: ${{ github.event.inputs.article_url }} INPUT_TAGS: ${{ github.event.inputs.tags }} outputs: final_b64: ${{ steps.collect.outputs.final_b64 }} @@ -71,10 +71,10 @@ jobs: return new URL(encoded).toString(); } - const raw = process.env.SEO_URL || ''; + const raw = process.env.article_url || ''; const url = normalizeUrl(raw); if (!url) { - console.error('SEO_URL is empty'); + console.error('article_url is empty'); process.exit(1); }