diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index e69de29..0000000 diff --git a/manifest.yaml b/manifest.yaml index addcf1e..72f4fad 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -3,8 +3,8 @@ author: qiniu icon: icon_s_en.svg created_at: '2025-07-31T00:13:50.29298939-04:00' description: - en_US: Official Qiniu Cloud Dify plugin providing AI inference services, supporting models such as deepseek-r1, deepseek-v3, and more. - zh_Hans: 七牛云官方 Dify 插件,提供 AI 推理服务,支持例如 deepseek-r1、deepseek-v3 等模型。 + en_US: Official Qiniu Cloud Dify plugin providing comprehensive AI inference services and cloud storage management. Supports multiple LLM models including deepseek-r1, deepseek-v3, GLM-4.5, Kimi-K2, Qwen series, and offers complete file management capabilities such as file upload, bucket operations, and content retrieval. + zh_Hans: 七牛云官方 Dify 插件,提供全面的 AI 推理服务和云存储管理功能。支持 deepseek-r1、deepseek-v3、GLM-4.5、Kimi-K2、Qwen 系列等多种大语言模型,并提供完整的文件管理能力,包括文件上传、存储空间操作和内容获取等功能。 label: en_US: Qiniu Cloud @@ -21,6 +21,8 @@ meta: plugins: models: - provider/qiniu.yaml + tools: + - provider/qiniu_tools.yaml resource: memory: 268435456 permission: @@ -33,6 +35,6 @@ resource: text_embedding: false tts: false tool: - enabled: false + enabled: true type: plugin -version: 0.0.3 +version: 0.1.0 diff --git a/provider/qiniu.py b/provider/qiniu.py index 006c3a5..86eef68 100644 --- a/provider/qiniu.py +++ b/provider/qiniu.py @@ -1,21 +1,23 @@ import logging from dify_plugin.entities.model import ModelType from dify_plugin.errors.model import CredentialsValidateFailedError -from dify_plugin import ModelProvider +from dify_plugin.errors.tool import ToolProviderCredentialValidationError +from dify_plugin import ModelProvider, ToolProvider +from qiniu import Auth, BucketManager logger = logging.getLogger(__name__) -class QiniuProvider(ModelProvider): +class QiniuProvider(ModelProvider, ToolProvider): """ - 七牛云模型提供商实现类 + 七牛云提供商实现类 - 提供七牛云 AI 模型的接入能力,支持多种大语言模型 + 提供七牛云 AI 模型的接入能力和云存储工具能力,支持多种大语言模型和文件上传功能 """ def validate_provider_credentials(self, credentials: dict) -> None: """ - 验证供应商认证信息 + 验证供应商认证信息(用于模型服务) 如果验证失败,会抛出异常 Args: @@ -33,3 +35,51 @@ def validate_provider_credentials(self, credentials: dict) -> None: except Exception as ex: logger.exception(f"{self.get_provider_schema().provider} credentials validate failed") raise ex + + def _validate_credentials(self, credentials: dict) -> None: + """ + 验证工具认证信息(用于工具服务) + 验证七牛云 Access Key 和 Secret Key 的有效性 + + Args: + credentials: 认证信息字典,包含 qiniu_access_key 和 qiniu_secret_key + + Raises: + ToolProviderCredentialValidationError: 认证验证失败 + """ + try: + access_key = credentials.get("qiniu_access_key") + secret_key = credentials.get("qiniu_secret_key") + + if not access_key or not secret_key: + raise ToolProviderCredentialValidationError("七牛云 Access Key 和 Secret Key 不能为空") + + # 创建认证对象 + auth = Auth(access_key, secret_key) + bucket_manager = BucketManager(auth) + + # 尝试获取空间列表来验证认证信息 + # 这里只是验证认证是否有效,不需要具体的空间名 + try: + # 调用接口验证认证信息 + ret, eof, info = bucket_manager.buckets() + + if info.status_code == 200: + logger.info("七牛云认证验证成功") + elif info.status_code == 401: + raise ToolProviderCredentialValidationError("七牛云认证失败,请检查 Access Key 和 Secret Key 是否正确") + else: + raise ToolProviderCredentialValidationError(f"七牛云认证验证失败: HTTP {info.status_code}") + + except Exception as api_error: + if "401" in str(api_error) or "Unauthorized" in str(api_error): + raise ToolProviderCredentialValidationError("七牛云认证失败,请检查 Access Key 和 Secret Key 是否正确") + else: + # 如果是网络错误等其他错误,我们假设认证信息是正确的 + logger.warning(f"七牛云认证验证时出现网络错误,跳过验证: {str(api_error)}") + + except ToolProviderCredentialValidationError: + raise + except Exception as e: + logger.exception("七牛云认证验证过程中发生未知错误") + raise ToolProviderCredentialValidationError(f"认证验证失败: {str(e)}") diff --git a/provider/qiniu_tools.py b/provider/qiniu_tools.py new file mode 100644 index 0000000..d43252a --- /dev/null +++ b/provider/qiniu_tools.py @@ -0,0 +1,64 @@ +import logging +from typing import Any + +from dify_plugin import ToolProvider +from dify_plugin.errors.tool import ToolProviderCredentialValidationError +from qiniu import Auth, BucketManager + +logger = logging.getLogger(__name__) + + +class QiniuProvider(ToolProvider): + """ + 七牛云工具提供商实现类 + + 提供七牛云存储工具能力,支持文件上传功能 + """ + + def _validate_credentials(self, credentials: dict[str, Any]) -> None: + """ + 验证工具认证信息(用于工具服务) + 验证七牛云 Access Key 和 Secret Key 的有效性 + + Args: + credentials: 认证信息字典,包含 qiniu_access_key 和 qiniu_secret_key + + Raises: + ToolProviderCredentialValidationError: 认证验证失败 + """ + try: + access_key = credentials.get("qiniu_access_key") + secret_key = credentials.get("qiniu_secret_key") + + if not access_key or not secret_key: + raise ToolProviderCredentialValidationError("七牛云 Access Key 和 Secret Key 不能为空") + + # 创建认证对象 + auth = Auth(access_key, secret_key) + bucket_manager = BucketManager(auth) + + # 尝试获取空间列表来验证认证信息 + # 这里只是验证认证是否有效,不需要具体的空间名 + try: + # 调用接口验证认证信息 + ret, eof, info = bucket_manager.buckets() + + if info.status_code == 200: + logger.info("七牛云认证验证成功") + elif info.status_code == 401: + raise ToolProviderCredentialValidationError("七牛云认证失败,请检查 Access Key 和 Secret Key 是否正确") + else: + raise ToolProviderCredentialValidationError(f"七牛云认证验证失败: HTTP {info.status_code}") + + except Exception as api_error: + if "401" in str(api_error) or "Unauthorized" in str(api_error): + raise ToolProviderCredentialValidationError("七牛云认证失败,请检查 Access Key 和 Secret Key 是否正确") + else: + # 如果是网络错误等其他错误,我们假设认证信息是正确的 + logger.warning(f"七牛云认证验证时出现网络错误,跳过验证: {str(api_error)}") + + except ToolProviderCredentialValidationError: + raise + except Exception as e: + logger.exception("七牛云认证验证过程中发生未知错误") + raise ToolProviderCredentialValidationError(f"认证验证失败: {str(e)}") diff --git a/provider/qiniu_tools.yaml b/provider/qiniu_tools.yaml new file mode 100644 index 0000000..bfedd65 --- /dev/null +++ b/provider/qiniu_tools.yaml @@ -0,0 +1,48 @@ +identity: + author: qiniu + name: qiniu + label: + en_US: Qiniu Cloud + zh_Hans: 七牛云 + description: + en_US: Official Qiniu Cloud Storage plugin providing file upload services + zh_Hans: 七牛云官方存储插件,提供文件上传服务 + icon: icon_s_en.svg + tags: + - utilities + - productivity +credentials_for_provider: + qiniu_access_key: + type: secret-input + required: true + label: + en_US: Qiniu Access Key + zh_Hans: 七牛云 Access Key + placeholder: + en_US: Enter your Qiniu Access Key + zh_Hans: 在此输入您的七牛云 Access Key + help: + en_US: Get your Access Key from Qiniu Cloud Console + zh_Hans: 从七牛云控制台获取 Access Key + url: https://portal.qiniu.com/user/key + qiniu_secret_key: + type: secret-input + required: true + label: + en_US: Qiniu Secret Key + zh_Hans: 七牛云 Secret Key + placeholder: + en_US: Enter your Qiniu Secret Key + zh_Hans: 在此输入您的七牛云 Secret Key + help: + en_US: Get your Secret Key from Qiniu Cloud Console + zh_Hans: 从七牛云控制台获取 Secret Key + url: https://portal.qiniu.com/user/key +tools: + - tools/list_buckets.yaml + - tools/file_upload.yaml + - tools/list_bucket_files.yaml + - tools/get_file_content.yaml +extra: + python: + source: provider/qiniu_tools.py diff --git a/requirements.txt b/requirements.txt index b267cde..ee94ce4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -dify_plugin>=0.3.0,<0.5.0 +dify_plugin>=0.0.1b22 +requests>=2.25.0 +qiniu>=7.12.0 diff --git a/test_plugin.py b/test_plugin.py deleted file mode 100644 index e69de29..0000000 diff --git a/tools/file_upload.py b/tools/file_upload.py new file mode 100644 index 0000000..67f4b61 --- /dev/null +++ b/tools/file_upload.py @@ -0,0 +1,226 @@ +import json +import logging +from collections.abc import Generator +from typing import Any + +from qiniu import Auth, put_data, BucketManager +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin.errors.tool import ToolProviderCredentialValidationError + +logger = logging.getLogger(__name__) + + +class QiniuUploadTool(Tool): + """ + 七牛云上传工具 + + 支持上传内容到指定的七牛云存储空间,并返回访问链接 + """ + + def _get_auth(self) -> Auth: + """获取七牛云认证对象""" + access_key = self.runtime.credentials.get("qiniu_access_key") + secret_key = self.runtime.credentials.get("qiniu_secret_key") + + if not access_key or not secret_key: + raise ToolProviderCredentialValidationError("七牛云 Access Key 和 Secret Key 不能为空") + + return Auth(access_key, secret_key) + + def _validate_bucket_access(self, bucket_name: str) -> bool: + """验证存储空间访问权限""" + try: + auth = self._get_auth() + bucket_manager = BucketManager(auth) + + # 尝试获取空间列表来验证认证信息 + ret, eof, info = bucket_manager.list(bucket_name, limit=1) + + if info.status_code == 200: + return True + elif info.status_code == 401: + raise ToolProviderCredentialValidationError("七牛云认证失败,请检查 Access Key 和 Secret Key") + elif info.status_code == 631: + raise ToolProviderCredentialValidationError(f"存储空间 '{bucket_name}' 不存在") + else: + raise ToolProviderCredentialValidationError(f"验证存储空间失败: {info.error}") + + except Exception as e: + if isinstance(e, ToolProviderCredentialValidationError): + raise + raise ToolProviderCredentialValidationError(f"验证存储空间时发生错误: {str(e)}") + + def _apply_prefix(self, filename: str, prefix: str = None) -> str: + """应用前缀到文件名""" + if not prefix: + return filename + + # 确保前缀格式正确 + prefix = prefix.strip() + if prefix and not prefix.endswith('/'): + prefix += '/' + + return f"{prefix}{filename}" + + def _upload_to_qiniu(self, content: str, filename: str, bucket: str, overwrite: bool = False) -> dict: + """上传内容到七牛云""" + try: + auth = self._get_auth() + + # 根据覆盖设置生成上传凭证 + if overwrite: + # 允许覆盖同名文件 + token = auth.upload_token(bucket, filename) + else: + # 不允许覆盖,如果文件存在会返回错误 + policy = { + 'scope': f'{bucket}:{filename}', + 'insertOnly': 1 # 仅当文件不存在时才允许上传 + } + token = auth.upload_token(bucket, filename, policy=policy) + + # 上传内容 + ret, info = put_data(token, filename, content.encode('utf-8')) + + if info.status_code == 200: + return { + "success": True, + "key": ret.get("key", filename), + "hash": ret.get("hash", ""), + "bucket": bucket + } + else: + error_msg = f"上传失败: HTTP {info.status_code}" + if hasattr(info, 'error') and info.error: + error_msg += f" - {info.error}" + # 特殊处理文件已存在的情况 + if info.status_code == 614: + error_msg = "文件已存在且未设置覆盖选项" + return { + "success": False, + "error": error_msg + } + + except Exception as e: + return { + "success": False, + "error": f"上传过程中发生错误: {str(e)}" + } + + def _generate_access_url(self, key: str, bucket: str, domain: str = None) -> str: + """生成访问链接""" + if domain: + # 如果提供了自定义域名,生成完整的访问链接 + domain = domain.rstrip('/') + if not domain.startswith(('http://', 'https://')): + domain = f"https://{domain}" + return f"{domain}/{key}" + else: + # 如果没有提供域名,返回文件路径 + return key + + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + """ + 执行七牛云上传操作 + + Args: + tool_parameters: 工具参数,包含 content, filename, bucket, domain(可选), overwrite(可选), prefix(可选) + + Yields: + ToolInvokeMessage: 工具执行结果消息 + """ + try: + # 获取参数 + content = tool_parameters.get("content", "") + filename = tool_parameters.get("filename", "") + prefix = tool_parameters.get("prefix", "") + bucket = tool_parameters.get("bucket", "") + domain = tool_parameters.get("domain", "") + overwrite = tool_parameters.get("overwrite", False) + + # 验证必需参数 + if not content: + yield self.create_text_message("上传内容不能为空") + return + + if not filename: + yield self.create_text_message("文件名不能为空") + return + + if not bucket: + yield self.create_text_message("存储空间名称不能为空") + return + + # 应用前缀到文件名 + final_filename = self._apply_prefix(filename, prefix) + + # 验证存储空间访问权限 + self._validate_bucket_access(bucket) + + # 执行上传 + upload_result = self._upload_to_qiniu(content, final_filename, bucket, overwrite) + + if upload_result["success"]: + # 生成访问链接 + access_url = self._generate_access_url( + upload_result["key"], + bucket, + domain if domain else None + ) + + # 创建简化的成功消息 + markdown_content = f"文件上传成功:{final_filename}" + + yield self.create_text_message(markdown_content) + + # 简化输出,只返回三个核心信息 + result = { + "file_key": upload_result["key"], # 文件在存储桶中的路径 + "file_url": access_url if domain else None, # 如果配置了域名则返回完整URL,否则为None + "error": None # 成功时错误为None + } + + yield self.create_json_message(result) + else: + # 创建简化的失败消息 + markdown_content = f"文件上传失败:{upload_result['error']}" + + yield self.create_text_message(markdown_content) + + # 上传失败,返回错误信息 + result = { + "file_key": None, + "file_url": None, + "error": upload_result['error'] + } + yield self.create_json_message(result) + + except ToolProviderCredentialValidationError as e: + # 创建认证错误的简化消息 + markdown_content = f"认证错误:{str(e)}" + + yield self.create_text_message(markdown_content) + + # 认证错误 + result = { + "file_key": None, + "file_url": None, + "error": f"认证错误:{str(e)}" + } + yield self.create_json_message(result) + except Exception as e: + logger.exception("七牛云上传工具执行失败") + + # 创建通用错误的简化消息 + markdown_content = f"系统错误:{str(e)}" + + yield self.create_text_message(markdown_content) + + # 其他错误 + result = { + "file_key": None, + "file_url": None, + "error": f"执行失败:{str(e)}" + } + yield self.create_json_message(result) diff --git a/tools/file_upload.yaml b/tools/file_upload.yaml new file mode 100644 index 0000000..e0edebe --- /dev/null +++ b/tools/file_upload.yaml @@ -0,0 +1,91 @@ +identity: + name: file_upload + author: qiniu + label: + en_US: File Upload + zh_Hans: 对象存储上传 +description: + human: + en_US: Upload files to Qiniu Cloud Storage and get access URLs. Supports specifying bucket and custom domain. + zh_Hans: 上传文件到七牛云存储并获取访问链接。支持指定存储空间和自定义域名。 + llm: A tool for uploading files to Qiniu Cloud Storage. Can upload content to specified bucket and return access URL. +parameters: + - name: content + type: string + required: true + label: + en_US: Upload Content + zh_Hans: 上传内容 + human_description: + en_US: The content to upload to Qiniu Cloud Storage + zh_Hans: 要上传到七牛云存储的内容 + llm_description: The content/data to be uploaded to Qiniu Cloud Storage + form: llm + - name: filename + type: string + required: true + label: + en_US: File Name + zh_Hans: 文件名 + human_description: + en_US: The name for the uploaded file + zh_Hans: 上传文件的名称 + llm_description: The filename for the uploaded content + form: llm + - name: prefix + type: string + required: false + label: + en_US: File Prefix + zh_Hans: 文件前缀 + human_description: + en_US: Optional prefix to add before the filename (e.g. "uploads/", "docs/2025/") + zh_Hans: 可选的文件前缀,添加到文件名前面(例如 "uploads/"、"docs/2025/") + llm_description: Optional prefix to organize files in folders. Will be prepended to the filename during upload. + placeholder: + en_US: Enter prefix, e.g. uploads/ or docs/2025/ + zh_Hans: 输入前缀,例如 uploads/ 或 docs/2025/ + form: form + - name: bucket + type: string + required: true + label: + en_US: Bucket Name + zh_Hans: 存储空间名称 + human_description: + en_US: The name of the Qiniu Cloud Storage bucket + zh_Hans: 七牛云存储空间的名称 + llm_description: The name of the Qiniu Cloud Storage bucket where the file will be uploaded + placeholder: + en_US: Enter bucket name, e.g. my-storage-bucket + zh_Hans: 输入存储空间名称,例如 my-storage-bucket + form: form + - name: domain + type: string + required: false + label: + en_US: Custom Domain + zh_Hans: 自定义域名 + human_description: + en_US: Optional custom domain with protocol for accessing the uploaded file + zh_Hans: 可选的自定义域名(包含协议),用于访问上传的文件 + llm_description: Optional custom domain with protocol (http:// or https://) to generate complete access URL. If not provided, will return the file path only. + placeholder: + en_US: Enter custom domain with protocol, e.g. https://cdn.example.com + zh_Hans: 输入包含协议的自定义域名,例如 https://cdn.example.com + form: form + - name: overwrite + type: boolean + required: false + default: false + label: + en_US: Overwrite Existing File + zh_Hans: 覆盖已有文件 + human_description: + en_US: Whether to overwrite the file if it already exists + zh_Hans: 如果文件已存在是否覆盖 + llm_description: Set to true to overwrite existing file with the same filename, false to keep existing file + form: form +extra: + python: + source: tools/file_upload.py diff --git a/tools/get_file_content.py b/tools/get_file_content.py new file mode 100644 index 0000000..093b319 --- /dev/null +++ b/tools/get_file_content.py @@ -0,0 +1,153 @@ +import json +import logging +import requests +from collections.abc import Generator +from typing import Any + +from qiniu import Auth +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin.errors.tool import ToolProviderCredentialValidationError + +logger = logging.getLogger(__name__) + + +class QiniuGetContentTool(Tool): + """ + 七牛云获取文件内容工具 + + 通过文件 key 和域名获取签名 URL 并读取文件内容 + """ + + def _get_auth(self) -> Auth: + """获取七牛云认证对象""" + access_key = self.runtime.credentials.get("qiniu_access_key") + secret_key = self.runtime.credentials.get("qiniu_secret_key") + + if not access_key or not secret_key: + raise ToolProviderCredentialValidationError("七牛云 Access Key 和 Secret Key 不能为空") + + return Auth(access_key, secret_key) + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + """ + 获取七牛云文件内容 + + Args: + tool_parameters: 工具参数 + - file_key: 文件的 key(路径) + - domain: 七牛云绑定的域名 + - expire_time: 链接有效期(秒),默认 3600 秒 + + Returns: + Generator[ToolInvokeMessage, None, None]: 工具调用消息生成器 + """ + file_key = tool_parameters.get("file_key", "").strip() + domain = tool_parameters.get("domain", "").strip() + expire_time = tool_parameters.get("expire_time", 3600) + + # 参数验证 + if not file_key: + yield self.create_text_message("文件 key 不能为空") + return + + if not domain: + yield self.create_text_message("域名不能为空") + return + + # 确保域名格式正确 + if not domain.startswith(('http://', 'https://')): + domain = f"https://{domain}" + + try: + # 获取认证对象 + auth = self._get_auth() + + # 生成私有下载链接 + base_url = f"{domain}/{file_key}" + private_url = auth.private_download_url(base_url, expires=expire_time) + + yield self.create_text_message("正在获取文件内容...") + + # 请求文件内容 + response = requests.get(private_url, timeout=30) + response.raise_for_status() + + # 获取文件内容 + content = response.text + content_type = response.headers.get('content-type', 'text/plain') + file_size = len(response.content) + + # 检查文件大小(限制在 10MB 以内) + max_size = 10 * 1024 * 1024 # 10MB + if file_size > max_size: + yield self.create_text_message( + f"文件过大({file_size/1024/1024:.2f}MB),超过 10MB 限制" + ) + return + + # 返回结果 + result = { + "success": True, + "file_key": file_key, + "domain": domain, + "content_type": content_type, + "file_size": file_size, + "content": content, + "signed_url": private_url + } + + # 创建简化的文本结果 + markdown_content = f"文件内容获取成功:{file_key}({file_size/1024:.2f} KB)" + + yield self.create_text_message(markdown_content) + + # 创建文件 blob 消息,供大模型直接使用 + blob_meta = { + "file_key": file_key, + "file_name": file_key.split('/')[-1], # 从 key 中提取文件名 + "file_size": file_size, + "content_type": content_type, + "domain": domain + } + yield self.create_blob_message(response.content, meta=blob_meta) + + # 返回 JSON 格式的详细结果 + yield self.create_json_message(result) + + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + error_msg = f"文件不存在: {file_key}" + status_desc = "文件未找到" + elif e.response.status_code == 403: + error_msg = "访问被拒绝,请检查文件权限或域名配置" + status_desc = "访问权限不足" + else: + error_msg = f"HTTP 错误: {e.response.status_code}" + status_desc = f"HTTP {e.response.status_code} 错误" + + # 创建 HTTP 错误的简化消息 + markdown_content = f"获取失败:{error_msg}" + + logger.error(error_msg) + yield self.create_text_message(markdown_content) + + except requests.exceptions.RequestException as e: + error_msg = f"网络请求失败: {str(e)}" + + # 创建网络错误的简化消息 + markdown_content = f"网络错误:{error_msg}" + + logger.error(error_msg) + yield self.create_text_message(markdown_content) + + except Exception as e: + error_msg = f"获取文件内容失败: {str(e)}" + + # 创建通用错误的简化消息 + markdown_content = f"系统错误:{error_msg}" + + logger.error(error_msg, exc_info=True) + yield self.create_text_message(markdown_content) diff --git a/tools/get_file_content.yaml b/tools/get_file_content.yaml new file mode 100644 index 0000000..ef387d9 --- /dev/null +++ b/tools/get_file_content.yaml @@ -0,0 +1,58 @@ +identity: + name: get_file_content + author: qiniu + label: + en_US: Get File Content + zh_Hans: 获取文件内容 +description: + human: + en_US: Get file content from Qiniu Cloud Storage by file key and domain. Generates signed URL and retrieves content. + zh_Hans: 通过文件 key 和域名从七牛云存储获取文件内容。生成签名链接并读取内容。 + llm: A tool for retrieving file content from Qiniu Cloud Storage using file key and domain. Generates signed private download URL and fetches the content. +parameters: + - name: file_key + type: string + required: true + label: + en_US: File Key + zh_Hans: 文件 Key + human_description: + en_US: The key (path) of the file in Qiniu Cloud Storage + zh_Hans: 七牛云存储中文件的 key(路径) + llm_description: The file key or path in Qiniu Cloud Storage that identifies the file to retrieve + form: llm + placeholder: + en_US: Enter file key, e.g. docs/example.txt + zh_Hans: 输入文件 key,例如 docs/example.txt + - name: domain + type: string + required: true + label: + en_US: Domain + zh_Hans: 域名 + human_description: + en_US: The domain bound to your Qiniu Cloud Storage bucket + zh_Hans: 绑定到七牛云存储空间的域名 + llm_description: The domain name bound to the Qiniu bucket for accessing files + form: llm + placeholder: + en_US: Enter domain, e.g. example.com or https://example.com + zh_Hans: 输入域名,例如 example.com 或 https://example.com + - name: expire_time + type: number + required: false + default: 3600 + label: + en_US: Link Expiration Time + zh_Hans: 链接有效期 + human_description: + en_US: The expiration time for the signed URL in seconds (default 3600 seconds = 1 hour) + zh_Hans: 签名链接的有效期,单位为秒(默认 3600 秒 = 1 小时) + llm_description: The expiration time in seconds for the generated signed download URL + form: form + placeholder: + en_US: Enter expiration time in seconds + zh_Hans: 输入有效期(秒) +extra: + python: + source: tools/get_file_content.py diff --git a/tools/list_bucket_files.py b/tools/list_bucket_files.py new file mode 100644 index 0000000..487e456 --- /dev/null +++ b/tools/list_bucket_files.py @@ -0,0 +1,267 @@ +import json +import logging +from collections.abc import Generator +from typing import Any + +from qiniu import Auth, BucketManager +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin.errors.tool import ToolProviderCredentialValidationError + +logger = logging.getLogger(__name__) + + +class QiniuListFilesTool(Tool): + """ + 七牛云文件列表工具 + + 根据前缀列出指定存储空间中的文件 + """ + + def _get_auth(self) -> Auth: + """获取七牛云认证对象""" + access_key = self.runtime.credentials.get("qiniu_access_key") + secret_key = self.runtime.credentials.get("qiniu_secret_key") + + if not access_key or not secret_key: + raise ToolProviderCredentialValidationError("七牛云 Access Key 和 Secret Key 不能为空") + + return Auth(access_key, secret_key) + + def _validate_bucket_access(self, bucket_name: str) -> bool: + """验证存储空间访问权限""" + try: + auth = self._get_auth() + bucket_manager = BucketManager(auth) + + # 尝试获取空间列表来验证认证信息 + ret, eof, info = bucket_manager.list(bucket_name, limit=1) + + if info.status_code == 200: + return True + elif info.status_code == 401: + raise ToolProviderCredentialValidationError("七牛云认证失败,请检查 Access Key 和 Secret Key") + elif info.status_code == 631: + raise ToolProviderCredentialValidationError(f"存储空间 '{bucket_name}' 不存在") + else: + raise ToolProviderCredentialValidationError(f"验证存储空间失败: {info.error}") + + except Exception as e: + if isinstance(e, ToolProviderCredentialValidationError): + raise + raise ToolProviderCredentialValidationError(f"验证存储空间时发生错误: {str(e)}") + + def _list_files(self, bucket: str, prefix: str = None, limit: int = 100, marker: str = None) -> dict: + """列出文件""" + try: + auth = self._get_auth() + bucket_manager = BucketManager(auth) + + # 获取文件列表 + ret, eof, info = bucket_manager.list( + bucket, + prefix=prefix, + marker=marker, + limit=limit + ) + + if info.status_code == 200: + files = [] + next_marker = None + + if ret: + # 根据官方文档,响应应该包含 items 字段和可选的 marker 字段 + if isinstance(ret, dict): + # 标准响应格式:包含 items 和 marker + items = ret.get("items", []) + next_marker = ret.get("marker", None) + else: + logger.warning(f"意外的返回数据类型: {type(ret)} - {ret}") + items = [] + + # 处理文件条目 + for item in items: + # 确保 item 是字典类型 + if isinstance(item, dict): + file_info = { + "key": item.get("key", ""), + "size": item.get("fsize", 0), + "hash": item.get("hash", ""), + "put_time": item.get("putTime", 0), + "last_modify": item.get("lastModify", item.get("putTime", 0)), + "mime_type": item.get("mimeType", ""), + "end_user": item.get("endUser", ""), + "type": item.get("type", 0), + "status": item.get("status", 0), + "md5": item.get("md5", "") + } + files.append(file_info) + else: + # 如果 item 不是字典,记录警告并跳过 + logger.warning(f"跳过非字典类型的文件项: {type(item)} - {item}") + + # 如果没有从响应中获取到 marker,尝试从最后一个文件的 key 生成 + if next_marker is None and files: + next_marker = files[-1].get("key") + + return { + "success": True, + "files": files, + "count": len(files), + "eof": eof, # 是否已经到了最后一页 + "marker": next_marker # 下一页的标记 + } + elif info.status_code == 401: + raise ToolProviderCredentialValidationError("七牛云认证失败,请检查 Access Key 和 Secret Key") + elif info.status_code == 631: + raise ToolProviderCredentialValidationError(f"存储空间 '{bucket}' 不存在") + else: + return { + "success": False, + "error": f"获取文件列表失败: HTTP {info.status_code} - {info.error if hasattr(info, 'error') else ''}" + } + + except Exception as e: + if isinstance(e, ToolProviderCredentialValidationError): + raise + return { + "success": False, + "error": f"获取文件列表时发生错误: {str(e)}" + } + + def _generate_access_url(self, key: str, bucket: str, domain: str = None) -> str: + """生成访问链接""" + if domain: + # 如果提供了自定义域名,生成完整的访问链接 + domain = domain.rstrip('/') + if not domain.startswith(('http://', 'https://')): + domain = f"https://{domain}" + return f"{domain}/{key}" + else: + # 如果没有提供域名,返回文件路径 + return key + + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + """ + 执行获取文件列表操作 + + Args: + tool_parameters: 工具参数,包含 bucket, prefix(可选), limit(可选), marker(可选), domain(可选) + + Yields: + ToolInvokeMessage: 工具执行结果消息 + """ + try: + # 获取参数 + bucket = tool_parameters.get("bucket", "") + prefix = tool_parameters.get("prefix", "") + limit = tool_parameters.get("limit", 100) + marker = tool_parameters.get("marker", "") + domain = tool_parameters.get("domain", "") + + # 验证必需参数 + if not bucket: + yield self.create_text_message("存储空间名称不能为空") + return + + # 参数处理 + if prefix == "": + prefix = None + if marker == "": + marker = None + + # 限制 limit 范围 + limit = max(1, min(limit, 1000)) # 限制在 1-1000 之间 + + # 验证存储空间访问权限 + self._validate_bucket_access(bucket) + + # 执行文件列表获取 + list_result = self._list_files(bucket, prefix, limit, marker) + + if list_result["success"]: + # 为文件添加访问链接 + files_with_urls = [] + for file_info in list_result["files"]: + file_with_url = file_info.copy() + if domain: + file_with_url["url"] = self._generate_access_url( + file_info["key"], + bucket, + domain + ) + else: + file_with_url["url"] = None + files_with_urls.append(file_with_url) + + # 创建简化的成功消息 + markdown_content = f"文件列表获取成功,共 {list_result['count']} 个文件" + + yield self.create_text_message(markdown_content) + + # 成功获取列表 + result = { + "files": files_with_urls, + "count": list_result["count"], + "eof": list_result["eof"], + "next_marker": list_result["marker"] if not list_result["eof"] else None, + "bucket": bucket, + "prefix": prefix, + "error": None + } + + yield self.create_json_message(result) + else: + # 创建简化的失败消息 + markdown_content = f"文件列表获取失败:{list_result['error']}" + + yield self.create_text_message(markdown_content) + + # 获取失败,返回错误信息 + result = { + "files": [], + "count": 0, + "eof": True, + "next_marker": None, + "bucket": bucket, + "prefix": prefix, + "error": list_result['error'] + } + yield self.create_json_message(result) + + except ToolProviderCredentialValidationError as e: + # 创建认证错误的简化消息 + markdown_content = f"认证错误:{str(e)}" + + yield self.create_text_message(markdown_content) + + # 认证错误 + result = { + "files": [], + "count": 0, + "eof": True, + "next_marker": None, + "bucket": bucket if 'bucket' in locals() else "", + "prefix": prefix if 'prefix' in locals() else "", + "error": f"认证错误:{str(e)}" + } + yield self.create_json_message(result) + except Exception as e: + logger.exception("七牛云文件列表工具执行失败") + + # 创建通用错误的简化消息 + markdown_content = f"系统错误:{str(e)}" + + yield self.create_text_message(markdown_content) + + # 其他错误 + result = { + "files": [], + "count": 0, + "eof": True, + "next_marker": None, + "bucket": bucket if 'bucket' in locals() else "", + "prefix": prefix if 'prefix' in locals() else "", + "error": f"执行失败:{str(e)}" + } + yield self.create_json_message(result) diff --git a/tools/list_bucket_files.yaml b/tools/list_bucket_files.yaml new file mode 100644 index 0000000..f1cd744 --- /dev/null +++ b/tools/list_bucket_files.yaml @@ -0,0 +1,86 @@ +identity: + name: list_bucket_files + author: qiniu + label: + en_US: List Bucket Files + zh_Hans: 列出空间文件 +description: + human: + en_US: List files in a Qiniu Cloud Storage bucket with optional prefix filtering and pagination support + zh_Hans: 列出七牛云存储空间中的文件,支持前缀过滤和分页 + llm: A tool for listing files in a Qiniu Cloud Storage bucket with optional prefix filtering and pagination support +parameters: + - name: bucket + type: string + required: true + label: + en_US: Bucket Name + zh_Hans: 存储空间名称 + human_description: + en_US: The name of the Qiniu Cloud Storage bucket to list files from + zh_Hans: 要列出文件的七牛云存储空间名称 + llm_description: The name of the Qiniu Cloud Storage bucket to list files from + placeholder: + en_US: Enter bucket name, e.g. my-storage-bucket + zh_Hans: 输入存储空间名称,例如 my-storage-bucket + form: form + - name: prefix + type: string + required: false + label: + en_US: File Prefix + zh_Hans: 文件前缀 + human_description: + en_US: Optional prefix to filter files (e.g. "uploads/", "docs/2025/"). Leave empty to list all files + zh_Hans: 可选的文件前缀过滤(例如 "uploads/"、"docs/2025/")。留空则列出所有文件 + llm_description: Optional prefix to filter files. Only files with keys starting with this prefix will be returned. Leave empty to list all files. + placeholder: + en_US: Enter prefix, e.g. uploads/ or docs/2025/ + zh_Hans: 输入前缀,例如 uploads/ 或 docs/2025/ + form: form + - name: limit + type: number + required: false + default: 100 + label: + en_US: Max Results + zh_Hans: 最大结果数 + human_description: + en_US: Maximum number of files to return (1-1000) + zh_Hans: 返回的最大文件数量(1-1000) + llm_description: Maximum number of files to return in one request. Must be between 1 and 1000. + placeholder: + en_US: Enter number between 1-1000, default is 100 + zh_Hans: 输入1-1000之间的数字,默认100 + form: form + - name: marker + type: string + required: false + label: + en_US: Pagination Marker + zh_Hans: 分页标记 + human_description: + en_US: Pagination marker from previous request. Use the 'next_marker' from previous response to get next page + zh_Hans: 上一次请求的分页标记。使用上一次响应的 'next_marker' 来获取下一页 + llm_description: Pagination marker to get next page of results. Use the 'next_marker' value from a previous response. + placeholder: + en_US: Enter marker from previous response + zh_Hans: 输入上一次响应的标记 + form: form + - name: domain + type: string + required: false + label: + en_US: Custom Domain + zh_Hans: 自定义域名 + human_description: + en_US: Optional custom domain with protocol for generating file URLs + zh_Hans: 可选的自定义域名(包含协议),用于生成文件访问链接 + llm_description: Optional custom domain with protocol (http:// or https://) to generate complete access URLs for files. If not provided, URLs will be null. + placeholder: + en_US: Enter custom domain with protocol, e.g. https://cdn.example.com + zh_Hans: 输入包含协议的自定义域名,例如 https://cdn.example.com + form: form +extra: + python: + source: tools/list_bucket_files.py diff --git a/tools/list_buckets.py b/tools/list_buckets.py new file mode 100644 index 0000000..1beeb9f --- /dev/null +++ b/tools/list_buckets.py @@ -0,0 +1,131 @@ +import json +import logging +from collections.abc import Generator +from typing import Any + +from qiniu import Auth, BucketManager +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin.errors.tool import ToolProviderCredentialValidationError + +logger = logging.getLogger(__name__) + + +class QiniuListBucketsTool(Tool): + """ + 七牛云存储空间列表工具 + + 获取当前账户下的所有存储空间列表 + """ + + def _get_auth(self) -> Auth: + """获取七牛云认证对象""" + access_key = self.runtime.credentials.get("qiniu_access_key") + secret_key = self.runtime.credentials.get("qiniu_secret_key") + + if not access_key or not secret_key: + raise ToolProviderCredentialValidationError("七牛云 Access Key 和 Secret Key 不能为空") + + return Auth(access_key, secret_key) + + def _list_buckets(self) -> dict: + """获取存储空间列表""" + try: + auth = self._get_auth() + bucket_manager = BucketManager(auth) + + # 获取存储空间列表 + ret, info = bucket_manager.buckets() + + if info.status_code == 200: + return { + "success": True, + "buckets": ret if ret else [], + "count": len(ret) if ret else 0 + } + elif info.status_code == 401: + raise ToolProviderCredentialValidationError("七牛云认证失败,请检查 Access Key 和 Secret Key") + else: + return { + "success": False, + "error": f"获取存储空间列表失败: HTTP {info.status_code} - {info.error if hasattr(info, 'error') else ''}" + } + + except Exception as e: + if isinstance(e, ToolProviderCredentialValidationError): + raise + return { + "success": False, + "error": f"获取存储空间列表时发生错误: {str(e)}" + } + + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + """ + 执行获取存储空间列表操作 + + Args: + tool_parameters: 工具参数(此工具无需额外参数) + + Yields: + ToolInvokeMessage: 工具执行结果消息 + """ + try: + # 获取存储空间列表 + list_result = self._list_buckets() + + if list_result["success"]: + # 创建简化的成功消息 + markdown_content = f"存储桶列表获取成功,共 {list_result['count']} 个存储桶" + + yield self.create_text_message(markdown_content) + + # 成功获取列表 + result = { + "buckets": list_result["buckets"], + "count": list_result["count"], + "error": None + } + + yield self.create_json_message(result) + else: + # 创建简化的失败消息 + markdown_content = f"存储桶列表获取失败:{list_result['error']}" + + yield self.create_text_message(markdown_content) + + # 获取失败,返回错误信息 + result = { + "buckets": [], + "count": 0, + "error": list_result['error'] + } + yield self.create_json_message(result) + + except ToolProviderCredentialValidationError as e: + # 创建认证错误的简化消息 + markdown_content = f"认证错误:{str(e)}" + + yield self.create_text_message(markdown_content) + + # 认证错误 + result = { + "buckets": [], + "count": 0, + "error": f"认证错误:{str(e)}" + } + yield self.create_json_message(result) + except Exception as e: + logger.exception("七牛云存储空间列表工具执行失败") + + # 创建通用错误的简化消息 + markdown_content = f"系统错误:{str(e)}" + + yield self.create_text_message(markdown_content) + + # 其他错误 + result = { + "buckets": [], + "count": 0, + "error": f"执行失败:{str(e)}" + } + yield self.create_json_message(result) diff --git a/tools/list_buckets.yaml b/tools/list_buckets.yaml new file mode 100644 index 0000000..2d095a6 --- /dev/null +++ b/tools/list_buckets.yaml @@ -0,0 +1,15 @@ +identity: + name: list_buckets + author: qiniu + label: + en_US: List Buckets + zh_Hans: 列出存储空间 +description: + human: + en_US: List all storage buckets in the current Qiniu Cloud account + zh_Hans: 列出当前七牛云账户下的所有存储空间 + llm: A tool for listing all storage buckets in the current Qiniu Cloud account +parameters: [] +extra: + python: + source: tools/list_buckets.py