/**
* ============================================================
* Notebook Enterprise × 生成AI最新ニュース自動収集 GAS
* ============================================================
*
* 機能:
* 1. 複数のRSSフィードから生成AIの最新ニュースを収集し、各記事の本文テキストを抽出
* 2. 本文テキストをまとめたGoogleドキュメントを自動作成
* 3. Notebook Enterprise APIでノートブックを新規作成
* 4. 作成したGoogleドキュメントをノートブックのソースとして登録
* 5. 作成したノートブックを指定アカウントに自動共有
* 6. ノートブックから音声解説(Audio Overview)を自動生成
* 7. Gmailで指定メールアドレスにノートブックURL・GoogleドキュメントURL等を通知
*
* 必要な前提条件:
* - Notebook Enterprise ライセンス (Gemini Enterprise / Education Premium)
* - Google Cloud プロジェクトで Discovery Engine API を有効化
* - 実行アカウントに roles/discoveryengine.notebookLmUser IAMロールを付与
* - 共有先アカウントに roles/discoveryengine.notebookLmUser IAMロールを付与
* - Gmail 送信権限 / Google ドキュメント・Drive 操作権限
*
* セットアップ手順:
* 1. 下記 CONFIG の各値を自分の環境に合わせて設定する
* 2. スクリプトエディタで appsscript.json を表示し、OAuthスコープを確認する
* 3. setupDailyTrigger() を一度だけ手動実行してトリガーを登録する
* 4. main() を手動実行して動作確認する
* ============================================================
*/
// ============================================================
// 設定セクション(必ず自分の環境に合わせて変更してください)
// ============================================================
const CONFIG = {
// Google Cloud プロジェクト番号(数値)
// Cloud Console の「プロジェクト情報」から確認できます
GCP_PROJECT_NUMBER: '<プロジェクト番号>',
// ノートブックのロケーション(通常は 'global' または 'us')
NOTEBOOK_LOCATION: 'us',
// APIエンドポイントのリージョン: 'us', 'eu', または 'global'
ENDPOINT_LOCATION: 'us',
// 通知先 & 共有先メールアドレス
NOTIFICATION_EMAIL: '通知先のメールアドレス',
// ノートブック共有時のロール ('PROJECT_ROLE_WRITER' または 'PROJECT_ROLE_READER')
SHARE_ROLE: 'PROJECT_ROLE_WRITER',
// 1回の実行で取得するニュース件数の上限
MAX_NEWS_ITEMS: 10,
// ニュース収集対象のRSSフィード一覧
RSS_FEEDS: [
{
name: 'Google News (日本語)',
url: 'https://news.google.com/rss/search?q=%E7%94%9F%E6%88%90AI+OR+%E7%94%9F%E6%88%90%E7%9A%84AI+OR+LLM&hl=ja&gl=JP&ceid=JP:ja',
lang: 'ja'
},
{
name: 'Google News (英語)',
url: 'https://news.google.com/rss/search?q=generative+AI+OR+LLM+OR+ChatGPT+OR+Gemini&hl=en-US&gl=US&ceid=US:en',
lang: 'en'
},
{
name: 'OpenAI Blog',
url: 'https://openai.com/news/rss.xml',
lang: 'en'
},
{
name: 'Google DeepMind Blog',
url: 'https://deepmind.google/discover/blog/feed/',
lang: 'en'
}
],
// ノートブック名・ドキュメント名のプレフィックス
NOTEBOOK_TITLE_PREFIX: '生成AI最新ニュース',
// スクリプトプロパティのキー(重複実行防止用)
PROP_LAST_RUN: 'LAST_RUN_DATE'
};
// ============================================================
// メインエントリーポイント
// ============================================================
/**
* メイン関数 - GASトリガーから毎日呼び出されます
* 手動実行でもデバッグできます
*/
function main() {
Logger.log('=== Notebook ニュースボット 開始 ===');
const startTime = new Date();
try {
const today = Utilities.formatDate(startTime, 'Asia/Tokyo', 'yyyy-MM-dd');
const baseTitle = CONFIG.NOTEBOOK_TITLE_PREFIX + ' ' + today;
// --- Step 1: ニュース収集&本文抽出 ---
Logger.log('[Step 1] RSSフィードからニュースを収集し、本文テキストを抽出中...');
const articles = fetchAiNews(CONFIG.MAX_NEWS_ITEMS);
if (articles.length === 0) {
const msg = '収集できたニュース記事が0件でした。RSSフィードの設定を確認してください。';
Logger.log('[WARN] ' + msg);
sendErrorToGmail(msg);
return;
}
Logger.log('[Step 1] ' + articles.length + ' 件の記事・本文を取得完了');
// --- Step 2: Google ドキュメントの作成 ---
Logger.log('[Step 2] 本文テキストをまとめたGoogleドキュメントを作成中...');
const docInfo = createNewsDocument(baseTitle + ' (記事まとめ)', articles);
Logger.log('[Step 2] Googleドキュメント作成完了: ID = ' + docInfo.id);
// --- Step 3: ノートブック作成 ---
Logger.log('[Step 3] Notebook Enterprise ノートブックを作成中...');
const notebook = createNotebook(baseTitle);
Logger.log('[Step 3] ノートブック作成完了: ID = ' + notebook.name);
// --- Step 4: 作成したGoogleドキュメントをノートブックのソースとして登録 ---
Logger.log('[Step 4] Googleドキュメントをノートブックのソースとして登録中...');
addGoogleDocSourceToNotebook(notebook.name, docInfo.id);
Logger.log('[Step 4] ソース登録完了');
// --- Step 5: ノートブックの共有設定 ---
Logger.log('[Step 5] ノートブックを共有中: ' + CONFIG.NOTIFICATION_EMAIL);
shareNotebook(notebook.name, CONFIG.NOTIFICATION_EMAIL);
// --- Step 6: 音声解説(Audio Overview)の自動生成 ---
Logger.log('[Step 6] 音声解説(Audio Overview)の生成をトリガー中...');
Utilities.sleep(3000); // ソース登録の反映を待機
generateAudioOverview(notebook.name);
// ノートブックのURLを構築
const notebookId = notebook.name.split('/').pop();
const notebookUrl = buildNotebookUrl(notebookId);
// --- Step 7: Gmail に通知 ---
Logger.log('[Step 7] Gmailに通知メールを送信中: ' + CONFIG.NOTIFICATION_EMAIL);
sendToGmail(articles, notebookUrl, baseTitle, docInfo.url, startTime);
Logger.log('[Step 7] Gmail通知完了');
// 最終実行日をプロパティに記録
PropertiesService.getScriptProperties().setProperty(CONFIG.PROP_LAST_RUN, today);
const elapsed = (new Date() - startTime) / 1000;
Logger.log('=== 完了 (所要時間: ' + elapsed.toFixed(1) + '秒) ===');
} catch (e) {
const errMsg = 'エラーが発生しました: ' + e.message + '\n' + (e.stack || '');
Logger.log('[ERROR] ' + errMsg);
sendErrorToGmail(errMsg);
throw e;
}
}
// ============================================================
// Step 1: ニュース収集&本文抽出
// ============================================================
/**
* 複数のRSSフィードから生成AIの最新ニュースを収集し、各記事の本文テキストを抽出する
* @param {number} maxItems 取得する最大件数
* @returns {Array<{title: string, url: string, source: string, pubDate: string, content: string}>}
*/
function fetchAiNews(maxItems) {
const allArticles = [];
const seenUrls = {};
for (const feed of CONFIG.RSS_FEEDS) {
try {
Logger.log(' フィード取得中: ' + feed.name);
const articles = fetchRssFeed(feed);
for (const article of articles) {
// URL重複チェック
if (!seenUrls[article.url]) {
seenUrls[article.url] = true;
allArticles.push(article);
}
}
Logger.log(' ' + feed.name + ': ' + articles.length + ' 件取得');
} catch (e) {
Logger.log(' [WARN] ' + feed.name + ' の取得に失敗: ' + e.message);
// 1つのフィードが失敗しても続行
}
}
// 公開日の新しい順にソートしてmaxItems件に絞る
allArticles.sort(function(a, b) {
return new Date(b.pubDate) - new Date(a.pubDate);
});
const targetArticles = allArticles.slice(0, maxItems);
// 各記事の本文テキストを取得
Logger.log(' 各記事の本文テキストを取得中 (' + targetArticles.length + '件)...');
for (const [k, item] of targetArticles.entries()) {
Logger.log(' [' + (k + 1) + '/' + targetArticles.length + '] 本文取得: ' + item.title.substring(0, 30) + '...');
item.content = fetchArticleContent(item);
// レートリミット配慮
Utilities.sleep(300);
}
return targetArticles;
}
/**
* 単一のRSSフィードを取得してパースする
* @param feed
* @returns {Array<{title: string, url: string, source: string, pubDate: string, description: string}>}
*/
function fetchRssFeed(feed) {
const response = UrlFetchApp.fetch(feed.url, {
muteHttpExceptions: true,
followRedirects: true
});
if (response.getResponseCode() !== 200) {
throw new Error('HTTPエラー: ' + response.getResponseCode());
}
const xml = response.getContentText('UTF-8');
const doc = XmlService.parse(xml);
const root = doc.getRootElement();
const ns = root.getNamespace();
let items = [];
try {
const channel = root.getChild('channel', ns) || root.getChild('channel');
if (channel) {
// RSS 2.0
items = channel.getChildren('item');
} else {
// Atom形式
items = root.getChildren('entry', root.getNamespace());
}
} catch (e) {
Logger.log(' XMLパースエラー (' + feed.name + '): ' + e.message);
return [];
}
const articles = [];
for (const item of items) {
try {
// タイトル取得
const titleEl = item.getChild('title') || item.getChild('title', item.getNamespace());
const title = titleEl ? titleEl.getValue().trim() : '(タイトルなし)';
// URL取得
let url = '';
const linkEl = item.getChild('link') || item.getChild('link', item.getNamespace());
if (linkEl) {
url = linkEl.getValue().trim();
// Atom の 形式に対応
if (!url) {
const hrefAttr = linkEl.getAttribute('href');
if (hrefAttr) url = hrefAttr.getValue();
}
}
// Google News の場合は guid にURLが入ることがある
if (!url) {
const guidEl = item.getChild('guid');
if (guidEl) url = guidEl.getValue().trim();
}
if (!url || !title) continue;
// 公開日取得
const pubDateEl = item.getChild('pubDate') || item.getChild('published') || item.getChild('updated');
const pubDate = pubDateEl ? pubDateEl.getValue().trim() : new Date().toISOString();
// 概要(フォールバック用)
const descEl = item.getChild('description') || item.getChild('summary');
const description = descEl ? descEl.getValue().trim() : '';
articles.push({
title: title,
url: url,
source: feed.name,
pubDate: pubDate,
description: description
});
} catch (e) {
// 個別アイテムのパース失敗は無視して続行
}
}
return articles;
}
/**
* 記事URLにアクセスして本文テキストを抽出する
* @param article
* @returns {string} 本文テキスト
*/
function fetchArticleContent(article) {
try {
const response = UrlFetchApp.fetch(article.url, {
muteHttpExceptions: true,
followRedirects: true,
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
});
if (response.getResponseCode() !== 200) {
Logger.log(' [WARN] 本文取得HTTPエラー (' + response.getResponseCode() + '): ' + article.title);
return cleanHtmlToText(article.description) || '(本文の取得に失敗しました)';
}
const html = response.getContentText('UTF-8');
let text = cleanHtmlToText(html);
// テキストが極端に短い場合は description をフォールバック
if (!text || text.length < 100) {
if (article.description) {
text = cleanHtmlToText(article.description);
}
}
// ドキュメント肥大化防止のため最大5,000文字でトリミング
if (text.length > 5000) {
text = text.substring(0, 5000) + '\n\n...(以下省略)';
}
return text || '(本文なし)';
} catch (e) {
Logger.log(' [WARN] 本文取得エラー (' + article.title + '): ' + e.message);
return cleanHtmlToText(article.description) || '(本文取得中にエラーが発生しました)';
}
}