LINEにGASをつなぎたいと思い、試してみました。
備忘録を兼ねて、記事にしました。
LINEにGASをつなぐ方法は、Difyプラグインでつなぐ方法、GASでつなぐ方法、MAKEを使いつなぐ方法などがありますが、このうち以下の2つの方法を記載します。
- A. Difyプラグインでつなぐ方法
- B. GASでつなぐ方法
以下の記事は、LINE bot を作成していることを前提とします。
A. Difyプラグインでつなぐ方法
”チャットフロー” で新規フローを作成。
開始 → LLM → 終了 のシンプルな構造でOK。
Difyの基本画面の右上「プラグイン」をクリック。
検索欄に「LINE」と入力し検索。
Line Botを見つけ、クリック、インストールする。

インストールが終わったら、プラグイン画面に戻り、「Line Bot」をクリック。
右側に出現したウインドウの「+」をクリック。

各種設定を入力し、保存。
- エンドポイント名 お好きな名前を
- チャンネルシークレット LINEbotのチャンネルシークレットを入力
- チャンネルアクセストークン LINEbotのチャンネルアクセストークンを入力
- Dify APIキー 使用したいフローのDify APIキーを入力
- アプリ クリックし、使用したいフローをクリック。
POSTのURLをコピーし、LINEbotのWebhook設定のWebhook URLへ登録。
(Webhookの利用をONにするのを忘れずに)

B. GASでつなぐ方法
LINE → GAS → Dify(Workflow) → GAS → LINE
- テキスト: inputs.Line_text に渡す
- 画像 : LINE画像を取得→ImgBBにアップ→公開URLを
inputs.LINE_picture(files配列)に渡す
※LINEの画像はそのまま利用できないので、ImgBBへアップロードしてから、そのURLを利用します。
1. 以下のフローを作成する。
今回はワークフローで作成しています。
(チャットフローでもできますが、GASは別の設定が必要なようです)

開始ノードの設定
入力フィールド
以下の変数を作成
- 開始/{x}Line_text(フィールドタイプ:短文, string)
- 開始/{x}LINE_picture(フィールドタイプ:ファイルリスト, array[file])
分岐ノードの設定
LLMノードの設定
LLM1:
コンテキスト 開始/{x}Line_text
SYSTEM 開始/{x}Line_text に対し、適切なアドバイスをしてください。
USER 開始/{x}Line_text
LLM2:
コンテキスト (空欄)
SYSTEM (空欄)
USER 開始/{x}LINE_picture の文字を読み取って、内容を整理してください。絵の場合は、その絵を解説してください。
ビジョン 開始/{x}LINE_picture
終了ノードの設定
2. スクリプトプロパティの設定
GASの左側メニュー一番下の歯車をクリック。
一番下のスクリプト プロパティに、以下の環境変数を設定。
- DIFY_API_KEY DifyでAPIキーを取得してください
- IMGBB_API_KEY imgBBでAPIキーを取得してください
- LINE_CHANNEL_ACCESS_TOKEN LINEbotのチャンネルアクセストークン(長期)を取得してください
3. GASコードを書く。
以下のGASコードをGASにコピペしてください。
(以下のGASコードはDifyのワークフロー用のコードですが、Difyのチャットフローで作りたい場合は生成AI等で作り直してください。)
デプロイ(webアプリ)して、URLをコピー
/******************************
* LINE → GAS → Dify(Workflow) → GAS → LINE
* - テキスト: inputs.Line_text に渡す
* - 画像 : LINE画像を取得→ImgBBにアップ→公開URLを
* inputs.LINE_picture(files配列)に渡す
******************************/
// ===== 設定(必ず置き換えてください) =====
const API_URL = 'https://api.dify.ai/v1/workflows/run'; // ← Workflowアプリ用エンドポイント
const DIFY_API_KEY = PropertiesService.getScriptProperties().getProperty("DIFY_API_KEY"); // ← DifyのService API Key
const LINE_CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_CHANNEL_ACCESS_TOKEN"); // ← LINEのチャネルアクセストークン
const IMGBB_API_KEY = PropertiesService.getScriptProperties().getProperty("IMGBB_API_KEY"); // ImgBB API Key(https://api.imgbb.com/)
// ==== LINE Webhook 受信 ====
function doPost(e) {
const json = JSON.parse(e.postData?.contents || '{}');
const event = json?.events?.[0];
const replyToken = event?.replyToken;
const userId = event?.source?.userId || 'anonymous';
const message = event?.message;
if (!replyToken || !message) return ContentService.createTextOutput('OK');
// Startノード入力:Line_text(文字列)/ LINE_picture(files配列)
const inputs = { Line_text: '', LINE_picture: [] };
// --- テキスト or 画像 を振り分け ---
if (message.type === 'text') {
inputs.Line_text = String(message.text || '');
} else if (message.type === 'image') {
try {
// LINE画像 → 取得 → ImgBBアップロード → 公開URL
const { url, mimeType } = getPublicImageUrlFromLineEvent(event);
// Dify の files型(配列)に合わせたファイルオブジェクトを設定
inputs.LINE_picture = [ makeDifyImageFile(url, mimeType) ];
// 画像のみ送られた場合、Line_text は空のままでOK(Workflow側でOCR)
} catch (err) {
Logger.log(err);
replyToLine(replyToken, '画像の取得またはアップロードに失敗しました。もう一度お試しください。');
return ContentService.createTextOutput('OK');
}
} else {
replyToLine(replyToken, 'テキストまたは画像で送ってください。');
return ContentService.createTextOutput('OK');
}
// --- Dify Workflow 実行 ---
const payload = {
inputs, // { Line_text: string, LINE_picture: [file,...] }
response_mode: 'blocking',
user: userId
};
const options = {
method: 'post',
contentType: 'application/json',
headers: { Authorization: 'Bearer ' + DIFY_API_KEY },
payload: JSON.stringify(payload),
muteHttpExceptions: true, // エラー本文も取得
};
try {
const res = UrlFetchApp.fetch(API_URL, options);
const code = res.getResponseCode();
const text = res.getContentText();
if (code !== 200) {
Logger.log('Dify error code: ' + code);
Logger.log('Dify error body: ' + text);
replyToLine(replyToken, '(詳細)' + safeShort(text));
return ContentService.createTextOutput('OK');
}
const data = JSON.parse(text);
// Workflow の最終出力(OCRテキストなど)を取り出す
const answer = extractWorkflowAnswer(data) || 'OCRテキストの取得に失敗しました。';
replyToLine(replyToken, answer);
} catch (err) {
Logger.log(err);
replyToLine(replyToken, 'エラーが発生しました。後ほどお試しください。');
}
return ContentService.createTextOutput('OK');
}
// ==== Dify の files型用:ファイルオブジェクト生成 ====
function makeDifyImageFile(url, mimeType) {
// Dify の files 入力は { type, transfer_method, url, [mime_type] } を要素とする配列
const f = { type: 'image', transfer_method: 'remote_url', url };
if (mimeType) f.mime_type = mimeType; // 任意
return f;
}
// ==== 画像の公開URLを取得 ====
// 1) contentProvider.type === 'external' ならそのURLを使用
// 2) それ以外は LINE コンテンツAPIでバイナリ取得 → ImgBBへアップロード → 公開URL
function getPublicImageUrlFromLineEvent(event) {
const msg = event?.message;
const provider = msg?.contentProvider;
if (provider?.type === 'external' && provider?.originalContentUrl) {
return { url: provider.originalContentUrl, mimeType: null };
}
const messageId = msg?.id;
if (!messageId) throw new Error('messageId not found');
const blob = fetchLineContentAsBlob(messageId);
const mimeType = blob.getContentType() || null;
const url = uploadToImgBB(blob); // 直接画像URL(i.ibb.co/~)を返す
return { url, mimeType };
}
// ==== LINE コンテンツAPIから画像Blobを取得 ====
function fetchLineContentAsBlob(messageId) {
const url = 'https://api-data.line.me/v2/bot/message/' + messageId + '/content';
const res = UrlFetchApp.fetch(url, {
method: 'get',
headers: { Authorization: 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN },
muteHttpExceptions: true,
});
const code = res.getResponseCode();
if (code !== 200) {
throw new Error('LINE content fetch failed: ' + code + ' ' + res.getContentText());
}
return res.getBlob();
}
// ==== ImgBB へアップロードして公開URLを返す ====
// Base64 で送信(application/x-www-form-urlencoded)
function uploadToImgBB(blob) {
const url = 'https://api.imgbb.com/1/upload?key=' + encodeURIComponent(IMGBB_API_KEY);
const payload = {
image: Utilities.base64Encode(blob.getBytes()),
name: 'line_' + Date.now()
};
const res = UrlFetchApp.fetch(url, {
method: 'post',
payload, // フォームURLエンコードで送信
muteHttpExceptions: true,
});
const code = res.getResponseCode();
const text = res.getContentText();
if (code !== 200) {
throw new Error('ImgBB upload failed: ' + code + ' ' + text);
}
const js = JSON.parse(text);
if (!js?.success) {
throw new Error('ImgBB upload not success: ' + text);
}
// 直接画像のURLを優先(フォールバックで page/display URL)
return js?.data?.image?.url || js?.data?.url || js?.data?.display_url;
}
// ==== LINE 返信 ====
function replyToLine(replyToken, message) {
const reply = {
replyToken,
messages: [{ type: 'text', text: String(message || '') }]
};
const options = {
method: 'post',
contentType: 'application/json',
headers: { Authorization: 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN },
payload: JSON.stringify(reply),
};
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', options);
}
// ==== Workflow 応答からテキストを取り出す(必要に応じて1行に最適化可) ====
// あなたのフローで最終出力キーが確定しているなら、例:return data.data.outputs.final_text; に置き換えOK。
function extractWorkflowAnswer(data) {
try {
const d = data?.data;
const out = d?.outputs;
if (out && typeof out === 'object') {
if (typeof out.text === 'string' && out.text.trim()) return out.text;
if (typeof out.result === 'string' && out.result.trim()) return out.result;
if (typeof out.answer === 'string' && out.answer.trim()) return out.answer;
// 最初に見つかった非空文字列を返す
for (const k in out) {
const v = out[k];
if (typeof v === 'string' && v.trim()) return v;
}
}
if (typeof d?.message === 'string' && d.message.trim()) return d.message;
if (typeof data?.answer === 'string' && data.answer.trim()) return data.answer;
} catch (_) {}
return '';
}
// ==== 長文を省略して返す(エラー本文をユーザーへ返す際の保険) ====
function safeShort(s, lim = 500) {
if (!s) return '';
s = String(s);
return s.length > lim ? s.slice(0, lim) + '…' : s;
}
まとめ
LINEとDifyをつなげると、さまざまなものに応用できると思います。
これを元に、更に作成していきたいと思います。








