はじめに
皆さん、twitter(旧X)で時間溶かしてますか?私は1日に36時間くらいtwitterとにらめっこしています。
最新情報、特にLLMまわりは流れが速すぎて、タイムラインを眺めているだけでは追いつけませんよね。かといって自分で定期的に検索しに行くのもめんどくさい。
そこで、AIエージェント「muse」に、毎日決まった時刻に最新トピックを集めてもらい、ウィジェットにまとめてiphoneのホーム画面に表示させる仕組みを作ってみました。
当記事では、その運用のやり方と考え方を紹介します。
迷える子羊の皆さんを照らす、一筋の光になれれば幸いです。
全体の流れ
[定期実行 1日4回]
│
├─ 1. 候補集め
│ ・muse経由でGrokに「直近数時間でXで話題の技術トピック」を聞く
│ ・他のSNS検索・ニュース検索でも補完
│
├─ 2. 裏取り(真偽確認)
│ ・museから各ニュースを公式発表・元の投稿・技術メディアで照合
│ ・確認できないものは載せない or「噂・未確認」と明示
│
├─ 3. 重複チェック
│ ・既にウィジェットで配信済みのニュースを除く(続報だけ例外)
│
├─ 4. 配信
│ ・museのチャットに見出し+要点+出典リンクで投稿
│ ・同時にJSONとしてGoogle Driveに書き出し
│
└─ 5. 表示
・Google DriveのJSONをScriptableから取得し、
iPhoneホーム画面のウィジェットに反映
実装方法 作業手順
museへの命令
私は以下のような命令をmuseに丸投げして、情報を拾ってきてもらってます(専用のサイドチャットを作ると管理が楽です)。
有線カテゴリ、配信の回数などは好みに応じてカスタマイズしてください。
毎日4回、Twitter・その他SNSやニュースサイト、arXiv、GitHub、Hugging Face等から最新の技術トピックを集めて、
私のGoogle DriveにJSON形式で保存してください。毎回同じファイル名・同じファイルに上書きしてください。
毎回の作業ごとに、以下の点に留意してください。
①そのニュースと同じ内容を既に配信していないかどうか(重複の管理)
②その情報に信憑性があるかどうか、一次情報や出所を確認してください。
⇨信憑性がない噂レベルの情報には、その旨を明記してください。
以下のカテゴリのニュースを優先的に集めてください。
集めるニュースの数に上限は設けませんので、重要な情報は漏らさずキャッチするようにしてください。
逆に目立ったニュースがない場合、正直に無いと言ってください。
①ローカルLLM(新しいモデル、話題のモデル)
②クラウドLLM(新モデルのリリースなど)
③ローカルで動く画像生成AI
④Apple製品(Mac・iPhoneなど)
⑤新しい脆弱性などのセキュリティ情報
⑥その他(GPU・PCハードウェア、ガジェット、開発ツールなど)
JSONは以下の形式で出力してください。
{
"version": 1,
"title": "ニュースフィード名",
"date": "YYYY-MM-DD",
"edition": "配信回",
"updatedAt": "YYYY-MM-DDThh:mm:ss+09:00",
"items": [
{
"category": "カテゴリ",
"title": "記事タイトル",
"summary": "要約(1〜2行)",
"url": "リンクURL",
"edition": "配信回"
}
]
}
私は、できれば多くの情報を仕入れたいので、取得するトピックの件数に上限は付けませんでした。
逆に、大きな動きがない時間帯は無理に埋めず、「特になし」と返すようにしています。
水増しされるくらいなら、何も返されない方が信頼できますからねー。
JSONをウィジェットに反映
Google Driveに吐き出されたJSONをScriptableから読み取って、ウィジェットに表示させる仕組みを作ります。
まだScriptable入れてないよーって方は、以下のリンクから。
DLできたらアプリを開いて、右上の+マークから新規スクリプトを追加⇨以下のJavascriptをコピペしてください。スクリプトはmuseに作ってもらいました。
Google Driveのリンクとかは各自で埋めてね。
const DATA_URL = "YOUR_GOOGLE_DRIVE_LINK";
const SCRIPT_NAME_FALLBACK = "テック最新情報";
const PRIMARY_TEXT = Color.dynamic(new Color("#1a1a1a"), new Color("#f5f5f5"));
const SECONDARY_TEXT = Color.dynamic(new Color("#666666"), new Color("#a8a8a8"));
const ACCENT = new Color("#6c5ce7");
const CATEGORY_COLORS = {
"ローカルLLM": "#6c5ce7",
"クラウドLLM": "#0984e3",
"画像生成AI": "#e84393",
"Apple": "#636e72",
"脆弱性": "#d63031",
"その他": "#00a087"
};
function colorFor(category) {
const hex = CATEGORY_COLORS[category] || "#636e72";
return new Color(hex);
}
// 戻り値: { state: "ok" | "empty" | "error", data: object | null }
// ok = ニュースが取れた
// empty = 取れたが目立ったニュースが0件だった
// error = 取得できなかった
async function loadData() {
try {
const req = new Request(DATA_URL);
req.timeoutInterval = 20;
const text = await req.loadString();
const data = JSON.parse(text);
if (data && Array.isArray(data.items)) {
return { state: data.items.length > 0 ? "ok" : "empty", data: data };
}
return { state: "error", data: null };
} catch (e) {
return { state: "error", data: null };
}
}
function formatTime(iso) {
if (!iso) return "";
const d = new Date(iso);
if (isNaN(d.getTime())) return "";
const hh = String(d.getHours()).padStart(2, "0");
const mm = String(d.getMinutes()).padStart(2, "0");
return hh + ":" + mm;
}
function esc(s) {
return String(s || "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
function domainOf(url) {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch (e) {
return "";
}
}
function runURL() {
let scriptName = SCRIPT_NAME_FALLBACK;
try {
const n = Script.name();
if (n) scriptName = n;
} catch (e) {}
return "scriptable:///run?scriptName=" + encodeURIComponent(scriptName);
}
// ---------- 一覧ページ(HTML) ----------
function pageShell(data, innerHTML) {
const updated = data ? formatTime(data.updatedAt) : "";
const css = ""
+ ":root{color-scheme:light dark}"
+ "*{box-sizing:border-box}"
+ "body{margin:0;padding:20px 16px 32px;font-family:-apple-system,BlinkMacSystemFont,'Hiragino Sans',sans-serif;"
+ "background:linear-gradient(180deg,#f6f7fc 0%,#eceef7 100%);color:#1a1a1a}"
+ "@media (prefers-color-scheme: dark){body{background:linear-gradient(180deg,#171924 0%,#222636 100%);color:#f5f5f5}}"
+ "header{margin:4px 2px 16px}"
+ "h1{font-size:22px;margin:0 0 6px}"
+ ".sub{font-size:13px;opacity:.65}"
+ ".accent{height:3px;border-radius:2px;background:linear-gradient(90deg,#6c5ce7,#0984e3,#00a087);margin-top:12px}"
+ ".card{display:block;text-decoration:none;color:inherit;background:rgba(255,255,255,.85);"
+ "border-radius:16px;padding:14px 16px;margin:0 0 12px;box-shadow:0 2px 10px rgba(30,35,70,.08)}"
+ "@media (prefers-color-scheme: dark){.card{background:rgba(255,255,255,.07);box-shadow:none}}"
+ ".chip{display:inline-block;color:#fff;font-size:11px;font-weight:700;"
+ "padding:3px 9px;border-radius:999px;margin-bottom:8px}"
+ ".title{display:block;font-size:16px;font-weight:700;line-height:1.45}"
+ ".summary{display:block;font-size:13px;line-height:1.55;opacity:.75;margin-top:6px}"
+ ".meta{display:block;font-size:12px;opacity:.5;margin-top:8px}"
+ ".notice{background:rgba(255,255,255,.85);border-radius:16px;padding:22px 16px;"
+ "text-align:center;font-size:15px;font-weight:600;line-height:1.6}"
+ "@media (prefers-color-scheme: dark){.notice{background:rgba(255,255,255,.07)}}"
+ ".notice .small{display:block;font-size:12px;font-weight:400;opacity:.6;margin-top:6px}"
+ "footer{font-size:12px;opacity:.5;text-align:center;margin-top:20px}";
const sub = data
? esc(data.date || "") + " " + esc(data.edition || "")
+ (updated ? " ・ " + updated + "更新" : "")
+ " ・ 全" + data.items.length + "件"
: "";
return "<!DOCTYPE html><html lang='ja'><head><meta charset='utf-8'>"
+ "<meta name='viewport' content='width=device-width, initial-scale=1'>"
+ "<title>テック最新情報</title><style>" + css + "</style></head><body>"
+ "<header><h1>📰 テック最新情報</h1>"
+ (sub ? "<div class='sub'>" + sub + "</div>" : "")
+ "<div class='accent'></div></header>"
+ innerHTML
+ "</body></html>";
}
function buildListHTML(result) {
if (result.state === "error") {
return pageShell(null,
"<div class='notice'>ニュースを取得できませんでした"
+ "<span class='small'>通信環境を確認して、時間をおいて再度お試しください</span></div>");
}
if (result.state === "empty") {
return pageShell(result.data,
"<div class='notice'>目立ったニュースはありません"
+ "<span class='small'>次回の配信をお待ちください</span></div>"
+ "<footer>配信は毎日 8:20頃・12:20頃・17:20頃・21:20頃です</footer>");
}
const data = result.data;
const cards = data.items.map(function (item) {
const hex = CATEGORY_COLORS[item.category] || "#636e72";
const domain = domainOf(item.url);
const meta = [item.edition || "", domain].filter(Boolean).join(" ・ ");
return ""
+ '<a class="card" href="' + esc(item.url) + '">'
+ '<span class="chip" style="background:' + hex + '">' + esc(item.category || "その他") + "</span>"
+ '<span class="title">' + esc(item.title) + "</span>"
+ (item.summary ? '<span class="summary">' + esc(item.summary) + "</span>" : "")
+ (meta ? '<span class="meta">' + esc(meta) + "</span>" : "")
+ "</a>";
}).join("\n");
return pageShell(data, cards + "<footer>カードをタップすると出典の記事・ポストが開きます</footer>");
}
async function presentListPage() {
const result = await loadData();
const wv = new WebView();
await wv.loadHTML(buildListHTML(result));
await wv.present();
}
// ---------- ウィジェット ----------
function buildWidgetBase() {
const widget = new ListWidget();
widget.setPadding(12, 14, 12, 14);
const gradient = new LinearGradient();
gradient.colors = [
Color.dynamic(new Color("#ffffff"), new Color("#1d2030")),
Color.dynamic(new Color("#eef1fa"), new Color("#262a3e"))
];
gradient.locations = [0, 1];
widget.backgroundGradient = gradient;
return widget;
}
function addHeader(widget, subText) {
const header = widget.addStack();
header.centerAlignContent();
const hTitle = header.addText("📰 テック最新情報");
hTitle.font = Font.boldSystemFont(14);
hTitle.textColor = PRIMARY_TEXT;
header.addSpacer();
if (subText) {
const hSub = header.addText(subText);
hSub.font = Font.systemFont(9);
hSub.textColor = SECONDARY_TEXT;
}
widget.addSpacer(5);
const line = widget.addStack();
line.size = new Size(0, 2);
line.backgroundColor = ACCENT;
line.cornerRadius = 1;
widget.addSpacer(9);
}
function addNotice(widget, mainText, subText) {
const main = widget.addText(mainText);
main.font = Font.semiboldSystemFont(12);
main.textColor = PRIMARY_TEXT;
main.lineLimit = 2;
if (subText) {
widget.addSpacer(4);
const sub = widget.addText(subText);
sub.font = Font.systemFont(10);
sub.textColor = SECONDARY_TEXT;
sub.lineLimit = 2;
}
}
function addItem(widget, item) {
const row = widget.addStack();
row.topAlignContent();
const chip = row.addStack();
chip.backgroundColor = colorFor(item.category);
chip.cornerRadius = 6;
chip.setPadding(2, 6, 2, 6);
const chipText = chip.addText(item.category || "その他");
chipText.font = Font.boldSystemFont(8);
chipText.textColor = Color.white();
chipText.lineLimit = 1;
row.addSpacer(7);
const title = row.addText(item.title || "");
title.font = Font.semiboldSystemFont(12);
title.textColor = PRIMARY_TEXT;
title.lineLimit = 2;
}
async function createWidget() {
const result = await loadData();
const widget = buildWidgetBase();
if (result.state === "error") {
addHeader(widget, "");
addNotice(widget, "ニュースを取得できませんでした", "時間をおいて再度お試しください");
} else if (result.state === "empty") {
addHeader(widget, (result.data.edition || "") + " " + formatTime(result.data.updatedAt));
addNotice(widget, "目立ったニュースはありません", "次回の配信をお待ちください");
} else {
const data = result.data;
addHeader(widget, (data.edition || "") + " " + formatTime(data.updatedAt));
let maxItems = 4;
if (config.widgetFamily === "small") maxItems = 2;
if (config.widgetFamily === "large") maxItems = 8;
const items = data.items.slice(0, maxItems);
for (let i = 0; i < items.length; i++) {
addItem(widget, items[i]);
if (i < items.length - 1) widget.addSpacer(8);
}
}
// タップでこのスクリプトを開き、一覧ページを表示する
widget.url = runURL();
widget.refreshAfterDate = new Date(Date.now() + 1000 * 60 * 30);
return widget;
}
// ---------- 実行 ----------
if (config.runsInWidget) {
const widget = await createWidget();
Script.setWidget(widget);
} else {
await presentListPage();
}
Script.complete();
最後に、iphoneのホーム画面を長押しで編集モードに入ったあと、Scriptableのウィジェットを追加してください。
追加したウィジェットを長押し⇨ウィジェットを編集⇨Scriptに先ほど作ったスクリプトを指定すれば、完了‼️お疲れ様でした‼️
出力結果

⬅️ホーム画面
ウィジェットをタッチすると記事の一覧ページに飛んで、それぞれ一次情報を確認できる➡️
重要な点‼️‼️
- 誇張しない。未確認は未確認と書くようにしっかり指示する
- 速報性はGrok、正確さはmuseで一次情報を探し裏取り、と役割を分ける
- 分野と優先順位を先に決めておくと、件数が増えてもブレない
これで無駄にtwitterを徘徊しなくても、携帯を開くだけで自動でローカルLLMの情報が集まりますね。
良かったね。それじゃ、いい情報漬けライフを‼️