diff --git a/plugins/seedream/.difyignore b/plugins/seedream/.difyignore index 4685c5eb861d69..aa2d33ca5ba053 100644 --- a/plugins/seedream/.difyignore +++ b/plugins/seedream/.difyignore @@ -34,6 +34,7 @@ pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports +tests/ htmlcov/ .tox/ .nox/ @@ -126,6 +127,7 @@ celerybeat.pid # Environments .env +.env.* .venv env/ venv/ diff --git a/plugins/seedream/PRIVACY.md b/plugins/seedream/PRIVACY.md index 16e94492b55624..33a8c265bbf120 100644 --- a/plugins/seedream/PRIVACY.md +++ b/plugins/seedream/PRIVACY.md @@ -1,102 +1,39 @@ -## Privacy +# Privacy -This document describes how the seedream plugin handles your data. +This policy describes the data flow of the Ace Data Cloud Seedream plugin for Dify. -### Scope +## Data processed -This privacy statement applies only to the seedream plugin and to data -processed through its features. It does not cover any other plugins, tools, -or services you may use alongside it. +When a tool runs, the plugin processes: -### Data We Collect +- the Ace Data Cloud bearer token configured in Dify; +- the prompt and selected generation options; +- image URLs or image strings supplied for editing or layer decomposition; +- callback URL when the user explicitly provides one; +- generated image metadata, task ID, error details, and trace ID returned by the API. -Depending on how you use the plugin, seedream may process the following -categories of data: +Prompts and images may contain personal or confidential information. Do not submit content that you are not authorized to process. -- **Configuration data**: Settings and preferences you configure for the - plugin (for example, enabling or disabling features, thresholds, or - integration options). -- **Operational data**: Information necessary for the plugin to function, - such as file paths, project identifiers, plugin state, and error messages. -- **Usage data (telemetry)**: Anonymous or aggregated metrics about which - features are used and basic performance information, if telemetry is - enabled in your host environment. +## Data transfer -The plugin is not designed to intentionally collect directly identifying -personal information (such as your name, email address, or payment details) -beyond what is already handled by the platform or service hosting the -plugin. However, personal information could be present in the content of -files or project metadata you work with; such data is processed only to -provide the plugin's functionality. +The plugin sends the selected inputs over HTTPS only to the fixed endpoint `https://api.acedata.cloud/seedream/images`. The bearer token is sent only in the `Authorization` header. The plugin does not expose a configurable destination host. -### How We Use Data +The response is returned to the Dify workflow. The plugin does not send data to advertising or analytics services and contains no telemetry code. A callback URL is sent only when the user explicitly configures it; that destination then receives data according to the user's workflow design. -We use data processed by the plugin solely for the following purposes: +## Storage and retention -- To provide and operate the plugin's core features. -- To maintain and improve reliability, performance, and security. -- To diagnose and fix bugs or issues you report. -- To understand, in aggregate, how features are used (if telemetry is - enabled in your environment). +The plugin does not write prompts, images, responses, or credentials to local files or its own database. Dify controls how plugin credentials, workflow inputs, outputs, and execution logs are stored and retained. Data processed by Ace Data Cloud is subject to the [Ace Data Cloud Privacy Policy](https://platform.acedata.cloud/privacy). -We do not use plugin data to build user profiles for advertising or -marketing. +Generated URLs may expire. Users who need durable artifacts must save them in storage they control and are authorized to use. -### Data Storage and Retention +## Logging and errors -- **Local data**: Configuration and operational data are typically stored - locally within your development environment, project, or host application. - Retention is controlled by that environment (for example, by deleting or - modifying configuration files or logs). -- **Remote services**: If the plugin is configured to communicate with - external services or APIs, any data sent to those services is subject to - their respective privacy policies and retention practices. -- **Diagnostic data**: If you choose to share logs or error reports with the - maintainers (for example, via issue trackers), these may be retained for as - long as needed to investigate and resolve the issue. +The plugin does not log the bearer token. It redacts the configured token from network and API error messages before returning them. Structured failures may include an error code, HTTP status, message, and trace ID for troubleshooting. -### Data Sharing +## Your choices -We do not sell your data. +You can stop further processing by disabling or uninstalling the plugin and removing its credential in Dify. Use Dify's controls to delete retained workflow data or logs. Omit `callback_url` to prevent callback delivery. -We may share data only in the following limited circumstances: - -- With service providers or infrastructure used to host issue trackers, - crash reports, or similar tooling, and only to the extent necessary to - operate those services. -- When required by law, regulation, or legal process, or to protect the - rights, property, or safety of users or others. - -Any third-party services you configure the plugin to interact with (for -example, source control platforms, CI/CD systems, or external APIs) will -process data under their own terms and privacy policies. - -### Security - -We take reasonable technical and organizational measures to help protect -data processed by the plugin from unauthorized access, alteration, or -destruction. However, no software system or transmission method is entirely -secure, and we cannot guarantee absolute security. - -You are responsible for configuring and securing your own environment, -including access controls, network security, and backups. - -### Your Choices and Rights - -- You can configure or disable the plugin through your host environment's - extension or plugin management features. -- You can edit or remove configuration files used by the plugin. -- You may choose not to enable integrations with any optional third-party - services. - -If you believe the plugin is handling data in a way that is inconsistent -with this policy, you should discontinue use of the plugin and contact the -maintainers. - -### Contact - -If you have questions about this privacy information, or if you wish to -raise a privacy-related concern, please contact the maintainers of the -seedream plugin through the official project repository or distribution -channel where you obtained this plugin. +## Contact +For plugin questions, open an issue in the [source repository](https://github.com/AceDataCloud/Dify/tree/main/plugins/seedream) or email [office@acedata.cloud](mailto:office@acedata.cloud). diff --git a/plugins/seedream/README.md b/plugins/seedream/README.md index d4714bbd5a941e..a99cd8ea3d2e04 100644 --- a/plugins/seedream/README.md +++ b/plugins/seedream/README.md @@ -1,32 +1,55 @@ -## Seedream +# Seedream + +**Author:** Ace Data Cloud + +**Type:** Tool provider plugin -**Author:** acedatacloud -**Type:** tool provider plugin **API:** `https://api.acedata.cloud/seedream/images` -### What it does +**Source:** [AceDataCloud/Dify — Seedream plugin](https://github.com/AceDataCloud/Dify/tree/main/plugins/seedream) + +## What it does + +This plugin adds image generation, reference-image editing, and editable-layer decomposition to Dify workflows and agents. + +## Tools + +- `seedream_generate_image` generates one image or a related image set from a prompt. +- `seedream_edit_image` edits one or more referenced images from a prompt. +- `seedream_decompose_image` splits one image into a base image and positioned layers. + +Each tool returns `success`, `task_id`, `trace_id`, `data`, and `error` variables. Successful synchronous results also emit image messages for returned image URLs. + +## Setup and reviewer check + +1. [Create an Ace Data Cloud API token](https://platform.acedata.cloud/console/credentials?utm_source=dify&utm_medium=plugin&utm_campaign=dify-seedream). +2. Install the plugin in Dify and open its provider credentials. +3. Paste the token into `acedata_bearer_token` without the `Bearer` scheme. +4. Add **Seedream Generate Image** to a private test workflow, provide a short non-sensitive prompt, and run it synchronously. +5. Confirm `success=true`, a non-empty `data` array, an image message, and a `trace_id` that can be shared with support if troubleshooting is required. + +Image generation and editing can consume credits. Review the [current Seedream pricing](https://platform.acedata.cloud/services/seedream?tab=pricing) before running the test. Do not put production secrets or sensitive source images in a reviewer workflow. -This plugin integrates **Ace Data Cloud Seedream Images API** as Dify tools for: +When `async=true`, the tool may return a task ID before media is ready. This plugin does not expose task retrieval, so use synchronous mode for the reviewer check and for workflows that require an inline image result. -- Generating images from a prompt -- Editing images with a prompt and `image_urls` +## Data and support -### Tools +The plugin calls only `https://api.acedata.cloud/seedream/images`. It sends the configured token in the `Authorization` header and sends only the tool inputs needed for the selected operation. See [PRIVACY.md](PRIVACY.md) for exact processing and retention boundaries. -- `seedream_generate_image` - - Inputs: `prompt` (required), `model`, `size`, `sequential_image_generation`, `stream`, `response_format`, `watermark`, `callback_url` - - Outputs: `success`, `task_id`, `trace_id`, `data` (array of objects with `image_url`, optional `prompt`), `error` -- `seedream_edit_image` - - Inputs: `prompt` (required), `image_urls` (required), other optional params same as generate - - Outputs: same as generate +- [Privacy policy](https://platform.acedata.cloud/privacy) +- [Terms](https://platform.acedata.cloud/terms) +- [Support](https://platform.acedata.cloud/support?utm_source=dify&utm_medium=plugin&utm_campaign=dify-seedream-support) +- [Report an issue](https://github.com/AceDataCloud/Dify/issues) +- Email: [office@acedata.cloud](mailto:office@acedata.cloud) -### Credentials +Last contract review: **2026-09-10**. The manifest intentionally remains `verified: false`; local validation does not represent Dify Marketplace approval or publication. -Requires `acedata_bearer_token` (paste the token without the `Bearer ` prefix). +## Packaging -### Packaging +From the repository root: ```bash dify plugin package plugins/seedream -o seedream.difypkg ``` +The package excludes tests, local environment files, caches, and previously built packages through `.difyignore`. diff --git a/plugins/seedream/tests/test_marketplace_readiness.py b/plugins/seedream/tests/test_marketplace_readiness.py new file mode 100644 index 00000000000000..1cf74c2944d787 --- /dev/null +++ b/plugins/seedream/tests/test_marketplace_readiness.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + +import yaml + + +PLUGIN_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PLUGIN_ROOT)) + +from tools.acedata_client import AceDataSeedreamClient, AceDataSeedreamError # noqa: E402 + + +class SeedreamClientSecurityTest(unittest.TestCase): + @patch("tools.acedata_client.requests.post") + def test_uses_fixed_public_endpoint_and_authorization_header( + self, post: Mock + ) -> None: + response = Mock(status_code=200) + response.json.return_value = {"success": True, "trace_id": "trace", "data": []} + post.return_value = response + + AceDataSeedreamClient(" Bearer secret-token ").generate_images( + payload={"action": "generate", "prompt": "test"} + ) + + post.assert_called_once_with( + "https://api.acedata.cloud/seedream/images", + json={"action": "generate", "prompt": "test"}, + headers={ + "authorization": "Bearer secret-token", + "accept": "application/json", + "content-type": "application/json", + }, + timeout=150, + ) + + @patch("tools.acedata_client.requests.post") + def test_api_error_redacts_configured_token(self, post: Mock) -> None: + response = Mock(status_code=401) + response.json.return_value = { + "error": {"code": "invalid_token", "message": "Rejected secret-token"}, + "trace_id": "trace-401", + } + post.return_value = response + + with self.assertRaises(AceDataSeedreamError) as raised: + AceDataSeedreamClient("secret-token").generate_images( + payload={"prompt": "test"} + ) + + self.assertEqual(raised.exception.trace_id, "trace-401") + self.assertNotIn("secret-token", str(raised.exception)) + self.assertIn("[redacted]", str(raised.exception)) + + def test_empty_token_fails_closed(self) -> None: + with self.assertRaises(AceDataSeedreamError) as raised: + AceDataSeedreamClient("Bearer ") + self.assertEqual(raised.exception.code, "token_empty") + + +class SeedreamMarketplaceContractTest(unittest.TestCase): + def test_manifest_does_not_claim_external_verification(self) -> None: + manifest = yaml.safe_load((PLUGIN_ROOT / "manifest.yaml").read_text()) + self.assertIs(manifest["verified"], False) + self.assertEqual(manifest["privacy"], "PRIVACY.md") + + def test_package_excludes_development_and_environment_files(self) -> None: + ignore = (PLUGIN_ROOT / ".difyignore").read_text().splitlines() + self.assertIn("tests/", ignore) + self.assertIn(".env.*", ignore) + self.assertIn("*.difypkg", ignore) + + def test_readme_has_review_setup_cost_support_and_external_gate(self) -> None: + readme = (PLUGIN_ROOT / "README.md").read_text() + for text in ( + "https://github.com/AceDataCloud/Dify/tree/main/plugins/seedream", + "utm_campaign=dify-seedream", + "can consume credits", + "current Seedream pricing", + "success=true", + "does not represent Dify Marketplace approval or publication", + "Last contract review: **2026-09-10**", + ): + self.assertIn(text, readme) + + def test_privacy_matches_fixed_endpoint_and_no_local_storage(self) -> None: + privacy = (PLUGIN_ROOT / "PRIVACY.md").read_text() + self.assertIn("https://api.acedata.cloud/seedream/images", privacy) + self.assertIn("contains no telemetry code", privacy) + self.assertIn( + "does not write prompts, images, responses, or credentials", privacy + ) + self.assertIn("redacts the configured token", privacy) + self.assertNotIn("usage data (telemetry)", privacy.lower()) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/seedream/tools/acedata_client.py b/plugins/seedream/tools/acedata_client.py index e3e967245c2b07..e7f10952deceb0 100644 --- a/plugins/seedream/tools/acedata_client.py +++ b/plugins/seedream/tools/acedata_client.py @@ -37,20 +37,28 @@ def image_urls(self) -> list[str]: def _normalize_token(raw_token: str) -> str: token = raw_token.strip() - if token.lower().startswith("bearer "): - token = token[7:].strip() + parts = token.split(maxsplit=1) + if parts and parts[0].lower() == "bearer": + token = parts[1].strip() if len(parts) == 2 else "" return token class AceDataSeedreamClient: - def __init__(self, bearer_token: str, base_url: str = "https://api.acedata.cloud") -> None: + def __init__( + self, bearer_token: str, base_url: str = "https://api.acedata.cloud" + ) -> None: token = _normalize_token(bearer_token) if not token: - raise AceDataSeedreamError(code="token_empty", message="Empty bearer token.") + raise AceDataSeedreamError( + code="token_empty", message="Empty bearer token." + ) self._token = token self._base_url = base_url.rstrip("/") + def _safe_message(self, value: object) -> str: + return str(value).replace(self._token, "[redacted]") + def generate_images( self, *, payload: dict[str, Any], timeout_s: int = 150 ) -> AceDataSeedreamImagesResult: @@ -58,25 +66,37 @@ def generate_images( task_id = body.get("task_id") if isinstance(body.get("task_id"), str) else None if body.get("success") is not True and not task_id: - trace_id = body.get("trace_id") if isinstance(body.get("trace_id"), str) else None + trace_id = ( + body.get("trace_id") if isinstance(body.get("trace_id"), str) else None + ) error = body.get("error") if isinstance(body.get("error"), dict) else {} code = error.get("code") if isinstance(error.get("code"), str) else None - message = error.get("message") if isinstance(error.get("message"), str) else None + message = ( + error.get("message") if isinstance(error.get("message"), str) else None + ) raise AceDataSeedreamError( code=code or "api_error", - message=message or str(body), + message=self._safe_message(message or body), trace_id=trace_id, ) raw_data: Any = body.get("data") - data = [item for item in raw_data if isinstance(item, dict)] if isinstance(raw_data, list) else [] + data = ( + [item for item in raw_data if isinstance(item, dict)] + if isinstance(raw_data, list) + else [] + ) return AceDataSeedreamImagesResult( task_id=task_id, - trace_id=body.get("trace_id") if isinstance(body.get("trace_id"), str) else None, + trace_id=body.get("trace_id") + if isinstance(body.get("trace_id"), str) + else None, data=data, ) - def _post(self, *, path: str, payload: dict[str, Any], timeout_s: int) -> dict[str, Any]: + def _post( + self, *, path: str, payload: dict[str, Any], timeout_s: int + ) -> dict[str, Any]: url = f"{self._base_url}{path}" headers = { "authorization": f"Bearer {self._token}", @@ -87,7 +107,9 @@ def _post(self, *, path: str, payload: dict[str, Any], timeout_s: int) -> dict[s try: resp = requests.post(url, json=payload, headers=headers, timeout=timeout_s) except requests.RequestException as e: - raise AceDataSeedreamError(code="request_failed", message=str(e)) from e + raise AceDataSeedreamError( + code="request_failed", message=self._safe_message(e) + ) from e try: body = resp.json() @@ -95,7 +117,7 @@ def _post(self, *, path: str, payload: dict[str, Any], timeout_s: int) -> dict[s snippet = (resp.text or "")[:500] raise AceDataSeedreamError( code="invalid_json", - message=f"Invalid JSON response: {snippet}", + message=self._safe_message(f"Invalid JSON response: {snippet}"), status_code=resp.status_code, ) from e @@ -107,13 +129,17 @@ def _post(self, *, path: str, payload: dict[str, Any], timeout_s: int) -> dict[s ) if resp.status_code >= 400: - trace_id = body.get("trace_id") if isinstance(body.get("trace_id"), str) else None + trace_id = ( + body.get("trace_id") if isinstance(body.get("trace_id"), str) else None + ) error = body.get("error") if isinstance(body.get("error"), dict) else {} code = error.get("code") if isinstance(error.get("code"), str) else None - message = error.get("message") if isinstance(error.get("message"), str) else None + message = ( + error.get("message") if isinstance(error.get("message"), str) else None + ) raise AceDataSeedreamError( code=code or "error", - message=message or str(body), + message=self._safe_message(message or body), trace_id=trace_id, status_code=resp.status_code, ) @@ -139,8 +165,12 @@ def parse_image_inputs(value: Any) -> list[str]: loaded = json.loads(text) if not isinstance(loaded, list): - raise ValueError("`image_urls` must be a JSON array or a list of strings.") + raise ValueError( + "`image_urls` must be a JSON array or a list of strings." + ) return [str(item).strip() for item in loaded if str(item).strip()] return [line.strip() for line in text.splitlines() if line.strip()] - raise ValueError("`image_urls` must be an array of strings or a string (one URL per line).") + raise ValueError( + "`image_urls` must be an array of strings or a string (one URL per line)." + )