From afa89fb8576a8656b22fec995f69846c6f60f2de Mon Sep 17 00:00:00 2001 From: AllenWriter Date: Wed, 26 Mar 2025 00:42:17 +0800 Subject: [PATCH] Docs: update docs image link --- docs.json | 4 +- .../maintain-dataset-via-api.mdx | 4 +- ja-jp/guides/knowledge-base/metadata.mdx | 1 + scripts/convert_image_format.py | 331 +++++++-- scripts/doc_link_checker.py | 268 +++++++ scripts/doc_migration_helper.py | 588 +++++++++++++++ scripts/fix_links_interactive-3.25-backup.py | 365 +++++++++ scripts/sync_image_links.py | 66 +- scripts/test_conversion.py | 99 +++ tools/check_specific_file.py | 164 +++++ tools/link_checker.py | 218 ++++++ .../models-integration/gpustack.mdx | 4 +- .../development/models-integration/ollama.mdx | 3 - .../based-on-frontend-templates.mdx | 1 - .../developing-with-apis.mdx | 23 +- .../embedding-in-websites.mdx | 5 +- .../conversation-application.mdx | 20 +- .../text-generator.mdx | 13 +- .../web-app-settings.mdx | 1 - .../maintain-dataset-via-api.mdx | 4 +- .../knowledge-base-creation/introduction.mdx | 2 +- zh-hans/guides/knowledge-base/metadata.mdx | 1 + zh-hans/guides/management/app-management.mdx | 30 +- .../personal-account-management.mdx | 59 +- .../management/subscription-management.mdx | 74 ++ .../management/team-members-management.mdx | 11 +- zh-hans/guides/monitoring/analysis.mdx | 4 +- .../integrate-langfuse.mdx | 59 +- .../integrate-langsmith.mdx | 37 +- .../api-based/api-based-extension.mdx | 16 +- .../api-based/cloudflare-workers.mdx | 8 +- .../code-based/external-data-tool.mdx | 198 ++++- .../extensions/code-based/moderation.mdx | 2 - zh-hans/guides/workflow/file-upload.mdx | 63 +- zh-hans/guides/workflow/nodes/code.mdx | 50 +- .../guides/workflow/nodes/doc-extractor.mdx | 16 +- zh-hans/guides/workflow/nodes/end.mdx | 15 +- .../guides/workflow/nodes/http-request.mdx | 49 +- zh-hans/guides/workflow/nodes/ifelse.mdx | 8 +- zh-hans/guides/workflow/nodes/iteration.mdx | 175 +++-- .../workflow/nodes/knowledge-retrieval.mdx | 2 +- .../guides/workflow/nodes/list-operator.mdx | 26 +- zh-hans/guides/workflow/nodes/loop.mdx | 76 +- .../workflow/nodes/parameter-extractor.mdx | 21 +- .../workflow/nodes/question-classifier.mdx | 8 +- zh-hans/guides/workflow/nodes/start.mdx | 14 +- zh-hans/guides/workflow/nodes/template.mdx | 6 +- zh-hans/guides/workflow/nodes/tools.mdx | 47 +- ...ggregation.mdx => variable-aggregator.mdx} | 9 +- .../workflow/nodes/variable-assigner.mdx | 16 +- zh-hans/guides/workflow/orchestrate-node.mdx | 31 +- .../extended-reading/prompt-engineering.mdx | 6 +- .../build-an-notion-ai-assistant.mdx | 20 +- ...-chatbot-with-business-data-in-minutes.mdx | 12 +- ...-using-a-full-set-of-open-source-tools.mdx | 3 +- ...train-a-qa-chatbot-that-belongs-to-you.mdx | 24 +- zh-hans/plugins/best-practice/README.mdx | 2 +- zh-hans/plugins/introduction.mdx | 16 +- zh-hans/plugins/publish-plugins/README.mdx | 4 +- zh-hans/plugins/quick-start/README.mdx | 24 +- .../quick-start/develop-plugins/README.mdx | 23 +- .../initialize-development-tools.mdx | 18 +- .../model-plugin/customizable-model.mdx | 2 +- .../plugins/quick-start/install-plugins.mdx | 2 +- zh-hans/plugins/schema-definition/README.mdx | 10 +- .../plugins/schema-definition/endpoint.mdx | 6 +- .../plugins/schema-definition/manifest.mdx | 6 +- .../schema-definition/model/README.mdx | 4 +- .../app.mdx | 2 - zh-hans/user-guide/.DS_Store | Bin 6148 -> 0 bytes .../external-knowledge-api-documentation.mdx | 161 ---- .../external-knowledge-api.json | 192 ----- .../maintain-dataset-via-api.mdx | 550 -------------- .../connect-external-knowledge-base.mdx | 123 ---- .../chunking-and-cleaning-text.mdx | 141 ---- .../import-content-data/readme.mdx | 34 - .../import-content-data/sync-from-notion.mdx | 56 -- .../import-content-data/sync-from-website.mdx | 59 -- .../readme.mdx | 57 -- .../setting-indexing-methods.mdx | 167 ----- zh-hans/user-guide/knowledge-base/faq.mdx | 20 - .../indexing-and-retrieval/hybrid-search.mdx | 103 --- .../indexing-and-retrieval/rerank.mdx | 64 -- .../retrieval-augment.mdx | 26 - .../indexing-and-retrieval/retrieval.mdx | 24 - ...integrate-knowledge-within-application.mdx | 204 ----- .../knowledge-and-documents-maintenance.mdx | 134 ---- .../introduction.mdx | 48 -- .../maintain-dataset-via-api.mdx | 695 ------------------ .../maintain-knowledge-documents.mdx | 133 ---- .../knowledge-base-creation/introduction.mdx | 63 -- .../upload-documents.mdx | 254 ------- .../user-guide/knowledge-base/metadata.mdx | 408 ---------- zh-hans/user-guide/knowledge-base/readme.mdx | 43 -- .../retrieval-test-and-citation.mdx | 74 -- 95 files changed, 2855 insertions(+), 4477 deletions(-) create mode 100644 scripts/doc_link_checker.py create mode 100644 scripts/doc_migration_helper.py create mode 100644 scripts/fix_links_interactive-3.25-backup.py create mode 100644 scripts/test_conversion.py create mode 100644 tools/check_specific_file.py create mode 100644 tools/link_checker.py create mode 100644 zh-hans/guides/management/subscription-management.mdx rename zh-hans/guides/workflow/nodes/{variable-aggregation.mdx => variable-aggregator.mdx} (75%) delete mode 100644 zh-hans/user-guide/.DS_Store delete mode 100644 zh-hans/user-guide/knowledge-base/api-documentation/external-knowledge-api-documentation.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/api-documentation/external-knowledge-api.json delete mode 100644 zh-hans/user-guide/knowledge-base/api-documentation/maintain-dataset-via-api.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/connect-external-knowledge-base.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/create-knowledge-and-upload-documents/chunking-and-cleaning-text.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/create-knowledge-and-upload-documents/import-content-data/readme.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-notion.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/create-knowledge-and-upload-documents/readme.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/faq.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/indexing-and-retrieval/hybrid-search.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/indexing-and-retrieval/rerank.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/indexing-and-retrieval/retrieval-augment.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/indexing-and-retrieval/retrieval.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/integrate-knowledge-within-application.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/knowledge-and-documents-maintenance.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/knowledge-and-documents-maintenance/introduction.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/knowledge-and-documents-maintenance/maintain-dataset-via-api.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/knowledge-and-documents-maintenance/maintain-knowledge-documents.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/knowledge-base-creation/introduction.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/knowledge-base-creation/upload-documents.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/metadata.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/readme.mdx delete mode 100644 zh-hans/user-guide/knowledge-base/retrieval-test-and-citation.mdx diff --git a/docs.json b/docs.json index 40ce7672..30e5dfce 100644 --- a/docs.json +++ b/docs.json @@ -667,6 +667,7 @@ "zh-hans/guides/management/app-management", "zh-hans/guides/management/team-members-management", "zh-hans/guides/management/personal-account-management", + "zh-hans/guides/management/subscription-management", "zh-hans/guides/management/version-control" ] } @@ -737,7 +738,6 @@ { "group": "接口定义", "pages": [ - "zh-hans/plugins/schema-definition/README", "zh-hans/plugins/schema-definition/manifest", "zh-hans/plugins/schema-definition/endpoint", "zh-hans/plugins/schema-definition/tool", @@ -745,7 +745,6 @@ { "group": "Model", "pages": [ - "zh-hans/plugins/schema-definition/model/README", "zh-hans/plugins/schema-definition/model/model-designing-rules", "zh-hans/plugins/schema-definition/model/model-schema" ] @@ -767,7 +766,6 @@ { "group": "最佳实践", "pages": [ - "zh-hans/plugins/best-practice/README", "zh-hans/plugins/best-practice/develop-a-slack-bot-plugin" ] }, diff --git a/ja-jp/guides/knowledge-base/knowledge-and-documents-maintenance/maintain-dataset-via-api.mdx b/ja-jp/guides/knowledge-base/knowledge-and-documents-maintenance/maintain-dataset-via-api.mdx index a336a3e5..38963092 100644 --- a/ja-jp/guides/knowledge-base/knowledge-and-documents-maintenance/maintain-dataset-via-api.mdx +++ b/ja-jp/guides/knowledge-base/knowledge-and-documents-maintenance/maintain-dataset-via-api.mdx @@ -20,9 +20,7 @@ version: '简体中文' 进入知识库页面,在左侧的导航中切换至 **API** 页面。在该页面中你可以查看 Dify 提供的知识库 API 文档,并可以在 **API 密钥** 中管理可访问知识库 API 的凭据。 - - - +![](https://assets-docs.dify.ai/2025/03/82ef51dc6886fb8301a2b85a920b12d0.png) ### API 调用示例 diff --git a/ja-jp/guides/knowledge-base/metadata.mdx b/ja-jp/guides/knowledge-base/metadata.mdx index 39823071..3005087e 100644 --- a/ja-jp/guides/knowledge-base/metadata.mdx +++ b/ja-jp/guides/knowledge-base/metadata.mdx @@ -18,6 +18,7 @@ title: 元数据 field_name_and_value diff --git a/scripts/convert_image_format.py b/scripts/convert_image_format.py index b77f907c..22629533 100644 --- a/scripts/convert_image_format.py +++ b/scripts/convert_image_format.py @@ -3,15 +3,37 @@ Mintlify图片格式转换工具 这个脚本用于扫描dify-docs-mintlify目录中的所有.mdx文件, -并将标签中的图片转换为标准Markdown格式。 +并将标签中的图片转换为标准Markdown格式或HTML格式。 -转换前: - - 示例 - +支持以下转换: +1. 基本Frame转Markdown: + + 描述 + + + 转换为: + ![描述](https://example.com/image.png) -转换后: -![示例](https://assets-docs.dify.ai/example.png) +2. 带自闭合标签的Frame: + + + + + 转换为: + ![](https://example.com/image.png) + +3. 带宽度的Frame转HTML: + + 描述 + + + 转换为: + 描述 """ import os @@ -33,12 +55,32 @@ class Colors: BOLD = '\033[1m' UNDERLINE = '\033[4m' -# 匹配Frame标签中的图片 -FRAME_IMG_PATTERN = re.compile(r'\s*\s*', re.DOTALL) +# 匹配Frame标签中的图片,处理多种情况: +# 1. 标准Frame标签: ......... +# 2. 特殊结束的img标签: ...... +# 3. 支持特殊字符和空格 +FRAME_IMG_PATTERN = re.compile( + r'\s*' + r'|\/ >|>\s*<\/img>)\s*' + r'<\/Frame>', + re.DOTALL +) -def convert_frame_to_markdown(content: str) -> Tuple[str, List[Tuple[str, str]]]: +# 另一种每行写一个属性的格式,更付特征,匹配如 +# metadata_field +HTML_IMG_PATTERN = re.compile( + r'', + re.DOTALL +) + +def convert_frame_to_markdown(content: str) -> Tuple[str, List[Tuple[str, str, str]]]: """ - 将Frame标签中的图片转换为Markdown格式 + 将Frame标签中的图片转换为Markdown或HTML格式 Args: content: 文件内容 @@ -50,32 +92,67 @@ def convert_frame_to_markdown(content: str) -> Tuple[str, List[Tuple[str, str]]] def replace_frame(match): caption = match.group(1) or "" - src = match.group(2) - alt = match.group(3) or caption or "" + width = match.group(2) # 可能为None + src = match.group(3) + alt = match.group(4) or caption or "" # 原始内容 original = match.group(0) - # 转换为Markdown格式 - markdown = f"![{alt}]({src})" + # 转换格式 + if width: + # 带宽度的转为HTML格式 + new_format = "HTML" + markdown = f"""""" + else: + # 不带宽度的转为Markdown格式 + new_format = "Markdown" + markdown = f"![{alt}]({src})" # 记录替换 - replacements.append((original, markdown)) + replacements.append((original, markdown, new_format)) return markdown - # 执行替换 + # 先处理Frame标签 new_content = FRAME_IMG_PATTERN.sub(replace_frame, content) - return new_content, replacements + # 再处理HTML格式的img标签 + def replace_html_img(match): + src = match.group(1) + width = match.group(2) # 可能为None + alt = match.group(3) or "" + + # 原始内容 + original = match.group(0) + + # HTML格式的图片保持为HTML格式,但直接转为Markdown + new_format = "Markdown" + markdown = f"![{alt}]({src})" + + # 记录替换 + replacements.append((original, markdown, new_format)) + + return markdown + + # 处理HTML格式的img标签 + final_content = HTML_IMG_PATTERN.sub(replace_html_img, new_content) + + return final_content, replacements -def process_file(file_path: str, dry_run: bool = False) -> Tuple[int, List[Tuple[str, str]]]: +def process_file(file_path: str, dry_run: bool = False, debug: bool = False) -> Tuple[int, List[Tuple[str, str, str]]]: """ 处理单个文件 Args: file_path: 文件路径 dry_run: 是否只预览修改而不实际写入 + debug: 是否显示调试信息 Returns: Tuple[替换的数量, 替换记录列表] @@ -84,6 +161,45 @@ def process_file(file_path: str, dry_run: bool = False) -> Tuple[int, List[Tuple with open(file_path, 'r', encoding='utf-8') as f: content = f.read() + if debug: + # 直接检查正则达式匹配 + frame_matches = FRAME_IMG_PATTERN.findall(content) + html_matches = HTML_IMG_PATTERN.findall(content) + + print(f"\n{Colors.CYAN}匹配结果调试信息:{Colors.ENDC}") + print(f"在文件 {file_path} 中找到:") + print(f"- {len(frame_matches)} 个Frame标签中的图片") + print(f"- {len(html_matches)} 个HTML格式的图片") + + # 打印Frame标签匹配 + for i, match in enumerate(frame_matches): + caption, width, src, alt = match + print(f"\n Frame图片 {i+1}:") + print(f" caption: '{caption}'") + print(f" width: '{width}'") + print(f" src: '{src}'") + print(f" alt: '{alt}'") + + # 显示原始文本片段 + pattern_matches = list(FRAME_IMG_PATTERN.finditer(content)) + if i < len(pattern_matches): + orig_text = pattern_matches[i].group(0) + print(f" 原始文本: '{orig_text[:100]}...'") + + # 打印HTML图片匹配 + for i, match in enumerate(html_matches): + src, width, alt = match + print(f"\n HTML图片 {i+1}:") + print(f" src: '{src}'") + print(f" width: '{width}'") + print(f" alt: '{alt}'") + + # 显示原始文本片段 + pattern_matches = list(HTML_IMG_PATTERN.finditer(content)) + if i < len(pattern_matches): + orig_text = pattern_matches[i].group(0) + print(f" 原始文本: '{orig_text[:100]}...'") + # 转换内容 new_content, replacements = convert_frame_to_markdown(content) @@ -97,29 +213,48 @@ def process_file(file_path: str, dry_run: bool = False) -> Tuple[int, List[Tuple print(f"{Colors.FAIL}处理文件时出错 {file_path}: {e}{Colors.ENDC}") return 0, [] -def scan_directory(dir_path: str, dry_run: bool = False, extensions: List[str] = ['.mdx']) -> Tuple[int, int, int]: +def scan_directory(dir_path: str, dry_run: bool = False, auto_confirm: bool = False, debug: bool = False, extensions: List[str] = ['.mdx']) -> Tuple[int, int, int, int]: """ 扫描目录并处理文件 Args: dir_path: 目录路径 dry_run: 是否只预览修改而不实际写入 + auto_confirm: 是否自动确认所有修改 + debug: 是否显示调试信息 extensions: 要处理的文件扩展名列表 Returns: - Tuple[处理的文件数, 包含Frame的文件数, 替换的总数] + Tuple[处理的文件数, 修改的文件数, 转为Markdown的数量, 转为HTML的数量] """ file_count = 0 modified_file_count = 0 - total_replacements = 0 + markdown_count = 0 + html_count = 0 - for root, _, files in os.walk(dir_path): + for root, dirs, files in os.walk(dir_path): + # 跳过.git等特殊目录 + dirs[:] = [d for d in dirs if not d.startswith('.')] + for file in files: if any(file.endswith(ext) for ext in extensions): file_path = os.path.join(root, file) + rel_path = os.path.relpath(file_path, dir_path) + + # 如果不是自动确认模式,则询问是否处理此文件 + if not auto_confirm: + print(f"\n{Colors.CYAN}文件 ({file_count+1}): {rel_path}{Colors.ENDC}") + response = input(f"{Colors.BOLD}是否处理此文件? (y/n/q-退出): {Colors.ENDC}") + if response.lower() == 'n': + print(f"{Colors.BLUE}跳过此文件{Colors.ENDC}") + file_count += 1 + continue + elif response.lower() == 'q': + print(f"{Colors.BLUE}退出处理{Colors.ENDC}") + break # 处理文件 - count, replacements = process_file(file_path, dry_run) + count, replacements = process_file(file_path, dry_run, debug) file_count += 1 if count > 0: @@ -127,17 +262,22 @@ def scan_directory(dir_path: str, dry_run: bool = False, extensions: List[str] = print(f"\n{Colors.CYAN}文件: {rel_path}{Colors.ENDC}") print(f"{Colors.GREEN}找到 {count} 个需要转换的图片{Colors.ENDC}") + # 统计转换类型 + md_count = sum(1 for _, _, fmt in replacements if fmt == "Markdown") + html_count = sum(1 for _, _, fmt in replacements if fmt == "HTML") + # 显示替换详情 - for i, (original, markdown) in enumerate(replacements): + for i, (original, converted, format_type) in enumerate(replacements): # 为了简洁,截断过长的内容 orig_short = original[:100] + "..." if len(original) > 100 else original - print(f" {i+1}. {Colors.WARNING}{orig_short}{Colors.ENDC}") - print(f" -> {Colors.GREEN}{markdown}{Colors.ENDC}") + print(f" {i+1}. {Colors.WARNING}[{format_type}] {orig_short}{Colors.ENDC}") + print(f" -> {Colors.GREEN}{converted}{Colors.ENDC}") modified_file_count += 1 - total_replacements += count + markdown_count += md_count + html_count += html_count - return file_count, modified_file_count, total_replacements + return file_count, modified_file_count, markdown_count, html_count def main(): """主程序入口""" @@ -150,52 +290,97 @@ def main(): print(f"{Colors.HEADER} Mintlify图片格式转换工具 {Colors.ENDC}") print(f"{Colors.HEADER}{'='*60}{Colors.ENDC}") - # 参数处理 - if len(sys.argv) > 1: - if sys.argv[1] == '--help' or sys.argv[1] == '-h': - print("\n使用方法:") - print(" python convert_image_format.py [目录路径] [选项]") - print("\n选项:") - print(" --dry-run 仅预览修改,不实际写入") - print(" --help, -h 显示此帮助信息") - return + # 交互式菜单 + while True: + print(f"\n{Colors.BOLD}请选择操作模式:{Colors.ENDC}") + print("1. 处理单个文件") + print("2. 处理指定目录中的所有文件") + print("3. 退出") + + choice = input(f"{Colors.BOLD}请输入选项 (1-3): {Colors.ENDC}") + + if choice == '1': + # 处理单个文件 + file_path = input(f"{Colors.BOLD}请输入文件路径 (绝对或相对路径): {Colors.ENDC}") + + # 如果是相对路径,则基于默认目录 + if not os.path.isabs(file_path): + file_path = os.path.join(default_dir, file_path) + + if not os.path.isfile(file_path): + print(f"{Colors.FAIL}错误: 文件不存在: {file_path}{Colors.ENDC}") + continue + + # 询问是否只预览修改 + preview = input(f"{Colors.BOLD}是否只预览修改而不实际写入? (y/n): {Colors.ENDC}").lower() == 'y' + + # 询问是否显示调试信息 + debug = input(f"{Colors.BOLD}是否显示调试信息? (y/n): {Colors.ENDC}").lower() == 'y' + + # 处理文件 + start_time = time.time() + count, replacements = process_file(file_path, preview, debug) + end_time = time.time() + + if count > 0: + # 统计转换类型 + md_count = sum(1 for _, _, fmt in replacements if fmt == "Markdown") + html_count = sum(1 for _, _, fmt in replacements if fmt == "HTML") + + print(f"\n{Colors.GREEN}处理完成! 耗时: {end_time - start_time:.2f}秒{Colors.ENDC}") + print(f"{Colors.GREEN}发现 {count} 个需要转换的图片:{Colors.ENDC}") + print(f"{Colors.GREEN}- 转换为Markdown格式: {md_count}{Colors.ENDC}") + print(f"{Colors.GREEN}- 转换为HTML格式: {html_count}{Colors.ENDC}") + + if preview: + print(f"\n{Colors.BLUE}这是预览模式,没有实际写入修改。{Colors.ENDC}") + else: + print(f"{Colors.BLUE}没有找到需要转换的图片{Colors.ENDC}") + + elif choice == '2': + # 处理目录 + dir_path = input(f"{Colors.BOLD}请输入目录路径 (绝对或相对路径): {Colors.ENDC}") + + # 如果是相对路径,则基于默认目录 + if not os.path.isabs(dir_path): + dir_path = os.path.join(default_dir, dir_path) + + if not os.path.isdir(dir_path): + print(f"{Colors.FAIL}错误: 目录不存在: {dir_path}{Colors.ENDC}") + continue + + # 询问是否只预览修改 + preview = input(f"{Colors.BOLD}是否只预览修改而不实际写入? (y/n): {Colors.ENDC}").lower() == 'y' + + # 询问是否自动确认所有修改 + auto_confirm = input(f"{Colors.BOLD}是否自动确认所有修改? (y/n): {Colors.ENDC}").lower() == 'y' + + # 询问是否显示调试信息 + debug = input(f"{Colors.BOLD}是否显示调试信息? (y/n): {Colors.ENDC}").lower() == 'y' + + # 开始处理 + start_time = time.time() + file_count, modified_file_count, markdown_count, html_count = scan_directory(dir_path, preview, auto_confirm=auto_confirm, debug=debug) + end_time = time.time() + + # 显示总结 + print(f"\n{Colors.HEADER}{'='*60}{Colors.ENDC}") + print(f"{Colors.GREEN}处理完成! 耗时: {end_time - start_time:.2f}秒{Colors.ENDC}") + print(f"{Colors.GREEN}扫描了 {file_count} 个文件{Colors.ENDC}") + print(f"{Colors.GREEN}修改了 {modified_file_count} 个文件中的图片格式{Colors.ENDC}") + print(f"{Colors.GREEN}- 转换为Markdown格式: {markdown_count}{Colors.ENDC}") + print(f"{Colors.GREEN}- 转换为HTML格式: {html_count}{Colors.ENDC}") + + if preview: + print(f"\n{Colors.BLUE}这是预览模式,没有实际写入修改。{Colors.ENDC}") + + elif choice == '3': + # 退出 + print(f"{Colors.BLUE}感谢使用,再见!{Colors.ENDC}") + break - if os.path.exists(sys.argv[1]) and os.path.isdir(sys.argv[1]): - target_dir = sys.argv[1] else: - print(f"{Colors.FAIL}错误: 无效的目录路径: {sys.argv[1]}{Colors.ENDC}") - return - else: - # 使用默认目录 - target_dir = default_dir - - # 检查是否为预览模式 - dry_run = '--dry-run' in sys.argv - - print(f"目标目录: {target_dir}") - print(f"预览模式: {'是' if dry_run else '否'}\n") - - # 确认操作 - if not dry_run: - response = input(f"{Colors.BOLD}这将修改所有.mdx文件中的图片格式。确认继续? (y/n): {Colors.ENDC}") - if response.lower() != 'y': - print(f"{Colors.BLUE}操作已取消{Colors.ENDC}") - return - - # 开始处理 - start_time = time.time() - file_count, modified_file_count, total_replacements = scan_directory(target_dir, dry_run) - end_time = time.time() - - # 显示总结 - print(f"\n{Colors.HEADER}{'='*60}{Colors.ENDC}") - print(f"{Colors.GREEN}处理完成! 耗时: {end_time - start_time:.2f}秒{Colors.ENDC}") - print(f"{Colors.GREEN}扫描了 {file_count} 个文件{Colors.ENDC}") - print(f"{Colors.GREEN}修改了 {modified_file_count} 个文件中的 {total_replacements} 处图片格式{Colors.ENDC}") - - if dry_run: - print(f"\n{Colors.BLUE}这是预览模式,没有实际写入修改。{Colors.ENDC}") - print(f"{Colors.BLUE}如需实际应用修改,请去掉 --dry-run 选项重新运行脚本。{Colors.ENDC}") + print(f"{Colors.WARNING}无效选项,请重试{Colors.ENDC}") if __name__ == "__main__": main() diff --git a/scripts/doc_link_checker.py b/scripts/doc_link_checker.py new file mode 100644 index 00000000..ed5250df --- /dev/null +++ b/scripts/doc_link_checker.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +import os +import re +import argparse +from pathlib import Path +from typing import List, Tuple, Dict, Set +import time +import sys + +# 颜色代码,用于终端输出 +class Colors: + HEADER = '\033[95m' + BLUE = '\033[94m' + GREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + +def log_info(message): + """输出信息日志""" + print(f"{Colors.BLUE}[INFO]{Colors.ENDC} {message}") + +def log_warning(message): + """输出警告日志""" + print(f"{Colors.WARNING}[WARNING]{Colors.ENDC} {message}") + +def log_error(message): + """输出错误日志""" + print(f"{Colors.FAIL}[ERROR]{Colors.ENDC} {message}") + +def log_success(message): + """输出成功日志""" + print(f"{Colors.GREEN}[SUCCESS]{Colors.ENDC} {message}") + +def find_all_md_files(base_dir: str) -> List[Path]: + """查找指定目录下的所有 .md 和 .mdx 文件""" + md_files = [] + base_path = Path(base_dir) + + for ext in ["*.md", "*.mdx"]: + md_files.extend(base_path.glob(f"**/{ext}")) + + return md_files + +def extract_links(file_content: str) -> List[Tuple[str, str, str]]: + """从文件内容中提取所有链接 + 返回格式: [(完整匹配文本, 链接文本, 链接URL)] + """ + links = [] + + # 提取 Markdown 链接 [text](url) + md_links = re.findall(r'\[(.*?)\]\((.*?)\)', file_content) + for text, url in md_links: + full_match = f"[{text}]({url})" + links.append((full_match, text, url)) + + # 提取 标签链接 + a_links = re.findall(r']*?\s+)?href="([^"]*)"[^>]*>(.*?)<\/a>', file_content) + for url, text in a_links: + full_match = f'{text}' + links.append((full_match, text, url)) + + # 提取 Mintlify Card 组件链接 + card_links = re.findall(r']*\s+href="([^"]*)"[^>]*>(.*?)<\/Card>', file_content, re.DOTALL) + for title, url, content in card_links: + full_match = f'{content}' + links.append((full_match, title, url)) + + return links + +def check_link_extensions(links: List[Tuple[str, str, str]], + file_path: Path, + all_files: Dict[str, Path], + base_dir: Path) -> List[Tuple[str, str, str, str]]: + """检查链接是否包含不需要的扩展名 + 返回格式: [(完整匹配文本, 链接文本, 原始URL, 修复后URL)] + """ + issues = [] + + for full_match, text, url in links: + # 忽略外部链接和锚点链接 + if url.startswith(('http://', 'https://', '#', 'mailto:', 'tel:')): + continue + + # 忽略以 / 开头的绝对路径 + if url.startswith('/'): + continue + + # 检查链接是否包含 .md 或 .mdx 扩展名 + if url.endswith('.md') or url.endswith('.mdx'): + # 计算修复后的 URL + fixed_url = url.rsplit('.', 1)[0] + issues.append((full_match, text, url, fixed_url)) + + return issues + +def fix_links(file_path: Path, issues: List[Tuple[str, str, str, str]], dry_run: bool = True) -> bool: + """修复文件中的链接问题 + + Args: + file_path: 文件路径 + issues: 需要修复的问题列表 [(完整匹配文本, 链接文本, 原始URL, 修复后URL)] + dry_run: 如果为 True,只显示将要进行的修改,不实际修改文件 + + Returns: + bool: 是否进行了修改 + """ + if not issues: + return False + + # 读取文件内容 + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + modified_content = content + + # 遍历所有问题并修复 + for full_match, text, old_url, new_url in issues: + if "Card" in full_match: + # 修复 Card 组件链接 + old_pattern = f'href="{old_url}"' + new_pattern = f'href="{new_url}"' + modified_content = modified_content.replace(old_pattern, new_pattern) + elif " 标签链接 + old_pattern = f'href="{old_url}"' + new_pattern = f'href="{new_url}"' + modified_content = modified_content.replace(old_pattern, new_pattern) + else: + # 修复 Markdown 链接 + old_pattern = f']({old_url})' + new_pattern = f']({new_url})' + modified_content = modified_content.replace(old_pattern, new_pattern) + + # 如果内容有变化,写回文件 + if modified_content != content and not dry_run: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(modified_content) + return True + + return not dry_run and modified_content != content + +def process_file(file_path: Path, all_files: Dict[str, Path], base_dir: Path, args): + """处理单个文件中的链接问题""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + links = extract_links(content) + issues = check_link_extensions(links, file_path, all_files, base_dir) + + if issues: + rel_path = file_path.relative_to(base_dir) + print(f"\n{Colors.HEADER}{Colors.BOLD}检查文件: {rel_path}{Colors.ENDC}") + + for i, (full_match, text, old_url, new_url) in enumerate(issues, 1): + print(f" {i}. 发现问题: {Colors.WARNING}{old_url}{Colors.ENDC} -> {Colors.GREEN}{new_url}{Colors.ENDC}") + + # 询问用户是否修复 + if not args.auto_fix: + choice = input(f"\n{Colors.BOLD}修复这些问题? (y/n/a/q): {Colors.ENDC}") + if choice.lower() == 'q': # q 代表退出脚本 + log_info("用户请求退出脚本") + sys.exit(0) + elif choice.lower() == 'a': # a 代表全部修复,并设置 auto_fix 标志 + args.auto_fix = True + + if choice.lower() not in ('y', 'a'): + log_info(f"跳过修复 {rel_path}") + return False + + # 修复问题 + fixed = fix_links(file_path, issues, dry_run=args.dry_run) + + if args.dry_run: + log_info(f"已检测到 {len(issues)} 个需要修复的链接 (模拟运行,实际未修改)") + elif fixed: + log_success(f"已修复 {len(issues)} 个链接问题") + + # 如果不是自动修复模式,在每个文件处理完后暂停一下,让用户有时间查看结果 + if not args.auto_fix and fixed and not args.dry_run: + input(f"\n{Colors.BOLD}已完成修复,按回车继续下一个文件...{Colors.ENDC}") + + return fixed + + return False + + except Exception as e: + log_error(f"处理文件 {file_path} 时出错: {str(e)}") + return False + +def main(): + parser = argparse.ArgumentParser(description='检查并修复文档中的链接问题') + parser.add_argument('doc_path', nargs='?', help='文档根目录路径') + parser.add_argument('--dry-run', action='store_true', help='只显示将要修改的内容,不实际修改文件') + parser.add_argument('--auto-fix', action='store_true', help='自动修复所有问题,不询问') + args = parser.parse_args() + + # 如果命令行未提供路径,则交互式询问 + if args.doc_path is None: + doc_path = input(f"{Colors.BOLD}请输入文档根目录路径: {Colors.ENDC}") + args.doc_path = doc_path.strip() + + base_dir = Path(args.doc_path) + + if not base_dir.exists() or not base_dir.is_dir(): + log_error(f"指定的目录 '{args.doc_path}' 不存在或不是一个目录") + return 1 + + # 添加确认步骤 + print(f"\n{Colors.BOLD}将要扫描的目录:{Colors.ENDC} {Colors.GREEN}{base_dir}{Colors.ENDC}") + if args.dry_run: + print(f"{Colors.BOLD}模式:{Colors.ENDC} {Colors.BLUE}仅检查,不修改文件{Colors.ENDC}") + elif args.auto_fix: + print(f"{Colors.BOLD}模式:{Colors.ENDC} {Colors.BLUE}自动修复所有问题{Colors.ENDC}") + else: + print(f"{Colors.BOLD}模式:{Colors.ENDC} {Colors.BLUE}交互式修复{Colors.ENDC}") + + confirm = input(f"\n{Colors.BOLD}确认开始扫描? (y/n): {Colors.ENDC}") + if confirm.lower() != 'y': + log_info("操作已取消") + return 0 + + log_info(f"开始扫描目录: {base_dir}") + + # 查找所有文档文件 + all_files_list = find_all_md_files(base_dir) + log_info(f"共找到 {len(all_files_list)} 个文档文件") + + # 创建文件路径映射,用于链接验证 + all_files = {} + for file_path in all_files_list: + rel_path = file_path.relative_to(base_dir) + all_files[str(rel_path)] = file_path + + # 处理所有文件 + fixed_count = 0 + total_files = len(all_files_list) + + try: + for i, file_path in enumerate(all_files_list, 1): + # 清空当前行并显示进度 + sys.stdout.write("\r" + " " * 80) # 清空当前行 + sys.stdout.write(f"\r{Colors.BOLD}进度: {i}/{total_files} ({i/total_files*100:.1f}%){Colors.ENDC}") + sys.stdout.flush() + + # 处理文件,如果有修复则增加计数 + if process_file(file_path, all_files, base_dir, args): + fixed_count += 1 + except KeyboardInterrupt: + print("\n") + log_warning("用户中断了处理过程") + # 继续执行后面的代码,显示已完成的统计信息 + + print("\n") + log_info(f"扫描完成,共处理 {total_files} 个文件") + + if args.dry_run: + log_info(f"发现 {fixed_count} 个文件中有链接问题需要修复") + else: + log_success(f"已修复 {fixed_count} 个文件中的链接问题") + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/scripts/doc_migration_helper.py b/scripts/doc_migration_helper.py new file mode 100644 index 00000000..52684902 --- /dev/null +++ b/scripts/doc_migration_helper.py @@ -0,0 +1,588 @@ +#!/usr/bin/env python3 +""" +文档迁移助手 + +这个脚本用于辅助 gitbook 文档(dify-docs)迁移至 mintlify(dify-docs-mintlify) +主要功能包括: +1. 图片路径替换:从原始文档查找并替换为在线图片链接 +2. 文档引用路径替换:将相对路径替换为绝对路径 +3. 支持交互式确认每个修改 + +使用方法: +python doc_migration_helper.py <目标文件路径> +例如: +python doc_migration_helper.py /Users/allen/Documents/dify-docs-mintlify/zh-hans/guides/workflow/nodes/parameter-extractor.mdx +""" + +import os +import re +import sys +import json +from pathlib import Path + +# ANSI 颜色代码 +class Colors: + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + +class DocMigrationHelper: + def __init__(self, target_file, source_dir="/Users/allen/Documents/dify-docs", + mintlify_dir="/Users/allen/Documents/dify-docs-mintlify"): + """ + 初始化文档迁移助手 + + Args: + target_file: 要处理的目标文件路径 + source_dir: 源文档目录路径 + mintlify_dir: mintlify文档目录路径 + """ + self.target_file = target_file + self.source_dir = source_dir + self.mintlify_dir = mintlify_dir + + # 获取docs.json内容用于路径映射 + self.docs_config = self._load_docs_config() + + # 解析目标文件的相对路径 + self.rel_path = os.path.relpath(target_file, mintlify_dir) + + # 推断对应的源文件路径 + self.source_file = self._infer_source_file_path() + + # 图片映射缓存 + self.image_url_cache = {} + + def _load_docs_config(self): + """加载docs.json配置文件""" + try: + docs_config_path = os.path.join(self.mintlify_dir, "docs.json") + with open(docs_config_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"{Colors.RED}无法加载docs.json: {e}{Colors.ENDC}") + return {} + + def _infer_source_file_path(self): + """推断源文件路径""" + # 从mintlify路径推断原始文档中对应的路径 + parts = self.rel_path.split(os.sep) + + # 处理语言差异 (zh-hans -> zh_CN) + if parts[0] == "zh-hans": + lang_dir = "zh_CN" + elif parts[0] == "en": + lang_dir = "en_US" + else: + lang_dir = parts[0] + + # 实际目标文件名称 + target_basename = os.path.basename(self.target_file) + if target_basename.endswith(".mdx"): + target_basename = target_basename[:-4] + + # 收集可能的路径 + potential_paths = [] + + # 处理文件扩展名 (.mdx -> .md) + rest_path = os.path.join(*parts[1:]) + if rest_path.endswith(".mdx"): + rest_path = rest_path[:-4] + ".md" + + # 1. 直接对应路径 + direct_path = os.path.join(self.source_dir, lang_dir, rest_path) + potential_paths.append(direct_path) + + # 2. 处理节点路径差异 (nodes -> node) + node_path = direct_path.replace("nodes", "node") + if node_path != direct_path: + potential_paths.append(node_path) + + # 3. 可能添加了 guides 前缀 + guides_path = os.path.join(self.source_dir, lang_dir, "guides", rest_path) + if guides_path != direct_path: + potential_paths.append(guides_path) + # 也考虑 guides 和 node 的组合 + guides_node_path = guides_path.replace("nodes", "node") + if guides_node_path != guides_path: + potential_paths.append(guides_node_path) + + # 4. 如果是工作流节点文件,尝试特定目录 + if "workflow" in rest_path and "nodes" in rest_path: + workflow_node_path = os.path.join(self.source_dir, lang_dir, "guides", "workflow", "node", target_basename + ".md") + potential_paths.append(workflow_node_path) + + # 先检查所有可能的直接匹配路径 + for path in potential_paths: + if os.path.exists(path): + print(f"{Colors.GREEN}找到源文件: {path}{Colors.ENDC}") + return path + + # 如果上面的匹配都失败,尝试一些含有繁体/简体变体的目录 + if "workflow" in rest_path and "nodes" in rest_path: + # 尝试搜索node目录 + node_dir = os.path.join(self.source_dir, lang_dir, "guides", "workflow", "node") + if os.path.exists(node_dir): + # 对比文件名,考虑字符替换(如 - 和 _) + target_name_variants = [ + target_basename, + target_basename.replace("-", "_"), + target_basename.replace("_", "-") + ] + + for file in os.listdir(node_dir): + if file.endswith(".md"): + file_basename = os.path.splitext(file)[0] + # 尝试各种变体 + for variant in target_name_variants: + if file_basename == variant: + found_path = os.path.join(node_dir, file) + print(f"{Colors.GREEN}找到匹配的源文件: {found_path}{Colors.ENDC}") + return found_path + + # 如果仍然找不到,尝试搜索整个文档目录 + print(f"{Colors.YELLOW}尝试搜索整个文档目录...{Colors.ENDC}") + found_files = [] + + for root, _, files in os.walk(os.path.join(self.source_dir, lang_dir)): + for file in files: + if file.endswith(".md"): + file_basename = os.path.splitext(file)[0] + # 比较文件名的各种变体 + if (file_basename == target_basename or + file_basename == target_basename.replace("-", "_") or + file_basename == target_basename.replace("_", "-")): + found_files.append(os.path.join(root, file)) + + if found_files: + # 如果找到多个文件,选择路径最相似的 + if len(found_files) > 1: + best_match = None + best_score = -1 + current_parts = rest_path.split(os.sep) + + for file_path in found_files: + rel_path = os.path.relpath(file_path, self.source_dir) + rel_parts = rel_path.split(os.sep) + # 计算路径部分的重叠数量 + score = sum(1 for a, b in zip(current_parts, rel_parts) if a == b or a.replace("nodes", "node") == b) + if score > best_score: + best_score = score + best_match = file_path + + print(f"{Colors.GREEN}找到最匹配的源文件: {best_match}{Colors.ENDC}") + return best_match + else: + print(f"{Colors.GREEN}找到源文件: {found_files[0]}{Colors.ENDC}") + return found_files[0] + + print(f"{Colors.YELLOW}无法找到对应的源文件{Colors.ENDC}") + return None + + def get_corresponding_image_url(self, local_path): + """ + 根据本地图片路径找到对应的在线URL + + Args: + local_path: 本地图片路径,例如 /zh-cn/user-guide/.gitbook/assets/image (66).png + + Returns: + online_url: 在线图片URL + """ + # 如果已经缓存过,直接返回 + if local_path in self.image_url_cache: + return self.image_url_cache[local_path] + + # 获取本地图片文件名和图片序号 + local_img_name = os.path.basename(local_path) + img_number_match = re.search(r'\((\d+)\)', local_img_name) + img_number = img_number_match.group(1) if img_number_match else None + + # 直接尝试根据目标文件路径推断对应的源文件 + if not self.source_file: + print(f"{Colors.YELLOW}无法找到对应的源文件,尝试查找相关文件...{Colors.ENDC}") + # 尝试从目标文件名推断源文件名 + target_basename = os.path.basename(self.target_file).replace('.mdx', '') + + # 构建可能的源文件路径 + parts = self.rel_path.split(os.sep) + if parts[0] == "zh-hans": + lang_dir = "zh_CN" + elif parts[0] == "en": + lang_dir = "en_US" + else: + lang_dir = parts[0] + + # 尝试在guides/workflow/node目录下查找 + possible_source_dir = os.path.join(self.source_dir, lang_dir, "guides", "workflow", "node") + if os.path.exists(possible_source_dir): + for file in os.listdir(possible_source_dir): + if file.endswith(".md") and file.startswith(target_basename.replace("-", "_")): + self.source_file = os.path.join(possible_source_dir, file) + print(f"{Colors.GREEN}找到可能的源文件: {self.source_file}{Colors.ENDC}") + break + + # 如果找不到源文件,尝试在整个文档中搜索图片 + if not self.source_file or not os.path.exists(self.source_file): + print(f"{Colors.YELLOW}尝试在整个文档中搜索图片...{Colors.ENDC}") + # 搜索整个源目录中的所有.md文件 + all_md_files = [] + for root, _, files in os.walk(os.path.join(self.source_dir, "zh_CN")): + for file in files: + if file.endswith(".md"): + all_md_files.append(os.path.join(root, file)) + + # 在所有文件中搜索图片URL + for md_file in all_md_files: + try: + with open(md_file, 'r', encoding='utf-8') as f: + content = f.read() + + # 查找图片序号匹配 + if img_number: + # 查找包含特定序号的图片 + url_matches = re.findall(r'!\[.*?\]\((https://assets-docs\.dify\.ai/[^)]+)\)', content) + for url in url_matches: + # 如果URL包含图片名字的关键部分,可能是匹配项 + if url.endswith(".png") or url.endswith(".jpg") or url.endswith(".jpeg") or url.endswith(".gif"): + self.image_url_cache[local_path] = url + print(f"{Colors.GREEN}在文件 {md_file} 中找到可能匹配的图片URL: {url}{Colors.ENDC}") + return url + except Exception as e: + continue + + # 如果找不到,返回构造的URL + # 默认路径构造 + if parts[0] == "zh-hans": + constructed_url = f"https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/workflow/node/{img_number}.png" + print(f"{Colors.YELLOW}未找到匹配图片,构造URL: {constructed_url}{Colors.ENDC}") + return constructed_url + return None + + # 读取源文件内容 + try: + with open(self.source_file, 'r', encoding='utf-8') as f: + source_content = f.read() + + # 在源文件中查找图片链接 + online_urls = re.findall(r'!\[.*?\]\((https://assets-docs\.dify\.ai/[^)]+)\)', source_content) + + # 首先尝试基于图片序号匹配 + if img_number: + for url in online_urls: + # 检查URL是否包含相同序号或相似模式 + if f"{img_number}.png" in url or f"{img_number})" in url: + self.image_url_cache[local_path] = url + return url + + # 然后尝试文件名匹配 + for url in online_urls: + url_basename = os.path.basename(url) + # 精确匹配 + if url_basename == local_img_name: + self.image_url_cache[local_path] = url + return url + + # 尝试模糊匹配(移除数字和特殊字符后比较) + clean_local = re.sub(r'[^a-zA-Z]', '', local_img_name) + clean_url = re.sub(r'[^a-zA-Z]', '', url_basename) + + if clean_local and clean_url and clean_local == clean_url: + self.image_url_cache[local_path] = url + return url + + # 查找Frame组件中的图片 + frame_urls = re.findall(r']*>.*?]*src="(https://assets-docs\.dify\.ai/[^"]+)".*?', + source_content, re.DOTALL) + + for url in frame_urls: + url_basename = os.path.basename(url) + if url_basename == local_img_name or re.sub(r'[^a-zA-Z]', '', url_basename) == re.sub(r'[^a-zA-Z]', '', local_img_name): + self.image_url_cache[local_path] = url + return url + + # 如果在源文件中找不到匹配的URL,尝试在相关文件中查找 + related_files = [] + source_dir = os.path.dirname(self.source_file) + for file in os.listdir(source_dir): + if file.endswith(".md") and file != os.path.basename(self.source_file): + related_files.append(os.path.join(source_dir, file)) + + for related_file in related_files: + try: + with open(related_file, 'r', encoding='utf-8') as f: + related_content = f.read() + + related_urls = re.findall(r'!\[.*?\]\((https://assets-docs\.dify\.ai/[^)]+)\)', related_content) + for url in related_urls: + if img_number and (f"{img_number}.png" in url or f"{img_number})" in url): + self.image_url_cache[local_path] = url + print(f"{Colors.GREEN}在相关文件 {related_file} 中找到匹配图片: {url}{Colors.ENDC}") + return url + except Exception as e: + continue + + # 最后尝试根据目录结构构造URL + relative_source_path = os.path.relpath(self.source_file, self.source_dir) + dir_parts = os.path.dirname(relative_source_path).split(os.sep) + + if img_number and len(dir_parts) >= 2: + # 使用目录结构构造可能的URL + if dir_parts[0] == "zh_CN": + constructed_url = f"https://assets-docs.dify.ai/dify-enterprise-mintlify/{dir_parts[0]}/{'/'.join(dir_parts[1:])}/{img_number}.png" + print(f"{Colors.YELLOW}未找到匹配图片,构造URL: {constructed_url}{Colors.ENDC}") + return constructed_url + + return None + + except Exception as e: + print(f"{Colors.RED}读取源文件时出错: {e}{Colors.ENDC}") + return None + + def get_absolute_doc_path(self, relative_path): + """ + 将相对文档路径转换为绝对路径 + + Args: + relative_path: 相对路径,例如 ./iteration.md 或 http-request.md + + Returns: + absolute_path: 绝对路径,例如 /zh-hans/guides/workflow/nodes/iteration + """ + # 如果已经是绝对路径,直接返回 + if relative_path.startswith('/'): + return relative_path + + # 如果是外部链接,直接返回 + if relative_path.startswith(('http://', 'https://')): + return relative_path + + # 提取锁点信息(如果有的话) + fragment = "" + if '#' in relative_path: + relative_path, fragment = relative_path.split('#', 1) + fragment = f'#{fragment}' + + # 移除.md或.mdx扩展名 + if relative_path.endswith(('.md', '.mdx')): + extension = '.md' if relative_path.endswith('.md') else '.mdx' + relative_path = relative_path[:-len(extension)] + + # 获取当前文件的语言前缀(例如 zh-hans) + lang_prefix = self.rel_path.split(os.sep)[0] + + # 处理相对路径 + current_dir = os.path.dirname(self.rel_path) + current_dir_parts = current_dir.split(os.sep) + + # 根据不同类型的相对路径进行处理 + if relative_path.startswith('./'): + # ./file.md 形式 + relative_path = relative_path[2:] + full_path = os.path.normpath(os.path.join(current_dir, relative_path)) + elif relative_path.startswith('../'): + # ../file.md 形式 + full_path = os.path.normpath(os.path.join(current_dir, relative_path)) + else: + # 简单名称 file.md 形式 + # 首先检查是否在同一目录下 + basename = os.path.basename(relative_path) + same_level_path = os.path.normpath(os.path.join(current_dir, basename)) + + # 检查实际文件是否存在 + if os.path.exists(os.path.join(self.mintlify_dir, same_level_path + '.mdx')): + full_path = same_level_path + else: + # 如果是节点文件,通常在 /nodes/ 目录下 + # 查找是否在当前语言的 workflow/nodes 目录下 + if "workflow" in current_dir and ("node" in current_dir or "nodes" in current_dir): + # 构造可能的节点路径 + possible_path = f"{lang_prefix}/guides/workflow/nodes/{basename}" + if os.path.exists(os.path.join(self.mintlify_dir, possible_path + '.mdx')): + full_path = possible_path + else: + # 如果不存在,使用默认的同级目录路径 + full_path = same_level_path + print(f"{Colors.YELLOW}警告: 无法找到文件 {possible_path}.mdx,使用默认路径{Colors.ENDC}") + else: + # 尝试搜索整个 mintlify 目录 + matches = [] + for root, _, files in os.walk(os.path.join(self.mintlify_dir, lang_prefix)): + for file in files: + if file == f"{basename}.mdx" or file == f"{basename}.md": + rel_file_path = os.path.relpath(os.path.join(root, file), self.mintlify_dir) + # 移除扩展名 + rel_file_path = os.path.splitext(rel_file_path)[0] + matches.append(rel_file_path) + + if matches: + # 如果找到多个匹配,选择与当前目录最相似的 + if len(matches) > 1: + best_match = None + best_score = -1 + + for match in matches: + match_parts = match.split(os.sep) + # 计算路径部分的重叠数量 + score = sum(1 for a, b in zip(current_dir_parts, match_parts[1:]) if a == b) + if score > best_score: + best_score = score + best_match = match + + full_path = best_match + else: + full_path = matches[0] + else: + # 如果找不到匹配的文件,使用默认的同级目录路径 + full_path = same_level_path + print(f"{Colors.YELLOW}警告: 无法找到文件 {basename}.mdx,使用默认路径{Colors.ENDC}") + + # 确保路径以 / 开头 + if not full_path.startswith('/'): + full_path = '/' + full_path + + # 添加锁点(如果有的话) + return full_path + fragment + + def process_file(self): + """处理文件,替换图片路径和文档引用路径""" + try: + # 读取目标文件内容 + with open(self.target_file, 'r', encoding='utf-8') as f: + content = f.read() + + # 存储修改项 + changes = [] + + # 1. 查找并替换Markdown格式图片 + # ![alt text](/zh-cn/user-guide/.gitbook/assets/image.png) + md_img_pattern = re.compile(r'!\[([^\]]*)\]\((/[^)]+)\)') + for match in md_img_pattern.finditer(content): + alt_text = match.group(1) + local_path = match.group(2) + full_match = match.group(0) + + # 获取对应的在线URL + online_url = self.get_corresponding_image_url(local_path) + if online_url: + new_text = f'![{alt_text}]({online_url})' + changes.append((full_match, new_text, '图片链接')) + + # 2. 查找并替换Frame组件中的图片 + frame_img_pattern = re.compile(r'(]*>[\s\S]*?]*src=")(/[^"]+)("[^>]*>[\s\S]*?)') + for match in frame_img_pattern.finditer(content): + prefix = match.group(1) + local_path = match.group(2) + suffix = match.group(3) + full_match = match.group(0) + + # 获取对应的在线URL + online_url = self.get_corresponding_image_url(local_path) + if online_url: + new_text = f'{prefix}{online_url}{suffix}' + changes.append((full_match, new_text, 'Frame组件图片')) + + # 3. 查找并替换文档引用链接 + # [link text](./path/to/file.md) 或 [link text](path/to/file.md) + doc_link_pattern = re.compile(r'\[([^\]]+)\]\((\./[^)]+\.md(?:#[^)]*)?|\.\./[^)]+\.md(?:#[^)]*)?|[^)]+\.md(?:#[^)]*)?)\)') + for match in doc_link_pattern.finditer(content): + link_text = match.group(1) + rel_path = match.group(2) + full_match = match.group(0) + + # 检查是否包含锚点 + fragment = "" + if '#' in rel_path: + rel_path, fragment = rel_path.split('#', 1) + fragment = f'#{fragment}' + + # 获取绝对路径 + abs_path = self.get_absolute_doc_path(rel_path) + if abs_path: + new_text = f'[{link_text}]({abs_path}{fragment})' + changes.append((full_match, new_text, '文档链接')) + + # 如果没有需要修改的内容 + if not changes: + print(f"{Colors.GREEN}文件不需要修改{Colors.ENDC}") + return True + + # 显示找到的修改项 + print(f"\n{Colors.BLUE}找到 {len(changes)} 个需要修改的内容:{Colors.ENDC}") + for i, (old, new, change_type) in enumerate(changes): + print(f"{Colors.CYAN}修改 {i+1} ({change_type}):{Colors.ENDC}") + print(f" - 原始内容: {Colors.YELLOW}{old[:100]}{'...' if len(old) > 100 else ''}{Colors.ENDC}") + print(f" - 新内容: {Colors.GREEN}{new[:100]}{'...' if len(new) > 100 else ''}{Colors.ENDC}") + print() + + # 询问是否执行修改 + selected_changes = [] + response = input(f"{Colors.BOLD}是否应用这些修改? (y/n/部分修改输入数字如1,3,5): {Colors.ENDC}") + + if response.lower() == 'n': + print(f"{Colors.BLUE}已取消修改{Colors.ENDC}") + return False + elif response.lower() == 'y': + selected_changes = changes + else: + try: + # 解析用户选择的修改索引 + indices = [int(i.strip()) - 1 for i in response.split(',')] + selected_changes = [changes[i] for i in indices if 0 <= i < len(changes)] + if not selected_changes: + print(f"{Colors.YELLOW}未选择任何有效修改,操作取消{Colors.ENDC}") + return False + except: + print(f"{Colors.YELLOW}输入格式有误,操作取消{Colors.ENDC}") + return False + + # 应用修改 + modified_content = content + for old, new, _ in selected_changes: + modified_content = modified_content.replace(old, new) + + # 写入文件 + with open(self.target_file, 'w', encoding='utf-8') as f: + f.write(modified_content) + + print(f"{Colors.GREEN}成功应用 {len(selected_changes)} 个修改到文件{Colors.ENDC}") + return True + + except Exception as e: + print(f"{Colors.RED}处理文件时出错: {e}{Colors.ENDC}") + import traceback + traceback.print_exc() + return False + +def main(): + """主函数""" + # 检查命令行参数 + if len(sys.argv) != 2: + print(f"用法: {sys.argv[0]} <目标文件路径>") + print(f"例如: {sys.argv[0]} /Users/allen/Documents/dify-docs-mintlify/zh-hans/guides/workflow/nodes/parameter-extractor.mdx") + return + + # 获取目标文件路径 + target_file = sys.argv[1] + + # 检查文件是否存在 + if not os.path.isfile(target_file): + print(f"{Colors.RED}文件不存在: {target_file}{Colors.ENDC}") + return + + # 初始化并处理文件 + helper = DocMigrationHelper(target_file) + + print(f"{Colors.HEADER}开始处理文件: {target_file}{Colors.ENDC}") + print(f"对应的源文件: {helper.source_file or '未找到'}") + + helper.process_file() + +if __name__ == "__main__": + main() diff --git a/scripts/fix_links_interactive-3.25-backup.py b/scripts/fix_links_interactive-3.25-backup.py new file mode 100644 index 00000000..69a8eb63 --- /dev/null +++ b/scripts/fix_links_interactive-3.25-backup.py @@ -0,0 +1,365 @@ +#!/usr/bin/env python3 +""" +交互式Markdown链接修复工具 + +这个脚本用于交互式地修复Markdown文件中的相对路径引用,将它们转换为 +从根目录开始的绝对路径格式(如 /zh-hans/xxx),以符合Mintlify文档要求。 +脚本支持处理单个文件或指定目录内的所有.mdx文件。 + +特点: +- 交互式操作,精确可控 +- 提供修改预览 +- 支持单文件或目录处理 +- 将相对路径转换为绝对路径 +- 支持锚点保留 +- 移除文件扩展名 +""" + +import os +import re +import sys +from pathlib import Path +import glob + +# 正则表达式来匹配Markdown链接引用,支持.md和.mdx文件 +MD_LINK_PATTERN = re.compile(r'\[([^\]]+)\]\(([^)]+\.(md|mdx))(?:#([^)]*))?(\))') +REL_LINK_PATTERN = re.compile(r'\[([^\]]+)\]\(([^)/][^)]+)(?:#([^)]*))?(\))') # 匹配不以/开头的相对路径 + +# 颜色代码,用于美化终端输出 +class Colors: + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + + +def find_file_in_project(root_dir, rel_path, current_file_dir): + """ + 根据相对路径在项目中查找实际文件 + + Args: + root_dir: 项目根目录 + rel_path: 相对路径引用 + current_file_dir: 当前文件所在目录 + + Returns: + 找到的文件绝对路径,或None如果未找到 + """ + # 移除扩展名,稍后会添加回.mdx + if rel_path.endswith(('.md', '.mdx')): + extension = '.md' if rel_path.endswith('.md') else '.mdx' + rel_path = rel_path[:-len(extension)] + + # 如果是以../或./开头的相对路径 + if rel_path.startswith(('./','../')): + # 计算实际路径 + actual_path = os.path.normpath(os.path.join(current_file_dir, rel_path)) + + # 尝试匹配.mdx文件 + matches = glob.glob(f"{actual_path}.mdx") + if matches: + return matches[0] + + # 尝试匹配.md文件 + matches = glob.glob(f"{actual_path}.md") + if matches: + return matches[0] + + # 尝试在项目中搜索匹配的文件名 + basename = os.path.basename(rel_path) + # 搜索所有.mdx文件 + mdx_matches = [] + md_matches = [] + + for root, _, files in os.walk(root_dir): + for file in files: + if file.endswith('.mdx') and os.path.splitext(file)[0] == basename: + mdx_matches.append(os.path.join(root, file)) + elif file.endswith('.md') and os.path.splitext(file)[0] == basename: + md_matches.append(os.path.join(root, file)) + + # 优先使用.mdx文件 + if mdx_matches: + return mdx_matches[0] + elif md_matches: + return md_matches[0] + + return None + +def get_absolute_path(file_path, root_dir): + """ + 获取相对于项目根目录的绝对路径 + + Args: + file_path: 文件的完整路径 + root_dir: 项目根目录 + + Returns: + /zh-hans/xxx 格式的绝对路径 + """ + # 获取相对于根目录的路径 + rel_path = os.path.relpath(file_path, root_dir) + # 移除扩展名 + rel_path = os.path.splitext(rel_path)[0] + # 添加前导斜杠 + abs_path = f"/{rel_path}" + + return abs_path + +def process_file(file_path, root_dir, dry_run=False, auto_confirm=False): + """ + 处理单个Markdown文件中的链接引用 + + Args: + file_path: 要处理的文件路径 + root_dir: 项目根目录 + dry_run: 是否只预览修改,不实际写入 + auto_confirm: 是否自动确认所有修改 + + Returns: + 修改的链接数量 + """ + print(f"\n{Colors.HEADER}处理文件:{Colors.ENDC} {file_path}") + + # 获取文件内容 + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + except Exception as e: + print(f"{Colors.FAIL}错误: 无法读取文件 - {e}{Colors.ENDC}") + return 0 + + # 当前文件所在目录 + current_file_dir = os.path.dirname(file_path) + + # 存储所有要修改的内容 + changes = [] + + # 查找带有.md或.mdx后缀的链接 + for m in MD_LINK_PATTERN.finditer(content): + link_text = m.group(1) + link_path = m.group(2) + fragment = m.group(4) or "" # 锚点可能不存在 + full_match = m.group(0) + + # 跳过外部链接 + if link_path.startswith(('http://', 'https://', 'mailto:', 'ftp://')): + continue + + # 查找实际文件 + actual_file = find_file_in_project(root_dir, link_path, current_file_dir) + if actual_file: + # 转换为绝对路径 + abs_path = get_absolute_path(actual_file, root_dir) + fragment_text = f"#{fragment}" if fragment else "" + new_link = f"[{link_text}]({abs_path}{fragment_text})" + changes.append((full_match, new_link, actual_file)) + + # 查找其他相对路径链接(不带.md或.mdx后缀) + for m in REL_LINK_PATTERN.finditer(content): + link_text = m.group(1) + link_path = m.group(2) + fragment = m.group(3) or "" # 锚点可能不存在 + full_match = m.group(0) + + # 跳过已经是绝对路径的链接 + if link_path.startswith('/'): + continue + + # 跳过外部链接 + if link_path.startswith(('http://', 'https://', 'mailto:', 'ftp://')): + continue + + # 查找实际文件 + actual_file = find_file_in_project(root_dir, link_path, current_file_dir) + if actual_file: + # 转换为绝对路径 + abs_path = get_absolute_path(actual_file, root_dir) + fragment_text = f"#{fragment}" if fragment else "" + new_link = f"[{link_text}]({abs_path}{fragment_text})" + changes.append((full_match, new_link, actual_file)) + + # 如果没有找到需要修改的链接 + if not changes: + print(f"{Colors.GREEN}没有找到需要修改的链接{Colors.ENDC}") + return 0 + + # 显示找到的修改 + print(f"\n{Colors.BLUE}找到 {len(changes)} 个需要修改的链接:{Colors.ENDC}") + for i, (old, new, target) in enumerate(changes): + print(f"{Colors.CYAN}修改 {i+1}:{Colors.ENDC}") + print(f" - 原始链接: {Colors.WARNING}{old}{Colors.ENDC}") + print(f" - 新链接: {Colors.GREEN}{new}{Colors.ENDC}") + print(f" - 目标文件: {os.path.relpath(target, root_dir)}\n") + + # 如果是预览模式,返回 + if dry_run: + print(f"{Colors.BLUE}预览模式 - 未执行实际修改{Colors.ENDC}") + return len(changes) + + # 确认修改 + if not auto_confirm: + response = input(f"{Colors.BOLD}是否应用这些修改? (y/n/部分修改输入数字如1,3,5): {Colors.ENDC}") + + if response.lower() == 'n': + print(f"{Colors.BLUE}已取消修改{Colors.ENDC}") + return 0 + elif response.lower() == 'y': + selected_changes = changes + else: + try: + # 解析用户选择的修改索引 + indices = [int(i.strip()) - 1 for i in response.split(',')] + selected_changes = [changes[i] for i in indices if 0 <= i < len(changes)] + if not selected_changes: + print(f"{Colors.WARNING}未选择任何有效修改,操作取消{Colors.ENDC}") + return 0 + except: + print(f"{Colors.WARNING}输入格式有误,操作取消{Colors.ENDC}") + return 0 + else: + selected_changes = changes + + # 应用修改 + modified_content = content + for old, new, _ in selected_changes: + modified_content = modified_content.replace(old, new) + + # 写入文件 + try: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(modified_content) + print(f"{Colors.GREEN}成功应用 {len(selected_changes)} 个修改到文件{Colors.ENDC}") + return len(selected_changes) + except Exception as e: + print(f"{Colors.FAIL}错误: 无法写入文件 - {e}{Colors.ENDC}") + return 0 + +def scan_directory(dir_path, root_dir, dry_run=False, auto_confirm=False): + """ + 扫描目录中的所有.mdx文件 + + Args: + dir_path: 要扫描的目录路径 + root_dir: 项目根目录 + dry_run: 是否只预览修改 + auto_confirm: 是否自动确认所有修改 + + Returns: + 处理的文件数量,修改的链接总数 + """ + file_count = 0 + total_changes = 0 + + print(f"{Colors.HEADER}扫描目录: {dir_path}{Colors.ENDC}") + + # 获取所有.mdx文件 + mdx_files = [] + for root, _, files in os.walk(dir_path): + for file in files: + if file.endswith('.mdx'): + mdx_files.append(os.path.join(root, file)) + + if not mdx_files: + print(f"{Colors.WARNING}在目录中未找到.mdx文件{Colors.ENDC}") + return 0, 0 + + print(f"{Colors.BLUE}找到 {len(mdx_files)} 个.mdx文件{Colors.ENDC}") + + # 处理每个文件 + for file_path in mdx_files: + # 显示文件的相对路径 + rel_path = os.path.relpath(file_path, root_dir) + print(f"\n{Colors.BOLD}处理文件 ({file_count+1}/{len(mdx_files)}): {rel_path}{Colors.ENDC}") + + # 询问是否处理此文件 + if not auto_confirm: + response = input(f"{Colors.BOLD}是否处理此文件? (y/n/q-退出): {Colors.ENDC}") + if response.lower() == 'n': + print(f"{Colors.BLUE}跳过此文件{Colors.ENDC}") + continue + elif response.lower() == 'q': + print(f"{Colors.BLUE}退出处理{Colors.ENDC}") + break + + # 处理文件 + changes = process_file(file_path, root_dir, dry_run, auto_confirm) + if changes > 0: + file_count += 1 + total_changes += changes + + return file_count, total_changes + +def main(): + """主程序入口""" + # 确定项目根目录 + script_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.dirname(script_dir) # 脚本在scripts目录下,上一级是项目根目录 + + # 显示欢迎信息 + print(f"\n{Colors.HEADER}{'='*60}{Colors.ENDC}") + print(f"{Colors.HEADER} Mintlify文档链接修复工具 {Colors.ENDC}") + print(f"{Colors.HEADER}{'='*60}{Colors.ENDC}") + print(f"项目根目录: {project_root}\n") + + # 交互式菜单 + while True: + print(f"\n{Colors.BOLD}请选择操作模式:{Colors.ENDC}") + print("1. 处理单个文件") + print("2. 处理指定目录中的所有.mdx文件") + print("3. 退出") + + choice = input(f"{Colors.BOLD}请输入选项 (1-3): {Colors.ENDC}") + + if choice == '1': + # 处理单个文件 + file_path = input(f"{Colors.BOLD}请输入文件路径 (相对于项目根目录): {Colors.ENDC}") + file_path = os.path.join(project_root, file_path) + + if not os.path.isfile(file_path): + print(f"{Colors.FAIL}错误: 文件不存在{Colors.ENDC}") + continue + + # 询问是否只预览修改 + dry_run = input(f"{Colors.BOLD}是否只预览修改而不实际写入? (y/n): {Colors.ENDC}").lower() == 'y' + + # 处理文件 + changes = process_file(file_path, project_root, dry_run) + + print(f"\n{Colors.GREEN}处理完成! 共发现 {changes} 个需要修改的链接{Colors.ENDC}") + + elif choice == '2': + # 处理目录 + dir_path = input(f"{Colors.BOLD}请输入目录路径 (相对于项目根目录): {Colors.ENDC}") + dir_path = os.path.join(project_root, dir_path) + + if not os.path.isdir(dir_path): + print(f"{Colors.FAIL}错误: 目录不存在{Colors.ENDC}") + continue + + # 询问是否只预览修改 + dry_run = input(f"{Colors.BOLD}是否只预览修改而不实际写入? (y/n): {Colors.ENDC}").lower() == 'y' + + # 询问是否自动确认所有修改 + auto_confirm = input(f"{Colors.BOLD}是否自动确认所有修改? (y/n): {Colors.ENDC}").lower() == 'y' + + # 处理目录 + file_count, total_changes = scan_directory(dir_path, project_root, dry_run, auto_confirm) + + print(f"\n{Colors.GREEN}处理完成! 共处理 {file_count} 个文件,修改了 {total_changes} 个链接{Colors.ENDC}") + + elif choice == '3': + # 退出 + print(f"{Colors.BLUE}感谢使用,再见!{Colors.ENDC}") + break + + else: + print(f"{Colors.WARNING}无效选项,请重试{Colors.ENDC}") + +if __name__ == "__main__": + main() diff --git a/scripts/sync_image_links.py b/scripts/sync_image_links.py index ed623a7a..ad3ed388 100644 --- a/scripts/sync_image_links.py +++ b/scripts/sync_image_links.py @@ -37,7 +37,7 @@ class Colors: # 1. Markdown格式: ![alt text](https://assets-docs.dify.ai/...) # 2. HTML格式: ... # 3. Frame标签中的图片: ...... -# 4. 相对路径图片: ![alt](/zh-cn/user-guide/.gitbook/assets/...) +# 4. 相对路径图片: ![alt](/zh-cn/img/...) # Markdown格式图片 MD_IMG_PATTERN = re.compile(r'!\[(.*?)\]\((https?://[^)]+|/[^)]+)\)') @@ -49,7 +49,7 @@ HTML_IMG_PATTERN = re.compile(r']*>') ASSETS_URL_PREFIX = 'https://assets-docs.dify.ai/' # 相对路径特征 -RELATIVE_PATH_PREFIX = '/zh-cn/user-guide/.gitbook/assets/' +RELATIVE_PATH_PREFIX = '/zh-cn/' def find_corresponding_file(source_file: str, source_dir: str, target_dir: str) -> Optional[str]: """查找源文件在目标目录中的对应文件""" @@ -120,6 +120,18 @@ def extract_image_links(content: str) -> List[Tuple[str, str, str]]: return images +def generate_markdown_replacement(old_match: str, old_url: str, new_url: str) -> str: + """ + 生成Markdown图片标签的替换内容 + """ + return old_match.replace(old_url, new_url) + +def generate_html_replacement(old_match: str, old_url: str, new_url: str) -> str: + """ + 生成HTML图片标签的替换内容 + """ + return old_match.replace(f'src="{old_url}"', f'src="{new_url}"') + def generate_frame_replacement(old_content: str, new_image_url: str) -> str: """ 生成Frame标签的替换内容 @@ -171,8 +183,16 @@ def sync_image_links(source_file: str, target_file: str, dry_run: bool = False) online_images = [(match, alt, url) for match, alt, url in source_images if url.startswith(ASSETS_URL_PREFIX)] if not online_images: + print(f"{Colors.WARNING}警告: 源文件中没有找到在线图片链接{Colors.ENDC}") return 0, [] + # 提取目标文件中的图片链接 + target_images = extract_image_links(target_content) + relative_images = [(match, alt, url) for match, alt, url in target_images if url.startswith(RELATIVE_PATH_PREFIX)] + + if not relative_images: + print(f"{Colors.BLUE}目标文件中没有找到相对路径图片链接{Colors.ENDC}") + # 处理目标文件中的内容 new_content = target_content modified_links = [] @@ -183,7 +203,7 @@ def sync_image_links(source_file: str, target_file: str, dry_run: bool = False) # 处理每个在线图片链接 for _, _, image_url in online_images: - # 查找目标文件中可能的相对路径格式 + # 1. 首先处理Frame标签中的图片 for match in frame_matches: frame_text = match.group(0) old_url = match.group(1) @@ -197,6 +217,25 @@ def sync_image_links(source_file: str, target_file: str, dry_run: bool = False) new_frame = generate_frame_replacement(frame_text, image_url) new_content = new_content.replace(frame_text, new_frame) modified_links.append((old_url, image_url)) + + # 2. 处理Markdown格式的图片 + for match, alt, url in relative_images: + # 跳过已经是在线链接的图片 + if url.startswith(ASSETS_URL_PREFIX): + continue + + # 如果是相对路径,替换为在线链接 + if url.startswith('/'): + if "!(" in match or "![" in match: + # Markdown格式 + new_md = generate_markdown_replacement(match, url, image_url) + new_content = new_content.replace(match, new_md) + else: + # HTML格式 + new_html = generate_html_replacement(match, url, image_url) + new_content = new_content.replace(match, new_html) + + modified_links.append((url, image_url)) # 如果是预览模式,不写入修改 if dry_run: @@ -225,6 +264,11 @@ def process_file(source_file: str, source_dir: str, target_dir: str, dry_run: bo """ print(f"{Colors.HEADER}处理文件: {source_file}{Colors.ENDC}") + # 检查源文件是否存在 + if not os.path.isfile(source_file): + print(f"{Colors.FAIL}错误: 源文件不存在: {source_file}{Colors.ENDC}") + return False, 0 + # 查找对应文件 target_file = find_corresponding_file(source_file, source_dir, target_dir) @@ -334,8 +378,11 @@ def main(): if choice == '1': # 处理单个文件 - source_file = input(f"{Colors.BOLD}请输入源文件路径 (相对于源目录): {Colors.ENDC}") - source_file = os.path.join(default_source_dir, source_file) + source_file = input(f"{Colors.BOLD}请输入源文件路径: {Colors.ENDC}") + + # 如果用户输入的是相对路径,则转换为绝对路径 + if not os.path.isabs(source_file): + source_file = os.path.join(default_source_dir, source_file) if not os.path.isfile(source_file): print(f"{Colors.FAIL}错误: 文件不存在: {source_file}{Colors.ENDC}") @@ -356,8 +403,11 @@ def main(): elif choice == '2': # 处理目录 - dir_path = input(f"{Colors.BOLD}请输入要处理的源目录路径 (相对于源目录): {Colors.ENDC}") - dir_path = os.path.join(default_source_dir, dir_path) + dir_path = input(f"{Colors.BOLD}请输入要处理的源目录路径: {Colors.ENDC}") + + # 如果用户输入的是相对路径,则转换为绝对路径 + if not os.path.isabs(dir_path): + dir_path = os.path.join(default_source_dir, dir_path) if not os.path.isdir(dir_path): print(f"{Colors.FAIL}错误: 目录不存在: {dir_path}{Colors.ENDC}") @@ -386,4 +436,4 @@ def main(): print(f"{Colors.WARNING}无效选项,请重试{Colors.ENDC}") if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/scripts/test_conversion.py b/scripts/test_conversion.py new file mode 100644 index 00000000..d0849095 --- /dev/null +++ b/scripts/test_conversion.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +测试脚本,用于测试图片格式转换 +""" + +import re +from typing import Tuple, List + +# 匹配Frame标签中的图片 +FRAME_IMG_PATTERN = re.compile( + r'\s*' + r'|\/ >|>\s*<\/img>)\s*' + r'<\/Frame>', + re.DOTALL +) + +def convert_frame_to_markdown(content: str) -> Tuple[str, List[Tuple[str, str, str]]]: + """ + 将Frame标签中的图片转换为Markdown或HTML格式 + + Args: + content: 文件内容 + + Returns: + Tuple[转换后的内容, 替换记录列表] + """ + replacements = [] + + def replace_frame(match): + caption = match.group(1) or "" + width = match.group(2) # 可能为None + src = match.group(3) + alt = match.group(4) or caption or "" + + # 原始内容 + original = match.group(0) + + # 转换格式 + if width: + # 带宽度的转为HTML格式 + new_format = "HTML" + markdown = f"""""" + else: + # 不带宽度的转为Markdown格式 + new_format = "Markdown" + markdown = f"![{alt}]({src})" + + # 记录替换 + replacements.append((original, markdown, new_format)) + + return markdown + + # 执行替换 + new_content = FRAME_IMG_PATTERN.sub(replace_frame, content) + + return new_content, replacements + +# 测试 +test_file = "/Users/allen/Documents/dify-docs-mintlify/zh-hans/guides/workflow/nodes/ifelse.mdx" + +# 读取文件 +with open(test_file, 'r', encoding='utf-8') as f: + content = f.read() + +# 测试匹配 +matches = FRAME_IMG_PATTERN.findall(content) +print(f"找到 {len(matches)} 个匹配") + +# 打印匹配详情 +for i, match in enumerate(matches): + caption, width, src, alt = match + print(f"Match {i+1}:") + print(f" caption: '{caption}'") + print(f" width: '{width}'") + print(f" src: '{src}'") + print(f" alt: '{alt}'") + +# 测试转换 +new_content, replacements = convert_frame_to_markdown(content) + +# 打印替换详情 +print(f"\n找到 {len(replacements)} 个需要替换的内容") +for i, (original, new, format_type) in enumerate(replacements): + print(f"替换 {i+1} ({format_type}):") + print(f"原始: {original[:100]}...") + print(f"新的: {new}") + print() + +# 如果找到替换内容,则写入文件 +if replacements: + print("替换后的内容示例:") + # 显示部分替换后的内容 + for line in new_content.split('\n')[:20]: + print(line) diff --git a/tools/check_specific_file.py b/tools/check_specific_file.py new file mode 100644 index 00000000..dce16e30 --- /dev/null +++ b/tools/check_specific_file.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +针对特定文档的链接检查工具 +""" + +import os +import re +import json +from pathlib import Path +from urllib.parse import urlparse + +def is_external_link(link): + """检查是否为外部链接""" + parsed = urlparse(link) + return bool(parsed.netloc) or link.startswith('http') + +def load_valid_paths(docs_json_path): + """从docs.json加载有效路径信息""" + valid_paths = set() + external_links = set() + + with open(docs_json_path, 'r', encoding='utf-8') as f: + docs_data = json.load(f) + + def process_pages(items, prefix=""): + """递归处理docs.json中的页面层级结构""" + for item in items: + if isinstance(item, dict): + # 如果是字符串URL (用于外部链接) + if isinstance(item.get('pages'), str): + if item['pages'].startswith('http'): + external_links.add(item['pages']) + else: + valid_paths.add(item['pages']) + + # 如果是字典或列表类型的pages + if 'pages' in item and isinstance(item['pages'], (list, dict)): + process_pages(item['pages'], prefix) + + # 处理group情况下的pages + if 'group' in item and 'pages' in item: + process_pages(item['pages'], prefix) + elif isinstance(item, str): + # 直接是文档路径 + if item.startswith('http'): + external_links.add(item) + else: + valid_paths.add(item) + + # 处理navigation部分 + if 'navigation' in docs_data and 'languages' in docs_data['navigation']: + for lang in docs_data['navigation']['languages']: + if 'tabs' in lang: + for tab in lang['tabs']: + if 'groups' in tab: + for group in tab['groups']: + if 'pages' in group: + process_pages(group['pages']) + + print(f"从docs.json中加载了 {len(valid_paths)} 个有效文档路径") + return valid_paths, external_links + +def extract_links_from_file(file_path): + """从文件中提取链接""" + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + links = [] + + # 提取Markdown链接 [text](url) + md_links = re.findall(r'\[.+?\]\((.+?)\)', content) + links.extend(md_links) + + # 提取HTML链接 href="url" + html_links = re.findall(r'href=[\'"](.+?)[\'"]', content) + links.extend(html_links) + + # 提取Card组件链接 href="url" + card_links = re.findall(r' + 在最佳实践中,API 密钥应通过后端调用,而不是直接以明文暴露在前端代码或请求中,这样可以防止你的应用被滥用或攻击。 - + -你可以为一个应用**创建多个访问凭据**,以实现交付给不同的用户或开发者。这意味着 API 的使用者虽然使用了应用开发者提供的 AI 能力,但背后的 Promp 工程、数据集和工具能力是经封装的。 +你可以为一个应用**创建多个访问凭据**,以实现交付给不同的用户或开发者。这意味着 API 的使用者虽然使用了应用开发者提供的 AI 能力,但背后的 Promp 工程、知识库和工具能力是经封装的。 #### 文本生成型应用 @@ -36,7 +35,7 @@ Dify 基于“**后端即服务**”理念为所有应用提供了 API,为 AI 例如,创建文本补全信息的 API 的调用示例: -``` +```bash title="cURL" curl --location --request POST 'https://api.dify.ai/v1/completion-messages' \ --header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \ --header 'Content-Type: application/json' \ @@ -47,7 +46,7 @@ curl --location --request POST 'https://api.dify.ai/v1/completion-messages' \ }' ``` -```python +```python title="Python" import requests import json @@ -77,13 +76,13 @@ print(response.text) - **生成 `conversation_id`:** 开始新对话时,请将 `conversation_id` 字段留空。系统将生成并返回一个新的 `conversation_id`,未来的交互中会使用该 `conversation_id` 继续对话。 - **处理现有会话中的 `conversation_id`:** 生成 `conversation_id` 后,对 API 的未来调用应包含此 `conversation_id`,以确保与 Dify 机器人的对话连续性。传递上一个 `conversation_id` 时,将忽略任何新的 `inputs`,仅处理正在进行的对话的 `query`。 -- **管理动态变量:** 如果在会话期间需要修改逻辑或变量,您可以使用会话变量(特定于会话的变量)来调整bot的行为或回应。 +- **管理动态变量:** 如果在会话期间需要修改逻辑或变量,你可以使用会话变量(特定于会话的变量)来调整 bot 的行为或回应。 你可以在**应用 -> 访问 API** 中找到该应用的 API 文档与范例请求。 例如,发送对话信息的 `chat-messages` API的调用示例: -``` +```bash title="cURL" curl --location --request POST 'https://api.dify.ai/v1/chat-messages' \ --header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \ --header 'Content-Type: application/json' \ @@ -98,7 +97,7 @@ curl --location --request POST 'https://api.dify.ai/v1/chat-messages' \ ``` -```python +```python title="Python" import requests import json @@ -117,5 +116,5 @@ data = { response = requests.post(url, headers=headers, data=json.dumps(data)) -print(response.json()) +print(response.text) ``` diff --git a/zh-hans/guides/application-publishing/embedding-in-websites.mdx b/zh-hans/guides/application-publishing/embedding-in-websites.mdx index 447c98c7..7e093280 100644 --- a/zh-hans/guides/application-publishing/embedding-in-websites.mdx +++ b/zh-hans/guides/application-publishing/embedding-in-websites.mdx @@ -1,6 +1,5 @@ --- title: 嵌入网站 -version: '简体中文' --- Dify 支持将你的 AI 应用嵌入到业务网站中,你可以使用该能力在几分钟内制作具有业务数据的官网 AI 客服、业务知识问答等应用。点击 WebApp 卡片上的嵌入按钮,复制嵌入代码,粘贴到你网站的目标位置。 @@ -15,13 +14,13 @@ Dify 支持将你的 AI 应用嵌入到业务网站中,你可以使用该能 将 script 代码复制到你网站 `` 或 `` 标签中。 - 显示嵌入网站的代码示例 + 显示嵌入网站的代码示例 如果将 script 代码粘贴到官网的 `` 处,你将得到一个官网 AI 机器人: - 展示嵌入到网站后的 AI 机器人效果 + 展示嵌入到网站后的 AI 机器人效果 ## 自定义 Dify 聊天机器人气泡按钮 diff --git a/zh-hans/guides/application-publishing/launch-your-webapp-quickly/conversation-application.mdx b/zh-hans/guides/application-publishing/launch-your-webapp-quickly/conversation-application.mdx index 9859988e..ecd6822d 100644 --- a/zh-hans/guides/application-publishing/launch-your-webapp-quickly/conversation-application.mdx +++ b/zh-hans/guides/application-publishing/launch-your-webapp-quickly/conversation-application.mdx @@ -16,20 +16,16 @@ version: '简体中文' 如你在应用编排时有设置变量的填写要求,则在对话前需要按提示填写信息才可进入对话窗口: - - 对话前变量填写界面 - +![](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/application-publishing/launch-your-webapp-quickly/ea12b7209fcaad39d92b6c54c296f9cd.png) 填写必要内容,点击 "开始对话" 按钮,开始聊天。 - - 开始对话后的聊天界面 - +![](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/application-publishing/launch-your-webapp-quickly/852c2412400eccbc17fa5be9794b7c47.png) 移动到 AI 的回答上,可以复制会话内容,给回答 "赞" 和 "踩"。 - AI回答上的操作选项 + AI回答上的操作选项 ### 对话的创建、置顶和删除 @@ -37,7 +33,7 @@ version: '简体中文' 点击 "新对话" 按钮开始一个新的对话。移动到一个会话上,可以对会话进行 "置顶" 和 "删除" 操作。 - 对话的创建、置顶和删除操作 + 对话的创建、置顶和删除操作 ### 对话开场白 @@ -45,7 +41,7 @@ version: '简体中文' 若在应用编排时开启了「对话开场白」功能,则在创建一个新对话时 AI 应用会自动发起第一句对话: - AI自动发起的对话开场白 + AI自动发起的对话开场白 ### 下一步问题建议 @@ -53,7 +49,7 @@ version: '简体中文' 若在应用编排时开启了「下一步问题建议」功能,则在对话后系统自动生成 3 个相关问题建议: - 系统生成的下一步问题建议 + 系统生成的下一步问题建议 ### 语音转文字 @@ -63,11 +59,11 @@ version: '简体中文' _请注意确保你使用的设备环境已经授权使用麦克风。_ - 语音转文字功能界面 + 语音转文字功能界面 ### 引用与归属 开启该功能后,当 LLM 回复问题来自知识库的内容时,可以在回复内容下面查看到具体的引用段落信息,包括原始分段文本、分段序号、匹配度等。 -详细说明请参考[《引用与归属》](/zh-cn/user-guide/knowledge-base/retrieval-test-and-citation)。 \ No newline at end of file +详细说明请参考[《引用与归属》](/zh-hans/guides/knowledge-base/retrieval-test-and-citation)。 diff --git a/zh-hans/guides/application-publishing/launch-your-webapp-quickly/text-generator.mdx b/zh-hans/guides/application-publishing/launch-your-webapp-quickly/text-generator.mdx index 6bd6af5f..0343eed4 100644 --- a/zh-hans/guides/application-publishing/launch-your-webapp-quickly/text-generator.mdx +++ b/zh-hans/guides/application-publishing/launch-your-webapp-quickly/text-generator.mdx @@ -1,6 +1,5 @@ --- title: 文本生成型应用 -version: '简体中文' --- 文本生成类应用是一种根据用户提供的提示,自动生成高质量文本的应用。它可以生成各种类型的文本,例如文章摘要、翻译等。 @@ -19,7 +18,7 @@ version: '简体中文' 输入查询内容,点击运行按钮,右侧会生成结果,如下图所示: - 文本生成应用运行一次的界面 + 文本生成应用运行一次的界面 在生成的结果部分,点 "复制" 按钮可以将内容复制到剪贴板。点 "保存" 按钮可以保存内容。可以在 "已保存" 选项卡中看到保存过的内容。也可以对生成的内容点 "赞" 和 "踩"。 @@ -35,7 +34,7 @@ version: '简体中文' 点击 "批量运行" 选项卡,则会进入批量运行页面。 - 批量运行页面界面 + 批量运行页面界面 #### 第 2 步 下载模版并填写内容 @@ -43,13 +42,13 @@ version: '简体中文' 点击下载模版按钮,下载模版。编辑模版,填写内容,并另存为 `.csv` 格式的文件。 - 下载模板按钮位置 + 下载模板按钮位置 #### 第 3 步 上传文件并运行 - 上传文件并运行界面 + 上传文件并运行界面 如果需要导出生成的内容,可以点右上角的下载 "按钮" 来导出为 `csv` 文件。 @@ -61,7 +60,7 @@ version: '简体中文' 点击生成结果下面的 "保存" 按钮,可以保存运行结果。在 "已保存" 选项卡中,可以看到所有已保存的内容。 - 保存运行结果界面 + 保存运行结果界面 ### 生成更多类似结果 @@ -69,5 +68,5 @@ version: '简体中文' 如果在应用编排时开启了 "更多类似" 的功能。在 Web 应用中可以点击 "更多类似" 的按钮来生成和当前结果相似的内容。如下图所示: - 生成更多类似结果界面 + 生成更多类似结果界面 \ No newline at end of file diff --git a/zh-hans/guides/application-publishing/launch-your-webapp-quickly/web-app-settings.mdx b/zh-hans/guides/application-publishing/launch-your-webapp-quickly/web-app-settings.mdx index 189c05d7..f0353d26 100644 --- a/zh-hans/guides/application-publishing/launch-your-webapp-quickly/web-app-settings.mdx +++ b/zh-hans/guides/application-publishing/launch-your-webapp-quickly/web-app-settings.mdx @@ -1,6 +1,5 @@ --- title: Web 应用设置 -version: '简体中文' --- Web 应用是给应用使用者用的。应用开发者在 Dify 创建一个应用,就会获得一个对应的 Web 应用。Web 应用的使用者无需登陆,即可使用。Web 应用已适配不同尺寸的设备:PC,平板和手机。 diff --git a/zh-hans/guides/knowledge-base/knowledge-and-documents-maintenance/maintain-dataset-via-api.mdx b/zh-hans/guides/knowledge-base/knowledge-and-documents-maintenance/maintain-dataset-via-api.mdx index a336a3e5..38963092 100644 --- a/zh-hans/guides/knowledge-base/knowledge-and-documents-maintenance/maintain-dataset-via-api.mdx +++ b/zh-hans/guides/knowledge-base/knowledge-and-documents-maintenance/maintain-dataset-via-api.mdx @@ -20,9 +20,7 @@ version: '简体中文' 进入知识库页面,在左侧的导航中切换至 **API** 页面。在该页面中你可以查看 Dify 提供的知识库 API 文档,并可以在 **API 密钥** 中管理可访问知识库 API 的凭据。 - - - +![](https://assets-docs.dify.ai/2025/03/82ef51dc6886fb8301a2b85a920b12d0.png) ### API 调用示例 diff --git a/zh-hans/guides/knowledge-base/knowledge-base-creation/introduction.mdx b/zh-hans/guides/knowledge-base/knowledge-base-creation/introduction.mdx index 576eb7ef..cf63160a 100644 --- a/zh-hans/guides/knowledge-base/knowledge-base-creation/introduction.mdx +++ b/zh-hans/guides/knowledge-base/knowledge-base-creation/introduction.mdx @@ -23,7 +23,7 @@ title: 创建步骤 5. 等待分段嵌入 -6. 完成上传,在应用内关联知识库并使用。你可以参考[在应用内集成知识库](../integrate-knowledge-within-application),搭建出能够基于知识库进行问答的 LLM 应用。如需修改或管理知识库,请参考[知识库管理与文档维护](../knowledge-and-documents-maintenance/)。 +6. 完成上传,在应用内关联知识库并使用。你可以参考[在应用内集成知识库](../integrate-knowledge-within-application),搭建出能够基于知识库进行问答的 LLM 应用。如需修改或管理知识库,请参考[知识库管理与文档维护](/zh-hans/guides/knowledge-base/knowledge-and-documents-maintenance/introduction)。 ![完成知识库的创建](https://assets-docs.dify.ai/2024/12/a3362a1cd384cb2b539c9858de555518.png) diff --git a/zh-hans/guides/knowledge-base/metadata.mdx b/zh-hans/guides/knowledge-base/metadata.mdx index 39823071..3005087e 100644 --- a/zh-hans/guides/knowledge-base/metadata.mdx +++ b/zh-hans/guides/knowledge-base/metadata.mdx @@ -18,6 +18,7 @@ title: 元数据 field_name_and_value diff --git a/zh-hans/guides/management/app-management.mdx b/zh-hans/guides/management/app-management.mdx index f26d3c70..fd4ca47a 100644 --- a/zh-hans/guides/management/app-management.mdx +++ b/zh-hans/guides/management/app-management.mdx @@ -1,19 +1,27 @@ --- title: 应用管理 -version: '简体中文' --- -## [编辑应用信息](#edit-app-info) +## 导入应用 + +将 DSL 文件上传至 Dify 平台内即可完成 Dify 应用的导入。导入 DSL 文件时会进行版本检查,如果导入较低版本的 DSL 文件将进行提示。 + +- 对于 SaaS 用户而言,在 SaaS 平台导出的 DSL 文件版本为最新版本。 +- 对于社区版用户而言,建议参考[更新 Dify](/zh-hans/getting-started/install-self-hosted/docker-compose#更新-dify)更新社区版以导出更高版本的 DSL 文件,避免潜在的兼容性问题。 + +![导入应用](https://assets-docs.dify.ai/2024/11/487d2c1cc8b86666feb35ea8a346c053.png) + +## 编辑应用信息 创建应用后,如果你想要修改应用名称或描述,可以点击应用左上角的 「编辑信息」 ,重新修改应用的图标、名称或描述。 -![](/zh-cn/img/493d7ed37931d11ebb973e6c376c6e95.png) +![编辑应用信息](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/management/ec718ad1050311b01a3f342ef1f45ae9.png) -## [复制应用](#copy-app) +## 复制应用 应用均支持复制操作,点击应用左上角的 「复制」。 -## [导出应用](#export-app) +## 导出应用 在 Dify 内创建的应用均支持以 DSL 格式进行导出,你可以自由地将配置文件导入至任意 Dify 团队。 @@ -22,20 +30,20 @@ version: '简体中文' * 在 “工作室” 页点击应用菜单按钮中 “导出 DSL”; * 进入应用的编排页后,点击左上角 “导出 DSL”。 -![](/zh-cn/img/37a012ebb30449ccebcb29f3ee01d62f.png) +![导出应用](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/management/544c18d770e230db93d6756bba98d8a7.png) DSL 文件不包含自定义工具节点内已填写的授权信息,例如第三方服务的 API Key;如果环境变量中包含 `Secret`类型变量,导出文件时将提示是否允许导出该敏感信息。 -![](/zh-cn/img/9d2b1f92367982fa4416e07c5b5669cc.png) +![导出Secret选项](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/management/25ce002ef7f0392fc6b3b6975ae137ec.png) - + Dify DSL 格式文件是 Dify.AI 定义的 AI 应用工程文件标准,文件格式为 YML。该标准涵盖应用的基本描述、模型参数、编排配置等信息。 - + ## 删除应用 如果你想要清理应用,可以点击应用左上角的 「删除」 。 - + ⚠️ 应用的删除操作无法撤销,所有用户将无法访问你的应用,应用内的所有 Prompt、编排配置和日志均会被删除。 - + diff --git a/zh-hans/guides/management/personal-account-management.mdx b/zh-hans/guides/management/personal-account-management.mdx index 83c3736e..1153acdb 100644 --- a/zh-hans/guides/management/personal-account-management.mdx +++ b/zh-hans/guides/management/personal-account-management.mdx @@ -3,6 +3,19 @@ title: 个人账号管理 version: '简体中文' --- +## 登录方式 + +Dify 不同版本支持的登录方式如下: + +| 版本 | 登录方式 | +| :--- | :--- | +| 社区版 | 邮箱与密码 | +| 云服务 | GitHub 账户授权、Google 账户授权、邮箱与验证码 | + +> 注意:Dify 的云服务版本账户,如果 GitHub 或 Google 账户的绑定邮箱与使用邮箱验证码登录的邮箱相同,系统会自动将它们关联为同一账户,无需手动绑定,避免重复注册。 + +![](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/management/36e3f4fb5fc15ae6949e2b2693ecc9e7.png) + ## 修改账号信息 如需修改个人账号的相关信息,请点击 Dify 团队首页右上角头像,轻点 **“账户”** 修改以下信息: @@ -14,11 +27,11 @@ version: '简体中文' > 注意:重置密码功能仅在社区版提供 -![](/zh-cn/img/6cb4f24598fe8b40d4ff1881d8257c76.png) +![](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/management/700817f2c2e3ba0bfce873bb5c5316c9.png) -### 绑定登录方式 +### 登录方式 -支持绑定 GitHub 与 Google 账号作为 Dify 团队的登录方式。请点击 Dify 团队首页右上角头像,轻点 **“集成”** 进行绑定。 +支持通过邮箱+验证码、Google 授权和 Github 授权三种方式登录 Dify 平台。同一个 Dify 账户,可通过邮箱+验证码登录,或使用绑定相同邮箱的 Google/Github 账户授权登录,无需额外绑定操作。 ### 切换界面语言 @@ -37,4 +50,42 @@ version: '简体中文' * Bahasa Indonesia * Українська(Україна) -Dify 欢迎更多社区志愿者一同参与贡献更多语言版本,前往[ Github 代码仓库](https://github.com/langgenius/dify/blob/main/CONTRIBUTING)进行贡献! +Dify 欢迎更多社区志愿者一同参与贡献更多语言版本,前往[ Github 代码仓库](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)进行贡献! + +### 查看账号关联的应用数据 + +你可以在账户页中查看当前账号所关联的应用数据。 + +### 删除个人账号 + +⚠️ 危险操作,请谨慎处理。 + +如需确认删除 Dify SaaS 服务账号,请点击右上角的头像,轻点下拉框中的“账号”,点击“删除账号”按钮。删除账号操作不可逆,相同的邮箱地址在 30 天内无法重新注册。当前账号所拥有的工作空间也会一并删除,并被自动移除出所有已加入的工作空间。 + +输入需要删除的邮箱,输入确认验证码后,系统将彻底删除账号的所有信息。 + +![删除个人账户](https://assets-docs.dify.ai/2024/12/ded326f27886b5884969c220ead998d7.png) + +#### 常见问题 + +**1. 如果不小心删除了账户,可以撤销账户删除操作吗?** + +删除账户后不可撤销。如有特殊情况,请在账户删除后 20 天内联系 `support@dify.ai` 并说明原因。 + +**2. 删除账户后,我在团队中的角色和数据会如何处理?** + +删除账户后: + 1. 你以**团队所有者**角色所创建的工作空间(Workspace)将被解散并删除空间内所有数据,所有团队成员将失去访问权限。 + 2. 你以**团队成员或管理员**角色所加入的工作空间(Workspace)将继续保留空间内的数据,包括由当前账号所创建的各个应用数据。你的账户将被移出工作空间的成员列表。 + +**3. 删除账户后,我可以重新使用相同的邮箱注册新账户吗?** + +删除账户后,30 天内无法使用相同的邮箱重新注册新账户。 + +**4. 删除账户后,与第三方服务的授权(如 Google、GitHub)会被取消吗?** + +是的,删除账户后,所有与第三方服务(如 Google、GitHub)的授权将自动取消。 + +**5. 删除账户后,我的 Dify 账户订阅服务会自动取消并退款吗?** + +删除账户后,你的 Dify 账户订阅服务将自动取消且不可退款,未来将不会继续收取订阅费用。 diff --git a/zh-hans/guides/management/subscription-management.mdx b/zh-hans/guides/management/subscription-management.mdx new file mode 100644 index 00000000..c17b6af1 --- /dev/null +++ b/zh-hans/guides/management/subscription-management.mdx @@ -0,0 +1,74 @@ +--- +title: 订阅管理 +--- + +### 升级 Dify 团队订阅 + +团队所有者和管理员具备升级团队订阅计划的权限。点击 Dify 团队首页右上角的 **“升级”** 按钮,选择合适的套餐,支付后即可升级团队。 + +### 管理 Dify 团队订阅 + +订阅 Dify 付费服务后(Professional 和 Team),团队所有者和管理员可以前往 **“设置”** → **“账单”**,管理团队的账单及订阅计划。 + +你可以在账单页查看团队内各项资源的使用情况。 + +![团队账单管理](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/management/8a0becb402c9abdb697f8ab6ff67c839.png) + +### 常见问题 + +#### 1. 如何升级/降级团队,以及如何取消订阅? + +团队所有者和管理员可以前往 **“设置”** → **“账单”**,轻点“管理账单及订阅”更改订阅方案。 + +* 从 Professional 升级至 Team 需补足当月差额,立即生效。 +* 从 Team 降级至 Professional 立即生效。 + +![更改付费方案](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/management/5f330cc59a9c6646531876ba92b856f8.png) + +如果取消订阅方案,**团队将在当月账单周期结束后**自动切换至 Sandbox/Free 版本。已超出的团队人数及团队资源将无法访问。 + +#### 2. 升级订阅计划后,团队的可用资源会有什么变化? + +**Free → Professional** + +成员数量:从 1 人升级为 3 人 + +团队内的应用程序数限额:10 → 50 + +团队内的向量空间容量:5MB → 200MB + +应用程序的[标记](https://docs.dify.ai/v/zh-hans/guides/biao-zhu/logs)回复数:10 → 2000 + +知识库的文档上传数量:50 → 500 + +OpenAI 的对话额度:总共 200 → 每月 5000 + +*** + +**Professional → Team** + +成员数量:从 3 人升级为无限 + +团队内的应用程序数限额:50 → 无限 + +团队内的向量空间容量:200MB → 1GB + +应用程序的[标记](https://docs.dify.ai/v/zh-hans/guides/biao-zhu/logs)回复数:2000 → 5000 + +知识库的文档上传数量:500 → 1000 + +OpenAI 的对话额度:每月 5000 → 每月 10000 + +升级订阅计划后,OpenAI 的对话额度将被重置;已使用的计算资源则不会重置。 + +#### 3. 忘记及时续费怎么办? + +如果忘记续费,团队将自动降级为 Sandbox/Free 版本。除了团队所有者以外,其他人无法继续访问团队。团队内超出的计算资源(例如文档、向量空间等)也会被锁定。 + +#### 4. 团队所有者账号删除后会影响团队吗? + +一个团队需绑定一位团队所有者。如果团队所有者未及时转移至其他团队成员,当前团队的所有数据将一同被删除。 + +#### 5. 各个订阅版本间有什么区别? + +详细的功能对比请参考 [Dify 定价页](https://dify.ai/pricing)。 diff --git a/zh-hans/guides/management/team-members-management.mdx b/zh-hans/guides/management/team-members-management.mdx index 095d9d29..7adf987a 100644 --- a/zh-hans/guides/management/team-members-management.mdx +++ b/zh-hans/guides/management/team-members-management.mdx @@ -13,9 +13,14 @@ version: '简体中文' 团队所有者点击右上角头像,然后轻点 **“成员”** → **“添加”**,输入邮箱,分配成员权限完成添加。 -![](/zh-cn/img/4cc19d40803e1be0017ce00e4aa160de.png) +![为团队成员分配权限](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/management/764276a102e3264747c64f02dafa5f5d.png) -被邀请成员可以通过 URL 或邮箱进行注册。 +> 社区版团队需在[环境变量](https://docs.dify.ai/v/zh-hans/getting-started/install-self-hosted/environments)中添加并启用邮件服务,被邀成员才能收到邀请邮件。 + +- 若被邀请的成员未注册 Dify,将收到一封邀请邮件,点击邮件中的链接即可完成注册。 +- 若被邀请成员已注册 Dify,系统将自动分配权限,**同时不再发送邀请邮件**。被邀成员可以在右上角头像处切换至新的 Workspace。 + +![](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/management/93a6f055cfaf65dfe138e8ac332f71d1.png) ### 成员权限 @@ -42,7 +47,7 @@ version: '简体中文' 点击 Dify 团队首页右上角头像,前往 **“设置”** → **“成员”** ,选择需要被移除的成员的角色,轻点 **“移除团队”**。 -![](/zh-cn/img/bdc84fc19ababab441c3f98b72f134e2.png) +![移除成员](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/management/85c39a391714c110ec9e1769a8c52e06.png) ### 常见问题 diff --git a/zh-hans/guides/monitoring/analysis.mdx b/zh-hans/guides/monitoring/analysis.mdx index 48fd9edb..4c3aba55 100644 --- a/zh-hans/guides/monitoring/analysis.mdx +++ b/zh-hans/guides/monitoring/analysis.mdx @@ -5,12 +5,10 @@ version: '简体中文' 你可以在 **概览** 内监控、跟踪应用程序在生产环境中的性能,在数据分析仪表盘内分析生产环境中应用的使用成本、延迟、用户反馈、性能等指标,并通过持续调试、迭代不断改进你的应用程序。 -![](/zh-cn/img/44847ddbf9994ddc45f5cc0b31432dfb.png) +![概览—数据分析](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/monitoring/83974553fee361687db31bda35198013.png) **概览 -- 数据分析** 内显示了用量、活跃用户数和 LLM 调用消耗等,这使你可以持续改进应用运营的效果、活跃度、经济性。 -![](/zh-cn/img/f0c771cd4079d9ebf637ac56243769ee.png) - *** **全部消息数(Total Messages)** diff --git a/zh-hans/guides/monitoring/integrate-external-ops-tools/integrate-langfuse.mdx b/zh-hans/guides/monitoring/integrate-external-ops-tools/integrate-langfuse.mdx index 3a8fbf14..4fb880d9 100644 --- a/zh-hans/guides/monitoring/integrate-external-ops-tools/integrate-langfuse.mdx +++ b/zh-hans/guides/monitoring/integrate-external-ops-tools/integrate-langfuse.mdx @@ -1,67 +1,66 @@ --- title: 集成 Langfuse -version: '简体中文' --- -### 什么是 Langfuse +### Langfuse 简介 Langfuse 是一个开源的 LLM 工程平台,可以帮助团队协作调试、分析和迭代他们的应用程序。 - + Langfuse 官网介绍:[https://langfuse.com/](https://langfuse.com/) - + *** -### 如何配置 Langfuse +### 配置 Langfuse 1. 在 [官网注册](https://langfuse.com/) 并登录 Langfuse 2. 在 Langfuse 内创建项目,登录后在主页点击 **New** ,创建一个自己的项目,**项目** 将用于与 Dify 内的 **应用** 关联进行数据监测。 - Langfuse 创建项目界面 + Langfuse 创建项目界面 为项目编辑一个名称。 - Langfuse 项目命名界面 + Langfuse 项目命名界面 3. 创建项目 API 凭据,在项目内左侧边栏中点击 **Settings** 打开设置 - Langfuse 项目设置界面 + Langfuse 项目设置界面 在 Settings 内点击 **Create API Keys** 创建一个项目 API 凭据。 - Langfuse 创建 API 凭据界面 + Langfuse 创建 API 凭据界面 复制并保存 **Secret Key** ,**Public Key,Host** - Langfuse API Key 配置界面 + Langfuse API Key 配置界面 4. 在 Dify 内配置 Langfuse,打开需要监测的应用,在侧边菜单打开 **监测 **,在页面中选择 **配置。** - Dify 监测配置界面 + Dify 监测配置界面 点击配置后,将在 Langfuse 内创建的 **Secret Key, Public Key, Host** 粘贴到配置内并保存。 - Dify Langfuse 配置界面 + Dify Langfuse 配置界面 成功保存后可以在当前页面查看到状态,显示已启动即正在监测。 - Dify Langfuse 配置状态界面 + Dify Langfuse 配置状态界面 *** @@ -71,22 +70,22 @@ Langfuse 官网介绍:[https://langfuse.com/](https://langfuse.com/) 配置完成后, Dify 内应用的调试或生产数据可以在 Langfuse 查看监测数据。 - Dify 应用调试界面 + Dify 应用调试界面 - Langfuse 应用数据监测界面 1 + Langfuse 应用数据监测界面 1 - Langfuse 应用数据监测界面 2 + Langfuse 应用数据监测界面 2 ### 监测数据清单 -#### Workflow /Chatflow Trace 信息 +#### Workflow /Chatflow Trace信息 -** 用于追踪 workflow 以及 chatflow** +**用于追踪workflow以及chatflow** | Workflow | LangFuse Trace | | ---------------------------------------- | ----------------------- | @@ -138,9 +137,9 @@ Langfuse 官网介绍:[https://langfuse.com/](https://langfuse.com/) * file\_list - 处理的文件列表 * triggered\_from - 触发来源 -#### Message Trace 信息 +#### Message Trace信息 -** 用于追踪 llm 对话相关 ** +**用于追踪llm对话相关** | Message | LangFuse Generation/Trace | | -------------------------------- | ------------------------- | @@ -188,9 +187,9 @@ Langfuse 官网介绍:[https://langfuse.com/](https://langfuse.com/) * from\_source - 消息来源 * message\_id - 消息 ID -#### Moderation Trace 信息 +#### Moderation Trace信息 -** 用于追踪对话审查 ** +**用于追踪对话审查** | Moderation | LangFuse Generation/Trace | | ------------- | ------------------------- | @@ -221,9 +220,9 @@ Langfuse 官网介绍:[https://langfuse.com/](https://langfuse.com/) * action - 执行的具体行动 * preset\_response - 预设响应 -#### Suggested Question Trace 信息 +#### Suggested Question Trace信息 -** 用于追踪建议问题 ** +**用于追踪建议问题** | Suggested Question | LangFuse Generation/Trace | | ---------------------- | ------------------------- | @@ -266,9 +265,9 @@ Langfuse 官网介绍:[https://langfuse.com/](https://langfuse.com/) * workflow\_run\_id - 工作流运行 ID * from\_source - 消息来源 -#### Dataset Retrieval Trace 信息 +#### Dataset Retrieval Trace信息 -** 用于追踪知识库检索 ** +**用于追踪知识库检索** | Dataset Retrieval | LangFuse Generation/Trace | | --------------------- | ------------------------- | @@ -301,9 +300,9 @@ Langfuse 官网介绍:[https://langfuse.com/](https://langfuse.com/) * workflow\_run\_id 工作流运行 ID * from\_source 消息来源 -#### Tool Trace 信息 +#### Tool Trace信息 -** 用于追踪工具调用 ** +**用于追踪工具调用** | Tool | LangFuse Generation/Trace | | --------------------- | ------------------------- | @@ -346,9 +345,9 @@ Langfuse 官网介绍:[https://langfuse.com/](https://langfuse.com/) * created\_by\_role 创建者角色 * created\_user\_id 创建者用户 ID -#### Generate Name Trace 信息 +#### Generate Name Trace信息 -** 用于追踪会话标题生成 ** +**用于追踪会话标题生成** | Generate Name | LangFuse Generation/Trace | | ----------------- | ------------------------- | diff --git a/zh-hans/guides/monitoring/integrate-external-ops-tools/integrate-langsmith.mdx b/zh-hans/guides/monitoring/integrate-external-ops-tools/integrate-langsmith.mdx index bb16e4d1..f091be8b 100644 --- a/zh-hans/guides/monitoring/integrate-external-ops-tools/integrate-langsmith.mdx +++ b/zh-hans/guides/monitoring/integrate-external-ops-tools/integrate-langsmith.mdx @@ -1,19 +1,18 @@ --- title: 集成 LangSmith -version: '简体中文' --- -### 什么是 LangSmith +### LangSmith 简介 LangSmith 是一个用于构建生产级 LLM 应用程序的平台,它用于开发、协作、测试、部署和监控 LLM 应用程序。 - + LangSmith [官网介绍](https://www.langchain.com/langsmith) - + *** -## 如何配置 LangSmith +### 配置 LangSmith 本章节将指引你注册 LangSmith 并将其集成至 Dify 平台内。 @@ -24,13 +23,13 @@ LangSmith [官网介绍](https://www.langchain.com/langsmith) 在 LangSmith 内创建项目,登录后在主页点击 **New Project** 创建一个自己的项目,**项目**将用于与 Dify 内的**应用**关联进行数据监测。 - LangSmith 创建项目界面 + LangSmith 创建项目界面 创建完成之后在 Projects 内可以查看该项目。 - LangSmith 项目列表界面 + LangSmith 项目列表界面 #### 3. 创建项目凭据 @@ -38,25 +37,25 @@ LangSmith [官网介绍](https://www.langchain.com/langsmith) 创建项目凭据,在左侧边栏内找到项目设置 **Settings**。 - LangSmith 项目设置界面 + LangSmith 项目设置界面 点击 **Create API Key**,创建一个项目凭据。 - LangSmith 创建 API Key 界面 + LangSmith 创建 API Key 界面 选择 **Personal Access Token** ,用于后续的 API 身份校验。 - LangSmith 选择 API Key 类型界面 + LangSmith 选择 API Key 类型界面 将创建的 API key 复制保存。 - LangSmith API Key 复制界面 + LangSmith API Key 复制界面 #### 4. 将 LangSmith 集成至 Dify 平台 @@ -64,23 +63,23 @@ LangSmith [官网介绍](https://www.langchain.com/langsmith) 在 Dify 应用内配置 LangSmith。打开需要监测的应用,在左侧边菜单内打开**监测**,点击页面内的**配置。** - Dify 配置 LangSmith 入口界面 + Dify 配置 LangSmith 入口界面 点击配置后,将在 LangSmith 内创建的 **API Key** 和**项目名**粘贴到配置内并保存。 - Dify 配置 LangSmith 详情界面 + Dify 配置 LangSmith 详情界面 - + 配置项目名需要与 LangSmith 内设置的项目一致,若项目名不一致,数据同步时 LangSmith 会自动创建一个新的项目。 - + 成功保存后可以在当前页面查看监测状态。 - Dify LangSmith 配置状态界面 + Dify LangSmith 配置状态界面 ### 在 LangSmith 内查看监测数据 @@ -88,15 +87,15 @@ LangSmith [官网介绍](https://www.langchain.com/langsmith) 配置完成后, Dify 内应用的调试或生产数据可以在 LangSmith 查看监测数据。 - Dify 应用调试界面 + Dify 应用调试界面 - LangSmith 应用数据监测界面1 + LangSmith 应用数据监测界面1 - LangSmith 应用数据监测界面2 + LangSmith 应用数据监测界面2 ### 监测数据清单 diff --git a/zh-hans/guides/tools/extensions/api-based/api-based-extension.mdx b/zh-hans/guides/tools/extensions/api-based/api-based-extension.mdx index 68856e57..0bc4b4b6 100644 --- a/zh-hans/guides/tools/extensions/api-based/api-based-extension.mdx +++ b/zh-hans/guides/tools/extensions/api-based/api-based-extension.mdx @@ -12,9 +12,7 @@ version: '简体中文' 除了需要开发对应的模块能力,还需要遵照以下规范,以便 Dify 正确调用 API。 - - 基于 API 扩展 - +![基于 API 扩展](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/extension/api-based-extension/10f17984eae28c7854983e7112738fa8.png) ### API 规范 @@ -240,9 +238,7 @@ API 返回为: 1. 进入 [https://ngrok.com](https://ngrok.com) 官网,注册并下载 Ngrok 文件。 - - Download - +![Download](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/extension/api-based-extension/c44d6cc5425508daac8d31bc4af113df.png) 2. 下载完成后,进入下载目录,根据下方说明解压压缩包,并执行说明中的初始化脚本。 * ```Shell @@ -251,9 +247,7 @@ API 返回为: ``` 3. 查看本地 API 服务的端口: - - 查看端口 - +![查看端口](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/extension/api-based-extension/7ac8ee0f0955f36255e0261b36499db7.png) 并运行以下命令启动: @@ -263,9 +257,7 @@ API 返回为: 启动成功的样例如下: - - Ngrok 启动 - +![Ngrok 启动](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/extension/api-based-extension/2b4adbe0bb1ff203da521ea6eea401f8.png) 4. 我们找到 Forwarding 中,如上图:`https://177e-159-223-41-52.ngrok-free.app`(此为示例域名,请替换为自己的)即为公网域名。 diff --git a/zh-hans/guides/tools/extensions/api-based/cloudflare-workers.mdx b/zh-hans/guides/tools/extensions/api-based/cloudflare-workers.mdx index 76e56f2c..20b5587b 100644 --- a/zh-hans/guides/tools/extensions/api-based/cloudflare-workers.mdx +++ b/zh-hans/guides/tools/extensions/api-based/cloudflare-workers.mdx @@ -46,13 +46,9 @@ npm run deploy 部署成功之后,你会得到一个公网地址,你可以在 Dify 中添加这个地址作为 API Endpoint。请注意不要遗漏 `endpoint` 这个路径。 - - 在 Dify 中添加 API Endpoin - +![在 Dify 中添加 API Endpoint](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/extension/api-based-extension/9433a486a441713ade6270e9dc6c0544.png) - - 在 App 编辑页面中添加上 API Tool - +![在 App 编辑页面中添加上 API Tool](https://assets-docs.dify.ai/dify-enterprise-mintlify/zh_CN/guides/extension/api-based-extension/72c285e58ec136a1a9a66f61b4b41ca1.png) ## 其他逻辑 diff --git a/zh-hans/guides/tools/extensions/code-based/external-data-tool.mdx b/zh-hans/guides/tools/extensions/code-based/external-data-tool.mdx index 3edb861d..4e3f90ad 100644 --- a/zh-hans/guides/tools/extensions/code-based/external-data-tool.mdx +++ b/zh-hans/guides/tools/extensions/code-based/external-data-tool.mdx @@ -1,47 +1,189 @@ --- title: 外部数据工具 -version: '简体中文' --- -## 功能介绍 +外部数据工具用于在终端用户提交数据后,利用外部工具获取额外数据组装至提示词中作为 LLM 额外上下文信息。Dify 默认提供了外部 API 调用的工具,具体参见 [基于 API 的扩展](/zh-hans/guides/tools/extensions/api-based/api-based-extension)。 -[知识库](https://docs.dify.ai/zh-hans/guides/knowledge-base/create-knowledge-and-upload-documents)功能允许开发者可以直接上传各类格式的长文本、结构化数据来构建数据集,使 AI 应用基于用户上传的最新上下文进行对话。 +而对于本地部署 Dify 的开发者,为了满足更加定制化的需求,或者不希望额外开发一个 API Server,可以直接在 Dify 服务的基础上,以插件的形式插入定制的外部数据工具实现逻辑。扩展自定义工具后,将会在工具类型的下拉列表中增加你的自定义工具选项,团队成员即可使用自定义的工具来获取外部数据。 -而**外部数据工具**赋能开发者可以使用自有的搜索能力或内部知识库等外部数据作为 LLM 的上下文,通过 API 扩展的方式实现外部数据的获取并嵌入提示词。相比在云端上传数据集,使用**外部数据工具**可以在保障私有数据安全,自定义搜索,获取实时数据等方面有显著优势。 +## 快速开始 -## 具体实现 +这里以一个 `天气查询` 外部数据工具扩展为例,步骤如下: -当终端用户向对话系统提出请求时,平台后端会触发外部数据工具(即调用自己的 API),它会查询用户问题相关的外部信息,如员工资料、实时记录等,通过 API 返回与当前请求相关的部分。平台后端会将返回的结果组装成文本作为上下文注入到提示词中,以输出更加个性化和符合用户需求的回复内容。 +1. 初始化目录 +2. 添加前端表单规范 +3. 添加实现类 +4. 预览前端界面 +5. 调试扩展 -## 操作说明 +### 1. **初始化目录** -1. 在使用外部数据工具之前,你需要准备一个 API 和用于鉴权的 API Key,请阅读[external-data-tool.md](../extension/api-based-extension/external-data-tool.md "mention") -2. Dify 提供了集中式的 API 管理,在设置界面统一添加 API 扩展配置后,即可在 Dify 上的各类应用中直接使用。 +新增自定义类型 `Weather Search` ,需要在 `api/core/external_data_tool` 目录下新建相关的目录和文件。 - - API-based Extension - +```python +. +└── api + └── core + └── external_data_tool + └── weather_search + ├── __init__.py + ├── weather_search.py + └── schema.json +``` -3. 我们以"查询天气"为例,在"新增基于 API 的扩展"对话框输入名字,API 端点,API Key。保存后我们就可以调用 API 了。 +### 2. **添加前端组件规范** - - Weather Inquiry - +* `schema.json`,定义前端组件,详细请参考[基于代码的扩展](/zh-hans/guides/tools/extensions/code-based/external-data-tool) -4. 在提示词编排页面,点击"工具"右侧的"+添加"按钮,在打开的"添加 工具"对话框,填写名称和变量名称(变量名称会被引用到提示词中,请填写英文),以及选择第 2 步中已经添加的基于 API 的扩展。 +```json +{ + "label": { + "en-US": "Weather Search", + "zh-Hans": "天气查询" + }, + "form_schema": [ + { + "type": "select", + "label": { + "en-US": "Temperature Unit", + "zh-Hans": "温度单位" + }, + "variable": "temperature_unit", + "required": true, + "options": [ + { + "label": { + "en-US": "Fahrenheit", + "zh-Hans": "华氏度" + }, + "value": "fahrenheit" + }, + { + "label": { + "en-US": "Centigrade", + "zh-Hans": "摄氏度" + }, + "value": "centigrade" + } + ], + "default": "centigrade", + "placeholder": "Please select temperature unit" + } + ] +} +``` - - External_data_tool - +### 3. 添加实现类 -5. 这样,我们在提示词编排框就可以把查询到的外部数据拼装到提示词中。比如我们要查询今天的伦敦天气,可以添加`location` 变量,输入"London",结合外部数据工具的扩展变量名称`weather_data`,调试输出如下: +`weather_search.py` 代码模版,你可以在这里实现具体的业务逻辑。 - - Weather_search_tool - + +注意:类变量 name 为自定义类型名称,需要跟目录和文件名保持一致,而且唯一。 + -在对话日志中,我们也可以看到 API 返回的实时数据: +```python +from typing import Optional - - Prompt Log - \ No newline at end of file +from core.external_data_tool.base import ExternalDataTool + + +class WeatherSearch(ExternalDataTool): + """ + The name of custom type must be unique, keep the same with directory and file name. + """ + name: str = "weather_search" + + @classmethod + def validate_config(cls, tenant_id: str, config: dict) -> None: + """ + schema.json validation. It will be called when user save the config. + + Example: + .. code-block:: python + config = { + "temperature_unit": "centigrade" + } + + :param tenant_id: the id of workspace + :param config: the variables of form config + :return: + """ + + if not config.get('temperature_unit'): + raise ValueError('temperature unit is required') + + def query(self, inputs: dict, query: Optional[str] = None) -> str: + """ + Query the external data tool. + + :param inputs: user inputs + :param query: the query of chat app + :return: the tool query result + """ + city = inputs.get('city') + temperature_unit = self.config.get('temperature_unit') + + if temperature_unit == 'fahrenheit': + return f'Weather in {city} is 32°F' + else: + return f'Weather in {city} is 0°C' +``` + +### 4. **调试扩展** + +至此,即可在 Dify 应用编排界面选择自定义的 `Weather Search` 外部数据工具扩展类型进行调试。 + +## 实现类模版 + +```python +from typing import Optional + +from core.external_data_tool.base import ExternalDataTool + + +class WeatherSearch(ExternalDataTool): + """ + The name of custom type must be unique, keep the same with directory and file name. + """ + name: str = "weather_search" + + @classmethod + def validate_config(cls, tenant_id: str, config: dict) -> None: + """ + schema.json validation. It will be called when user save the config. + + :param tenant_id: the id of workspace + :param config: the variables of form config + :return: + """ + + # implement your own logic here + + def query(self, inputs: dict, query: Optional[str] = None) -> str: + """ + Query the external data tool. + + :param inputs: user inputs + :param query: the query of chat app + :return: the tool query result + """ + + # implement your own logic here + return "your own data." +``` + +### 实现类开发详细介绍 + +### def validate\_config + +`schema.json` 表单校验方法,当用户点击「发布」保存配置时调用 + +* `config` 表单参数 + * `{{variable}}` 表单自定义变量 + +### def query + +用户自定义数据查询实现,返回的结果将会被替换到指定的变量。 + +* `inputs` :终端用户传入变量值 +* `query` :终端用户当前对话输入内容,对话型应用固定参数。 diff --git a/zh-hans/guides/tools/extensions/code-based/moderation.mdx b/zh-hans/guides/tools/extensions/code-based/moderation.mdx index 27760c88..78b4a506 100644 --- a/zh-hans/guides/tools/extensions/code-based/moderation.mdx +++ b/zh-hans/guides/tools/extensions/code-based/moderation.mdx @@ -309,5 +309,3 @@ class CloudServiceModeration(Moderation): * `overridden`覆写传入变量值 * `preset_response` 预设回复(仅当 action=direct\_output 返回) * `text` 覆写的 LLM 回答内容(仅当 action=overridden 返回)。 - -\\ diff --git a/zh-hans/guides/workflow/file-upload.mdx b/zh-hans/guides/workflow/file-upload.mdx index 3b444934..bae960c3 100644 --- a/zh-hans/guides/workflow/file-upload.mdx +++ b/zh-hans/guides/workflow/file-upload.mdx @@ -1,6 +1,5 @@ --- title: 文件上传 -version: '简体中文' --- 相较于聊天文本,文档文件能够承载大量的信息,例如学术报告、法律合同。受限于 LLM 自身仅能够支持文件或图片,难以获取文件内更加丰富的上下文信息,应用的使用者不得不手动复制粘贴大量信息与 LLM 对话,增加了许多不必要的使用成本。 @@ -36,13 +35,13 @@ version: '简体中文' ### 快速开始 -Dify 支持在 [ChatFlow](key-concept.md#chatflow-he-workflow) 和 [WorkFlow](key-concept.md#chatflow-he-workflow) 类型应用中上传文件,并通过[变量](variables)交由 LLM 处理。应用开发者可以参考以下方法为应用开启文件上传功能: +Dify 支持在 [ChatFlow](/zh-hans/guides/workflow/concepts#chatflow-和-workflow) 和 [WorkFlow](/zh-hans/guides/workflow/concepts#chatflow-和-workflow) 类型应用中上传文件,并通过[变量](/zh-hans/guides/workflow/variables)交由 LLM 处理。应用开发者可以参考以下方法为应用开启文件上传功能: * 在 Workflow 应用中: - * 在 ["开始节点"](node/start) 添加文件变量 + * 在 ["开始节点"](/zh-hans/guides/workflow/nodes/start) 添加文件变量 * 在 ChatFlow 应用中: - * 在 ["附加功能"](additional-features) 中开启文件上传,允许在聊天窗中直接上传文件 - * 在 ["开始节点"](node/start) 添加文件变量 + * 在 ["附加功能"](/zh-hans/guides/workflow/additional-feature) 中开启文件上传,允许在聊天窗中直接上传文件 + * 在 ["开始节点"](/zh-hans/guides/workflow/nodes/start) 添加文件变量 * 注意:这两种方法可以同时配置,它们是彼此独立的。附加功能中的文件上传设置(包括上传方式和数量限制)不会影响开始节点中的文件变量。例如只想通过开始节点创建文件变量,则无需开启附加功能中的文件上传功能。 这两种方法为应用提供了灵活的文件上传选项,以满足不同场景的需求。 @@ -59,42 +58,58 @@ file variables 和 array[file] variables 支持以下文件类型与格式: | 视频 | MP4, MOV, MPEG, MPGA. | | 其他 | 自定义后缀名支持 | -#### 方法一:在应用聊天框中开启文件上传(仅适用于 Chatflow) +#### 方法一:使用具备识别文件的 LLM + +部分 LLMs(例如 [Claude 3.5 Sonnet](https://docs.anthropic.com/en/docs/build-with-claude/pdf-support))已支持直接处理并分析文件内容,因此 LLM 节点的提示词已允许输入文件变量。 + +> 为了避免潜在异常,应用开发者在使用该文件变量前需前往 LLM 官网确认 LLM 支持何种文件类型。 + +1. 点击创建 Chatflow / Workflow 应用。 +2. 添加 LLM 节点,选择具备文件分析能力的 LLM。 +3. 在开始节点添加文件变量 +4. 在 LLM 的系统提示词内输入文件变量。 +5. 完成创建。 + + + 使用具备识别文件的 LLM 配置示意图 + + +#### 方法二:在应用聊天框中开启文件上传(仅适用于 Chatflow) 1. 点击 Chatflow 应用右上角的 **"功能"** 按钮即可为应用添加更多功能。 开启此功能后,应用使用者可以在应用对话的过程中随时上传并更新文件。最多支持同时上传 10 个文件,每个文件的大小上限为 15MB。 - Chatflow应用中文件上传功能的设置界面 + Chatflow应用中文件上传功能的设置界面 -开启该功能并不意味着赋予 LLM 直接读取文件的能力,还需要配备[**文档提取器**](node/doc-extractor)将文档解析为文本供 LLM 理解。 +开启该功能并不意味着赋予 LLM 直接读取文件的能力,还需要配备[**文档提取器**](/zh-hans/guides/workflow/nodes/doc-extractor)将文档解析为文本供 LLM 理解。 -* 对于音频文件,可以使用 gpt-4o-audio-preview 等支持多模态输入的模型直接处理音频,无需额外的提取器。 -* 对于视频和其他文件类型,暂无对应的提取器,需要应用开发者接入[外部工具](../tools/advanced-tool-integration)进行处理 +* 对于音频文件,可以使用 `gpt-4o-audio-preview` 等支持多模态输入的模型直接处理音频,无需额外的提取器。 +* 对于视频和其他文件类型,暂无对应的提取器,需要应用开发者接入[外部工具](/zh-hans/guides/tools/extensions/api-based/api-based-extension)进行处理 -2. 添加[文档提取器](node/doc-extractor)节点,在输入变量中选中 `sys.files` 变量。 +2. 添加[文档提取器](/zh-hans/guides/workflow/nodes/doc-extractor)节点,在输入变量中选中 `sys.files` 变量。 3. 添加 LLM 节点,在系统提示词中选中文档提取器节点的输出变量。 4. 在末尾添加 "直接回复" 节点,填写 LLM 节点的输出变量。 - 包含文件上传的工作流示意图 + 包含文件上传的工作流示意图 开启后,用户可以在对话框中上传文件并进行对话。但通过此方式, LLM 应用并不具备记忆文件内容的能力,每次对话时需要上传文件。 - 对话框中上传文件的界面 + 对话框中上传文件的界面 若希望 LLM 能够在对话中记忆文件内容,请参考下文。 -#### 方法二:通过添加文件变量开启文件上传功能 +#### 方法三:通过添加文件变量开启文件上传功能 #### 1. 在"开始"节点添加文件变量 -在应用的["开始"](node/start)节点内添加输入字段,选择 **"单文件"** 或 **"文件列表"** 字段类型的变量。 +在应用的["开始"](/zh-hans/guides/workflow/nodes/start)节点内添加输入字段,选择 **"单文件"** 或 **"文件列表"** 字段类型的变量。