3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LINEとDifyを接続する(Difyプラグイン、GAS)

Last updated at Posted at 2025-10-11

LINEにGASをつなぎたいと思い、試してみました。

備忘録を兼ねて、記事にしました。

LINEにGASをつなぐ方法は、Difyプラグインでつなぐ方法、GASでつなぐ方法、MAKEを使いつなぐ方法などがありますが、このうち以下の2つの方法を記載します。

  • A. Difyプラグインでつなぐ方法
  • B. GASでつなぐ方法

以下の記事は、LINE bot を作成していることを前提とします。

A. Difyプラグインでつなぐ方法

”チャットフロー” で新規フローを作成。
開始 → LLM → 終了 のシンプルな構造でOK。

スクリーンショット_11-10-2025_23218_cloud.dify.ai.jpeg

Difyの基本画面の右上「プラグイン」をクリック。

スクリーンショット_11-10-2025_224021_cloud.dify.ai.jpeg

検索欄に「LINE」と入力し検索。
Line Botを見つけ、クリック、インストールする。
スクリーンショット_11-10-2025_224051_cloud.dify.ai.jpeg

インストールが終わったら、プラグイン画面に戻り、「Line Bot」をクリック。
右側に出現したウインドウの「+」をクリック。
スクリーンショット_11-10-2025_224128_cloud.dify.ai.jpeg

各種設定を入力し、保存。

  • エンドポイント名 お好きな名前を
  • チャンネルシークレット LINEbotのチャンネルシークレットを入力
  • チャンネルアクセストークン LINEbotのチャンネルアクセストークンを入力
  • Dify APIキー 使用したいフローのDify APIキーを入力
  • アプリ クリックし、使用したいフローをクリック。

スクリーンショット_11-10-2025_224214_cloud.dify.ai.jpeg

POSTのURLをコピーし、LINEbotのWebhook設定のWebhook URLへ登録。
(Webhookの利用をONにするのを忘れずに)
スクリーンショット_11-10-2025_23245_cloud.dify.ai.jpeg

B. GASでつなぐ方法

LINE → GAS → Dify(Workflow) → GAS → LINE

  • テキスト: inputs.Line_text に渡す
  • 画像 : LINE画像を取得→ImgBBにアップ→公開URLを
    inputs.LINE_picture(files配列)に渡す
    ※LINEの画像はそのまま利用できないので、ImgBBへアップロードしてから、そのURLを利用します。

1. 以下のフローを作成する。

今回はワークフローで作成しています。
(チャットフローでもできますが、GASは別の設定が必要なようです)
スクリーンショット_11-10-2025_21274_cloud.dify.ai.jpeg

開始ノードの設定

入力フィールド
以下の変数を作成

  • 開始/{x}Line_text(フィールドタイプ:短文, string)
  • 開始/{x}LINE_picture(フィールドタイプ:ファイルリスト, array[file])

スクリーンショット_11-10-2025_212939_cloud.dify.ai.jpeg

開始ノード 変数設定.png

分岐ノードの設定

スクリーンショット_11-10-2025_212748_cloud.dify.ai.jpeg

LLMノードの設定

LLM1:

コンテキスト 開始/{x}Line_text
SYSTEM 開始/{x}Line_text に対し、適切なアドバイスをしてください。
USER 開始/{x}Line_text

LLM2:

コンテキスト (空欄)
SYSTEM (空欄)
USER 開始/{x}LINE_picture の文字を読み取って、内容を整理してください。絵の場合は、その絵を解説してください。
ビジョン 開始/{x}LINE_picture

LLM設定.png

終了ノードの設定

終了 設定.png

2. スクリプトプロパティの設定

GASの左側メニュー一番下の歯車をクリック。
一番下のスクリプト プロパティに、以下の環境変数を設定。

  • DIFY_API_KEY DifyでAPIキーを取得してください
  • IMGBB_API_KEY imgBBでAPIキーを取得してください
  • LINE_CHANNEL_ACCESS_TOKEN LINEbotのチャンネルアクセストークン(長期)を取得してください

スクリーンショット_11-10-2025_213324_script.google.com.jpeg

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をつなげると、さまざまなものに応用できると思います。
 これを元に、更に作成していきたいと思います。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?