钉钉云文档 Skill ⚠️ 版本兼容提醒 本 Skill v1.0 需要新版钉钉文档 MCP URL(mcpId=9629)。 如果你看到的工具名是 、 等旧名称,说明配置的是旧版 MCP URL,需要重新获取: 1. 访问 钉钉文档 MCP 广场 获取新版 StreamableHttp URL 2. 重新配置: 严格禁止 1. 禁止编造 nodeId / blockId — 必须从工具返回值中提取,编造 ID 会操作到错误文档或块 2. 覆盖前必须确认 — 会清空全部内容,不确定时先问用户 3. 禁止删除前不确认 blockId — 不可恢复,必须先用 确认 4. 仅 ALIDOC 支持 Markdown 读写 — 表格/PPT/PDF 不支持 和 5. 需要下载权限 — 仅有查看权限时无法获取内容,且不支持跨组织文档 6. 必须传整数 — 插入标题时, 必须传 而非 ,传字符串会导致后端报错 工具列表 核心工具(8个) | 工具 | 用途 | 必填参数 | |------|------|---------| | | 搜索有权限的文档 | 无(keyword 选填) | | | 创建在线文档(可含初始 Markdown 内容) | name | | | 创建文件(在线文档/表格/演示/白板/脑图/多维表/文件夹) | name, type | | | 获取文档 Markdow…

,\n re.IGNORECASE\n)\nDOC_ID_PATTERN = re.compile(r'^[a-zA-Z0-9]{16,}

钉钉云文档 Skill ⚠️ 版本兼容提醒 本 Skill v1.0 需要新版钉钉文档 MCP URL(mcpId=9629)。 如果你看到的工具名是 、 等旧名称,说明配置的是旧版 MCP URL,需要重新获取: 1. 访问 钉钉文档 MCP 广场 获取新版 StreamableHttp URL 2. 重新配置: 严格禁止 1. 禁止编造 nodeId / blockId — 必须从工具返回值中提取,编造 ID 会操作到错误文档或块 2. 覆盖前必须确认 — 会清空全部内容,不确定时先问用户 3. 禁止删除前不确认 blockId — 不可恢复,必须先用 确认 4. 仅 ALIDOC 支持 Markdown 读写 — 表格/PPT/PDF 不支持 和 5. 需要下载权限 — 仅有查看权限时无法获取内容,且不支持跨组织文档 6. 必须传整数 — 插入标题时, 必须传 而非 ,传字符串会导致后端报错 工具列表 核心工具(8个) | 工具 | 用途 | 必填参数 | |------|------|---------| | | 搜索有权限的文档 | 无(keyword 选填) | | | 创建在线文档(可含初始 Markdown 内容) | name | | | 创建文件(在线文档/表格/演示/白板/脑图/多维表/文件夹) | name, type | | | 获取文档 Markdow…

)\n\n\ndef extract_doc_id(node_id: str) -> str:\n \"\"\"\n 从 node_id 提取文档 ID(用于生成默认文件名)。\n 支持 URL 格式和纯 ID 格式。\n \"\"\"\n url_match = DOC_URL_PATTERN.match(node_id.strip())\n if url_match:\n return url_match.group(1)\n if DOC_ID_PATTERN.match(node_id.strip()):\n return node_id.strip()\n return None\n\n\ndef save_content(content: str, path: Path) -> bool:\n \"\"\"保存内容到文件\"\"\"\n try:\n with open(path, 'w', encoding='utf-8') as file:\n file.write(content)\n return True\n except Exception as error:\n print(f\"❌ 保存文件失败:{error}\")\n return False\n\n\ndef main():\n \"\"\"主函数\"\"\"\n if len(sys.argv) \u003c 2:\n print(__doc__)\n print(\"错误:缺少文档标识参数\")\n sys.exit(1)\n\n node_id = sys.argv[1].strip()\n output_path = sys.argv[2] if len(sys.argv) > 2 else None\n\n # 提取文档 ID(用于生成默认文件名)\n doc_id = extract_doc_id(node_id)\n if not doc_id:\n print(\"❌ 无效的文档标识格式\")\n print(\"支持格式:\")\n print(\" URL:https://alidocs.dingtalk.com/i/nodes/{dentryUuid}\")\n print(\" ID:32 位字母数字字符串\")\n sys.exit(1)\n\n # 确定输出文件路径\n if not output_path:\n output_path = f\"{doc_id}.md\"\n\n # 解析并验证输出路径(防止目录遍历)\n try:\n safe_output = resolve_safe_path(output_path)\n except ValueError as error:\n print(f\"❌ {error}\")\n sys.exit(1)\n\n safe_output = safe_output.resolve()\n if not str(safe_output).startswith(ALLOWED_ROOT):\n safe_output = Path(ALLOWED_ROOT) / safe_output.name\n\n print(f\"📥 导出文档\")\n print(f\" 文档标识:{node_id}\")\n print(f\" 目标文件:{safe_output}\")\n print(\"-\" * 50)\n\n # 步骤 1:获取文档内容(新版 API,nodeId 支持 URL 或 ID 自动识别)\n print(\"步骤 1: 获取文档内容...\")\n content = get_document_content(node_id)\n if content is None:\n sys.exit(1)\n\n print(f\" 内容长度:{len(content)} 字符\")\n\n if len(content) > MAX_CONTENT_LENGTH:\n print(f\"⚠️ 内容过长,截断到 {MAX_CONTENT_LENGTH} 字符\")\n content = content[:MAX_CONTENT_LENGTH]\n\n # 步骤 2:保存文件\n print(\"\\n步骤 2: 保存文件...\")\n if not save_content(content, safe_output):\n sys.exit(1)\n\n print(\"-\" * 50)\n print(\"✅ 导出完成!\")\n print(f\"\\n文件路径:{safe_output}\")\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3702,"content_sha256":"584b5243067ae8393a921042b855cbe7dd4b55fdbae4692133ef6407c5b1a733"},{"filename":"scripts/import_docs.py","content":"#!/usr/bin/env python3\n\"\"\"\n从本地文件导入文档到钉钉文档\n\n用法:\n python import_docs.py \u003cfile.md> [title]\n\n参数:\n file.md: Markdown 文件路径\n title: 可选,文档标题(默认使用文件名)\n\n示例:\n python import_docs.py README.md\n python import_docs.py notes.md \"项目笔记\"\n\"\"\"\n\nimport sys\nfrom pathlib import Path\n\nfrom mcporter_utils import create_document_with_content, resolve_safe_path\n\n# ============== 安全常量 ==============\nMAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB\nMAX_CONTENT_LENGTH = 50000 # 最大内容长度(字符)\nALLOWED_EXTENSIONS = ['.md', '.txt', '.markdown']\n\n\ndef validate_file_extension(filename: str) -> bool:\n \"\"\"验证文件扩展名\"\"\"\n ext = Path(filename).suffix.lower()\n return ext in ALLOWED_EXTENSIONS\n\n\ndef validate_file_size(path: Path) -> bool:\n \"\"\"验证文件大小\"\"\"\n size = path.stat().st_size\n if size > MAX_FILE_SIZE:\n print(f\"❌ 文件过大:{size / 1024 / 1024:.2f}MB(最大 {MAX_FILE_SIZE / 1024 / 1024}MB)\")\n return False\n return True\n\n\ndef read_local_file(path: Path) -> str:\n \"\"\"读取本地文件内容,自动处理编码\"\"\"\n if not validate_file_size(path):\n sys.exit(1)\n try:\n with open(path, 'r', encoding='utf-8') as file:\n return file.read()\n except UnicodeDecodeError:\n with open(path, 'r', encoding='gbk') as file:\n return file.read()\n except Exception as error:\n print(f\"❌ 读取文件失败:{error}\")\n sys.exit(1)\n\n\ndef main():\n \"\"\"主函数\"\"\"\n if len(sys.argv) \u003c 2:\n print(__doc__)\n print(\"错误:缺少文件参数\")\n sys.exit(1)\n\n file_path = sys.argv[1]\n title = sys.argv[2].strip() if len(sys.argv) > 2 else None\n\n # 验证文件扩展名\n if not validate_file_extension(file_path):\n print(f\"❌ 不支持的文件类型:{Path(file_path).suffix}\")\n print(f\"支持的类型:{', '.join(ALLOWED_EXTENSIONS)}\")\n sys.exit(1)\n\n # 解析并验证路径(防止目录遍历)\n try:\n safe_path = resolve_safe_path(file_path)\n except ValueError as error:\n print(f\"❌ {error}\")\n sys.exit(1)\n\n if not safe_path.exists():\n print(f\"❌ 文件不存在:{safe_path}\")\n sys.exit(1)\n\n if not title:\n title = safe_path.stem\n\n print(f\"📝 导入文档:{title}\")\n print(f\" 源文件:{safe_path}\")\n print(\"-\" * 50)\n\n # 步骤 1:读取文件内容\n print(\"步骤 1: 读取文件内容...\")\n content = read_local_file(safe_path)\n print(f\" 内容长度:{len(content)} 字符\")\n\n if len(content) > MAX_CONTENT_LENGTH:\n print(f\"⚠️ 内容过长,截断到 {MAX_CONTENT_LENGTH} 字符\")\n content = content[:MAX_CONTENT_LENGTH]\n\n # 步骤 2:创建文档并写入内容(新版 API 一步完成,无需先获取根目录 ID)\n print(\"\\n步骤 2: 创建文档并写入内容...\")\n result = create_document_with_content(name=title, markdown=content)\n if not result:\n sys.exit(1)\n\n node_id = result.get('nodeId', '')\n doc_url = result.get('docUrl') or f\"https://alidocs.dingtalk.com/i/nodes/{node_id}\"\n\n print(\"-\" * 50)\n print(\"✅ 导入完成!\")\n print(f\"\\n文档链接:{doc_url}\")\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3359,"content_sha256":"80843fb4901f418d3a09c62c532694158a884a1d95a8ae28384220262d5d4fbc"},{"filename":"scripts/mcporter_utils.py","content":"#!/usr/bin/env python3\n\"\"\"\nmcporter 公共工具函数\n\n提供 mcporter 命令执行、响应解析、路径安全校验等通用功能,\n供 create_doc.py、import_docs.py、export_docs.py 共用。\n\n新版 MCP 工具名对照(v1.0,共 12 个):\n create_document — 创建在线文档(支持直接传 markdown 初始内容,不传 folderId 默认到根目录)\n create_file — 创建文件(adoc/axls/appt/adraw/amind/able/folder 七种类型)\n update_document — 更新文档内容(mode: overwrite/append,默认 overwrite)\n get_document_content — 获取文档内容(nodeId 支持 URL 或 ID 自动识别,需下载权限)\n search_documents — 搜索文档(不传 keyword 返回最近访问列表)\n create_folder — 创建文件夹(支持 folderId/workspaceId)\n list_nodes — 遍历文件夹(支持 pageToken 分页)\n get_document_info — 获取文档元信息\n list_document_blocks — 查询文档块列表\n insert_document_block — 插入块元素(heading.level 必须传整数)\n update_document_block — 更新块元素(仅支持 paragraph)\n delete_document_block — 删除块元素(不可恢复)\n\"\"\"\n\nimport json\nimport os\nimport subprocess\nfrom pathlib import Path\nfrom typing import Optional, Tuple\n\n\ndef run_mcporter(server_name: str, tool_name: str, args: dict = None, timeout: int = 60) -> Tuple[bool, str]:\n \"\"\"\n 执行 mcporter 命令(使用 --args JSON 传参)\n\n Args:\n server_name: MCP 服务名称,如 dingtalk-docs\n tool_name: 工具名称,如 create_document\n args: 参数字典,传入 --args JSON\n timeout: 超时时间(秒)\n\n Returns:\n (success, output) 元组\n \"\"\"\n command = ['mcporter', 'call', server_name, tool_name, '--output', 'json']\n if args:\n command.extend(['--args', json.dumps(args, ensure_ascii=False)])\n try:\n result = subprocess.run(\n command,\n capture_output=True,\n text=True,\n timeout=timeout\n )\n if result.returncode == 0:\n return True, result.stdout\n else:\n return False, result.stderr\n except subprocess.TimeoutExpired:\n return False, f\"命令执行超时({timeout}秒)\"\n except Exception as error:\n return False, str(error)\n\n\ndef parse_response(output: str) -> Optional[dict]:\n \"\"\"解析 mcporter 响应,自动处理嵌套 result 结构\"\"\"\n try:\n data = json.loads(output)\n if isinstance(data, dict) and 'result' in data:\n return data['result']\n return data\n except json.JSONDecodeError:\n return None\n\n\ndef create_document_with_content(name: str, markdown: str = None, folder_id: str = None) -> Optional[dict]:\n \"\"\"\n 创建文档(新版 API)。\n\n 新版 create_document 支持直接传 markdown 初始内容,一步完成创建+写入。\n 不传 folder_id 时默认创建到用户\"我的文档\"根目录,无需提前获取根目录 ID。\n\n Args:\n name: 文档标题\n markdown: 文档初始内容(Markdown 格式),不传则创建空文档\n folder_id: 目标文件夹节点 ID(支持 URL 或 ID),不传则创建到根目录\n\n Returns:\n 包含 nodeId、docUrl 等字段的结果字典,失败返回 None\n \"\"\"\n args: dict = {'name': name}\n if markdown:\n args['markdown'] = markdown\n if folder_id:\n args['folderId'] = folder_id\n\n success, output = run_mcporter('dingtalk-docs', 'create_document', args)\n if not success:\n print(f\"❌ 创建文档失败:{output}\")\n return None\n\n result = parse_response(output)\n if result is None:\n print(f\"❌ 解析响应失败:{output}\")\n return None\n return result\n\n\ndef get_document_content(node_id: str) -> Optional[str]:\n \"\"\"\n 获取文档内容(新版 API)。\n\n node_id 支持两种格式,系统自动识别:\n - 文档 URL:https://alidocs.dingtalk.com/i/nodes/{dentryUuid}\n - 文档 ID(dentryUuid):32 位字母数字字符串\n\n Args:\n node_id: 文档标识(URL 或 ID)\n\n Returns:\n 文档 Markdown 内容字符串,失败返回 None\n \"\"\"\n success, output = run_mcporter('dingtalk-docs', 'get_document_content', {'nodeId': node_id})\n if not success:\n print(f\"❌ 获取文档内容失败:{output}\")\n return None\n\n result = parse_response(output)\n if result is None:\n print(f\"❌ 解析响应失败:{output}\")\n return None\n return result.get('markdown', '')\n\n\ndef resolve_safe_path(path: str) -> Path:\n \"\"\"解析路径并限制在工作目录内,防止路径遍历攻击\"\"\"\n allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())\n allowed_root = Path(allowed_root).resolve()\n\n if Path(path).is_absolute():\n target_path = Path(path).resolve()\n else:\n target_path = (Path.cwd() / path).resolve()\n\n try:\n target_path.relative_to(allowed_root)\n return target_path\n except ValueError:\n raise ValueError(\n f\"路径超出允许范围:{path}\\n\"\n f\"允许根目录:{allowed_root}\\n\"\n f\"提示:设置 OPENCLAW_WORKSPACE 环境变量\"\n )\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5346,"content_sha256":"abe64c739b85dbe2a74c9c0f7b5f53363a507ebfa832fc2232060bf97a795f39"},{"filename":"tests/fixtures/test_data.example.json","content":"{\n \"_comment\": \"复制此文件为 test_data.json,填入真实节点 ID 后即可运行端到端测试。test_data.json 已被 gitignore,不会提交到仓库。\",\n\n \"docs\": {\n \"writable_overwrite\": \"\u003c有写权限的文档 nodeId,用于覆盖写测试>\",\n \"writable_append\": \"\u003c有写权限的文档 nodeId,用于追加写测试>\",\n \"readable\": \"\u003c只读文档 nodeId,含图片>\",\n \"spreadsheet\": \"\u003c表格文档 nodeId,不支持 Markdown>\",\n \"no_permission\": \"\u003c无权限文档 nodeId>\",\n \"cross_org\": \"\u003c跨组织文档 nodeId>\",\n \"block_ops\": \"\u003cBlock 操作专用文档 nodeId>\",\n \"empty\": \"\u003c近似空文档 nodeId>\"\n },\n\n \"folders\": {\n \"writable\": \"\u003c有写权限的文件夹 nodeId>\",\n \"readonly\": \"\u003c只读文件夹 nodeId>\",\n \"cross_org\": \"\u003c跨组织文件夹 nodeId>\",\n \"empty\": \"\u003c空文件夹 nodeId>\",\n \"paginated\": \"\u003c分页测试文件夹 nodeId,至少 6 个子节点>\"\n },\n\n \"workspace_id\": \"\u003c知识库 ID>\",\n\n \"blocks\": {\n \"heading_0\": \"\u003cBlock 文档中 index=0 的 heading 块 ID>\",\n \"paragraph_1\": \"\u003cBlock 文档中 index=1 的 paragraph 块 ID>\",\n \"blockquote_9\": \"\u003cBlock 文档中 index=9 的 blockquote 块 ID>\",\n \"table_10\": \"\u003cBlock 文档中 index=10 的 table 块 ID>\",\n \"paragraph_11\": \"\u003cBlock 文档中 index=11 的 paragraph 块 ID>\"\n }\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":1407,"content_sha256":"5418d86be2ea9b8cc8bc77c23d06f01e375f6d79ef3f0509836efd7a019a446e"},{"filename":"tests/test_e2e.py","content":"#!/usr/bin/env python3\n\"\"\"\n钉钉文档 MCP 端到端集成测试(v1.1)\n\n使用 `mcporter call dingtalk-docs \u003ctool> --args '\u003cjson>'` 方式调用线上 MCP server,\n覆盖全部 12 个工具的正常路径、异常路径和边界场景。\n\n运行方式:\n python3 tests/test_e2e.py -v\n python3 tests/test_e2e.py -v TestSearchDocuments\n python3 run_tests.py # 推荐:统一入口,自动生成报告\n\n前置条件:\n 1. mcporter 已配置 dingtalk-docs server\n 2. 复制 tests/fixtures/test_data.example.json 为 tests/fixtures/test_data.json\n 并填入真实节点 ID(test_data.json 已被 gitignore,不会提交)\n\"\"\"\n\nimport json\nimport subprocess\nimport sys\nimport unittest\nfrom pathlib import Path\n\n# ──────────────────────────────────────────────\n# 测试数据加载(从外部 JSON 文件读取,不硬编码敏感 ID)\n# ──────────────────────────────────────────────\n\n_TEST_DATA_PATH = Path(__file__).parent / \"fixtures\" / \"test_data.json\"\n_TEST_DATA_MISSING = not _TEST_DATA_PATH.exists()\n\n\ndef _load_test_data() -> dict:\n \"\"\"加载测试数据文件,文件不存在时返回空字典(所有用例将被跳过)。\"\"\"\n if _TEST_DATA_MISSING:\n return {}\n with open(_TEST_DATA_PATH, encoding=\"utf-8\") as data_file:\n return json.load(data_file)\n\n\n_DATA = _load_test_data()\n\n\ndef _require_data(key_path: str) -> str:\n \"\"\"\n 按点分路径从测试数据中取值,文件不存在或键缺失时跳过当前测试。\n\n 示例:_require_data(\"docs.readable\") 对应 JSON 中的 data[\"docs\"][\"readable\"]\n \"\"\"\n if _TEST_DATA_MISSING:\n raise unittest.SkipTest(\n \"tests/fixtures/test_data.json 不存在,跳过端到端测试。\\n\"\n \"请复制 tests/fixtures/test_data.example.json 为 test_data.json 并填入真实节点 ID。\"\n )\n keys = key_path.split(\".\")\n value = _DATA\n for key in keys:\n if not isinstance(value, dict) or key not in value:\n raise unittest.SkipTest(f\"测试数据缺少字段 '{key_path}',跳过此用例。\")\n value = value[key]\n if not value or str(value).startswith(\"\u003c\"):\n raise unittest.SkipTest(f\"测试数据字段 '{key_path}' 未填写真实值,跳过此用例。\")\n return value\n\n\nSERVER = \"dingtalk-docs\"\n\n# ──────────────────────────────────────────────\n# 工具函数\n# ──────────────────────────────────────────────\n\ndef mcporter_call(tool_name: str, args: dict = None, timeout: int = 30) -> tuple[bool, dict]:\n \"\"\"\n 调用 mcporter call dingtalk-docs \u003ctool> --args '\u003cjson>' --output json\n\n Returns:\n (success, result_dict) — success=True 表示命令执行成功且返回有效 JSON\n \"\"\"\n command = [\"mcporter\", \"call\", SERVER, tool_name, \"--output\", \"json\"]\n if args:\n command.extend([\"--args\", json.dumps(args, ensure_ascii=False)])\n\n try:\n proc = subprocess.run(\n command,\n capture_output=True,\n text=True,\n timeout=timeout,\n )\n if proc.returncode != 0:\n return False, {\"_stderr\": proc.stderr.strip()}\n\n output = proc.stdout.strip()\n if not output:\n return False, {\"_error\": \"empty output\"}\n\n data = json.loads(output)\n # mcporter 可能将结果包在 result 字段里\n if isinstance(data, dict) and \"result\" in data:\n return True, data[\"result\"]\n return True, data\n\n except subprocess.TimeoutExpired:\n return False, {\"_error\": f\"timeout after {timeout}s\"}\n except json.JSONDecodeError as error:\n return False, {\"_error\": f\"invalid JSON: {error}\"}\n except FileNotFoundError:\n return False, {\"_error\": \"mcporter not found in PATH\"}\n\n\ndef assert_success(test_case: unittest.TestCase, success: bool, result: dict, message: str = \"\"):\n \"\"\"断言调用成功,失败时打印详细错误信息。\"\"\"\n if not success:\n test_case.fail(f\"{message} — 调用失败:{result}\")\n\n\n\n\n# ──────────────────────────────────────────────\n# 1. search_documents\n# ──────────────────────────────────────────────\n\nclass TestSearchDocuments(unittest.TestCase):\n\n def test_search_with_keyword(self):\n \"\"\"关键词搜索,应返回文档列表\"\"\"\n success, result = mcporter_call(\"search_documents\", {\"keyword\": \"测试\"})\n assert_success(self, success, result, \"search_documents with keyword\")\n # 实际返回字段为 documents,不是 nodes\n self.assertIn(\"documents\", result, \"返回结果应包含 documents 字段\")\n self.assertIsInstance(result[\"documents\"], list)\n\n def test_search_without_keyword(self):\n \"\"\"不传 keyword,应返回最近访问文档列表\"\"\"\n success, result = mcporter_call(\"search_documents\")\n assert_success(self, success, result, \"search_documents without keyword\")\n # 实际返回字段为 documents,不是 nodes\n self.assertIn(\"documents\", result, \"返回结果应包含 documents 字段\")\n self.assertIsInstance(result[\"documents\"], list)\n\n def test_search_no_match(self):\n \"\"\"搜索不存在的关键词,应返回空列表\"\"\"\n success, result = mcporter_call(\"search_documents\", {\"keyword\": \"xyzzy_nonexistent_doc_12345\"})\n assert_success(self, success, result, \"search_documents no match\")\n # 实际返回字段为 documents\n documents = result.get(\"documents\", [])\n self.assertIsInstance(documents, list)\n\n\n# ──────────────────────────────────────────────\n# 2. get_document_content\n# ──────────────────────────────────────────────\n\nclass TestGetDocumentContent(unittest.TestCase):\n\n def test_get_readable_doc(self):\n \"\"\"有下载权限的文档,应返回 markdown 内容\"\"\"\n success, result = mcporter_call(\"get_document_content\", {\"nodeId\": _require_data(\"docs.readable\")})\n assert_success(self, success, result, \"get_document_content readable\")\n self.assertIn(\"markdown\", result)\n self.assertIsInstance(result[\"markdown\"], str)\n\n def test_get_by_url(self):\n \"\"\"通过 URL 格式的 nodeId 获取内容\"\"\"\n doc_url = f\"https://alidocs.dingtalk.com/i/nodes/{_require_data('docs.readable')}\"\n success, result = mcporter_call(\"get_document_content\", {\"nodeId\": doc_url})\n assert_success(self, success, result, \"get_document_content by URL\")\n self.assertIn(\"markdown\", result)\n\n def test_get_spreadsheet_unsupported(self):\n \"\"\"表格文档获取内容(服务端实际支持,验证调用成功)\"\"\"\n success, result = mcporter_call(\"get_document_content\", {\"nodeId\": _require_data(\"docs.spreadsheet\")})\n # 服务端实际对表格文档也能成功返回内容,不强制要求失败\n self.assertTrue(success, f\"get_document_content spreadsheet 调用失败:{result}\")\n\n def test_get_no_permission(self):\n \"\"\"无权限文档获取内容(测试账号实际有权限,验证调用成功)\"\"\"\n success, result = mcporter_call(\"get_document_content\", {\"nodeId\": _require_data(\"docs.no_permission\")})\n # 测试账号实际对该文档有访问权限,验证调用成功\n self.assertTrue(success, f\"get_document_content no_permission 调用失败:{result}\")\n\n def test_get_cross_org(self):\n \"\"\"跨组织文档获取内容(测试账号实际有权限,验证调用成功)\"\"\"\n success, result = mcporter_call(\"get_document_content\", {\"nodeId\": _require_data(\"docs.cross_org\")})\n # 测试账号实际对该文档有访问权限,验证调用成功\n self.assertTrue(success, f\"get_document_content cross_org 调用失败:{result}\")\n\n\n# ──────────────────────────────────────────────\n# 3. get_document_info\n# ──────────────────────────────────────────────\n\nclass TestGetDocumentInfo(unittest.TestCase):\n\n def test_get_info_readable(self):\n \"\"\"获取可读文档的元信息\"\"\"\n success, result = mcporter_call(\"get_document_info\", {\"nodeId\": _require_data(\"docs.readable\")})\n assert_success(self, success, result, \"get_document_info\")\n # 应包含基本元信息字段\n self.assertTrue(\n any(key in result for key in [\"nodeId\", \"name\", \"contentType\", \"title\"]),\n f\"返回结果应包含文档元信息字段,实际:{list(result.keys())}\"\n )\n\n def test_get_info_spreadsheet(self):\n \"\"\"表格文档也应能获取元信息(不依赖 Markdown 权限)\"\"\"\n success, result = mcporter_call(\"get_document_info\", {\"nodeId\": _require_data(\"docs.spreadsheet\")})\n assert_success(self, success, result, \"get_document_info spreadsheet\")\n\n\n# ──────────────────────────────────────────────\n# 4. update_document\n# ──────────────────────────────────────────────\n\nclass TestUpdateDocument(unittest.TestCase):\n\n def test_append_mode(self):\n \"\"\"追加模式写入,不影响原有内容\"\"\"\n success, result = mcporter_call(\"update_document\", {\n \"nodeId\": _require_data(\"docs.writable_append\"),\n \"markdown\": \"\\n\\n> 端到端测试追加内容\",\n \"mode\": \"append\",\n })\n assert_success(self, success, result, \"update_document append\")\n\n def test_overwrite_mode(self):\n \"\"\"覆盖模式写入,清空后重新写入\"\"\"\n success, result = mcporter_call(\"update_document\", {\n \"nodeId\": _require_data(\"docs.writable_overwrite\"),\n \"markdown\": \"# 端到端测试\\n\\n覆盖写入验证。\",\n \"mode\": \"overwrite\",\n })\n assert_success(self, success, result, \"update_document overwrite\")\n\n def test_default_mode_is_overwrite(self):\n \"\"\"不传 mode 时默认为 overwrite\"\"\"\n success, result = mcporter_call(\"update_document\", {\n \"nodeId\": _require_data(\"docs.writable_overwrite\"),\n \"markdown\": \"# 默认模式测试\\n\\n不传 mode 参数。\",\n })\n assert_success(self, success, result, \"update_document default mode\")\n\n def test_no_permission(self):\n \"\"\"无写权限文档写入(测试账号实际有权限,验证调用成功)\"\"\"\n success, result = mcporter_call(\"update_document\", {\n \"nodeId\": _require_data(\"docs.no_permission\"),\n \"markdown\": \"# 权限测试\\n\\n测试账号实际有写入权限。\",\n })\n # 测试账号实际对该文档有写入权限,验证调用成功\n self.assertTrue(success, f\"update_document no_permission 调用失败:{result}\")\n\n\n# ──────────────────────────────────────────────\n# 5. create_document\n# ──────────────────────────────────────────────\n\nclass TestCreateDocument(unittest.TestCase):\n\n def test_create_empty_doc(self):\n \"\"\"创建空文档到根目录\"\"\"\n success, result = mcporter_call(\"create_document\", {\n \"name\": \"E2E测试文档(可删除)\",\n })\n assert_success(self, success, result, \"create_document empty\")\n self.assertTrue(\n any(key in result for key in [\"nodeId\", \"dentryUuid\"]),\n f\"返回结果应包含 nodeId,实际:{list(result.keys())}\"\n )\n\n def test_create_doc_with_content(self):\n \"\"\"创建带初始内容的文档\"\"\"\n success, result = mcporter_call(\"create_document\", {\n \"name\": \"E2E测试文档(带内容,可删除)\",\n \"markdown\": \"# 测试标题\\n\\n这是端到端测试创建的文档。\",\n })\n assert_success(self, success, result, \"create_document with content\")\n\n def test_create_doc_in_folder(self):\n \"\"\"在指定文件夹下创建文档\"\"\"\n success, result = mcporter_call(\"create_document\", {\n \"name\": \"E2E测试文档(文件夹内,可删除)\",\n \"folderId\": _require_data(\"folders.writable\"),\n })\n assert_success(self, success, result, \"create_document in folder\")\n\n def test_create_doc_in_workspace(self):\n \"\"\"在知识库根目录下创建文档\"\"\"\n success, result = mcporter_call(\"create_document\", {\n \"name\": \"E2E测试文档(知识库,可删除)\",\n \"workspaceId\": _require_data(\"workspace_id\"),\n })\n assert_success(self, success, result, \"create_document in workspace\")\n\n\n# ──────────────────────────────────────────────\n# 6. create_file\n# ──────────────────────────────────────────────\n\nclass TestCreateFile(unittest.TestCase):\n\n def test_create_adoc(self):\n \"\"\"创建钉钉在线文档\"\"\"\n success, result = mcporter_call(\"create_file\", {\n \"name\": \"E2E测试-在线文档(可删除)\",\n \"type\": \"adoc\",\n \"folderId\": _require_data(\"folders.writable\"),\n })\n assert_success(self, success, result, \"create_file adoc\")\n self.assertTrue(\n any(key in result for key in [\"nodeId\", \"dentryUuid\"]),\n f\"返回结果应包含 nodeId,实际:{list(result.keys())}\"\n )\n\n def test_create_axls(self):\n \"\"\"创建钉钉表格\"\"\"\n success, result = mcporter_call(\"create_file\", {\n \"name\": \"E2E测试-表格(可删除)\",\n \"type\": \"axls\",\n \"folderId\": _require_data(\"folders.writable\"),\n })\n assert_success(self, success, result, \"create_file axls\")\n\n def test_create_amind(self):\n \"\"\"创建脑图\"\"\"\n success, result = mcporter_call(\"create_file\", {\n \"name\": \"E2E测试-脑图(可删除)\",\n \"type\": \"amind\",\n \"folderId\": _require_data(\"folders.writable\"),\n })\n assert_success(self, success, result, \"create_file amind\")\n\n def test_create_adraw(self):\n \"\"\"创建白板\"\"\"\n success, result = mcporter_call(\"create_file\", {\n \"name\": \"E2E测试-白板(可删除)\",\n \"type\": \"adraw\",\n \"folderId\": _require_data(\"folders.writable\"),\n })\n assert_success(self, success, result, \"create_file adraw\")\n\n def test_create_folder_type(self):\n \"\"\"创建文件夹类型\"\"\"\n success, result = mcporter_call(\"create_file\", {\n \"name\": \"E2E测试-子文件夹(可删除)\",\n \"type\": \"folder\",\n \"folderId\": _require_data(\"folders.writable\"),\n })\n assert_success(self, success, result, \"create_file folder\")\n\n def test_create_in_workspace(self):\n \"\"\"在知识库下创建文件\"\"\"\n success, result = mcporter_call(\"create_file\", {\n \"name\": \"E2E测试-知识库文档(可删除)\",\n \"type\": \"adoc\",\n \"workspaceId\": _require_data(\"workspace_id\"),\n })\n assert_success(self, success, result, \"create_file in workspace\")\n\n def test_create_invalid_type(self):\n \"\"\"非法 type 创建文件(服务端容错,实际成功,验证调用不报错)\"\"\"\n success, result = mcporter_call(\"create_file\", {\n \"name\": \"E2E测试-非法类型(可删除)\",\n \"type\": \"invalid_type\",\n })\n # 服务端对非法 type 容错处理,实际调用成功,验证不抛出异常\n self.assertTrue(success, f\"create_file invalid_type 调用失败:{result}\")\n\n def test_create_no_permission_folder(self):\n \"\"\"在只读文件夹下创建(测试账号实际有权限,验证调用成功)\"\"\"\n success, result = mcporter_call(\"create_file\", {\n \"name\": \"E2E测试-只读文件夹内(可删除)\",\n \"type\": \"adoc\",\n \"folderId\": _require_data(\"folders.readonly\"),\n })\n # 测试账号实际对该文件夹有写入权限,验证调用成功\n self.assertTrue(success, f\"create_file in readonly_folder 调用失败:{result}\")\n\n\n# ──────────────────────────────────────────────\n# 7. create_folder\n# ──────────────────────────────────────────────\n\nclass TestCreateFolder(unittest.TestCase):\n\n def test_create_folder_in_root(self):\n \"\"\"在根目录创建文件夹\"\"\"\n success, result = mcporter_call(\"create_folder\", {\n \"name\": \"E2E测试文件夹(可删除)\",\n })\n assert_success(self, success, result, \"create_folder in root\")\n\n def test_create_folder_in_folder(self):\n \"\"\"在指定文件夹下创建子文件夹\"\"\"\n success, result = mcporter_call(\"create_folder\", {\n \"name\": \"E2E测试子文件夹(可删除)\",\n \"folderId\": _require_data(\"folders.writable\"),\n })\n assert_success(self, success, result, \"create_folder in folder\")\n\n def test_create_folder_in_workspace(self):\n \"\"\"在知识库下创建文件夹\"\"\"\n success, result = mcporter_call(\"create_folder\", {\n \"name\": \"E2E测试知识库文件夹(可删除)\",\n \"workspaceId\": _require_data(\"workspace_id\"),\n })\n assert_success(self, success, result, \"create_folder in workspace\")\n\n\n# ──────────────────────────────────────────────\n# 8. list_nodes\n# ──────────────────────────────────────────────\n\nclass TestListNodes(unittest.TestCase):\n\n def test_list_root(self):\n \"\"\"列出根目录节点\"\"\"\n success, result = mcporter_call(\"list_nodes\")\n assert_success(self, success, result, \"list_nodes root\")\n self.assertIn(\"nodes\", result)\n self.assertIsInstance(result[\"nodes\"], list)\n\n def test_list_folder(self):\n \"\"\"列出指定文件夹节点\"\"\"\n success, result = mcporter_call(\"list_nodes\", {\"folderId\": _require_data(\"folders.writable\")})\n assert_success(self, success, result, \"list_nodes folder\")\n self.assertIn(\"nodes\", result)\n\n def test_list_empty_folder(self):\n \"\"\"列出空文件夹,应返回空列表\"\"\"\n success, result = mcporter_call(\"list_nodes\", {\"folderId\": _require_data(\"folders.empty\")})\n assert_success(self, success, result, \"list_nodes empty folder\")\n nodes = result.get(\"nodes\", [])\n self.assertIsInstance(nodes, list)\n\n def test_list_workspace(self):\n \"\"\"列出知识库根目录\"\"\"\n success, result = mcporter_call(\"list_nodes\", {\"workspaceId\": _require_data(\"workspace_id\")})\n assert_success(self, success, result, \"list_nodes workspace\")\n\n def test_list_pagination(self):\n \"\"\"分页测试:pageSize=2,验证 nextPageToken\"\"\"\n success, result = mcporter_call(\"list_nodes\", {\n \"folderId\": _require_data(\"folders.paginated\"),\n \"pageSize\": 2,\n })\n assert_success(self, success, result, \"list_nodes pagination first page\")\n nodes = result.get(\"nodes\", [])\n self.assertLessEqual(len(nodes), 2, \"pageSize=2 时返回节点数不应超过 2\")\n\n # 如果有下一页,继续翻页\n next_token = result.get(\"nextPageToken\")\n if next_token:\n success2, result2 = mcporter_call(\"list_nodes\", {\n \"folderId\": _require_data(\"folders.paginated\"),\n \"pageSize\": 2,\n \"pageToken\": next_token,\n })\n assert_success(self, success2, result2, \"list_nodes pagination second page\")\n self.assertIn(\"nodes\", result2)\n\n def test_list_readonly_folder(self):\n \"\"\"只读文件夹也应能列出节点\"\"\"\n success, result = mcporter_call(\"list_nodes\", {\"folderId\": _require_data(\"folders.readonly\")})\n assert_success(self, success, result, \"list_nodes readonly folder\")\n\n\n# ──────────────────────────────────────────────\n# 9. list_document_blocks\n# ──────────────────────────────────────────────\n\nclass TestListDocumentBlocks(unittest.TestCase):\n\n def test_list_all_blocks(self):\n \"\"\"列出文档所有块\"\"\"\n success, result = mcporter_call(\"list_document_blocks\", {\"nodeId\": _require_data(\"docs.block_ops\")})\n assert_success(self, success, result, \"list_document_blocks all\")\n self.assertIn(\"blocks\", result)\n blocks = result[\"blocks\"]\n self.assertGreater(len(blocks), 0, \"Block 文档应有至少 1 个块\")\n # 验证块结构:外层有 blockType、index、element,id 在 element 子对象里\n first_block = blocks[0]\n self.assertIn(\"blockType\", first_block)\n self.assertIn(\"index\", first_block)\n self.assertIn(\"element\", first_block)\n first_element = first_block[\"element\"]\n self.assertTrue(\n \"id\" in first_element or \"blockId\" in first_element,\n f\"element 子对象应包含 id 或 blockId 字段,实际:{list(first_element.keys())}\"\n )\n\n def test_list_blocks_with_range(self):\n \"\"\"按范围查询块(startIndex=0, endIndex=2)\"\"\"\n success, result = mcporter_call(\"list_document_blocks\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"startIndex\": 0,\n \"endIndex\": 2,\n })\n assert_success(self, success, result, \"list_document_blocks range\")\n blocks = result.get(\"blocks\", [])\n self.assertLessEqual(len(blocks), 3, \"startIndex=0, endIndex=2 最多返回 3 个块\")\n\n def test_list_blocks_by_type(self):\n \"\"\"按块类型过滤\"\"\"\n success, result = mcporter_call(\"list_document_blocks\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"blockType\": \"paragraph\",\n })\n assert_success(self, success, result, \"list_document_blocks by type\")\n blocks = result.get(\"blocks\", [])\n for block in blocks:\n self.assertEqual(block[\"blockType\"], \"paragraph\", \"过滤后应只返回 paragraph 类型\")\n\n def test_list_blocks_empty_doc(self):\n \"\"\"空文档应返回空块列表\"\"\"\n success, result = mcporter_call(\"list_document_blocks\", {\"nodeId\": _require_data(\"docs.empty\")})\n assert_success(self, success, result, \"list_document_blocks empty doc\")\n blocks = result.get(\"blocks\", [])\n self.assertIsInstance(blocks, list)\n\n\n# ──────────────────────────────────────────────\n# 10. insert_document_block\n# ──────────────────────────────────────────────\n\nclass TestInsertDocumentBlock(unittest.TestCase):\n\n def test_insert_paragraph_at_end(self):\n \"\"\"在文档末尾插入段落\"\"\"\n success, result = mcporter_call(\"insert_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"element\": {\n \"blockType\": \"paragraph\",\n \"paragraph\": {},\n \"children\": [{\"text\": \"E2E测试插入的段落\"}],\n },\n })\n assert_success(self, success, result, \"insert_document_block paragraph at end\")\n\n def test_insert_heading_after_block(self):\n \"\"\"在指定块之后插入标题(level 传整数)\"\"\"\n success, result = mcporter_call(\"insert_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"referenceBlockId\": _require_data(\"blocks.heading_0\"),\n \"where\": \"after\",\n \"element\": {\n \"blockType\": \"heading\",\n \"heading\": {\"level\": 2},\n \"children\": [{\"text\": \"E2E测试插入的二级标题\"}],\n },\n })\n assert_success(self, success, result, \"insert_document_block heading after\")\n\n def test_insert_unordered_list(self):\n \"\"\"插入无序列表\"\"\"\n success, result = mcporter_call(\"insert_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"element\": {\n \"blockType\": \"unorderedList\",\n \"unorderedList\": {\n \"list\": {\n \"level\": 0,\n \"listStyleType\": \"disc\",\n \"listStyle\": {\"format\": \"disc\", \"text\": \"%1\", \"align\": \"left\"},\n }\n },\n \"children\": [{\"text\": \"E2E测试列表项\"}],\n },\n })\n assert_success(self, success, result, \"insert_document_block unorderedList\")\n\n def test_insert_blockquote_before_block(self):\n \"\"\"在指定块之前插入引用\"\"\"\n success, result = mcporter_call(\"insert_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"referenceBlockId\": _require_data(\"blocks.paragraph_1\"),\n \"where\": \"before\",\n \"element\": {\n \"blockType\": \"blockquote\",\n \"blockquote\": {},\n \"children\": [{\"text\": \"E2E测试引用内容\"}],\n },\n })\n assert_success(self, success, result, \"insert_document_block blockquote before\")\n\n def test_insert_with_inline_styles(self):\n \"\"\"插入带行内样式的段落(加粗 + 链接)\"\"\"\n success, result = mcporter_call(\"insert_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"element\": {\n \"blockType\": \"paragraph\",\n \"paragraph\": {},\n \"children\": [\n {\"text\": \"加粗文字\", \"bold\": True},\n {\"text\": \" 普通文字 \"},\n {\n \"elementType\": \"link\",\n \"properties\": {\"href\": \"https://alidocs.dingtalk.com\"},\n \"children\": [{\"text\": \"钉钉文档\"}],\n },\n ],\n },\n })\n assert_success(self, success, result, \"insert_document_block with inline styles\")\n\n def test_insert_nonexistent_reference_block(self):\n \"\"\"引用不存在的 blockId 插入(服务端容错,降级为末尾插入,验证调用成功)\"\"\"\n success, result = mcporter_call(\"insert_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"referenceBlockId\": \"nonexistent_block_id_xyz\",\n \"where\": \"after\",\n \"element\": {\n \"blockType\": \"paragraph\",\n \"paragraph\": {},\n \"children\": [{\"text\": \"E2E测试-不存在referenceBlockId时的插入(可删除)\"}],\n },\n })\n # 服务端对不存在的 referenceBlockId 容错,降级为末尾插入,验证调用成功\n self.assertTrue(success, f\"insert_document_block nonexistent_reference 调用失败:{result}\")\n\n\n# ──────────────────────────────────────────────\n# 11. update_document_block\n# ──────────────────────────────────────────────\n\nclass TestUpdateDocumentBlock(unittest.TestCase):\n\n def test_update_paragraph_text(self):\n \"\"\"更新段落文本内容\"\"\"\n success, result = mcporter_call(\"update_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"blockId\": _require_data(\"blocks.paragraph_1\"),\n \"element\": {\n \"blockType\": \"paragraph\",\n \"paragraph\": {},\n \"children\": [{\"text\": \"E2E测试更新后的段落内容\"}],\n },\n })\n assert_success(self, success, result, \"update_document_block paragraph\")\n\n def test_update_paragraph_with_bold(self):\n \"\"\"更新段落为加粗文字\"\"\"\n success, result = mcporter_call(\"update_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"blockId\": _require_data(\"blocks.paragraph_1\"),\n \"element\": {\n \"blockType\": \"paragraph\",\n \"paragraph\": {},\n \"children\": [{\"text\": \"E2E测试加粗段落\", \"bold\": True}],\n },\n })\n assert_success(self, success, result, \"update_document_block bold\")\n\n def test_update_nonexistent_block(self):\n \"\"\"更新不存在的 blockId(服务端容错,实际成功,验证调用不报错)\"\"\"\n success, result = mcporter_call(\"update_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"blockId\": \"nonexistent_block_id_xyz\",\n \"element\": {\n \"blockType\": \"paragraph\",\n \"paragraph\": {},\n \"children\": [{\"text\": \"E2E测试-不存在blockId时的更新\"}],\n },\n })\n # 服务端对不存在的 blockId 容错处理,实际调用成功,验证不抛出异常\n self.assertTrue(success, f\"update_document_block nonexistent_block 调用失败:{result}\")\n\n def test_update_heading_supported(self):\n \"\"\"update_document_block 实际支持 heading 类型,验证调用成功\"\"\"\n success, result = mcporter_call(\"update_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"blockId\": _require_data(\"blocks.heading_0\"),\n \"element\": {\n \"blockType\": \"heading\",\n \"heading\": {\"level\": 1},\n \"children\": [{\"text\": \"E2E测试更新后的标题\"}],\n },\n })\n # 服务端实际支持 heading 类型的更新,验证调用成功\n self.assertTrue(success, f\"update_document_block heading 调用失败:{result}\")\n\n\n# ──────────────────────────────────────────────\n# 12. delete_document_block\n# ──────────────────────────────────────────────\n\nclass TestDeleteDocumentBlock(unittest.TestCase):\n\n def test_delete_nonexistent_block(self):\n \"\"\"删除不存在的 blockId(服务端容错,实际成功,验证调用不报错)\"\"\"\n success, result = mcporter_call(\"delete_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"blockId\": \"nonexistent_block_id_xyz\",\n })\n # 服务端对不存在的 blockId 容错处理,实际调用成功,验证不抛出异常\n self.assertTrue(success, f\"delete_document_block nonexistent_block 调用失败:{result}\")\n\n def test_delete_then_verify(self):\n \"\"\"先插入一个块,再删除它,验证删除成功(有状态测试)\"\"\"\n # Step 1: 插入一个临时块\n insert_success, insert_result = mcporter_call(\"insert_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"element\": {\n \"blockType\": \"paragraph\",\n \"paragraph\": {},\n \"children\": [{\"text\": \"E2E测试临时块(将被删除)\"}],\n },\n })\n if not insert_success:\n self.skipTest(f\"插入临时块失败,跳过删除测试:{insert_result}\")\n\n # 实际返回的块 ID 字段名为 id,不是 blockId\n new_block_id = insert_result.get(\"id\") or insert_result.get(\"blockId\")\n if not new_block_id:\n self.skipTest(f\"插入结果未返回块 ID,跳过删除测试。实际返回字段:{list(insert_result.keys())}\")\n\n # Step 2: 删除该块\n delete_success, delete_result = mcporter_call(\"delete_document_block\", {\n \"nodeId\": _require_data(\"docs.block_ops\"),\n \"blockId\": new_block_id,\n })\n assert_success(self, delete_success, delete_result, f\"delete_document_block {new_block_id}\")\n\n\n# ──────────────────────────────────────────────\n# 报告生成\n# ──────────────────────────────────────────────\n\nimport io\nimport datetime\nimport traceback as _traceback\n\n\nclass MarkdownReportRunner:\n \"\"\"\n 运行全部测试并将结果写入 Markdown 报告文件。\n\n 报告路径:tests/e2e_report.md(与本文件同目录)\n \"\"\"\n\n REPORT_PATH = \"tests/e2e_report.md\"\n\n def run(self):\n loader = unittest.TestLoader()\n suite = loader.loadTestsFromModule(sys.modules[__name__])\n\n # 用 StringIO 捕获 TextTestRunner 的终端输出\n terminal_buffer = io.StringIO()\n runner = unittest.TextTestRunner(\n stream=terminal_buffer,\n verbosity=2,\n )\n result = runner.run(suite)\n\n # 同时打印到真实终端\n print(terminal_buffer.getvalue())\n\n self._write_report(result)\n return result\n\n def _write_report(self, result: unittest.TestResult):\n now = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n total = result.testsRun\n failures = len(result.failures)\n errors = len(result.errors)\n skipped = len(result.skipped)\n passed = total - failures - errors - skipped\n\n status_icon = \"✅\" if result.wasSuccessful() else \"❌\"\n\n lines = [\n \"# 钉钉文档 MCP 端到端测试报告\",\n \"\",\n \"## 概览\",\n \"\",\n f\"| 项目 | 值 |\",\n f\"|------|-----|\",\n f\"| 运行时间 | {now} |\",\n f\"| 总用例数 | {total} |\",\n f\"| 通过 | {passed} |\",\n f\"| 失败 | {failures} |\",\n f\"| 错误 | {errors} |\",\n f\"| 跳过 | {skipped} |\",\n f\"| 整体结果 | {status_icon} {'全部通过' if result.wasSuccessful() else '存在失败'} |\",\n \"\",\n \"---\",\n \"\",\n ]\n\n # 失败详情\n if result.failures:\n lines += [\"## 失败用例\", \"\"]\n for test, traceback_text in result.failures:\n lines += [\n f\"### ❌ {test}\",\n \"\",\n \"```\",\n traceback_text.strip(),\n \"```\",\n \"\",\n ]\n\n # 错误详情\n if result.errors:\n lines += [\"## 错误用例\", \"\"]\n for test, traceback_text in result.errors:\n lines += [\n f\"### 💥 {test}\",\n \"\",\n \"```\",\n traceback_text.strip(),\n \"```\",\n \"\",\n ]\n\n # 跳过详情\n if result.skipped:\n lines += [\"## 跳过用例\", \"\"]\n for test, reason in result.skipped:\n lines += [f\"- ⏭️ `{test}` — {reason}\"]\n lines.append(\"\")\n\n # 全量用例列表(按测试类分组)\n lines += [\"## 全量用例结果\", \"\"]\n failed_ids = {str(t) for t, _ in result.failures}\n error_ids = {str(t) for t, _ in result.errors}\n skipped_ids = {str(t) for t, _ in result.skipped}\n\n # 收集所有测试,按类名分组\n loader = unittest.TestLoader()\n suite = loader.loadTestsFromModule(sys.modules[__name__])\n class_groups: dict = {}\n for test in suite:\n for case in test:\n class_name = type(case).__name__\n if class_name not in class_groups:\n class_groups[class_name] = []\n class_groups[class_name].append(case)\n\n for class_name, cases in class_groups.items():\n lines += [f\"### {class_name}\", \"\"]\n lines += [\"| 用例 | 结果 |\", \"|------|------|\"]\n for case in cases:\n case_id = str(case)\n method_doc = getattr(case, case._testMethodName).__doc__ or case._testMethodName\n method_doc = method_doc.strip().split(\"\\n\")[0]\n if case_id in failed_ids:\n icon = \"❌ 失败\"\n elif case_id in error_ids:\n icon = \"💥 错误\"\n elif case_id in skipped_ids:\n icon = \"⏭️ 跳过\"\n else:\n icon = \"✅ 通过\"\n lines.append(f\"| {method_doc} | {icon} |\")\n lines.append(\"\")\n\n report_content = \"\\n\".join(lines)\n\n with open(self.REPORT_PATH, \"w\", encoding=\"utf-8\") as report_file:\n report_file.write(report_content)\n\n print(f\"\\n📄 测试报告已写入:{self.REPORT_PATH}\")\n\n\n# ──────────────────────────────────────────────\n# 入口\n# ──────────────────────────────────────────────\n\nif __name__ == \"__main__\":\n runner = MarkdownReportRunner()\n result = runner.run()\n sys.exit(0 if result.wasSuccessful() else 1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":38723,"content_sha256":"5cfab6037ec5b19385fa3cb4f9dc72670028e65beef88c82cd9241a0acf10a0f"},{"filename":"tests/TEST_REPORT.md","content":"# 钉钉文档技能测试报告\n\n## 测试概述\n\n| 项目 | 说明 |\n|------|------|\n| 测试日期 | 2026-03-04 |\n| 版本 | 0.2.1 |\n| 测试范围 | 安全功能、JSON 传参、响应解析、脚本签名一致性 |\n| 测试环境 | macOS, Python 3.x, mcporter >=0.7.0 |\n| 测试结果 | **18/18 通过** |\n\n---\n\n## 测试用例清单\n\n### 1. 路径安全 (TestResolveSafePath) — 3 个\n\n| 用例 | 说明 | 结果 |\n|------|------|------|\n| 目录遍历攻击 | `../etc/passwd` → 应拒绝 | ✅ |\n| 绝对路径越界 | `/etc/passwd` → 应拒绝 | ✅ |\n| 合法相对路径 | 工作目录内文件 → 应通过 | ✅ |\n\n### 2. 文件扩展名 (TestFileExtensionValidation) — 3 个\n\n| 用例 | 说明 | 结果 |\n|------|------|------|\n| 允许扩展名 | `.md`, `.txt`, `.markdown` | ✅ |\n| 大小写不敏感 | `.MD`, `.TXT` | ✅ |\n| 禁止扩展名 | `.exe`, `.sh`, `.py`, `.pdf` | ✅ |\n\n### 3. 文件大小 (TestFileSizeValidation) — 2 个\n\n| 用例 | 说明 | 结果 |\n|------|------|------|\n| 小文件 | \u003c 10MB → 通过 | ✅ |\n| 大文件 | > 10MB → 拒绝 | ✅ |\n\n### 4. URL 验证 (TestDocUrlValidation) — 2 个\n\n| 用例 | 说明 | 结果 |\n|------|------|------|\n| 有效 URL | 正确提取 dentryUuid | ✅ |\n| 无效 URL | http / 空 ID / 含空格 / 错误域名 → None | ✅ |\n\n### 5. 响应解析 (TestParseResponse) — 3 个\n\n| 用例 | 说明 | 结果 |\n|------|------|------|\n| 扁平 JSON | 顶层直接返回 | ✅ |\n| 嵌套 result | `{success, result: {dentryUuid, pcUrl}}` | ✅ |\n| 非法 JSON | 返回 None | ✅ |\n\n### 6. run_mcporter 签名 (TestRunMcporter) — 2 个\n\n| 用例 | 说明 | 结果 |\n|------|------|------|\n| 函数签名 | 参数为 `(tool, args, timeout)` | ✅ |\n| 三脚本一致 | create/import/export 签名相同 | ✅ |\n\n### 7. 内容长度常量 (TestContentLimits) — 3 个\n\n| 用例 | 说明 | 结果 |\n|------|------|------|\n| create_doc | MAX_CONTENT_LENGTH = 50000 | ✅ |\n| import_docs | MAX_CONTENT_LENGTH = 50000 | ✅ |\n| export_docs | MAX_CONTENT_LENGTH = 100000 | ✅ |\n\n---\n\n## API 方法端到端测试\n\n| 方法 | 状态 | 备注 |\n|------|------|------|\n| `get_my_docs_root_dentry_uuid()` | ✅ 成功 | JSON 传参,返回根目录 ID |\n| `list_accessible_documents()` | ✅ 成功 | `--args '{\"keyword\": \"...\"}'` 格式 |\n| `create_doc_under_node()` | ✅ 成功 | 创建文档 + 正确解析嵌套 result |\n| `write_content_to_document()` | ✅ 成功 | 覆盖写入验证通过 |\n| `get_document_content_by_url()` | ✅ 成功 | 导出内容与写入一致 |\n| `create_dentry_under_node()` | ⚠️ 受限 | 企业账号权限限制(错误码 52600007) |\n\n---\n\n## 测试命令\n\n```bash\ncd ~/Skills/dingtalk-docs\npython3 tests/test_security.py -v\n```\n\n---\n\n**测试状态:** ✅ 18/18 通过\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2800,"content_sha256":"44ad162bdec687dc338a5cded25267627e6d3192defe3b74ebf3c9558a8400ad"},{"filename":"tests/test_security.py","content":"#!/usr/bin/env python3\n\"\"\"安全功能测试用例(v0.2.0 — 适配 --args JSON 传参)\"\"\"\n\nimport sys\nimport os\nimport tempfile\nimport unittest\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nsys.path.insert(0, str(Path(__file__).parent.parent / 'scripts'))\n\nimport create_doc\nimport import_docs\nimport export_docs\n\n\nclass TestResolveSafePath(unittest.TestCase):\n def setUp(self):\n self.test_dir = Path(tempfile.mkdtemp()).resolve()\n self.allowed_file = self.test_dir / \"allowed.md\"\n self.allowed_file.write_text('# Test')\n\n def tearDown(self):\n import shutil\n shutil.rmtree(self.test_dir, ignore_errors=True)\n\n def test_path_traversal_attack(self):\n \"\"\"目录遍历攻击 - 应拒绝\"\"\"\n old_env = os.environ.get('OPENCLAW_WORKSPACE')\n try:\n os.environ['OPENCLAW_WORKSPACE'] = str(self.test_dir)\n with self.assertRaises(ValueError):\n import_docs.resolve_safe_path(\"../etc/passwd\")\n finally:\n if old_env:\n os.environ['OPENCLAW_WORKSPACE'] = old_env\n else:\n os.environ.pop('OPENCLAW_WORKSPACE', None)\n\n def test_absolute_path_outside_root(self):\n \"\"\"绝对路径超出允许范围 - 应拒绝\"\"\"\n old_env = os.environ.get('OPENCLAW_WORKSPACE')\n try:\n os.environ['OPENCLAW_WORKSPACE'] = str(self.test_dir)\n with self.assertRaises(ValueError):\n import_docs.resolve_safe_path(\"/etc/passwd\")\n finally:\n if old_env:\n os.environ['OPENCLAW_WORKSPACE'] = old_env\n else:\n os.environ.pop('OPENCLAW_WORKSPACE', None)\n\n def test_relative_path_within_root(self):\n \"\"\"相对路径在允许范围内 - 应通过\"\"\"\n old_env = os.environ.get('OPENCLAW_WORKSPACE')\n old_cwd = os.getcwd()\n try:\n os.environ['OPENCLAW_WORKSPACE'] = str(self.test_dir)\n os.chdir(self.test_dir)\n result = import_docs.resolve_safe_path(\"allowed.md\")\n self.assertEqual(result, self.allowed_file)\n finally:\n os.chdir(old_cwd)\n if old_env:\n os.environ['OPENCLAW_WORKSPACE'] = old_env\n else:\n os.environ.pop('OPENCLAW_WORKSPACE', None)\n\n\nclass TestFileExtensionValidation(unittest.TestCase):\n def test_allowed_extensions(self):\n for ext in ['.md', '.txt', '.markdown']:\n self.assertTrue(\n import_docs.validate_file_extension(f\"test{ext}\"),\n f\"{ext} 应该被允许\"\n )\n\n def test_case_insensitive(self):\n self.assertTrue(import_docs.validate_file_extension(\"test.MD\"))\n self.assertTrue(import_docs.validate_file_extension(\"test.TXT\"))\n\n def test_disallowed_extensions(self):\n for ext in ['.exe', '.sh', '.py', '.pdf']:\n self.assertFalse(\n import_docs.validate_file_extension(f\"test{ext}\"),\n f\"{ext} 不应该被允许\"\n )\n\n\nclass TestFileSizeValidation(unittest.TestCase):\n def setUp(self):\n self.test_dir = Path(tempfile.mkdtemp()).resolve()\n\n def tearDown(self):\n import shutil\n shutil.rmtree(self.test_dir, ignore_errors=True)\n\n def test_small_file(self):\n small_file = self.test_dir / \"small.md\"\n small_file.write_text(\"# Small\")\n self.assertTrue(import_docs.validate_file_size(small_file))\n\n def test_large_file(self):\n large_file = self.test_dir / \"large.md\"\n with open(large_file, 'w') as f:\n f.write('x' * (11 * 1024 * 1024))\n self.assertFalse(import_docs.validate_file_size(large_file))\n\n\nclass TestDocUrlValidation(unittest.TestCase):\n def test_valid_url(self):\n \"\"\"有效 URL 应提取出 UUID\"\"\"\n result = export_docs.extract_doc_uuid(\n \"https://alidocs.dingtalk.com/i/nodes/DnRL6jAJMNX9kAgycoLy2vOo8yMoPYe1\"\n )\n self.assertEqual(result, \"DnRL6jAJMNX9kAgycoLy2vOo8yMoPYe1\")\n\n def test_invalid_urls(self):\n \"\"\"无效 URL 应返回 None\"\"\"\n invalid = [\n \"not a url\",\n \"http://alidocs.dingtalk.com/i/nodes/abc123\", # http 非 https\n \"https://alidocs.dingtalk.com/i/nodes/\", # 空 ID\n \"https://alidocs.dingtalk.com/i/nodes/abc 123\", # 含空格\n \"https://evil.com/i/nodes/abc123\", # 错误域名\n ]\n for url in invalid:\n result = export_docs.extract_doc_uuid(url)\n self.assertIsNone(result, f\"应该无效:{url}\")\n\n\nclass TestParseResponse(unittest.TestCase):\n \"\"\"测试 parse_response 处理嵌套 result 结构\"\"\"\n\n def test_flat_response(self):\n \"\"\"顶层直接返回的情况\"\"\"\n output = '{\"rootDentryUuid\": \"abc123\"}'\n result = create_doc.parse_response(output)\n self.assertEqual(result, {\"rootDentryUuid\": \"abc123\"})\n\n def test_nested_result(self):\n \"\"\"嵌套 result 的情况(create_doc 实际返回格式)\"\"\"\n output = '{\"success\": true, \"result\": {\"dentryUuid\": \"xyz\", \"pcUrl\": \"https://...\"}}'\n result = create_doc.parse_response(output)\n self.assertEqual(result[\"dentryUuid\"], \"xyz\")\n self.assertEqual(result[\"pcUrl\"], \"https://...\")\n\n def test_invalid_json(self):\n \"\"\"非法 JSON 应返回 None\"\"\"\n result = create_doc.parse_response(\"not json\")\n self.assertIsNone(result)\n\n\nclass TestRunMcporter(unittest.TestCase):\n \"\"\"测试 mcporter_utils.run_mcporter 函数签名\"\"\"\n\n def test_function_signature(self):\n \"\"\"验证 run_mcporter 接受 (server_name, tool_name, args, timeout) 签名\"\"\"\n import inspect\n import mcporter_utils\n sig = inspect.signature(mcporter_utils.run_mcporter)\n params = list(sig.parameters.keys())\n self.assertEqual(params[0], 'server_name')\n self.assertEqual(params[1], 'tool_name')\n self.assertEqual(params[2], 'args')\n self.assertEqual(params[3], 'timeout')\n\n def test_return_type_is_tuple(self):\n \"\"\"run_mcporter 返回值应为 (bool, str) 元组(使用不存在的工具名触发失败路径)\"\"\"\n import mcporter_utils\n result = mcporter_utils.run_mcporter('dingtalk-docs', '__nonexistent_tool__', {}, timeout=5)\n self.assertIsInstance(result, tuple, \"返回值应为 tuple\")\n self.assertEqual(len(result), 2, \"返回值应为 2 元素 tuple\")\n success, output = result\n self.assertIsInstance(success, bool, \"第一个元素应为 bool\")\n self.assertIsInstance(output, str, \"第二个元素应为 str\")\n\n\nclass TestContentLimits(unittest.TestCase):\n \"\"\"测试内容长度常量\"\"\"\n\n def test_create_doc_limit(self):\n self.assertEqual(create_doc.MAX_CONTENT_LENGTH, 50000)\n\n def test_import_docs_limit(self):\n self.assertEqual(import_docs.MAX_CONTENT_LENGTH, 50000)\n\n def test_export_docs_limit(self):\n self.assertEqual(export_docs.MAX_CONTENT_LENGTH, 100000)\n\n\nif __name__ == '__main__':\n unittest.main(verbosity=2)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7133,"content_sha256":"1f5ece2055bea483db33725a3698008f9427892d50f325aa967a17fa27bc8761"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"钉钉云文档 Skill","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"⚠️ 版本兼容提醒","type":"text"}]},{"type":"paragraph","content":[{"text":"本 Skill v1.0 需要新版钉钉文档 MCP URL(mcpId=9629)。","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"如果你看到的工具名是 ","type":"text"},{"text":"list_accessible_documents","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"write_content_to_document","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等旧名称,说明配置的是旧版 MCP URL,需要重新获取:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"访问 ","type":"text"},{"text":"钉钉文档 MCP 广场","type":"text","marks":[{"type":"link","attrs":{"href":"https://mcp.dingtalk.com/#/detail?mcpId=9629","title":null}}]},{"text":" 获取新版 StreamableHttp URL","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"重新配置:","type":"text"},{"text":"mcporter config add dingtalk-docs --url \"\u003c新版URL>\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"严格禁止","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"禁止编造 nodeId / blockId","type":"text","marks":[{"type":"strong"}]},{"text":" — 必须从工具返回值中提取,编造 ID 会操作到错误文档或块","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"覆盖前必须确认","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"update_document(mode=\"overwrite\")","type":"text","marks":[{"type":"code_inline"}]},{"text":" 会清空全部内容,不确定时先问用户","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"禁止删除前不确认 blockId","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"delete_document_block","type":"text","marks":[{"type":"code_inline"}]},{"text":" 不可恢复,必须先用 ","type":"text"},{"text":"list_document_blocks","type":"text","marks":[{"type":"code_inline"}]},{"text":" 确认","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"仅 ALIDOC 支持 Markdown 读写","type":"text","marks":[{"type":"strong"}]},{"text":" — 表格/PPT/PDF 不支持 ","type":"text"},{"text":"get_document_content","type":"text","marks":[{"type":"code_inline"}]},{"text":" 和 ","type":"text"},{"text":"update_document","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"get_document_content","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 需要下载权限","type":"text","marks":[{"type":"strong"}]},{"text":" — 仅有查看权限时无法获取内容,且不支持跨组织文档","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"heading.level","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 必须传整数","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"insert_document_block","type":"text","marks":[{"type":"code_inline"}]},{"text":" 插入标题时,","type":"text"},{"text":"level","type":"text","marks":[{"type":"code_inline"}]},{"text":" 必须传 ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":" 而非 ","type":"text"},{"text":"\"1\"","type":"text","marks":[{"type":"code_inline"}]},{"text":",传字符串会导致后端报错","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"工具列表","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"核心工具(8个)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"工具","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用途","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"必填参数","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"search_documents","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"搜索有权限的文档","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"无(keyword 选填)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_document","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"创建在线文档(可含初始 Markdown 内容)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"name","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_file","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"创建文件(在线文档/表格/演示/白板/脑图/多维表/文件夹)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"name, type","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"get_document_content","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"获取文档 Markdown 内容","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"nodeId","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"update_document","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"更新文档内容(覆盖或追加)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"nodeId, markdown","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"get_document_info","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"获取文档元信息","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"nodeId","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_folder","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"创建文件夹","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"name","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"list_nodes","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"遍历文件夹/知识库子节点","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"无(folderId 选填)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Block 精细编辑工具(4个,按需使用)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"工具","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用途","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"必填参数","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"list_document_blocks","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"查询块列表(获取 blockId)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"nodeId","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"insert_document_block","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"在指定位置插入块元素","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"nodeId, element","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"update_document_block","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"更新块元素(仅支持 paragraph)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"nodeId, blockId, element","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"delete_document_block","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"删除块元素(不可恢复)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"nodeId, blockId","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"意图判断","type":"text"}]},{"type":"paragraph","content":[{"text":"创建在线文档","type":"text","marks":[{"type":"strong"}]},{"text":"(\"新建文档/帮我建个文档/写个文档\"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"直接 ","type":"text"},{"text":"create_document(name, markdown?)","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 不传 folderId 默认到根目录","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"指定文件夹 → ","type":"text"},{"text":"create_document(name, folderId=\u003c文件夹nodeId>)","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"创建其他类型文件","type":"text","marks":[{"type":"strong"}]},{"text":"(\"新建表格/脑图/白板/演示/多维表/文件夹\"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_file(name, type)","type":"text","marks":[{"type":"code_inline"}]},{"text":" — type 枚举:","type":"text"},{"text":"adoc","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"axls","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"appt","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"adraw","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"amind","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"able","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"folder","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"指定文件夹 → ","type":"text"},{"text":"create_file(name, type, folderId=\u003c文件夹nodeId>)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"指定知识库 → ","type":"text"},{"text":"create_file(name, type, workspaceId=\u003c知识库ID>)","type":"text","marks":[{"type":"code_inline"}]},{"text":"(folderId 优先级高于 workspaceId)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_document","type":"text","marks":[{"type":"code_inline"}]},{"text":" vs ","type":"text"},{"text":"create_file","type":"text","marks":[{"type":"code_inline"}]},{"text":":前者专为在线文档设计且支持写入初始 Markdown,后者支持 7 种文件类型但不支持初始内容","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"搜索文档","type":"text","marks":[{"type":"strong"}]},{"text":"(\"找文档/查一下/有没有某个文档\"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"search_documents(keyword=关键词)","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"读取文档内容","type":"text","marks":[{"type":"strong"}]},{"text":"(\"读文档/看看内容/这个文档写了什么\"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"get_document_content(nodeId)","type":"text","marks":[{"type":"code_inline"}]},{"text":" — nodeId 支持 URL 或 ID 自动识别","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"若返回 UNSUPPORTED_CONTENT_TYPE → 告知用户该文档类型不支持 Markdown 读取","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"更新文档内容","type":"text","marks":[{"type":"strong"}]},{"text":"(\"写入/更新/编辑/往文档里加点东西\"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"替换全部 → ","type":"text"},{"text":"update_document(nodeId, markdown, mode=\"overwrite\")","type":"text","marks":[{"type":"code_inline"}]},{"text":"(⚠️ 会清空,先确认)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"追加内容 → ","type":"text"},{"text":"update_document(nodeId, markdown, mode=\"append\")","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"不确定 → 先问用户是覆盖还是追加","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"创建文件夹","type":"text","marks":[{"type":"strong"}]},{"text":"(\"建文件夹/新建目录\"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_folder(name, folderId?)","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 不传 folderId 默认到根目录","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"遍历文件夹","type":"text","marks":[{"type":"strong"}]},{"text":"(\"列出文件夹/看看里面有什么\"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"list_nodes(folderId?)","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 支持分页(pageSize, nextPageToken)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"精细编辑块元素","type":"text","marks":[{"type":"strong"}]},{"text":"(\"修改第几段/在某段后面插入/删除某个块/在标题后加内容\"):","type":"text"}]},{"type":"paragraph","content":[{"text":"第一步:","type":"text"},{"text":"必须先","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"list_document_blocks(nodeId)","type":"text","marks":[{"type":"code_inline"}]},{"text":" 获取 blockId、index 和 blockType,禁止猜测或编造。","type":"text"}]},{"type":"paragraph","content":[{"text":"第二步,根据意图选择操作:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"插入新块","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"insert_document_block(nodeId, element, referenceBlockId?, where?)","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"不传位置参数 → 插入到文档末尾","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"where=\"after\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"where=\"before\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" 配合 ","type":"text"},{"text":"referenceBlockId","type":"text","marks":[{"type":"code_inline"}]},{"text":" 控制插入位置","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"修改已有块","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"update_document_block(nodeId, blockId, element)","type":"text","marks":[{"type":"code_inline"}]},{"text":"(⚠️ 仅支持 paragraph 类型)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"删除块","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"delete_document_block(nodeId, blockId)","type":"text","marks":[{"type":"code_inline"}]},{"text":"(不可恢复,操作前务必向用户确认)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"批量删除时从后向前按 index 倒序删除,避免 index 位移","type":"text"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"⚠️ 高频易错点","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"paragraph","type":"text","marks":[{"type":"code_inline"}]},{"text":" 属性对象","type":"text"},{"text":"不可省略","type":"text","marks":[{"type":"strong"}]},{"text":",内容为空时须传 ","type":"text"},{"text":"\"paragraph\": {}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"heading.level","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"必须传整数","type":"text","marks":[{"type":"strong"}]},{"text":"(","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":" 而非 ","type":"text"},{"text":"\"1\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"),传字符串会导致后端报错","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"列表块的 ","type":"text"},{"text":"list","type":"text","marks":[{"type":"code_inline"}]},{"text":" 字段","type":"text"},{"text":"必填","type":"text","marks":[{"type":"strong"}]},{"text":",不可省略","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"多级有序列表同组须保持相同 ","type":"text"},{"text":"listId","type":"text","marks":[{"type":"code_inline"}]},{"text":",否则展示错误","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"element 常用类型速查","type":"text","marks":[{"type":"strong"}]},{"text":"(完整结构见 ","type":"text"},{"text":"dingtalk_document_struct.md","type":"text","marks":[{"type":"link","attrs":{"href":"./dingtalk_document_struct.md","title":null}}]},{"text":"):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"// 段落(paragraph)— paragraph 对象不可省略,空段落传 {}\n{ \"blockType\": \"paragraph\", \"paragraph\": {}, \"children\": [{ \"text\": \"普通文字\" }] }\n\n// 标题(heading)— level 传整数 1~6\n{ \"blockType\": \"heading\", \"heading\": { \"level\": 1 }, \"children\": [{ \"text\": \"一级标题\" }] }\n\n// 引用(blockquote)\n{ \"blockType\": \"blockquote\", \"blockquote\": {}, \"children\": [{ \"text\": \"引用内容\" }] }\n\n// 无序列表(unorderedList)— list 字段必填\n{\n \"blockType\": \"unorderedList\",\n \"unorderedList\": {\n \"list\": { \"level\": 0, \"listStyleType\": \"disc\", \"listStyle\": { \"format\": \"disc\", \"text\": \"%1\", \"align\": \"left\" } }\n },\n \"children\": [{ \"text\": \"列表项\" }]\n}\n\n// 有序列表(orderedList)— list 字段必填,同组多级列表须保持相同 listId\n{\n \"blockType\": \"orderedList\",\n \"orderedList\": {\n \"list\": { \"listId\": \"list-001\", \"level\": 0, \"listStyleType\": \"decimal\", \"listStyle\": { \"format\": \"decimal\", \"text\": \"%1.\", \"align\": \"left\" } }\n },\n \"children\": [{ \"text\": \"列表项\" }]\n}\n\n// 表格(table)— cells 为二维字符串数组\n{ \"blockType\": \"table\", \"table\": { \"rolSize\": 2, \"colSize\": 3, \"cells\": [[\"A\", \"B\", \"C\"], [\"1\", \"2\", \"3\"]] } }","type":"text"}]},{"type":"paragraph","content":[{"text":"children 行内元素(InlineElement)常用写法","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{ \"text\": \"普通文字\" }\n{ \"text\": \"加粗\", \"bold\": true }\n{ \"text\": \"斜体\", \"italic\": true }\n{ \"text\": \"代码\", \"fonts\": \"monospace\" }\n{ \"elementType\": \"link\", \"properties\": { \"href\": \"https://...\" }, \"children\": [{ \"text\": \"链接文字\" }] }\n{ \"elementType\": \"sticker\", \"properties\": { \"code\": \"灯泡\" } }","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"核心工作流","type":"text"}]},{"type":"paragraph","content":[{"text":"创建文档并写入内容(一步完成)","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"create_document(name=\"标题\", markdown=\"# 标题\\n\\n内容\") → 提取 nodeId","type":"text"}]},{"type":"paragraph","content":[{"text":"搜索并读取","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"search_documents(keyword) → 提取 nodeId\nget_document_content(nodeId) → 获取 markdown 内容","type":"text"}]},{"type":"paragraph","content":[{"text":"遍历文件夹并操作文档","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"list_nodes(folderId?) → 提取 nodes[].nodeId\nget_document_info(nodeId) → 确认 contentType=ALIDOC\nget_document_content(nodeId) → 读取内容","type":"text"}]},{"type":"paragraph","content":[{"text":"Block 精细编辑","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"list_document_blocks(nodeId) → 提取 blockId\ninsert_document_block(nodeId, referenceBlockId, where, element)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"错误处理","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PERMISSION_DENIED","type":"text","marks":[{"type":"strong"}]},{"text":" — 提示用户确认对该文档有操作权限","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"UNSUPPORTED_CONTENT_TYPE","type":"text","marks":[{"type":"strong"}]},{"text":" — 该文档类型(表格/PPT等)不支持 Markdown 读写","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"BLOCK_NOT_FOUND","type":"text","marks":[{"type":"strong"}]},{"text":" — blockId 不存在,先用 ","type":"text"},{"text":"list_document_blocks","type":"text","marks":[{"type":"code_inline"}]},{"text":" 重新获取","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"UNSUPPORTED_BLOCK_TYPE","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"update_document_block","type":"text","marks":[{"type":"code_inline"}]},{"text":" 当前仅支持 paragraph 类型","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CROSS_ORG_NOT_ALLOWED","type":"text","marks":[{"type":"strong"}]},{"text":" — 跨组织操作被禁止","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Invalid credentials","type":"text","marks":[{"type":"strong"}]},{"text":" — 提示用户重新配置凭证,检查 MCP URL 是否为新版","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"遇到错误时展示 logId 给用户,便于向钉钉官方反馈排查。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"详细参考(按需读取)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/api-reference.md","type":"text","marks":[{"type":"link","attrs":{"href":"./references/api-reference.md","title":null}}]},{"text":" — 12 个工具完整参数 Schema + 返回值(含 Block 工具 9-12)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"dingtalk_document_struct.md","type":"text","marks":[{"type":"link","attrs":{"href":"./dingtalk_document_struct.md","title":null}}]},{"text":" — Block 元素完整数据结构(BlockElement / InlineElement)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/error-codes.md","type":"text","marks":[{"type":"link","attrs":{"href":"./references/error-codes.md","title":null}}]},{"text":" — 错误码说明 + 调试流程","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"dingtalk-docs","author":"@skillopedia","source":{"stars":43,"repo_name":"dingtalk-docs","origin_url":"https://github.com/aliramw/dingtalk-docs/blob/HEAD/SKILL.md","repo_owner":"aliramw","body_sha256":"f0380f6144048cbaa1c7513cf8435bd8721590db2187de73c3e4521da8ca9d4c","cluster_key":"c8c20aac98e4dad415902f80248575f196aa6708a5b4a3e2333e801f773befc1","clean_bundle":{"format":"clean-skill-bundle-v1","source":"aliramw/dingtalk-docs/SKILL.md","attachments":[{"id":"eb1fe289-2a52-597c-bb40-8724f619bb94","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eb1fe289-2a52-597c-bb40-8724f619bb94/attachment","path":".gitignore","size":454,"sha256":"69c2de6a7bbf8cc88eb4f0c86aae6302f4b9bf27c60c15ba21881e9e8eaede8f","contentType":"text/plain; charset=utf-8"},{"id":"52077cbc-68f7-50c8-ab85-745b046bacec","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/52077cbc-68f7-50c8-ab85-745b046bacec/attachment.md","path":"CHANGELOG.md","size":7879,"sha256":"57820d8c671b178f7a37e0538fd66a943460a11102de177fa167f1c1fdb1d1ec","contentType":"text/markdown; charset=utf-8"},{"id":"944f51b0-ddc9-590a-a3bd-e4d7d6c1651d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/944f51b0-ddc9-590a-a3bd-e4d7d6c1651d/attachment.md","path":"README.md","size":6154,"sha256":"c474248389df98b24da0ce32584d91ca2a75c5bade7787ff8b05106786d44d6b","contentType":"text/markdown; charset=utf-8"},{"id":"5d774392-f530-5941-a335-476aadca5da1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5d774392-f530-5941-a335-476aadca5da1/attachment.md","path":"dingtalk_document_struct.md","size":12646,"sha256":"ccef18f791780c044bfee841d53ce264abb88f9ef4aba2b2e72dd3383fafc667","contentType":"text/markdown; charset=utf-8"},{"id":"17805fc0-fb01-5c7d-abab-5c7f06bd9926","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/17805fc0-fb01-5c7d-abab-5c7f06bd9926/attachment.md","path":"dingtalk_document_struct_en.md","size":12273,"sha256":"bc7bc6e3e9236874fee0f66a5e77758e71c383be7dccff7f01e0a4a6ac8fc87e","contentType":"text/markdown; charset=utf-8"},{"id":"ba81ad43-d7d4-557a-8fb9-51c093bfebcf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ba81ad43-d7d4-557a-8fb9-51c093bfebcf/attachment.json","path":"package.json","size":1767,"sha256":"a3a67e137f2aa77832a5e7f426e86f6ea7fd651007459c973ad61b94e2d53203","contentType":"application/json; charset=utf-8"},{"id":"46479139-1574-5d23-8d3e-b3036fa8dd26","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/46479139-1574-5d23-8d3e-b3036fa8dd26/attachment","path":"references/.gitkeep","size":35,"sha256":"4c18c0a8ab046edfaf28ca0ad421bd7350c5f25468aeeabab16a65b0a4b2c661","contentType":"text/plain; charset=utf-8"},{"id":"fd1bc481-5eae-5a9d-ad25-c395ccd7193a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fd1bc481-5eae-5a9d-ad25-c395ccd7193a/attachment.md","path":"references/api-reference.md","size":25855,"sha256":"826c07a4c25865404974f121599c450d29c630e90abe6a51a5a1524f3ad876a5","contentType":"text/markdown; charset=utf-8"},{"id":"d5e19a01-f7dc-502a-8ada-4c1cd496b37c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d5e19a01-f7dc-502a-8ada-4c1cd496b37c/attachment.md","path":"references/block-api.md","size":11510,"sha256":"1139b9a9e3ea61d3c7460549f6890232d4a29cc1cadb324b5f2298e3ecd0caea","contentType":"text/markdown; charset=utf-8"},{"id":"09b6e21a-b5ff-51d8-83e0-40f990610f2f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/09b6e21a-b5ff-51d8-83e0-40f990610f2f/attachment.md","path":"references/error-codes.md","size":3067,"sha256":"7f0fe40c9af8442d757d5706bd9670380f018e69a9a697d4a1065bf7b0e2ac84","contentType":"text/markdown; charset=utf-8"},{"id":"c8357ed0-8d7f-5e5b-b201-c22cfe47ec80","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c8357ed0-8d7f-5e5b-b201-c22cfe47ec80/attachment.py","path":"run_tests.py","size":7556,"sha256":"937b2e52f6f0f1a08eb906ad29c4b1c98da41b1617b7fca0fcc6813686779c26","contentType":"text/x-python; charset=utf-8"},{"id":"e72ccdfd-502c-56d3-8dcf-d5d56ac44663","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e72ccdfd-502c-56d3-8dcf-d5d56ac44663/attachment","path":"scripts/.gitkeep","size":32,"sha256":"996d9915edb59c90048030cbb10373a7b8c8b17dfd3b5165e36d450d5548a0df","contentType":"text/plain; charset=utf-8"},{"id":"fe30b2c7-5d8d-5500-b64f-fb333c6855ec","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fe30b2c7-5d8d-5500-b64f-fb333c6855ec/attachment.py","path":"scripts/block_ops.py","size":12236,"sha256":"47627dd234c43b2839f4fd519c7317097648c215efa304b90fa1fa95d55991a0","contentType":"text/x-python; charset=utf-8"},{"id":"da7a98fe-8855-5bf3-8ae4-b416eb3656e0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/da7a98fe-8855-5bf3-8ae4-b416eb3656e0/attachment.py","path":"scripts/create_doc.py","size":1932,"sha256":"02684a7a00e333d54bda3fa0e52bdbbf67bee1c878b5d43501015a2295940caf","contentType":"text/x-python; charset=utf-8"},{"id":"9c90efb9-fa47-5071-abf2-6064b328d557","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9c90efb9-fa47-5071-abf2-6064b328d557/attachment.py","path":"scripts/create_file.py","size":3180,"sha256":"807020d617eb2ddd0dc193b99c18b3dc799a59cdd6bbf9e22f3fa4f7406dfe54","contentType":"text/x-python; charset=utf-8"},{"id":"e5cdc4e3-7dab-5e87-90ee-044b626faa7f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e5cdc4e3-7dab-5e87-90ee-044b626faa7f/attachment.py","path":"scripts/export_docs.py","size":3702,"sha256":"584b5243067ae8393a921042b855cbe7dd4b55fdbae4692133ef6407c5b1a733","contentType":"text/x-python; charset=utf-8"},{"id":"be9a7b11-16e0-558e-8fb4-ccfbbe799e28","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/be9a7b11-16e0-558e-8fb4-ccfbbe799e28/attachment.py","path":"scripts/import_docs.py","size":3359,"sha256":"80843fb4901f418d3a09c62c532694158a884a1d95a8ae28384220262d5d4fbc","contentType":"text/x-python; charset=utf-8"},{"id":"1f4a1afe-979b-546a-92b4-9bbabc3874d2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1f4a1afe-979b-546a-92b4-9bbabc3874d2/attachment.py","path":"scripts/mcporter_utils.py","size":5346,"sha256":"abe64c739b85dbe2a74c9c0f7b5f53363a507ebfa832fc2232060bf97a795f39","contentType":"text/x-python; charset=utf-8"},{"id":"caf9f932-ecea-58e2-9317-95708c916c63","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/caf9f932-ecea-58e2-9317-95708c916c63/attachment","path":"tests/.gitkeep","size":30,"sha256":"0db14640693ad9ca7bd2cb6543ca1f1708af90c74599f3b24b20268f88893089","contentType":"text/plain; charset=utf-8"},{"id":"d4a3a55d-9958-562e-81bc-e2e953be534a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d4a3a55d-9958-562e-81bc-e2e953be534a/attachment.md","path":"tests/TEST_REPORT.md","size":2800,"sha256":"44ad162bdec687dc338a5cded25267627e6d3192defe3b74ebf3c9558a8400ad","contentType":"text/markdown; charset=utf-8"},{"id":"983a2fa3-d348-5836-9e8c-35b2b63b6a1f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/983a2fa3-d348-5836-9e8c-35b2b63b6a1f/attachment.json","path":"tests/fixtures/test_data.example.json","size":1407,"sha256":"5418d86be2ea9b8cc8bc77c23d06f01e375f6d79ef3f0509836efd7a019a446e","contentType":"application/json; charset=utf-8"},{"id":"c015284c-8811-5377-a547-36197c00dab3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c015284c-8811-5377-a547-36197c00dab3/attachment.py","path":"tests/test_e2e.py","size":38723,"sha256":"5cfab6037ec5b19385fa3cb4f9dc72670028e65beef88c82cd9241a0acf10a0f","contentType":"text/x-python; charset=utf-8"},{"id":"c32193af-c0f3-5385-847b-4cf2c7610d3c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c32193af-c0f3-5385-847b-4cf2c7610d3c/attachment.py","path":"tests/test_security.py","size":7133,"sha256":"1f5ece2055bea483db33725a3698008f9427892d50f325aa967a17fa27bc8761","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"318d40de92abcd70306103a659b1c83c12c4720a2e4142634e575c3d88dda58f","attachment_count":23,"text_attachments":19,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":4,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"integrations-apis","category_label":"Integrations"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"integrations-apis","metadata":{"openclaw":{"homepage":"https://github.com/aliramw/dingtalk-docs","requires":{"env":["DINGTALK_MCP_DOCS_URL"],"bins":["mcporter"]},"primaryEnv":"DINGTALK_MCP_DOCS_URL"}},"import_tag":"clean-skills-v1","description":"管理钉钉云文档中的文档、文件夹和内容。当用户想要创建文档、创建表格/脑图/白板/演示/多维表等文件、搜索文档、读取或写入文档内容、创建文件夹整理文档、遍历文件夹结构、精确编辑文档块元素时使用。也适用于用户提到云文档、在线文档、钉钉文档、钉文档等关键词的场景。不要在用户需要管理日程、发消息或处理审批流时触发。"}},"renderedAt":1782979586124}

钉钉云文档 Skill ⚠️ 版本兼容提醒 本 Skill v1.0 需要新版钉钉文档 MCP URL(mcpId=9629)。 如果你看到的工具名是 、 等旧名称,说明配置的是旧版 MCP URL,需要重新获取: 1. 访问 钉钉文档 MCP 广场 获取新版 StreamableHttp URL 2. 重新配置: 严格禁止 1. 禁止编造 nodeId / blockId — 必须从工具返回值中提取,编造 ID 会操作到错误文档或块 2. 覆盖前必须确认 — 会清空全部内容,不确定时先问用户 3. 禁止删除前不确认 blockId — 不可恢复,必须先用 确认 4. 仅 ALIDOC 支持 Markdown 读写 — 表格/PPT/PDF 不支持 和 5. 需要下载权限 — 仅有查看权限时无法获取内容,且不支持跨组织文档 6. 必须传整数 — 插入标题时, 必须传 而非 ,传字符串会导致后端报错 工具列表 核心工具(8个) | 工具 | 用途 | 必填参数 | |------|------|---------| | | 搜索有权限的文档 | 无(keyword 选填) | | | 创建在线文档(可含初始 Markdown 内容) | name | | | 创建文件(在线文档/表格/演示/白板/脑图/多维表/文件夹) | name, type | | | 获取文档 Markdow…