📁

Difyの全アプリのDSLファイルを一括でダウンロードする

2025/03/02に公開

概要

この記事では、Difyの内部API(console-api)を利用して、ダッシュボード内にある全アプリのDSLファイルを一括でダウンロードする方法を紹介します。

ユースケース

  • Difyのバックアップのため全アプリのDSLを定期的に取得したい
  • Difyの別環境でもDSLファイルを同期したい

対象者

  • Difyを管理しているエンジニアや管理者
  • Pythonを実行できる環境を持っている方(PCやサーバ)

通常のDSLファイルのダウンロード

通常アプリのDSLファイルをダウンロードするとき、画像の通り一つ一つ操作が必要です。

例えば、アプリのバックアップを定期的に取りたい場合、これだと大変です

Difyの内部API

Difyはユーザ向けに用意されているAPI(ワークフローやチャットフローを実行するためのエンドポイント)、いわゆるAPP APIとは別に、Difyの内部処理を担うエンドポイント、いわゆるCONSOLE APIがあります。

今回は、CONSOLE APIを利用して全アプリのDSLファイルを一括でエクスポートする方法を紹介します。

リポジトリ

GitHubのリポジトリは以下の通りです。
https://github.com/sattosan/dify-apps-dsl-exporter

早く利用したい人は、セットアップを行い使用ください。一括でDSLファイルをダウンロードできるはずです。

前提条件

このスクリプトの実行には、以下の環境が必要です。

  • Python 3.13以上
  • Poetry(パッケージ管理ツール)
  • Dify v.1.0.0(動作確認)

インストール方法

  1. リポジトリをクローン
$ git clone https://github.com/sattosan/dify-apps-dsl-exporter.git
$ cd dify-apps-dsl-exporter
  1. Poetryを使って依存関係をインストール
$ poetry install
  1. .env ファイルを作成して必要な環境変数を設定
$ cp .env.example .env

.env ファイルの内容を編集し、Difyのログイン情報を設定します。

DIFY_ORIGIN=http://localhost  # DifyのURLを指定
EMAIL=your-email@example.com # ログイン用メールアドレス
PASSWORD=your-password # ログイン用パスワード

実装の紹介

以下に、コードの主要な処理を順を追って解説します。

アクセストークンの取得

まず、Difyの内部APIを利用するためにトークンを用意する必要があります。

DifyのログインAPIは POST /console/api/login を使用し、リクエストボディには emailpassword を含める必要があります。成功すると、レスポンスには access_token が含まれ、このトークンは以降のAPIリクエストで利用します。

async def login_and_get_token() -> str:
    params = {"page": page, "limit": limit}
    headers = {"Authorization": f"Bearer {access_token}"}

    for attempt in range(retries):
        response = await client.get(f"{BASE_URL}/apps", headers=headers, params=params)
        if response.status_code == 200:
            return response.json()
        else:
            print(
                f"Attempt {attempt + 1} failed: {response.status_code} - {response.text}"
            )
            await asyncio.sleep(0.5)  # Wait before retrying

ダッシュボードにあるアプリの情報一覧を取得

取得したアクセストークンを用いて、Difyに登録されている全アプリの情報を取得します。
DSLのダウンロードにはアプリごとのIDが必要なためこのAPIをコールする必要があります。

Difyの全アプリの情報取得は GET /console/api/apps を使用し、リクエストには Authorization: Bearer <access_token> ヘッダーとページングをパラメータに含める必要があります。

※ リトライ処理も含めているためネストが深い

async def fetch_app_per_page(
    access_token: str, page: int, limit: int, retries: int = 3
) -> dict:
    params = {"page": page, "limit": limit}
    headers = {"Authorization": f"Bearer {access_token}"}

    for attempt in range(retries):
        response = await client.get(f"{BASE_URL}/apps", headers=headers, params=params)
        if response.status_code == 200:
            return response.json()
        else:
            print(
                f"Attempt {attempt + 1} failed: {response.status_code} - {response.text}"
            )
            await asyncio.sleep(0.5)  # Wait before retrying

    raise Exception("Failed to fetch app list.")

async def get_app_list(access_token: str) -> tuple[list, int]:
    app_list = []
    page = 1
    limit = 30
    app_num = 0
    while True:
        content = await fetch_app_per_page(access_token, page, limit)

        if page == 1:
            # Calculate total pages from the first API response
            app_num = content.get("total", 0)
            max_page_num = app_num // limit + (app_num % limit > 0)
            print(f"Total apps: {app_num}, Total pages: {max_page_num}")

        if app_num == 0:
            return [], 0

        if page > max_page_num:
            break

        app_per_page = [
            {"id": app.get("id"), "name": app.get("name")}
            for app in content.get("data", [])
        ]
        app_list.extend(app_per_page)
        page += 1

    return app_list, app_num

アプリの情報一覧のレスポンス例

レスポンスには、アプリのIDや名前を含むデータのリストがJSON形式で返されます。
ここにレスポンス例を紹介します。

  • page:取得したアプリ一覧に対応するページ番号
  • limit:リクエストしたlimitに対応
  • data:アプリ情報がリストになっています
    • 今回はidnameのみ抽出します
{
  "page": 1,
  "limit": 30,
  "total": 4,
  "has_more": false,
  "data": [
     {
      "id": "9906b07e-bad4-4fec-809f-5fab43164d36",
      "name": "テストチャット",
      "max_active_requests": null,
      "description": "テストチャットボットです",
      "mode": "advanced-chat",
      "icon_type": "emoji",
      "icon": "older_adult",
      "icon_background": "#D5F5F6",
      "icon_url": null,
      "model_config": null,
      "workflow": null,
      "use_icon_as_answer_icon": false,
      "created_by": "0c4123cb-23b3-48c8-88ff-a15cea1f51bf",
      "created_at": 1734187926,
      "updated_by": "0c4123cb-23b3-48c8-88ff-a15cea1f51bf",
      "updated_at": 1734187961,
      "tags": []
    },
    {
      "id": "0e3110ac-66d9-47f4-ac6a-f15c5a5d3790",
      "name": "testset",
      "max_active_requests": null,
      "description": "",
      "mode": "workflow",
      "icon_type": "emoji",
      "icon": "🤖",
      "icon_background": "#FFEAD5",
      "icon_url": null,
      "model_config": null,
      "workflow": {
        "id": "e77b6145-57b9-4117-8e01-f4c6105b917f",
        "created_by": "0c4123cb-23b3-48c8-88ff-a15cea1f51bf",
        "created_at": 1733542770,
        "updated_by": null,
        "updated_at": 1733541702
      },
      "use_icon_as_answer_icon": false,
      "created_by": "0c4123cb-23b3-48c8-88ff-a15cea1f51bf",
      "created_at": 1733501248,
      "updated_by": "0c4123cb-23b3-48c8-88ff-a15cea1f51bf",
      "updated_at": 1733542793,
      "tags": []
    }
  ]
}

DSLファイルをダウンロード

取得したアプリのidをパスパラメータapp_idとして指定して、DSLファイルをダウンロードします。

DifyのDSLファイル取得APIは GET /console/api/apps/{app_id}/export?include_secret=true を使用し、リクエストには Authorization: Bearer <access_token> ヘッダーを含める必要があります。レスポンスには該当アプリのDSLファイルがYAML形式で返されます。

※ リトライ処理を追加しているためネストが深くなっています

async def download_yml_files(access_token: str, apps: list):
    create_dsl_folder()  # Create folder to save YML files
    tasks = [asyncio.create_task(download_yml_file(access_token, app)) for app in apps]
    await asyncio.gather(*tasks)  # Run all download tasks concurrently


async def download_yml_file(access_token, app, retries=3):
    app_id = app["id"]
    app_name = app["name"]

    headers = {"Authorization": f"Bearer {access_token}"}
    url = f"{BASE_URL}/apps/{app_id}/export?include_secret=true"
    # Limit the maximum number of concurrent tasks using a semaphore
    async with semaphore:
        for attempt in range(retries):
            response = await client.get(url, headers=headers)
            if response.status_code == 200:
                dsl_data = response.json().get("data").encode("utf-8")
                file_name = f"{DSL_FOLDER_PATH}/{app_name}.yml"
                with open(file_name, "wb") as file:
                    file.write(dsl_data)
                print(f"Downloaded: {file_name}")
                return
            else:
                print(
                    f"Attempt {attempt + 1} failed: {response.status_code} - {app_name}"
                )
                await asyncio.sleep(0.5)  # Wait before retrying

    raise Exception("Failed to fetch app list.")

実行して全DSLをダウンロード

以下コマンドで実行するとダウンロードが開始されます

$ poetry run python ./src/main.py

ダウンロードが成功すれば以下の様に ./dslが新規作成され、DSLファイルが配置されます。

$ ls ./dsl
xxxx.yml
xxxx.yml
xxxx.yml

まとめ

本記事では、Difyの内部APIを活用して、ダッシュボード内の全アプリのDSLファイルをダウンロードする方法を紹介しました。

重要なポイント

  • PythonとPoetryを用いた環境構築
  • アクセストークンの取得
  • APIを利用したアプリ情報の取得
  • DSLファイルのダウンロード

このスクリプトを活用することで、Difyのアプリ設定を一括管理しやすくなります。

株式会社ZOZO

Discussion