diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a207bdf --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 Tiny Fish, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index adfe7d8..aacd7ac 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# empty-repo +# TinyFish Web Agent Integrations + +TinyFish Web Agent provides AI-powered web automation using natural language instructions and can interact with any website including bot-protected pages. TinyFish Web Agent works across multiple sites and adapts to page changes without breaking, making it the perfect tool for automations and AI agents. Check out all our integrations or get started with our [API](https://docs.mino.ai). + +## What is TinyFish Web Agent useful for? + +- Navigating websites and completing workflows using plain English +- Scraping data from bot-protected or dynamic pages +- Automating form submissions and multi-step browser tasks +- Extracting structured data from any web page at scale +- Running browser automations across authenticated systems +- Opening hundreds of URLs and collecting results from every page + +## Integrations + +| Integration | Description | +| -------------- | -------------------------------------------------------------- | +| [Dify](./dify) | Plugin for the [Dify](https://dify.ai) AI application platform | + +## Contribution guidelines + +Want to contribute to these integrations? We love adding TinyFish Web Agent to new ecosystems! Check out individual integration directories for setup and development instructions. + +## License + +These integrations are licensed under the MIT License. diff --git a/dify/.difyignore b/dify/.difyignore new file mode 100644 index 0000000..4685c5e --- /dev/null +++ b/dify/.difyignore @@ -0,0 +1,184 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# Vscode +.vscode/ + +# Git +.git/ +.gitignore +.github/ + +# Mac +.DS_Store + +# Windows +Thumbs.db + +# Dify plugin packages +# To prevent packaging repetitively +*.difypkg + diff --git a/dify/.env.example b/dify/.env.example new file mode 100644 index 0000000..a13c57a --- /dev/null +++ b/dify/.env.example @@ -0,0 +1,3 @@ +INSTALL_METHOD=remote +REMOTE_INSTALL_KEY=********-****-****-****-************ +REMOTE_INSTALL_URL=debug.dify.ai:5003 diff --git a/dify/.github/workflows/plugin-publish.yml b/dify/.github/workflows/plugin-publish.yml new file mode 100644 index 0000000..d24c4dd --- /dev/null +++ b/dify/.github/workflows/plugin-publish.yml @@ -0,0 +1,109 @@ +name: Plugin Publish Workflow + +on: + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Download CLI tool + run: | + mkdir -p $RUNNER_TEMP/bin + cd $RUNNER_TEMP/bin + + wget https://github.com/langgenius/dify-plugin-daemon/releases/download/0.0.6/dify-plugin-linux-amd64 + chmod +x dify-plugin-linux-amd64 + + echo "CLI tool location:" + pwd + ls -la dify-plugin-linux-amd64 + + - name: Get basic info from manifest + id: get_basic_info + run: | + PLUGIN_NAME=$(grep "^name:" manifest.yaml | cut -d' ' -f2) + echo "Plugin name: $PLUGIN_NAME" + echo "plugin_name=$PLUGIN_NAME" >> $GITHUB_OUTPUT + + VERSION=$(grep "^version:" manifest.yaml | cut -d' ' -f2) + echo "Plugin version: $VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # If the author's name is not your github username, you can change the author here + AUTHOR=$(grep "^author:" manifest.yaml | cut -d' ' -f2) + echo "Plugin author: $AUTHOR" + echo "author=$AUTHOR" >> $GITHUB_OUTPUT + + - name: Package Plugin + id: package + run: | + cd $GITHUB_WORKSPACE + PACKAGE_NAME="${{ steps.get_basic_info.outputs.plugin_name }}-${{ steps.get_basic_info.outputs.version }}.difypkg" + $RUNNER_TEMP/bin/dify-plugin-linux-amd64 plugin package . -o "$PACKAGE_NAME" + + echo "Package result:" + ls -la "$PACKAGE_NAME" + echo "package_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT + + echo "\nFull file path:" + pwd + echo "\nDirectory structure:" + tree || ls -R + + - name: Checkout target repo + uses: actions/checkout@v3 + with: + repository: ${{steps.get_basic_info.outputs.author}}/dify-plugins + path: dify-plugins + token: ${{ secrets.PLUGIN_ACTION }} + fetch-depth: 1 + persist-credentials: true + + - name: Prepare and create PR + run: | + PACKAGE_NAME="${{ steps.get_basic_info.outputs.plugin_name }}-${{ steps.get_basic_info.outputs.version }}.difypkg" + mkdir -p dify-plugins/${{ steps.get_basic_info.outputs.author }}/${{ steps.get_basic_info.outputs.plugin_name }} + mv "$PACKAGE_NAME" dify-plugins/${{ steps.get_basic_info.outputs.author }}/${{ steps.get_basic_info.outputs.plugin_name }}/ + + cd dify-plugins + + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + + git fetch origin main + git checkout main + git pull origin main + + BRANCH_NAME="bump-${{ steps.get_basic_info.outputs.plugin_name }}-plugin-${{ steps.get_basic_info.outputs.version }}" + git checkout -b "$BRANCH_NAME" + + git add . + git commit -m "bump ${{ steps.get_basic_info.outputs.plugin_name }} plugin to version ${{ steps.get_basic_info.outputs.version }}" + + git push -u origin "$BRANCH_NAME" --force + + git branch -a + echo "Waiting for branch to sync..." + sleep 10 # Wait 10 seconds for branch sync + + - name: Create PR via GitHub API + env: + # How to config the token: + # 1. Profile -> Settings -> Developer settings -> Personal access tokens -> Generate new token (with repo scope) -> Copy the token + # 2. Go to the target repository -> Settings -> Secrets and variables -> Actions -> New repository secret -> Add the token as PLUGIN_ACTION + GH_TOKEN: ${{ secrets.PLUGIN_ACTION }} + run: | + gh pr create \ + --repo langgenius/dify-plugins \ + --head "${{ steps.get_basic_info.outputs.author }}:${{ steps.get_basic_info.outputs.plugin_name }}-${{ steps.get_basic_info.outputs.version }}" \ + --base main \ + --title "bump ${{ steps.get_basic_info.outputs.plugin_name }} plugin to version ${{ steps.get_basic_info.outputs.version }}" \ + --body "bump ${{ steps.get_basic_info.outputs.plugin_name }} plugin package to version ${{ steps.get_basic_info.outputs.version }} + + Changes: + - Updated plugin package file" || echo "PR already exists or creation skipped." # Handle cases where PR already exists diff --git a/dify/.gitignore b/dify/.gitignore new file mode 100644 index 0000000..a16dc97 --- /dev/null +++ b/dify/.gitignore @@ -0,0 +1,176 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# Vscode +.vscode/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride \ No newline at end of file diff --git a/dify/GUIDE.md b/dify/GUIDE.md new file mode 100644 index 0000000..897568e --- /dev/null +++ b/dify/GUIDE.md @@ -0,0 +1,137 @@ +# Dify Plugin Development Guide + +Welcome to Dify plugin development! This guide will help you get started quickly. + +## Plugin Types + +Dify plugins extend three main capabilities: + +| Type | Description | Example | +|------|-------------|---------| +| **Tool** | Perform specific tasks | Google Search, Stable Diffusion | +| **Model** | AI model integrations | OpenAI, Anthropic | +| **Endpoint** | HTTP services | Custom APIs, integrations | + +You can create: +- **Tool**: Tool provider with optional endpoints (e.g., Discord bot) +- **Model**: Model provider only +- **Extension**: Simple HTTP service + +## Setup + +### Requirements +- Python 3.11+ +- Dependencies: `pip install -r requirements.txt` + +## Development Process + +
+1. Manifest Structure + +Edit `manifest.yaml` to describe your plugin: + +```yaml +version: 0.1.0 # Required: Plugin version +type: plugin # Required: plugin or bundle +author: YourOrganization # Required: Organization name +label: # Required: Multi-language names + en_US: Plugin Name + zh_Hans: 插件名称 +created_at: 2023-01-01T00:00:00Z # Required: Creation time (RFC3339) +icon: assets/icon.png # Required: Icon path + +# Resources and permissions +resource: + memory: 268435456 # Max memory (bytes) + permission: + tool: + enabled: true # Tool permission + model: + enabled: true # Model permission + llm: true + text_embedding: false + # Other model types... + # Other permissions... + +# Extensions definition +plugins: + tools: + - tools/my_tool.yaml # Tool definition files + models: + - models/my_model.yaml # Model definition files + endpoints: + - endpoints/my_api.yaml # Endpoint definition files + +# Runtime metadata +meta: + version: 0.0.1 # Manifest format version + arch: + - amd64 + - arm64 + runner: + language: python + version: "3.12" + entrypoint: main +``` + +**Restrictions:** +- Cannot extend both tools and models +- Must have at least one extension +- Cannot extend both models and endpoints +- Limited to one supplier per extension type +
+ +
+2. Implementation Examples + +Study these examples to understand plugin implementation: + +- [OpenAI](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/openai) - Model provider +- [Google Search](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/google) - Tool provider +- [Neko](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/neko) - Endpoint group +
+ +
+3. Testing & Debugging + +1. Copy `.env.example` to `.env` and configure: + ```bash + INSTALL_METHOD=remote + REMOTE_INSTALL_URL=debug.dify.ai:5003 + REMOTE_INSTALL_KEY=your-debug-key + ``` + +2. Run your plugin: + ```bash + python -m main + ``` + +3. Refresh your Dify instance to see the plugin (marked as "debugging") +
+ +
+4. Publishing + +### Manual Packaging +```bash +dify-plugin plugin package ./YOUR_PLUGIN_DIR +``` + +### Automated GitHub Workflow + +Configure GitHub Actions to automate PR creation: + +1. Create a Personal Access Token for your forked repository +2. Add it as `PLUGIN_ACTION` secret in your source repo +3. Create `.github/workflows/plugin-publish.yml` + +When you create a release, the action will: +- Package your plugin +- Create a PR to your fork + +[Detailed workflow documentation](https://docs.dify.ai/plugins/publish-plugins/plugin-auto-publish-pr) +
+ +## Privacy Policy + +If publishing to the Marketplace, provide a privacy policy in [PRIVACY.md](PRIVACY.md). \ No newline at end of file diff --git a/dify/PRIVACY.md b/dify/PRIVACY.md new file mode 100644 index 0000000..eddbb2f --- /dev/null +++ b/dify/PRIVACY.md @@ -0,0 +1,3 @@ +## Privacy + +Please refer to the Privacy Policy of [TinyFish](https://www.tinyfish.ai/privacy-policy). diff --git a/dify/README.md b/dify/README.md new file mode 100644 index 0000000..e5cacb3 --- /dev/null +++ b/dify/README.md @@ -0,0 +1,117 @@ +# TinyFish Web Agent Plugin for Dify + +## Overview + +[TinyFish Web Agent](https://docs.mino.ai/) enables enterprises, builders, and developers to deploy AI agents that navigate real sites, complete real workflows across authenticated systems and dynamic interfaces, and return structured operational intelligence - through our visual platform or API. At scale. Reliably. + +## Configuration + +### 1. Install TinyFish Web Agent Tool + +1. On the Dify platform, access the [Plugin Marketplace](https://docs.dify.ai/plugins/quick-start/install-plugins#marketplace). +2. Locate and install the TinyFish Web Agent tool. + +### 2. Create a TinyFish Web Agent API Key + +Visit [TinyFish Dashboard](https://agent.tinyfish.ai/api-keys) and generate your API key. + +### 3. Authorize TinyFish Web Agent + +Go to **Plugins > TinyFish Web Agent > To Authorize** in Dify and input your API key. + +![Authorization](./_assets/authorization.png) + +## Workflow Usage + +Integrate TinyFish Web Agent into your pipeline by following these steps: + +1. Add TinyFish Web Agent's **Run Asynchronously** tool to your pipeline. +2. Configure input variables in the tool's UI. +3. Run the pipeline to extract any information from a web page. + +![Workflow](./_assets/workflow.png) + +## Agent Usage + +1. Add all TinyFish Web Agent tools to your Agent app. +2. Prompt the Agent to perform web automations using natural language. The Agent will choose the appropriate tool automatically. + +**Available tools:** + +| Tool | Description | +| -------------------------- | ----------------------------------------------------------------------------------------------------- | +| **Run Synchronously** | Execute browser automation and wait for the result in a single response. | +| **Run Asynchronously** | Start browser automation and return a `run_id` immediately. Use with **Get Run** to poll for results. | +| **Run with SSE Streaming** | Execute browser automation with real-time progress updates via Server-Sent Events. | +| **List Runs** | List previous automation runs, with optional status filtering and pagination. | +| **Get Run** | Get detailed information about a specific automation run by its ID. | + +**Example prompts:** + +- `"Extract the blog post titles and authors from https://example.com/blog"` +- `"Go to https://example.com/pricing and extract all plan names and prices"` +- `"List my recent automation runs that have completed"` + +## Debugging the Plugin + +Dify provides a remote debugging method. + +### 1. Get debugging key and server address + +On the platform, go to the "Plugin Management" page to get the debugging key and remote server address. + +![Debug](./_assets/debug.webp) + +### 2. Add server and key to your environment + +Go back to the plugin project, copy the `.env.example` file and rename it to .env. Fill it with the remote server address and debugging key. + +The `.env` file: + +```bash +INSTALL_METHOD=remote +REMOTE_INSTALL_URL=debug.dify.ai:5003 +REMOTE_INSTALL_KEY=********-****-****-****-************ +``` + +### 3. Launch the plugin + +Run the `python -m main` command to launch the plugin. You can see on the plugin page that the plugin has been installed into Workspace. + +![Debug Plugin](./_assets/debug_plugin.png) + +## Contributing + +We love getting contributions! To get started, here's how to set up development for the TinyFish Web Agent Dify Plugin: + +### 1. Fork this repository + +### 2. Clone the repository + +Clone the forked repository from your terminal: + +```shell +git clone git@github.com:/tinyfish-web-agent-integrations.git +``` + +### 3. Install dependencies with virtual environment (Recommended) + +```shell +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +### 4. Make your changes and save + +### 5. Ensure the plugin works + +See the previous [Debugging Guide](#debugging-the-plugin). + +### 6. Submit a Pull Request + +After confirming that the plugin works properly, submit a pull request to the `main` branch of this repository. If you run into issues like merge conflicts or don't know how to open a pull request, check out [GitHub's pull request tutorial](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests). + +## Support + +Need help or have a question while using or contributing to the plugin? File a GitHub issue. diff --git a/dify/_assets/authorization.png b/dify/_assets/authorization.png new file mode 100644 index 0000000..3421ea8 Binary files /dev/null and b/dify/_assets/authorization.png differ diff --git a/dify/_assets/debug.webp b/dify/_assets/debug.webp new file mode 100644 index 0000000..2112d04 Binary files /dev/null and b/dify/_assets/debug.webp differ diff --git a/dify/_assets/debug_plugin.png b/dify/_assets/debug_plugin.png new file mode 100644 index 0000000..57dedc5 Binary files /dev/null and b/dify/_assets/debug_plugin.png differ diff --git a/dify/_assets/icon.svg b/dify/_assets/icon.svg new file mode 100644 index 0000000..ca9bbe9 --- /dev/null +++ b/dify/_assets/icon.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dify/_assets/workflow.png b/dify/_assets/workflow.png new file mode 100644 index 0000000..3253497 Binary files /dev/null and b/dify/_assets/workflow.png differ diff --git a/dify/main.py b/dify/main.py new file mode 100644 index 0000000..7e1a983 --- /dev/null +++ b/dify/main.py @@ -0,0 +1,6 @@ +from dify_plugin import Plugin, DifyPluginEnv + +plugin = Plugin(DifyPluginEnv(MAX_REQUEST_TIMEOUT=120)) + +if __name__ == '__main__': + plugin.run() diff --git a/dify/manifest.yaml b/dify/manifest.yaml new file mode 100644 index 0000000..80abd53 --- /dev/null +++ b/dify/manifest.yaml @@ -0,0 +1,44 @@ +version: 0.0.1 +type: plugin +author: tinyfish +name: tinyfish-web-agent +label: + en_US: TinyFish Web Agent + ja_JP: TinyFish Web Agent + zh_Hans: TinyFish Web Agent + pt_BR: TinyFish Web Agent +description: + en_US: AI-powered web automation that turns natural language into browser actions + ja_JP: 自然言語をブラウザアクションに変換するAI搭載のウェブ自動化 + zh_Hans: AI驱动的网络自动化将自然语言转换为浏览器操作 + pt_BR: Automação web alimentada por IA que transforma linguagem natural em ações do navegador +icon: icon.svg +icon_dark: icon.svg +resource: + memory: 268435456 + permission: + tool: + enabled: true + endpoint: + enabled: true + app: + enabled: true + storage: + enabled: true + size: 1048576 +plugins: + tools: + - provider/tinyfish_web_agent.yaml +meta: + version: 0.0.1 + arch: + - amd64 + - arm64 + runner: + language: python + version: "3.13" + entrypoint: main + minimum_dify_version: null +created_at: 2026-02-08T22:40:58.248502-08:00 +privacy: PRIVACY.md +verified: false diff --git a/dify/provider/__init__.py b/dify/provider/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dify/provider/tinyfish_web_agent.py b/dify/provider/tinyfish_web_agent.py new file mode 100644 index 0000000..6e4ec62 --- /dev/null +++ b/dify/provider/tinyfish_web_agent.py @@ -0,0 +1,11 @@ +from typing import Any + +from dify_plugin import ToolProvider +from dify_plugin.errors.tool import ToolProviderCredentialValidationError + + +class TinyfishWebAgentProvider(ToolProvider): + def _validate_credentials(self, credentials: dict[str, Any]) -> None: + api_key = credentials.get("api_key") + if not api_key or not isinstance(api_key, str) or not api_key.strip(): + raise ToolProviderCredentialValidationError("API key is required") diff --git a/dify/provider/tinyfish_web_agent.yaml b/dify/provider/tinyfish_web_agent.yaml new file mode 100644 index 0000000..8f4374a --- /dev/null +++ b/dify/provider/tinyfish_web_agent.yaml @@ -0,0 +1,45 @@ +identity: + author: "tinyfish" + name: "tinyfish-web-agent" + label: + en_US: "TinyFish Web Agent" + zh_Hans: "TinyFish Web Agent" + pt_BR: "TinyFish Web Agent" + ja_JP: "TinyFish Web Agent" + description: + en_US: "AI-powered web automation that turns natural language into browser actions" + zh_Hans: "AI驱动的网络自动化将自然语言转换为浏览器操作" + pt_BR: "Automação web alimentada por IA que transforma linguagem natural em ações do navegador" + ja_JP: "自然言語をブラウザアクションに変換するAI搭載のウェブ自動化" + icon: "icon.svg" + +credentials_for_provider: + api_key: + type: secret-input + required: true + label: + en_US: API Key + zh_Hans: API 密钥 + pt_BR: Chave de API + ja_JP: APIキー + placeholder: + en_US: Please input your TinyFish Web Agent API key + zh_Hans: 请输入你的 TinyFish Web Agent API 密钥 + pt_BR: Insira sua chave de API do TinyFish Web Agent + ja_JP: TinyFish Web Agent APIキーを入力してください + help: + en_US: Get your API key from https://agent.tinyfish.ai/api-keys + zh_Hans: 从 https://agent.tinyfish.ai/api-keys 获取你的 API 密钥 + pt_BR: Obtenha sua chave de API em https://agent.tinyfish.ai/api-keys + ja_JP: https://agent.tinyfish.ai/api-keys からAPIキーを取得してください + url: https://agent.tinyfish.ai/api-keys + +tools: + - tools/run_sse.yaml + - tools/run_sync.yaml + - tools/run_async.yaml + - tools/get_run.yaml + - tools/list_runs.yaml +extra: + python: + source: provider/tinyfish_web_agent.py diff --git a/dify/readme/README_ja_JP.md b/dify/readme/README_ja_JP.md new file mode 100644 index 0000000..a87db4d --- /dev/null +++ b/dify/readme/README_ja_JP.md @@ -0,0 +1,3 @@ +## プラグイン Readme + +詳細は [README.md](../README.md) をご参照ください。 diff --git a/dify/readme/README_pt_BR.md b/dify/readme/README_pt_BR.md new file mode 100644 index 0000000..ae07d9e --- /dev/null +++ b/dify/readme/README_pt_BR.md @@ -0,0 +1,3 @@ +## Readme do Plugin + +Consulte [README.md](../README.md) para obter detalhes. diff --git a/dify/readme/README_zh_Hans.md b/dify/readme/README_zh_Hans.md new file mode 100644 index 0000000..414bec3 --- /dev/null +++ b/dify/readme/README_zh_Hans.md @@ -0,0 +1,3 @@ +## 插件 Readme + +详细说明请参阅 [README.md](../README.md)。 diff --git a/dify/requirements.txt b/dify/requirements.txt new file mode 100644 index 0000000..69adc2d --- /dev/null +++ b/dify/requirements.txt @@ -0,0 +1 @@ +dify_plugin>=0.4.0,<0.7.0 diff --git a/dify/tools/__init__.py b/dify/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dify/tools/base.py b/dify/tools/base.py new file mode 100644 index 0000000..6a0a916 --- /dev/null +++ b/dify/tools/base.py @@ -0,0 +1,52 @@ +from typing import Any + +import httpx + +from tools.constants import API_BASE_URL + + +class TinyfishMixin: + """Mixin for TinyFish tools with shared API request logic.""" + + @property + def _api_headers(self) -> dict[str, str]: + return {"X-API-Key": self.runtime.credentials["api_key"]} + + def _tf_request( + self, + method: str, + path: str, + *, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, + timeout: float = 30.0, + ) -> httpx.Response: + """Make an authenticated request to the TinyFish API.""" + response = httpx.request( + method, + f"{API_BASE_URL}{path}", + headers=self._api_headers, + params=params, + json=json, + timeout=timeout, + ) + response.raise_for_status() + return response + + def _build_automation_payload( + self, tool_parameters: dict[str, Any] + ) -> dict[str, Any]: + """Build the common payload for automation run endpoints.""" + payload: dict[str, Any] = { + "url": tool_parameters["url"], + "goal": tool_parameters["goal"], + "browser_profile": tool_parameters.get("browser_profile", "lite"), + } + + if tool_parameters.get("proxy_enabled"): + proxy_config: dict[str, Any] = {"enabled": True} + if tool_parameters.get("proxy_country_code"): + proxy_config["country_code"] = tool_parameters["proxy_country_code"] + payload["proxy_config"] = proxy_config + + return payload diff --git a/dify/tools/constants.py b/dify/tools/constants.py new file mode 100644 index 0000000..0b0d72c --- /dev/null +++ b/dify/tools/constants.py @@ -0,0 +1 @@ +API_BASE_URL = "https://agent.tinyfish.ai" diff --git a/dify/tools/get_run.py b/dify/tools/get_run.py new file mode 100644 index 0000000..9426e44 --- /dev/null +++ b/dify/tools/get_run.py @@ -0,0 +1,82 @@ +from collections.abc import Generator +from typing import Any + +import httpx +from dify_plugin.entities.tool import ToolInvokeMessage + +from dify_plugin import Tool + +from tools.base import TinyfishMixin + + +class GetRunTool(TinyfishMixin, Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + run_id = tool_parameters.get("run_id") + + if not run_id: + yield self.create_text_message("Error: run_id is required") + return + + try: + response = self._tf_request("GET", f"/v1/runs/{run_id}") + + result = response.json() + status = result.get("status") + + yield self.create_text_message(f"Run Status: {status}") + yield self.create_text_message(f"Run ID: {result.get('run_id')}") + yield self.create_text_message(f"Goal: {result.get('goal', 'N/A')}") + + if result.get("created_at"): + yield self.create_text_message(f"Created: {result['created_at']}") + if result.get("started_at"): + yield self.create_text_message(f"Started: {result['started_at']}") + if result.get("finished_at"): + yield self.create_text_message(f"Finished: {result['finished_at']}") + + if result.get("streaming_url"): + yield self.create_text_message(f"Watch live: {result['streaming_url']}") + + if status == "COMPLETED": + yield self.create_text_message("Automation completed successfully!") + if result.get("result"): + yield self.create_json_message(result["result"]) + else: + yield self.create_text_message("No result data available") + elif status == "FAILED": + error_info = result.get("error", {}) + if error_info: + error_message = ( + error_info.get("message", "Unknown error") + if isinstance(error_info, dict) + else str(error_info) + ) + yield self.create_text_message( + f"Automation failed: {error_message}" + ) + elif status == "RUNNING": + yield self.create_text_message("Automation is still running...") + elif status == "PENDING": + yield self.create_text_message("Automation is pending...") + elif status == "CANCELLED": + yield self.create_text_message("Automation was cancelled") + + yield self.create_json_message(result) + + except httpx.HTTPStatusError as e: + if e.response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + elif e.response.status_code == 404: + yield self.create_text_message( + f"Error: Run not found with ID: {run_id}" + ) + else: + yield self.create_text_message( + f"Error: API request failed with status {e.response.status_code}: {e.response.text}" + ) + except httpx.TimeoutException: + yield self.create_text_message("Error: Request timed out") + except httpx.HTTPError as e: + yield self.create_text_message(f"Error: HTTP error occurred: {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/dify/tools/get_run.yaml b/dify/tools/get_run.yaml new file mode 100644 index 0000000..b5b8b17 --- /dev/null +++ b/dify/tools/get_run.yaml @@ -0,0 +1,39 @@ +identity: + name: "get_run" + author: "tinyfish" + label: + en_US: "Get Run" + zh_Hans: "获取运行" + pt_BR: "Obter Execução" + ja_JP: "実行を取得" +description: + human: + en_US: "Get detailed information about a specific automation run by its ID." + zh_Hans: "通过ID获取特定自动化运行的详细信息。" + pt_BR: "Obtenha informações detalhadas sobre uma execução de automação específica por seu ID." + ja_JP: "IDによって特定の自動化実行の詳細情報を取得します。" + llm: "Retrieve detailed information about a specific automation run including status, results, errors, and configuration. Use this to check the status of async runs or get details about completed runs." +parameters: + - name: run_id + type: string + required: true + label: + en_US: Run ID + zh_Hans: 运行ID + pt_BR: ID de Execução + ja_JP: 実行ID + human_description: + en_US: "The unique identifier of the automation run" + zh_Hans: "自动化运行的唯一标识符" + pt_BR: "O identificador único da execução de automação" + ja_JP: "自動化実行の一意の識別子" + llm_description: "The unique run_id returned from run_async or run_sync. Format: UUID string like 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'" + placeholder: + en_US: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + zh_Hans: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + pt_BR: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + ja_JP: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + form: llm +extra: + python: + source: tools/get_run.py diff --git a/dify/tools/list_runs.py b/dify/tools/list_runs.py new file mode 100644 index 0000000..3005250 --- /dev/null +++ b/dify/tools/list_runs.py @@ -0,0 +1,66 @@ +from collections.abc import Generator +from typing import Any + +import httpx +from dify_plugin.entities.tool import ToolInvokeMessage + +from dify_plugin import Tool + +from tools.base import TinyfishMixin + + +class ListRunsTool(TinyfishMixin, Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + status = tool_parameters.get("status") + limit = tool_parameters.get("limit", 20) + cursor = tool_parameters.get("cursor") + + params: dict[str, Any] = {} + if status: + params["status"] = status + if limit: + params["limit"] = int(limit) + if cursor: + params["cursor"] = cursor + + try: + response = self._tf_request("GET", "/v1/runs", params=params) + + result = response.json() + data = result.get("data", []) + pagination = result.get("pagination", {}) + + if not data: + yield self.create_text_message("No runs found") + return + + yield self.create_text_message(f"Found {len(data)} run(s)") + + if pagination.get("has_more"): + yield self.create_text_message( + f"More results available. Use cursor: {pagination.get('next_cursor')}" + ) + + for idx, run in enumerate(data, 1): + yield self.create_text_message( + f"\n{idx}. {run.get('status')}" + f"\n Run ID: {run.get('run_id')}" + f"\n Goal: {run.get('goal', 'N/A')}" + f"\n Created: {run.get('created_at', 'N/A')}" + ) + + yield self.create_json_message(result) + + except httpx.HTTPStatusError as e: + if e.response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + else: + yield self.create_text_message( + f"Error: API request failed with status {e.response.status_code}: {e.response.text}" + ) + except httpx.TimeoutException: + yield self.create_text_message("Error: Request timed out") + except httpx.HTTPError as e: + yield self.create_text_message(f"Error: HTTP error occurred: {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/dify/tools/list_runs.yaml b/dify/tools/list_runs.yaml new file mode 100644 index 0000000..ea4b730 --- /dev/null +++ b/dify/tools/list_runs.yaml @@ -0,0 +1,103 @@ +identity: + name: "list_runs" + author: "tinyfish" + label: + en_US: "List Runs" + zh_Hans: "列出运行" + pt_BR: "Listar Execuções" + ja_JP: "実行をリスト" +description: + human: + en_US: "List automation runs with optional filtering by status. Returns paginated results ordered by creation date." + zh_Hans: "列出自动化运行,可选择按状态过滤。返回按创建日期排序的分页结果。" + pt_BR: "Liste execuções de automação com filtragem opcional por status. Retorna resultados paginados ordenados por data de criação." + ja_JP: "ステータスによるオプションのフィルタリングを使用して自動化実行をリストします。作成日順に並べられたページ分割された結果を返します。" + llm: "List automation runs with optional filtering by status (PENDING, RUNNING, COMPLETED, FAILED, CANCELLED). Returns paginated results ordered by creation date (newest first). Use cursor for pagination." +parameters: + - name: status + type: select + required: false + label: + en_US: Status Filter + zh_Hans: 状态过滤 + pt_BR: Filtro de Status + ja_JP: ステータスフィルター + human_description: + en_US: "Filter runs by status" + zh_Hans: "按状态过滤运行" + pt_BR: "Filtrar execuções por status" + ja_JP: "ステータスで実行をフィルター" + llm_description: "Optional status filter. Leave empty to get all runs." + options: + - value: "PENDING" + label: + en_US: "Pending" + zh_Hans: "待处理" + pt_BR: "Pendente" + ja_JP: "保留中" + - value: "RUNNING" + label: + en_US: "Running" + zh_Hans: "运行中" + pt_BR: "Em Execução" + ja_JP: "実行中" + - value: "COMPLETED" + label: + en_US: "Completed" + zh_Hans: "已完成" + pt_BR: "Concluído" + ja_JP: "完了" + - value: "FAILED" + label: + en_US: "Failed" + zh_Hans: "失败" + pt_BR: "Falhou" + ja_JP: "失敗" + - value: "CANCELLED" + label: + en_US: "Cancelled" + zh_Hans: "已取消" + pt_BR: "Cancelado" + ja_JP: "キャンセル済み" + form: form + - name: limit + type: number + required: false + default: 20 + min: 1 + max: 100 + label: + en_US: Limit + zh_Hans: 限制 + pt_BR: Limite + ja_JP: 制限 + human_description: + en_US: "Maximum number of results to return (1-100)" + zh_Hans: "要返回的最大结果数(1-100)" + pt_BR: "Número máximo de resultados a retornar (1-100)" + ja_JP: "返す結果の最大数(1-100)" + llm_description: "Maximum number of results to return. Must be between 1 and 100. Default is 20." + form: form + - name: cursor + type: string + required: false + label: + en_US: Cursor + zh_Hans: 游标 + pt_BR: Cursor + ja_JP: カーソル + human_description: + en_US: "Pagination cursor from previous response" + zh_Hans: "来自上一个响应的分页游标" + pt_BR: "Cursor de paginação da resposta anterior" + ja_JP: "前のレスポンスからのページネーションカーソル" + llm_description: "Cursor for pagination. Use the next_cursor value from a previous response to get the next page of results." + placeholder: + en_US: "eyJpZCI6ImFiYyIsImNyZWF0ZWRBdCI6IjIwMjYtMDEtMDFUMTI6MDA6MDBaIn0=" + zh_Hans: "eyJpZCI6ImFiYyIsImNyZWF0ZWRBdCI6IjIwMjYtMDEtMDFUMTI6MDA6MDBaIn0=" + pt_BR: "eyJpZCI6ImFiYyIsImNyZWF0ZWRBdCI6IjIwMjYtMDEtMDFUMTI6MDA6MDBaIn0=" + ja_JP: "eyJpZCI6ImFiYyIsImNyZWF0ZWRBdCI6IjIwMjYtMDEtMDFUMTI6MDA6MDBaIn0=" + form: form +extra: + python: + source: tools/list_runs.py diff --git a/dify/tools/run_async.py b/dify/tools/run_async.py new file mode 100644 index 0000000..72b3aeb --- /dev/null +++ b/dify/tools/run_async.py @@ -0,0 +1,60 @@ +from collections.abc import Generator +from typing import Any + +import httpx +from dify_plugin.entities.tool import ToolInvokeMessage + +from dify_plugin import Tool + +from tools.base import TinyfishMixin + + +class RunAsyncTool(TinyfishMixin, Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + if not tool_parameters.get("url"): + yield self.create_text_message("Error: URL is required") + return + + if not tool_parameters.get("goal"): + yield self.create_text_message("Error: Goal is required") + return + + payload = self._build_automation_payload(tool_parameters) + + try: + response = self._tf_request( + "POST", "/v1/automation/run-async", json=payload + ) + + result = response.json() + run_id = result.get("run_id") + error = result.get("error") + + if error: + error_message = ( + error.get("message", "Unknown error") + if isinstance(error, dict) + else str(error) + ) + yield self.create_text_message(f"Failed to create run: {error_message}") + else: + yield self.create_text_message("Automation run created successfully!") + yield self.create_text_message(f"Run ID: {run_id}") + yield self.create_text_message( + "Use the 'get_run' tool with this run_id to check the status and retrieve results." + ) + yield self.create_json_message({"run_id": run_id}) + + except httpx.HTTPStatusError as e: + if e.response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + else: + yield self.create_text_message( + f"Error: API request failed with status {e.response.status_code}: {e.response.text}" + ) + except httpx.TimeoutException: + yield self.create_text_message("Error: Request timed out") + except httpx.HTTPError as e: + yield self.create_text_message(f"Error: HTTP error occurred: {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/dify/tools/run_async.yaml b/dify/tools/run_async.yaml new file mode 100644 index 0000000..29e8e59 --- /dev/null +++ b/dify/tools/run_async.yaml @@ -0,0 +1,162 @@ +identity: + name: "run_async" + author: "tinyfish" + label: + en_US: "Run Asynchronously" + zh_Hans: "异步运行" + pt_BR: "Executar Assincronamente" + ja_JP: "非同期実行" +description: + human: + en_US: "Start browser automation asynchronously. Returns run_id immediately without waiting for completion." + zh_Hans: "异步启动浏览器自动化。立即返回run_id而无需等待完成。" + pt_BR: "Inicie a automação do navegador de forma assíncrona. Retorna run_id imediatamente sem aguardar a conclusão." + ja_JP: "ブラウザ自動化を非同期で開始します。完了を待たずにrun_idをすぐに返します。" + llm: "Creates and enqueues an automation run, returning the run_id immediately without waiting for completion. Use this for long-running automations where you want to poll for results separately using get_run." +parameters: + - name: url + type: string + required: true + label: + en_US: Target URL + zh_Hans: 目标网址 + pt_BR: URL de destino + ja_JP: ターゲットURL + human_description: + en_US: "The website URL to navigate to and interact with" + zh_Hans: "要导航和交互的网站URL" + pt_BR: "O URL do site para navegar e interagir" + ja_JP: "ナビゲートして操作するウェブサイトのURL" + llm_description: "The target website URL where the automation task should be performed." + placeholder: + en_US: "https://example.com" + zh_Hans: "https://example.com" + pt_BR: "https://example.com" + ja_JP: "https://example.com" + form: llm + - name: goal + type: string + required: true + label: + en_US: Goal + zh_Hans: 目标 + pt_BR: Objetivo + ja_JP: 目標 + human_description: + en_US: "Natural language description of what to accomplish" + zh_Hans: "用自然语言描述要完成的任务" + pt_BR: "Descrição em linguagem natural do que realizar" + ja_JP: "達成すべきことの自然言語による説明" + llm_description: "A clear, natural language instruction describing what the agent should accomplish." + placeholder: + en_US: "Extract product information from the page" + zh_Hans: "从页面中提取产品信息" + pt_BR: "Extrair informações do produto da página" + ja_JP: "ページから製品情報を抽出する" + form: llm + - name: browser_profile + type: select + required: false + default: "lite" + label: + en_US: Browser Profile + zh_Hans: 浏览器配置 + pt_BR: Perfil do navegador + ja_JP: ブラウザプロファイル + human_description: + en_US: "Browser profile for execution" + zh_Hans: "执行的浏览器配置" + pt_BR: "Perfil do navegador para execução" + ja_JP: "実行用のブラウザプロファイル" + llm_description: "Choose 'lite' for standard browser or 'stealth' for anti-detection browser." + options: + - value: "lite" + label: + en_US: "Lite" + zh_Hans: "Lite" + pt_BR: "Lite" + ja_JP: "Lite" + - value: "stealth" + label: + en_US: "Stealth" + zh_Hans: "Stealth" + pt_BR: "Stealth" + ja_JP: "Stealth" + form: form + - name: proxy_enabled + type: boolean + required: false + default: false + label: + en_US: Enable Proxy + zh_Hans: 启用代理 + pt_BR: Ativar Proxy + ja_JP: プロキシを有効にする + human_description: + en_US: "Enable proxy routing" + zh_Hans: "启用代理路由" + pt_BR: "Ativar roteamento de proxy" + ja_JP: "プロキシルーティングを有効にする" + llm_description: "Set to true to route requests through a proxy." + form: form + - name: proxy_country_code + type: select + required: false + label: + en_US: Proxy Country + zh_Hans: 代理国家 + pt_BR: País do Proxy + ja_JP: プロキシ国 + human_description: + en_US: "Country for proxy location. Only used when proxy is enabled." + zh_Hans: "代理位置的国家。仅在启用代理时使用。" + pt_BR: "País para localização do proxy. Usado apenas quando o proxy está ativado." + ja_JP: "プロキシの場所の国。プロキシが有効な場合のみ使用されます。" + llm_description: "ISO country code for proxy location. Only used when proxy is enabled. Valid values: US, GB, CA, DE, FR, JP, AU." + options: + - value: "US" + label: + en_US: "US - United States" + zh_Hans: "US - 美国" + pt_BR: "US - Estados Unidos" + ja_JP: "US - アメリカ" + - value: "GB" + label: + en_US: "GB - United Kingdom" + zh_Hans: "GB - 英国" + pt_BR: "GB - Reino Unido" + ja_JP: "GB - イギリス" + - value: "CA" + label: + en_US: "CA - Canada" + zh_Hans: "CA - 加拿大" + pt_BR: "CA - Canadá" + ja_JP: "CA - カナダ" + - value: "DE" + label: + en_US: "DE - Germany" + zh_Hans: "DE - 德国" + pt_BR: "DE - Alemanha" + ja_JP: "DE - ドイツ" + - value: "FR" + label: + en_US: "FR - France" + zh_Hans: "FR - 法国" + pt_BR: "FR - França" + ja_JP: "FR - フランス" + - value: "JP" + label: + en_US: "JP - Japan" + zh_Hans: "JP - 日本" + pt_BR: "JP - Japão" + ja_JP: "JP - 日本" + - value: "AU" + label: + en_US: "AU - Australia" + zh_Hans: "AU - 澳大利亚" + pt_BR: "AU - Austrália" + ja_JP: "AU - オーストラリア" + form: form +extra: + python: + source: tools/run_async.py diff --git a/dify/tools/run_sse.py b/dify/tools/run_sse.py new file mode 100644 index 0000000..b39ae31 --- /dev/null +++ b/dify/tools/run_sse.py @@ -0,0 +1,102 @@ +import json +from collections.abc import Generator +from typing import Any + +import httpx +from dify_plugin.entities.tool import ToolInvokeMessage + +from dify_plugin import Tool + +from tools.base import TinyfishMixin +from tools.constants import API_BASE_URL + + +class RunSseTool(TinyfishMixin, Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + if not tool_parameters.get("url"): + yield self.create_text_message("Error: URL is required") + return + + if not tool_parameters.get("goal"): + yield self.create_text_message("Error: Goal is required") + return + + payload = self._build_automation_payload(tool_parameters) + + try: + with httpx.Client(timeout=300.0) as client: + with client.stream( + "POST", + f"{API_BASE_URL}/v1/automation/run-sse", + headers=self._api_headers, + json=payload, + ) as response: + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code >= 400: + response.read() + yield self.create_text_message( + f"Error: API request failed with status {response.status_code}: {response.text}" + ) + return + + final_result = None + + for line in response.iter_lines(): + if not line or not line.startswith("data: "): + continue + + try: + event_data = json.loads(line[6:]) + except json.JSONDecodeError: + continue + + event_type = event_data.get("type") + + if event_type == "STARTED": + yield self.create_text_message( + f"Automation started (Run ID: {event_data.get('runId')})" + ) + elif event_type == "STREAMING_URL": + streaming_url = event_data.get("streamingUrl") + if streaming_url: + yield self.create_text_message( + f"Watch live: {streaming_url}" + ) + elif event_type == "PROGRESS": + yield self.create_text_message( + event_data.get("purpose", "Processing...") + ) + elif event_type == "COMPLETE": + status = event_data.get("status") + final_result = event_data.get("resultJson") + + if status == "COMPLETED": + yield self.create_text_message( + "Automation completed successfully!" + ) + if final_result: + yield self.create_json_message(final_result) + else: + yield self.create_text_message( + "No result data returned" + ) + else: + yield self.create_text_message( + f"Automation failed: {event_data.get('error', 'Unknown error')}" + ) + + if final_result is None: + yield self.create_text_message( + "Automation ended without returning a result" + ) + + except httpx.TimeoutException: + yield self.create_text_message( + "Error: Request timed out. The automation may be taking too long." + ) + except httpx.HTTPError as e: + yield self.create_text_message(f"Error: HTTP error occurred: {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/dify/tools/run_sse.yaml b/dify/tools/run_sse.yaml new file mode 100644 index 0000000..49fd50b --- /dev/null +++ b/dify/tools/run_sse.yaml @@ -0,0 +1,162 @@ +identity: + name: "run_sse" + author: "tinyfish" + label: + en_US: "Run with SSE Streaming" + zh_Hans: "使用SSE流运行" + pt_BR: "Executar com Streaming SSE" + ja_JP: "SSEストリーミングで実行" +description: + human: + en_US: "Execute browser automation with real-time SSE streaming. Watch automation progress live with step-by-step updates and streaming URL." + zh_Hans: "使用实时SSE流执行浏览器自动化。通过逐步更新和流URL实时观看自动化进度。" + pt_BR: "Execute automação do navegador com streaming SSE em tempo real. Assista ao progresso da automação ao vivo com atualizações passo a passo e URL de streaming." + ja_JP: "リアルタイムSSEストリーミングでブラウザ自動化を実行します。ステップバイステップの更新とストリーミングURLでライブで自動化の進行状況を確認します。" + llm: "Execute browser automation with Server-Sent Events (SSE) streaming. Returns real-time progress updates, browser streaming URL, and final results. Use this when you want to monitor automation progress in real-time." +parameters: + - name: url + type: string + required: true + label: + en_US: Target URL + zh_Hans: 目标网址 + pt_BR: URL de destino + ja_JP: ターゲットURL + human_description: + en_US: "The website URL to navigate to and interact with" + zh_Hans: "要导航和交互的网站URL" + pt_BR: "O URL do site para navegar e interagir" + ja_JP: "ナビゲートして操作するウェブサイトのURL" + llm_description: "The target website URL where the automation task should be performed. Must be a valid HTTP or HTTPS URL." + placeholder: + en_US: "https://example.com" + zh_Hans: "https://example.com" + pt_BR: "https://example.com" + ja_JP: "https://example.com" + form: llm + - name: goal + type: string + required: true + label: + en_US: Goal + zh_Hans: 目标 + pt_BR: Objetivo + ja_JP: 目標 + human_description: + en_US: "Natural language description of what to accomplish on the website" + zh_Hans: "用自然语言描述在网站上要完成的任务" + pt_BR: "Descrição em linguagem natural do que realizar no site" + ja_JP: "ウェブサイトで達成すべきことの自然言語による説明" + llm_description: "A clear, natural language instruction describing what the agent should accomplish on the website. Examples: 'Extract the first 5 product names and prices', 'Fill out the contact form with name John Doe and email john@example.com', 'Navigate to the pricing page and extract all plan details'." + placeholder: + en_US: "Extract product information from the page" + zh_Hans: "从页面中提取产品信息" + pt_BR: "Extrair informações do produto da página" + ja_JP: "ページから製品情報を抽出する" + form: llm + - name: browser_profile + type: select + required: false + default: "lite" + label: + en_US: Browser Profile + zh_Hans: 浏览器配置 + pt_BR: Perfil do navegador + ja_JP: ブラウザプロファイル + human_description: + en_US: "Browser profile for execution. LITE uses standard browser, STEALTH uses anti-detection browser." + zh_Hans: "执行的浏览器配置。LITE使用标准浏览器,STEALTH使用反检测浏览器。" + pt_BR: "Perfil do navegador para execução. LITE usa navegador padrão, STEALTH usa navegador anti-detecção." + ja_JP: "実行用のブラウザプロファイル。LITEは標準ブラウザを使用し、STEALTHは検出防止ブラウザを使用します。" + llm_description: "Choose 'lite' for standard browser automation (faster, cheaper) or 'stealth' for anti-detection browser (better for sites with bot protection)." + options: + - value: "lite" + label: + en_US: "Lite (Standard Browser)" + zh_Hans: "Lite(标准浏览器)" + pt_BR: "Lite (Navegador Padrão)" + ja_JP: "Lite(標準ブラウザ)" + - value: "stealth" + label: + en_US: "Stealth (Anti-Detection)" + zh_Hans: "Stealth(反检测)" + pt_BR: "Stealth (Anti-Detecção)" + ja_JP: "Stealth(検出防止)" + form: form + - name: proxy_enabled + type: boolean + required: false + default: false + label: + en_US: Enable Proxy + zh_Hans: 启用代理 + pt_BR: Ativar Proxy + ja_JP: プロキシを有効にする + human_description: + en_US: "Enable proxy routing for the browser automation" + zh_Hans: "为浏览器自动化启用代理路由" + pt_BR: "Ativar roteamento de proxy para automação do navegador" + ja_JP: "ブラウザ自動化のプロキシルーティングを有効にする" + llm_description: "Set to true to route requests through a proxy server." + form: form + - name: proxy_country_code + type: select + required: false + label: + en_US: Proxy Country + zh_Hans: 代理国家 + pt_BR: País do Proxy + ja_JP: プロキシ国 + human_description: + en_US: "Country for proxy location. Only used when proxy is enabled." + zh_Hans: "代理位置的国家。仅在启用代理时使用。" + pt_BR: "País para localização do proxy. Usado apenas quando o proxy está ativado." + ja_JP: "プロキシの場所の国。プロキシが有効な場合のみ使用されます。" + llm_description: "ISO country code for proxy location. Only used when proxy is enabled. Valid values: US, GB, CA, DE, FR, JP, AU." + options: + - value: "US" + label: + en_US: "US - United States" + zh_Hans: "US - 美国" + pt_BR: "US - Estados Unidos" + ja_JP: "US - アメリカ" + - value: "GB" + label: + en_US: "GB - United Kingdom" + zh_Hans: "GB - 英国" + pt_BR: "GB - Reino Unido" + ja_JP: "GB - イギリス" + - value: "CA" + label: + en_US: "CA - Canada" + zh_Hans: "CA - 加拿大" + pt_BR: "CA - Canadá" + ja_JP: "CA - カナダ" + - value: "DE" + label: + en_US: "DE - Germany" + zh_Hans: "DE - 德国" + pt_BR: "DE - Alemanha" + ja_JP: "DE - ドイツ" + - value: "FR" + label: + en_US: "FR - France" + zh_Hans: "FR - 法国" + pt_BR: "FR - França" + ja_JP: "FR - フランス" + - value: "JP" + label: + en_US: "JP - Japan" + zh_Hans: "JP - 日本" + pt_BR: "JP - Japão" + ja_JP: "JP - 日本" + - value: "AU" + label: + en_US: "AU - Australia" + zh_Hans: "AU - 澳大利亚" + pt_BR: "AU - Austrália" + ja_JP: "AU - オーストラリア" + form: form +extra: + python: + source: tools/run_sse.py diff --git a/dify/tools/run_sync.py b/dify/tools/run_sync.py new file mode 100644 index 0000000..e31316b --- /dev/null +++ b/dify/tools/run_sync.py @@ -0,0 +1,69 @@ +from collections.abc import Generator +from typing import Any + +import httpx +from dify_plugin.entities.tool import ToolInvokeMessage + +from dify_plugin import Tool + +from tools.base import TinyfishMixin + + +class RunSyncTool(TinyfishMixin, Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + if not tool_parameters.get("url"): + yield self.create_text_message("Error: URL is required") + return + + if not tool_parameters.get("goal"): + yield self.create_text_message("Error: Goal is required") + return + + payload = self._build_automation_payload(tool_parameters) + + try: + response = self._tf_request( + "POST", "/v1/automation/run", json=payload, timeout=300.0 + ) + + result = response.json() + status = result.get("status") + + if status == "COMPLETED": + yield self.create_text_message("Automation completed successfully!") + yield self.create_text_message(f"Run ID: {result.get('run_id')}") + yield self.create_text_message( + f"Steps taken: {result.get('num_of_steps', 'N/A')}" + ) + + if result.get("result"): + yield self.create_json_message(result["result"]) + else: + yield self.create_text_message("No result data returned") + elif status == "FAILED": + error_info = result.get("error", {}) + error_message = ( + error_info.get("message", "Unknown error") + if isinstance(error_info, dict) + else str(error_info) + ) + yield self.create_text_message(f"Automation failed: {error_message}") + yield self.create_text_message(f"Run ID: {result.get('run_id')}") + else: + yield self.create_json_message(result) + + except httpx.HTTPStatusError as e: + if e.response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + else: + yield self.create_text_message( + f"Error: API request failed with status {e.response.status_code}: {e.response.text}" + ) + except httpx.TimeoutException: + yield self.create_text_message( + "Error: Request timed out. The automation may be taking too long." + ) + except httpx.HTTPError as e: + yield self.create_text_message(f"Error: HTTP error occurred: {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/dify/tools/run_sync.yaml b/dify/tools/run_sync.yaml new file mode 100644 index 0000000..523fd89 --- /dev/null +++ b/dify/tools/run_sync.yaml @@ -0,0 +1,162 @@ +identity: + name: "run_sync" + author: "tinyfish" + label: + en_US: "Run Synchronously" + zh_Hans: "同步运行" + pt_BR: "Executar Sincronamente" + ja_JP: "同期実行" +description: + human: + en_US: "Execute browser automation synchronously and wait for completion. Returns the final result in a single response." + zh_Hans: "同步执行浏览器自动化并等待完成。在单个响应中返回最终结果。" + pt_BR: "Execute automação do navegador de forma síncrona e aguarde a conclusão. Retorna o resultado final em uma única resposta." + ja_JP: "ブラウザ自動化を同期的に実行し、完了を待ちます。単一のレスポンスで最終結果を返します。" + llm: "Execute a browser automation task synchronously and wait for completion. Returns the complete result once the automation finishes (success or failure). Use this when you need the complete result in a single response without streaming." +parameters: + - name: url + type: string + required: true + label: + en_US: Target URL + zh_Hans: 目标网址 + pt_BR: URL de destino + ja_JP: ターゲットURL + human_description: + en_US: "The website URL to navigate to and interact with" + zh_Hans: "要导航和交互的网站URL" + pt_BR: "O URL do site para navegar e interagir" + ja_JP: "ナビゲートして操作するウェブサイトのURL" + llm_description: "The target website URL where the automation task should be performed. Must be a valid HTTP or HTTPS URL." + placeholder: + en_US: "https://example.com" + zh_Hans: "https://example.com" + pt_BR: "https://example.com" + ja_JP: "https://example.com" + form: llm + - name: goal + type: string + required: true + label: + en_US: Goal + zh_Hans: 目标 + pt_BR: Objetivo + ja_JP: 目標 + human_description: + en_US: "Natural language description of what to accomplish on the website" + zh_Hans: "用自然语言描述在网站上要完成的任务" + pt_BR: "Descrição em linguagem natural do que realizar no site" + ja_JP: "ウェブサイトで達成すべきことの自然言語による説明" + llm_description: "A clear, natural language instruction describing what the agent should accomplish on the website." + placeholder: + en_US: "Extract product information from the page" + zh_Hans: "从页面中提取产品信息" + pt_BR: "Extrair informações do produto da página" + ja_JP: "ページから製品情報を抽出する" + form: llm + - name: browser_profile + type: select + required: false + default: "lite" + label: + en_US: Browser Profile + zh_Hans: 浏览器配置 + pt_BR: Perfil do navegador + ja_JP: ブラウザプロファイル + human_description: + en_US: "Browser profile for execution. LITE uses standard browser, STEALTH uses anti-detection browser." + zh_Hans: "执行的浏览器配置。LITE使用标准浏览器,STEALTH使用反检测浏览器。" + pt_BR: "Perfil do navegador para execução. LITE usa navegador padrão, STEALTH usa navegador anti-detecção." + ja_JP: "実行用のブラウザプロファイル。LITEは標準ブラウザを使用し、STEALTHは検出防止ブラウザを使用します。" + llm_description: "Choose 'lite' for standard browser automation or 'stealth' for anti-detection browser." + options: + - value: "lite" + label: + en_US: "Lite (Standard Browser)" + zh_Hans: "Lite(标准浏览器)" + pt_BR: "Lite (Navegador Padrão)" + ja_JP: "Lite(標準ブラウザ)" + - value: "stealth" + label: + en_US: "Stealth (Anti-Detection)" + zh_Hans: "Stealth(反检测)" + pt_BR: "Stealth (Anti-Detecção)" + ja_JP: "Stealth(検出防止)" + form: form + - name: proxy_enabled + type: boolean + required: false + default: false + label: + en_US: Enable Proxy + zh_Hans: 启用代理 + pt_BR: Ativar Proxy + ja_JP: プロキシを有効にする + human_description: + en_US: "Enable proxy routing for the browser automation" + zh_Hans: "为浏览器自动化启用代理路由" + pt_BR: "Ativar roteamento de proxy para automação do navegador" + ja_JP: "ブラウザ自動化のプロキシルーティングを有効にする" + llm_description: "Set to true to route requests through a proxy server." + form: form + - name: proxy_country_code + type: select + required: false + label: + en_US: Proxy Country + zh_Hans: 代理国家 + pt_BR: País do Proxy + ja_JP: プロキシ国 + human_description: + en_US: "Country for proxy location. Only used when proxy is enabled." + zh_Hans: "代理位置的国家。仅在启用代理时使用。" + pt_BR: "País para localização do proxy. Usado apenas quando o proxy está ativado." + ja_JP: "プロキシの場所の国。プロキシが有効な場合のみ使用されます。" + llm_description: "ISO country code for proxy location. Only used when proxy is enabled. Valid values: US, GB, CA, DE, FR, JP, AU." + options: + - value: "US" + label: + en_US: "US - United States" + zh_Hans: "US - 美国" + pt_BR: "US - Estados Unidos" + ja_JP: "US - アメリカ" + - value: "GB" + label: + en_US: "GB - United Kingdom" + zh_Hans: "GB - 英国" + pt_BR: "GB - Reino Unido" + ja_JP: "GB - イギリス" + - value: "CA" + label: + en_US: "CA - Canada" + zh_Hans: "CA - 加拿大" + pt_BR: "CA - Canadá" + ja_JP: "CA - カナダ" + - value: "DE" + label: + en_US: "DE - Germany" + zh_Hans: "DE - 德国" + pt_BR: "DE - Alemanha" + ja_JP: "DE - ドイツ" + - value: "FR" + label: + en_US: "FR - France" + zh_Hans: "FR - 法国" + pt_BR: "FR - França" + ja_JP: "FR - フランス" + - value: "JP" + label: + en_US: "JP - Japan" + zh_Hans: "JP - 日本" + pt_BR: "JP - Japão" + ja_JP: "JP - 日本" + - value: "AU" + label: + en_US: "AU - Australia" + zh_Hans: "AU - 澳大利亚" + pt_BR: "AU - Austrália" + ja_JP: "AU - オーストラリア" + form: form +extra: + python: + source: tools/run_sync.py