钉钉 AI 表格操作(新版 MCP) 🚀 5 分钟快速开始 1️⃣ 列出我的表格 2️⃣ 创建新表格 3️⃣ 添加记录 4️⃣ 查询记录 5️⃣ 批量导入 --- 核心概念 按 新版 MCP schema 工作: - Base: - Table: - Field: - Record: 不要再用旧版 。 推荐使用 及以上版本。 输出模式兼容说明: - 可直接调用 - 更低版本需要显式加 - AI 表格 MCP 无论使用哪种模式,返回体本身都是标准 JSON;差异主要在 的输出处理方式 版本守门规则(每个 MCP Server 地址只强制检查一次) 在真正开始任何 AI 表格操作前,必须先检查当前 注册的 MCP server 实际返回的 tools schema。 但这个检查不该每次都重复做;同一个 MCP Server 地址只需要强制检查一次。 一次性检查策略 1. 先读取当前 里 对应的 MCP Server 地址。 2. 用这个地址生成一个本地检查标记(例如基于完整 URL 或其 hash)。 3. 在工作区保存检查结果,例如放到: 建议文件名模式: 4. 如果当前地址对应的检查标记已经存在,并且结果是“已确认新版 schema”,则 跳过重复检查 ,直接继续后续 AI 表格操作。 5. 只有在以下情况才重新强制检查: - 第一次运行,没有检查标记 - 里的 MCP Ser…

)\nALLOWED_FIELD_TYPES = {\n 'text', 'number', 'singleSelect', 'multipleSelect', 'date', 'currency',\n 'user', 'department', 'group', 'progress', 'rating', 'checkbox',\n 'attachment', 'url', 'richText', 'telephone', 'email', 'idCard',\n 'barcode', 'geolocation', 'primaryDoc', 'formula', 'unidirectionalLink',\n 'bidirectionalLink', 'creator', 'lastModifier', 'createdTime', 'lastModifiedTime'\n}\nFIELD_TYPE_ALIASES = {\n 'phone': 'telephone',\n}\nMCPORTER_VERSION_PATTERN = re.compile(r'(\\d+)\\.(\\d+)\\.(\\d+)')\nMCPORTER_TEXT_OUTPUT_CUTOFF = (0, 8, 1)\n\n\ndef resolve_safe_path(path: str, allowed_root: Optional[str] = None) -> Path:\n if allowed_root is None:\n allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())\n\n allowed_root = Path(allowed_root).resolve()\n target_path = Path(path).resolve() if Path(path).is_absolute() else (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\"目标路径:{target_path}\\n\"\n f\"允许根目录:{allowed_root}\\n\"\n f\"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内\"\n )\n\n\ndef validate_resource_id(resource_id: str) -> bool:\n return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))\n\n\ndef validate_dentry_uuid(dentry_uuid: str) -> bool:\n \"\"\"兼容旧测试名;新 schema 实际用于校验 baseId/tableId/fieldId。\"\"\"\n return validate_resource_id(dentry_uuid)\n\n\ndef validate_file_extension(filename: str, allowed_extensions: list) -> bool:\n return any(filename.lower().endswith(ext) for ext in allowed_extensions)\n\n\ndef parse_mcporter_version(raw_text: str) -> Optional[Tuple[int, int, int]]:\n match = MCPORTER_VERSION_PATTERN.search(raw_text)\n if not match:\n return None\n return tuple(int(part) for part in match.groups())\n\n\n@lru_cache(maxsize=1)\ndef get_mcporter_version() -> Optional[Tuple[int, int, int]]:\n for cmd in (['mcporter', '--version'], ['mcporter', 'version']):\n try:\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)\n except (subprocess.TimeoutExpired, FileNotFoundError):\n return None\n\n if result.returncode != 0:\n continue\n\n version = parse_mcporter_version(f\"{result.stdout}\\n{result.stderr}\")\n if version is not None:\n return version\n\n return None\n\n\ndef build_mcporter_call(args: List[str]) -> List[str]:\n cmd = ['mcporter', 'call', 'dingtalk-ai-table']\n version = get_mcporter_version()\n if version is not None and version \u003c MCPORTER_TEXT_OUTPUT_CUTOFF:\n cmd.extend(['--output', 'text'])\n return cmd + args\n\n\ndef safe_json_load(file_path: Path, max_size: int = MAX_FILE_SIZE) -> JsonData:\n file_size = file_path.stat().st_size\n if file_size > max_size:\n raise ValueError(f\"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)\")\n with open(file_path, 'r', encoding='utf-8') as f:\n return json.load(f)\n\n\ndef normalize_field_config(field: Dict[str, Any]) -> Dict[str, Any]:\n normalized = dict(field)\n if 'fieldName' not in normalized and 'name' in normalized:\n normalized['fieldName'] = normalized.pop('name')\n normalized['type'] = FIELD_TYPE_ALIASES.get(normalized.get('type', 'text'), normalized.get('type', 'text'))\n return normalized\n\n\ndef validate_field_config(field: Dict[str, Any]) -> Tuple[bool, str]:\n if not isinstance(field, dict):\n return False, '字段配置必须是对象'\n\n field = normalize_field_config(field)\n\n if 'fieldName' not in field:\n return False, '缺少必需字段:fieldName'\n if not isinstance(field['fieldName'], str) or not field['fieldName'].strip():\n return False, 'fieldName 必须是非空字符串'\n\n field_type = field.get('type', 'text')\n if field_type not in ALLOWED_FIELD_TYPES:\n return False, f\"不支持的字段类型:{field_type}\"\n\n config = field.get('config')\n if config is not None and not isinstance(config, dict):\n return False, 'config 必须是对象'\n\n if field_type in {'singleSelect', 'multipleSelect'}:\n options = (config or {}).get('options')\n if not options or not isinstance(options, list):\n return False, 'singleSelect / multipleSelect 必须提供 config.options 数组'\n\n if field_type in {'unidirectionalLink', 'bidirectionalLink'}:\n linked_sheet_id = (config or {}).get('linkedSheetId')\n if not linked_sheet_id or not validate_resource_id(linked_sheet_id):\n return False, '关联字段必须提供合法的 config.linkedSheetId'\n\n return True, ''\n\n\ndef build_create_fields_payload(base_id: str, table_id: str, fields: List[Dict[str, Any]]) -> Dict[str, Any]:\n payload_fields = []\n for field in fields:\n normalized = normalize_field_config(field)\n item = {\n 'fieldName': normalized['fieldName'].strip(),\n 'type': normalized.get('type', 'text')\n }\n if 'config' in normalized and normalized['config'] is not None:\n item['config'] = normalized['config']\n payload_fields.append(item)\n return {\n 'baseId': base_id,\n 'tableId': table_id,\n 'fields': payload_fields,\n }\n\n\ndef run_mcporter(args: List[str]) -> Optional[Dict[str, Any]]:\n if not args:\n print('错误:空命令')\n return None\n\n cmd = build_mcporter_call(args)\n try:\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)\n if result.returncode != 0:\n print(f\"错误:{result.stderr.strip()}\")\n return None\n try:\n return json.loads(result.stdout)\n except json.JSONDecodeError as e:\n print(f\"无法解析响应:{result.stdout[:200]}...\")\n print(f\"JSON 解析错误:{e}\")\n return None\n except subprocess.TimeoutExpired:\n print('错误:命令执行超时(60 秒)')\n return None\n except FileNotFoundError:\n print('错误:未找到 mcporter 命令,请确认已安装')\n return None\n\n\ndef bulk_add_fields(base_id: str, table_id: str, fields_file: str) -> bool:\n try:\n safe_path = resolve_safe_path(fields_file)\n except ValueError as e:\n print(f\"路径验证失败:{e}\")\n return False\n\n if not validate_file_extension(fields_file, ALLOWED_FILE_EXTENSIONS):\n print(f\"错误:只允许 {', '.join(ALLOWED_FILE_EXTENSIONS)} 文件\")\n return False\n if not safe_path.exists():\n print(f\"错误:文件不存在:{safe_path}\")\n return False\n\n try:\n fields = safe_json_load(safe_path)\n except ValueError as e:\n print(f\"错误:{e}\")\n return False\n except json.JSONDecodeError as e:\n print(f\"错误:JSON 格式无效:{e}\")\n return False\n\n if not isinstance(fields, list) or not fields:\n print('错误:fields.json 必须是非空 JSON 数组')\n return False\n if len(fields) > 15:\n print('错误:单次最多创建 15 个字段,请拆分后重试')\n return False\n\n for i, field in enumerate(fields):\n valid, error = validate_field_config(field)\n if not valid:\n print(f\"错误:字段 #{i+1} 配置无效:{error}\")\n return False\n\n payload = build_create_fields_payload(base_id, table_id, fields)\n result = run_mcporter(['create_fields', '--args', json.dumps(payload, ensure_ascii=False)])\n\n if not result:\n return False\n\n print(json.dumps(result, ensure_ascii=False, indent=2))\n return True\n\n\ndef main():\n if len(sys.argv) != 4:\n print(__doc__)\n print('用法示例:')\n print(' python bulk_add_fields.py basexxx tablexxx fields.json')\n sys.exit(1)\n\n base_id = sys.argv[1]\n table_id = sys.argv[2]\n fields_file = sys.argv[3]\n\n if not validate_resource_id(base_id):\n print('错误:无效的 baseId 格式')\n sys.exit(1)\n if not validate_resource_id(table_id):\n print('错误:无效的 tableId 格式')\n sys.exit(1)\n\n success = bulk_add_fields(base_id, table_id, fields_file)\n sys.exit(0 if success else 1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9288,"content_sha256":"c101c7b58d9c36b393eb37a5c425b63561f56ce9afa2b8f192fd6cd838ea03c6"},{"filename":"scripts/check-schema.sh","content":"#!/bin/bash\n# 自动检查 dingtalk-ai-table MCP schema 版本\n# 一次性检查策略:同一 MCP Server 地址只检查一次\n\nset -e\n\nMCP_URL=\"${DINGTALK_MCP_URL:-}\"\nWORKSPACE=\"${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}\"\nCACHE_DIR=\"$WORKSPACE/.cache/dingtalk-ai-table\"\n\nif [ -z \"$MCP_URL\" ]; then\n echo \"❌ 错误:未设置 DINGTALK_MCP_URL\"\n exit 1\nfi\n\n# 生成 URL hash 作为检查标记\nURL_HASH=$(echo -n \"$MCP_URL\" | md5sum | cut -d' ' -f1)\nCACHE_FILE=\"$CACHE_DIR/schema-check-$URL_HASH.json\"\n\n# 如果已检查过且结果为新版,直接跳过\nif [ -f \"$CACHE_FILE\" ]; then\n RESULT=$(cat \"$CACHE_FILE\" | grep -o '\"status\":\"[^\"]*\"' | cut -d'\"' -f4)\n if [ \"$RESULT\" = \"new_schema\" ]; then\n echo \"✅ 已确认新版 schema(缓存)\"\n exit 0\n fi\nfi\n\n# 执行检查\necho \"🔍 检查 MCP schema 版本...\"\nSCHEMA=$(mcporter list dingtalk-ai-table --schema 2>/dev/null || echo \"\")\n\nif echo \"$SCHEMA\" | grep -q \"list_bases\\|get_base\\|create_records\"; then\n echo \"✅ 确认新版 schema\"\n mkdir -p \"$CACHE_DIR\"\n echo \"{\\\"status\\\":\\\"new_schema\\\",\\\"checked_at\\\":\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\"}\" > \"$CACHE_FILE\"\n exit 0\nelse\n echo \"❌ 检测到旧版 schema\"\n echo \"\"\n echo \"请按以下步骤更新:\"\n echo \"1. 打开:https://mcp.dingtalk.com/#/detail?mcpId=9555&detailType=marketMcpDetail\"\n echo \"2. 点击右侧「获取 MCP Server 配置」\"\n echo \"3. 复制新的 MCP Server 地址\"\n echo \"4. 运行:mcporter config update dingtalk-ai-table --url '\u003c新地址>'\"\n echo \"5. 重新运行此脚本\"\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1563,"content_sha256":"921fc7a94690d2e5942e200a7ae3e02852937c19e8f7c925063b45c71ef3d127"},{"filename":"scripts/import_records.py","content":"#!/usr/bin/env python3\n\"\"\"\n从 CSV / JSON 批量导入记录到钉钉 AI 表格(新 MCP schema)\n\n用法:\n python import_records.py \u003cbaseId> \u003ctableId> data.csv [batch_size]\n python import_records.py \u003cbaseId> \u003ctableId> data.json [batch_size]\n\n说明:\n- CSV 表头默认视为 fieldId\n- JSON 支持两种格式:\n 1. [{\"cells\": {\"fldxxx\": \"value\"}}, ...]\n 2. [{\"fldxxx\": \"value\"}, ...] # 会自动包装成 cells\n\"\"\"\n\nimport sys\nimport csv\nimport json\nimport subprocess\nimport os\nimport re\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom typing import Union, List, Dict, Any, Optional, Tuple\n\nJsonData = Union[List[Any], Dict[str, Any]]\nRecordDict = Dict[str, str]\n\nMAX_FILE_SIZE = 50 * 1024 * 1024\nALLOWED_CSV_EXTENSIONS = ['.csv']\nALLOWED_JSON_EXTENSIONS = ['.json']\nRESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}

钉钉 AI 表格操作(新版 MCP) 🚀 5 分钟快速开始 1️⃣ 列出我的表格 2️⃣ 创建新表格 3️⃣ 添加记录 4️⃣ 查询记录 5️⃣ 批量导入 --- 核心概念 按 新版 MCP schema 工作: - Base: - Table: - Field: - Record: 不要再用旧版 。 推荐使用 及以上版本。 输出模式兼容说明: - 可直接调用 - 更低版本需要显式加 - AI 表格 MCP 无论使用哪种模式,返回体本身都是标准 JSON;差异主要在 的输出处理方式 版本守门规则(每个 MCP Server 地址只强制检查一次) 在真正开始任何 AI 表格操作前,必须先检查当前 注册的 MCP server 实际返回的 tools schema。 但这个检查不该每次都重复做;同一个 MCP Server 地址只需要强制检查一次。 一次性检查策略 1. 先读取当前 里 对应的 MCP Server 地址。 2. 用这个地址生成一个本地检查标记(例如基于完整 URL 或其 hash)。 3. 在工作区保存检查结果,例如放到: 建议文件名模式: 4. 如果当前地址对应的检查标记已经存在,并且结果是“已确认新版 schema”,则 跳过重复检查 ,直接继续后续 AI 表格操作。 5. 只有在以下情况才重新强制检查: - 第一次运行,没有检查标记 - 里的 MCP Ser…

)\nMAX_RECORDS_PER_BATCH = 100\nDEFAULT_BATCH_SIZE = 50\nMCPORTER_VERSION_PATTERN = re.compile(r'(\\d+)\\.(\\d+)\\.(\\d+)')\nMCPORTER_TEXT_OUTPUT_CUTOFF = (0, 8, 1)\n\n\ndef resolve_safe_path(path: str, allowed_root: Optional[str] = None) -> Path:\n if allowed_root is None:\n allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())\n allowed_root = Path(allowed_root).resolve()\n target_path = Path(path).resolve() if Path(path).is_absolute() else (Path.cwd() / path).resolve()\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\"目标路径:{target_path}\\n\"\n f\"允许根目录:{allowed_root}\\n\"\n f\"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内\"\n )\n\n\ndef validate_resource_id(resource_id: str) -> bool:\n return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))\n\n\ndef validate_dentry_uuid(dentry_uuid: str) -> bool:\n return validate_resource_id(dentry_uuid)\n\n\ndef validate_file_extension(filename: str, allowed_extensions: list) -> bool:\n return any(filename.lower().endswith(ext) for ext in allowed_extensions)\n\n\ndef parse_mcporter_version(raw_text: str) -> Optional[Tuple[int, int, int]]:\n match = MCPORTER_VERSION_PATTERN.search(raw_text)\n if not match:\n return None\n return tuple(int(part) for part in match.groups())\n\n\n@lru_cache(maxsize=1)\ndef get_mcporter_version() -> Optional[Tuple[int, int, int]]:\n for cmd in (['mcporter', '--version'], ['mcporter', 'version']):\n try:\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)\n except (subprocess.TimeoutExpired, FileNotFoundError):\n return None\n\n if result.returncode != 0:\n continue\n\n version = parse_mcporter_version(f\"{result.stdout}\\n{result.stderr}\")\n if version is not None:\n return version\n\n return None\n\n\ndef build_mcporter_call(args: List[str]) -> List[str]:\n cmd = ['mcporter', 'call', 'dingtalk-ai-table']\n version = get_mcporter_version()\n if version is not None and version \u003c MCPORTER_TEXT_OUTPUT_CUTOFF:\n cmd.extend(['--output', 'text'])\n return cmd + args\n\n\ndef safe_csv_load(file_path: Path, max_size: int = MAX_FILE_SIZE) -> List[RecordDict]:\n file_size = file_path.stat().st_size\n if file_size > max_size:\n raise ValueError(f\"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)\")\n with open(file_path, 'r', encoding='utf-8', newline='') as f:\n return list(csv.DictReader(f))\n\n\ndef safe_json_load(file_path: Path, max_size: int = MAX_FILE_SIZE) -> JsonData:\n file_size = file_path.stat().st_size\n if file_size > max_size:\n raise ValueError(f\"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)\")\n with open(file_path, 'r', encoding='utf-8') as f:\n return json.load(f)\n\n\ndef sanitize_record_value(value: Any) -> Optional[Union[str, int, float, bool, list, dict]]:\n if value is None:\n return None\n if isinstance(value, (bool, int, float, list, dict)):\n return value\n if not isinstance(value, str):\n return value\n if not value.strip():\n return None\n\n value = value.strip()\n if value.lower() == 'true':\n return True\n if value.lower() == 'false':\n return False\n\n try:\n if '.' in value:\n return float(value)\n return int(value)\n except ValueError:\n return value\n\n\ndef normalize_record(record: Dict[str, Any]) -> Dict[str, Any]:\n if 'cells' in record and isinstance(record['cells'], dict):\n cells = record['cells']\n else:\n cells = record\n normalized = {}\n for key, value in cells.items():\n sanitized = sanitize_record_value(value)\n if sanitized is not None:\n normalized[key] = sanitized\n return {'cells': normalized}\n\n\ndef validate_record(record: Dict[str, Any], headers: List[str]) -> Tuple[bool, str]:\n if not isinstance(record, dict):\n return False, '记录必须是对象'\n normalized = normalize_record(record)\n cells = normalized.get('cells', {})\n if not cells or not isinstance(cells, dict):\n return False, '记录必须包含非空 cells 对象'\n return True, ''\n\n\ndef build_create_records_payload(base_id: str, table_id: str, records: List[Dict[str, Any]]) -> Dict[str, Any]:\n return {\n 'baseId': base_id,\n 'tableId': table_id,\n 'records': [normalize_record(record) for record in records],\n }\n\n\ndef run_mcporter(args: List[str]) -> Optional[Dict[str, Any]]:\n if not args:\n print('错误:空命令')\n return None\n cmd = build_mcporter_call(args)\n try:\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)\n if result.returncode != 0:\n print(f\"错误:{result.stderr.strip()}\")\n return None\n try:\n return json.loads(result.stdout)\n except json.JSONDecodeError as e:\n print(f\"无法解析响应:{result.stdout[:200]}...\")\n print(f\"JSON 解析错误:{e}\")\n return None\n except subprocess.TimeoutExpired:\n print('错误:命令执行超时(120 秒)')\n return None\n except FileNotFoundError:\n print('错误:未找到 mcporter 命令,请确认已安装')\n return None\n\n\ndef import_from_csv(base_id: str, table_id: str, csv_file: str, batch_size: int = DEFAULT_BATCH_SIZE) -> bool:\n try:\n safe_path = resolve_safe_path(csv_file)\n except ValueError as e:\n print(f\"路径验证失败:{e}\")\n return False\n\n if not validate_file_extension(csv_file, ALLOWED_CSV_EXTENSIONS):\n print(f\"错误:只允许 {', '.join(ALLOWED_CSV_EXTENSIONS)} 文件\")\n return False\n if not safe_path.exists():\n print(f\"错误:文件不存在:{safe_path}\")\n return False\n\n try:\n rows = safe_csv_load(safe_path)\n except ValueError as e:\n print(f\"错误:{e}\")\n return False\n except csv.Error as e:\n print(f\"错误:CSV 格式无效:{e}\")\n return False\n\n if not rows:\n print('错误:CSV 文件为空或没有有效数据行')\n return False\n\n records = [normalize_record(row) for row in rows if normalize_record(row)['cells']]\n return import_records(base_id, table_id, records, batch_size)\n\n\ndef import_from_json(base_id: str, table_id: str, json_file: str, batch_size: int = DEFAULT_BATCH_SIZE) -> bool:\n try:\n safe_path = resolve_safe_path(json_file)\n except ValueError as e:\n print(f\"路径验证失败:{e}\")\n return False\n\n if not validate_file_extension(json_file, ALLOWED_JSON_EXTENSIONS):\n print(f\"错误:只允许 {', '.join(ALLOWED_JSON_EXTENSIONS)} 文件\")\n return False\n if not safe_path.exists():\n print(f\"错误:文件不存在:{safe_path}\")\n return False\n\n try:\n records = safe_json_load(safe_path)\n except ValueError as e:\n print(f\"错误:{e}\")\n return False\n except json.JSONDecodeError as e:\n print(f\"错误:JSON 格式无效:{e}\")\n return False\n\n if not isinstance(records, list) or not records:\n print('错误:JSON 文件必须是非空数组')\n return False\n\n for i, record in enumerate(records):\n valid, error = validate_record(record, [])\n if not valid:\n print(f\"错误:记录 #{i+1} 格式无效:{error}\")\n return False\n\n return import_records(base_id, table_id, [normalize_record(r) for r in records], batch_size)\n\n\ndef import_records(base_id: str, table_id: str, records: List[Dict[str, Any]], batch_size: int) -> bool:\n if batch_size \u003c= 0:\n print('错误:batch_size 必须大于 0')\n return False\n if batch_size > MAX_RECORDS_PER_BATCH:\n batch_size = MAX_RECORDS_PER_BATCH\n\n total_batches = (len(records) + batch_size - 1) // batch_size\n success = True\n\n for i in range(0, len(records), batch_size):\n batch = records[i:i + batch_size]\n batch_num = (i // batch_size) + 1\n payload = build_create_records_payload(base_id, table_id, batch)\n result = run_mcporter(['create_records', '--args', json.dumps(payload, ensure_ascii=False)])\n if result:\n print(f\"[{batch_num}/{total_batches}] ✓ 已提交 {len(batch)} 条记录\")\n else:\n print(f\"[{batch_num}/{total_batches}] ✗ 导入失败\")\n success = False\n\n return success\n\n\ndef main():\n if len(sys.argv) \u003c 4 or len(sys.argv) > 5:\n print(__doc__)\n print('用法示例:')\n print(' python import_records.py basexxx tablexxx data.csv 50')\n sys.exit(1)\n\n base_id = sys.argv[1]\n table_id = sys.argv[2]\n input_file = sys.argv[3]\n batch_size = int(sys.argv[4]) if len(sys.argv) == 5 else DEFAULT_BATCH_SIZE\n\n if not validate_resource_id(base_id):\n print('错误:无效的 baseId 格式')\n sys.exit(1)\n if not validate_resource_id(table_id):\n print('错误:无效的 tableId 格式')\n sys.exit(1)\n\n if input_file.lower().endswith('.csv'):\n success = import_from_csv(base_id, table_id, input_file, batch_size)\n elif input_file.lower().endswith('.json'):\n success = import_from_json(base_id, table_id, input_file, batch_size)\n else:\n print('错误:仅支持 .csv 或 .json 文件')\n sys.exit(1)\n\n sys.exit(0 if success else 1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10552,"content_sha256":"0f7852ebf59a7c420d3618ba5fd5df7b5a44b41da36c05347f11a77fe4f58136"},{"filename":"tests/TEST_REPORT.md","content":"# 安全加固测试报告\n\n**技能**: dingtalk-ai-table \n**版本**: 0.3.4 (安全加固版) \n**测试日期**: 2025-02-27 \n**Python 版本**: 3.9.6\n\n---\n\n## 测试概览\n\n| 项目 | 结果 |\n|------|------|\n| 测试用例总数 | 25 |\n| 通过 | 25 ✅ |\n| 失败 | 0 |\n| 错误 | 0 |\n| 覆盖率 | 安全功能 100% |\n\n---\n\n## 测试类别\n\n### 1. 路径安全限制 (7 项测试)\n\n| 测试项 | 描述 | 结果 |\n|--------|------|------|\n| `test_relative_path_within_root` | 相对路径在允许范围内 | ✅ |\n| `test_subdirectory_path` | 子目录路径在允许范围内 | ✅ |\n| `test_absolute_path_within_root` | 绝对路径在允许范围内 | ✅ |\n| `test_path_traversal_attack` | 目录遍历攻击 (`../etc/passwd`) | ✅ 已阻止 |\n| `test_path_traversal_with_dots` | 多层目录遍历攻击 (`../../etc/passwd`) | ✅ 已阻止 |\n| `test_absolute_path_outside_root` | 绝对路径超出允许范围 (`/etc/passwd`) | ✅ 已阻止 |\n| `test_default_allowed_root` | 未指定允许根目录时使用环境变量 | ✅ |\n\n**安全措施**: `resolve_safe_path()` 函数确保所有文件操作限制在 `OPENCLAW_WORKSPACE` 环境变量或当前工作目录内。\n\n---\n\n### 2. UUID 格式验证 (2 项测试)\n\n| 测试项 | 描述 | 结果 |\n|--------|------|------|\n| `test_valid_uuid` | 有效的 UUID (含大小写、带换行) | ✅ |\n| `test_invalid_uuid` | 无效的 UUID (空、短、无连字符、无效字符) | ✅ 已拒绝 |\n\n**验证规则**: `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} dingtalk-ai-table — Skillopedia \n\n---\n\n### 3. 文件扩展名验证 (2 项测试)\n\n| 测试项 | 描述 | 结果 |\n|--------|------|------|\n| `test_allowed_extensions` | 允许的扩展名 (.json, .csv) | ✅ |\n| `test_disallowed_extensions` | 不允许的扩展名 (.txt, .exe, 无扩展名) | ✅ 已拒绝 |\n\n**白名单**:\n- `bulk_add_fields.py`: `['.json']`\n- `import_records.py`: `['.csv', '.json']`\n\n---\n\n### 4. JSON 安全加载 (3 项测试)\n\n| 测试项 | 描述 | 结果 |\n|--------|------|------|\n| `test_valid_json` | 有效的 JSON 文件 | ✅ |\n| `test_file_size_limit` | 文件大小限制 (10MB) | ✅ 已阻止 |\n| `test_invalid_json` | 无效的 JSON 格式 | ✅ 已捕获异常 |\n\n**限制**: 最大 10MB (bulk_add_fields) / 50MB (import_records)\n\n---\n\n### 5. 字段配置验证 (2 项测试)\n\n| 测试项 | 描述 | 结果 |\n|--------|------|------|\n| `test_valid_field_configs` | 有效的字段配置 (11 种类型) | ✅ |\n| `test_invalid_field_configs` | 无效的字段配置 (缺少 name、空 name、无效类型等) | ✅ 已拒绝 |\n\n**允许的字段类型**:\n```\ntext, number, singleSelect, multipleSelect,\ndate, user, attachment, checkbox, phone, email, url\n```\n\n---\n\n### 6. 记录验证 (2 项测试)\n\n| 测试项 | 描述 | 结果 |\n|--------|------|------|\n| `test_valid_record` | 有效的记录格式 | ✅ |\n| `test_invalid_record` | 无效的记录格式 (非对象、缺少 fields 等) | ✅ 已拒绝 |\n\n---\n\n### 7. 记录值清理 (5 项测试)\n\n| 测试项 | 描述 | 结果 |\n|--------|------|------|\n| `test_string_value` | 字符串值保持不变 | ✅ |\n| `test_integer_value` | 整数字符串转换为整数 | ✅ |\n| `test_float_value` | 浮点数字符串转换为浮点数 | ✅ |\n| `test_empty_value` | 空值返回 None | ✅ |\n| `test_whitespace_trimming` | 自动去除首尾空白 | ✅ |\n\n---\n\n### 8. 集成测试 (2 项测试)\n\n| 测试项 | 描述 | 结果 |\n|--------|------|------|\n| `test_bulk_add_fields_workflow` | bulk_add_fields 完整工作流程 | ✅ |\n| `test_import_records_workflow` | import_records 完整工作流程 | ✅ |\n\n---\n\n## 安全改进对比\n\n| 安全维度 | 改进前 | 改进后 |\n|----------|--------|--------|\n| 路径限制 | ❌ 无 | ✅ `resolve_safe_path()` 沙箱 |\n| UUID 验证 | ❌ 无 | ✅ 严格正则验证 |\n| 文件扩展名 | ❌ 无 | ✅ 白名单机制 |\n| 文件大小 | ❌ 无 | ✅ 10MB/50MB 限制 |\n| 字段类型 | ❌ 无 | ✅ 白名单验证 |\n| 命令超时 | ❌ 无 | ✅ 60-120 秒超时 |\n| 输入清理 | ❌ 无 | ✅ 空白修剪、空值处理 |\n| 测试覆盖 | ❌ 无 | ✅ 25 项自动化测试 |\n\n---\n\n## 运行测试\n\n```bash\ncd ~/.openclaw/workspace/skills/dingtalk-ai-table\npython3 tests/test_security.py\n```\n\n---\n\n## 结论\n\n✅ **所有安全加固措施已实施并通过测试**\n\n此次加固显著降低了以下风险:\n1. **目录遍历攻击** - 通过路径沙箱完全阻止\n2. **任意文件读取** - 通过扩展名白名单和路径限制阻止\n3. **命令注入** - 通过 UUID 验证和输入清理降低风险\n4. **DoS 攻击** - 通过文件大小限制和命令超时阻止\n5. **无效数据注入** - 通过字段类型白名单和记录验证阻止\n\n**剩余风险**(已知限制):\n- 依赖 `mcporter` CLI 工具的安全性(无法避免)\n- 钉钉 API 凭证的安全性(需用户妥善保管)\n\n---\n\n**测试执行者**: AI Agent (main - qwen3.5-397b) \n**测试环境**: macOS Darwin 25.3.0 (arm64), Python 3.9.6\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5005,"content_sha256":"e28f0a96c2fff1d447b91a6d9f25ba719145452b99fde3b80216fab9e5b2148c"},{"filename":"tests/test_security.py","content":"#!/usr/bin/env python3\n\"\"\"\n新 MCP schema 下的安全与构造测试\n\"\"\"\n\nimport sys\nimport os\nimport json\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / 'scripts'))\n\nimport bulk_add_fields\nimport import_records\n\n\nclass TestResolveSafePath(unittest.TestCase):\n def setUp(self):\n self.test_dir = Path(tempfile.mkdtemp())\n self.allowed_file = self.test_dir / 'allowed.json'\n self.allowed_file.write_text('[]')\n self.sub_dir = self.test_dir / 'subdir'\n self.sub_dir.mkdir()\n self.sub_file = self.sub_dir / 'data.csv'\n self.sub_file.write_text('a,b\\n1,2')\n\n def tearDown(self):\n import shutil\n shutil.rmtree(self.test_dir, ignore_errors=True)\n\n def test_relative_path_within_root(self):\n original_cwd = os.getcwd()\n try:\n os.chdir(self.test_dir)\n result = bulk_add_fields.resolve_safe_path('allowed.json', str(self.test_dir))\n self.assertEqual(result, self.allowed_file.resolve())\n finally:\n os.chdir(original_cwd)\n\n def test_subdirectory_path(self):\n original_cwd = os.getcwd()\n try:\n os.chdir(self.test_dir)\n result = bulk_add_fields.resolve_safe_path('subdir/data.csv', str(self.test_dir))\n self.assertEqual(result, self.sub_file.resolve())\n finally:\n os.chdir(original_cwd)\n\n def test_absolute_path_within_root(self):\n result = bulk_add_fields.resolve_safe_path(str(self.allowed_file), str(self.test_dir))\n self.assertEqual(result, self.allowed_file.resolve())\n\n def test_path_traversal_attack(self):\n with self.assertRaises(ValueError):\n bulk_add_fields.resolve_safe_path('../etc/passwd', str(self.test_dir))\n\n def test_absolute_path_outside_root(self):\n with self.assertRaises(ValueError):\n bulk_add_fields.resolve_safe_path('/etc/passwd', str(self.test_dir))\n\n\nclass TestResourceIdValidation(unittest.TestCase):\n def test_valid_resource_id(self):\n valid_ids = [\n '123e4567-e89b-12d3-a456-426614174000',\n 'base_example_id_12345678',\n 'tblABC123_-xyz789',\n 'fld_example_12345678\\n',\n ]\n for resource_id in valid_ids:\n with self.subTest(resource_id=resource_id):\n self.assertTrue(bulk_add_fields.validate_resource_id(resource_id))\n self.assertTrue(import_records.validate_resource_id(resource_id))\n\n def test_invalid_resource_id(self):\n invalid_ids = ['', 'short', '含中文', 'has space', 'bad/char', 'a' * 129]\n for resource_id in invalid_ids:\n with self.subTest(resource_id=resource_id):\n self.assertFalse(bulk_add_fields.validate_resource_id(resource_id))\n self.assertFalse(import_records.validate_resource_id(resource_id))\n\n\nclass TestFileExtensionValidation(unittest.TestCase):\n def test_allowed_extensions(self):\n self.assertTrue(bulk_add_fields.validate_file_extension('test.json', ['.json']))\n self.assertTrue(import_records.validate_file_extension('test.csv', ['.csv']))\n self.assertTrue(import_records.validate_file_extension('test.JSON', ['.json']))\n\n def test_disallowed_extensions(self):\n self.assertFalse(bulk_add_fields.validate_file_extension('test.txt', ['.json']))\n self.assertFalse(import_records.validate_file_extension('test.exe', ['.csv']))\n\n\nclass TestSafeJsonLoad(unittest.TestCase):\n def setUp(self):\n self.test_dir = Path(tempfile.mkdtemp())\n\n def tearDown(self):\n import shutil\n shutil.rmtree(self.test_dir, ignore_errors=True)\n\n def test_valid_json(self):\n test_file = self.test_dir / 'valid.json'\n data = [{'fieldName': 'test', 'type': 'text'}]\n test_file.write_text(json.dumps(data))\n self.assertEqual(bulk_add_fields.safe_json_load(test_file), data)\n\n def test_invalid_json(self):\n test_file = self.test_dir / 'invalid.json'\n test_file.write_text('{invalid json}')\n with self.assertRaises(json.JSONDecodeError):\n bulk_add_fields.safe_json_load(test_file)\n\n\nclass TestFieldConfigValidation(unittest.TestCase):\n def test_valid_field_configs(self):\n valid_configs = [\n {'fieldName': '姓名', 'type': 'text'},\n {'name': '数量', 'type': 'number'},\n {'fieldName': '状态', 'type': 'singleSelect', 'config': {'options': [{'name': '高'}]}},\n {'fieldName': '电话', 'type': 'phone'},\n {'fieldName': '负责人', 'type': 'user', 'config': {'multiple': False}},\n ]\n for config in valid_configs:\n valid, error = bulk_add_fields.validate_field_config(config)\n self.assertTrue(valid, f'{config} should be valid: {error}')\n\n def test_invalid_field_configs(self):\n invalid_configs = [\n {'type': 'text'},\n {'fieldName': ''},\n {'fieldName': '状态', 'type': 'singleSelect'},\n {'fieldName': '关联', 'type': 'bidirectionalLink', 'config': {}},\n {'fieldName': 'X', 'type': 'invalid_type'},\n ]\n for config in invalid_configs:\n valid, _ = bulk_add_fields.validate_field_config(config)\n self.assertFalse(valid)\n\n\nclass TestRecordValidation(unittest.TestCase):\n def test_valid_record(self):\n self.assertTrue(import_records.validate_record({'cells': {'fldName': '张三'}}, [])[0])\n self.assertTrue(import_records.validate_record({'fldName': '张三'}, [])[0])\n\n def test_invalid_record(self):\n self.assertFalse(import_records.validate_record({}, [])[0])\n self.assertFalse(import_records.validate_record({'cells': {}}, [])[0])\n self.assertFalse(import_records.validate_record('bad', [])[0])\n\n\nclass TestSanitizeRecordValue(unittest.TestCase):\n def test_string_and_number(self):\n self.assertEqual(import_records.sanitize_record_value('hello'), 'hello')\n self.assertEqual(import_records.sanitize_record_value('123'), 123)\n self.assertEqual(import_records.sanitize_record_value('123.45'), 123.45)\n\n def test_bool_and_empty(self):\n self.assertIs(import_records.sanitize_record_value('true'), True)\n self.assertIs(import_records.sanitize_record_value('false'), False)\n self.assertIsNone(import_records.sanitize_record_value(' '))\n self.assertIsNone(import_records.sanitize_record_value(None))\n\n\nclass TestPayloadBuilders(unittest.TestCase):\n def test_build_create_fields_payload(self):\n payload = bulk_add_fields.build_create_fields_payload('base12345', 'table12345', [\n {'name': '电话', 'type': 'phone'},\n {'fieldName': '状态', 'type': 'singleSelect', 'config': {'options': [{'name': '高'}]}}\n ])\n self.assertEqual(payload['baseId'], 'base12345')\n self.assertEqual(payload['tableId'], 'table12345')\n self.assertEqual(payload['fields'][0]['fieldName'], '电话')\n self.assertEqual(payload['fields'][0]['type'], 'telephone')\n\n def test_build_create_records_payload(self):\n payload = import_records.build_create_records_payload('base12345', 'table12345', [\n {'cells': {'fldName': '张三', 'fldAge': '25'}},\n {'fldName': '李四', 'fldActive': 'true'}\n ])\n self.assertEqual(payload['baseId'], 'base12345')\n self.assertEqual(payload['tableId'], 'table12345')\n self.assertEqual(payload['records'][0]['cells']['fldAge'], 25)\n self.assertEqual(payload['records'][1]['cells']['fldActive'], True)\n\n\nclass TestIntegration(unittest.TestCase):\n def setUp(self):\n self.test_dir = Path(tempfile.mkdtemp())\n os.environ['OPENCLAW_WORKSPACE'] = str(self.test_dir)\n\n def tearDown(self):\n import shutil\n shutil.rmtree(self.test_dir, ignore_errors=True)\n os.environ.pop('OPENCLAW_WORKSPACE', None)\n\n def test_bulk_add_fields_workflow(self):\n fields_file = self.test_dir / 'fields.json'\n fields = [\n {'fieldName': '任务名', 'type': 'text'},\n {'fieldName': '优先级', 'type': 'singleSelect', 'config': {'options': [{'name': '高'}]}},\n ]\n fields_file.write_text(json.dumps(fields), encoding='utf-8')\n loaded = bulk_add_fields.safe_json_load(fields_file)\n payload = bulk_add_fields.build_create_fields_payload('base12345', 'table12345', loaded)\n self.assertEqual(len(payload['fields']), 2)\n\n def test_import_records_workflow(self):\n csv_file = self.test_dir / 'data.csv'\n csv_file.write_text('fldName,fldAge\\nzhangsan,25\\nlisi,30', encoding='utf-8')\n rows = import_records.safe_csv_load(csv_file)\n payload = import_records.build_create_records_payload('base12345', 'table12345', rows)\n self.assertEqual(len(payload['records']), 2)\n self.assertEqual(payload['records'][0]['cells']['fldAge'], 25)\n\n\nif __name__ == '__main__':\n unittest.main(verbosity=2)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9079,"content_sha256":"fa8a5d58b0f51f61954259a8e26123e805b54e7f060ce8c3a117c709dbe43ecb"},{"filename":"tests/test_triggering.py","content":"#!/usr/bin/env python3\n\"\"\"\n触发测试:验证技能在正确的场景下被触发\n\"\"\"\n\nimport unittest\n\nclass TestSkillTriggering(unittest.TestCase):\n \"\"\"测试技能触发条件\"\"\"\n\n # 应该触发技能的查询\n SHOULD_TRIGGER = [\n \"帮我操作钉钉 AI 表格\",\n \"创建一个新的 Base\",\n \"批量导入记录到钉钉表格\",\n \"查询 AI 表格的记录\",\n \"更新多维表的数据\",\n \"添加字段到钉钉表格\",\n \"从 CSV 导入数据\",\n \"搜索我的 Base\",\n \"删除表格中的记录\",\n \"获取表格结构\",\n ]\n\n # 不应该触发技能的查询\n SHOULD_NOT_TRIGGER = [\n \"今天天气怎么样\",\n \"帮我写 Python 代码\",\n \"创建 Excel 文件\",\n \"发送钉钉消息\",\n \"查询数据库\",\n \"生成 PDF 报告\",\n \"翻译这段文字\",\n \"总结这篇文章\",\n ]\n\n def test_should_trigger_queries(self):\n \"\"\"测试应该触发的查询\"\"\"\n for query in self.SHOULD_TRIGGER:\n with self.subTest(query=query):\n # 这里只是记录测试用例\n # 实际触发测试需要在 Claude 环境中进行\n self.assertIsNotNone(query)\n\n def test_should_not_trigger_queries(self):\n \"\"\"测试不应该触发的查询\"\"\"\n for query in self.SHOULD_NOT_TRIGGER:\n with self.subTest(query=query):\n self.assertIsNotNone(query)\n\nif __name__ == '__main__':\n print(\"触发测试用例列表\")\n print(\"\\n✅ 应该触发的查询:\")\n for i, query in enumerate(TestSkillTriggering.SHOULD_TRIGGER, 1):\n print(f\" {i}. {query}\")\n\n print(\"\\n❌ 不应该触发的查询:\")\n for i, query in enumerate(TestSkillTriggering.SHOULD_NOT_TRIGGER, 1):\n print(f\" {i}. {query}\")\n\n print(\"\\n运行单元测试...\")\n unittest.main(verbosity=2)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1925,"content_sha256":"040b5e88bbff069063d326362a1336297936daf1cbf1fd410b20d5d11b620dba"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"钉钉 AI 表格操作(新版 MCP)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"🚀 5 分钟快速开始","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1️⃣ 列出我的表格","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter call '\u003cDINGTALK_MCP_URL>' .list_bases limit=5","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2️⃣ 创建新表格","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter call '\u003cDINGTALK_MCP_URL>' .create_base baseName='我的项目'","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3️⃣ 添加记录","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter call '\u003cDINGTALK_MCP_URL>' .create_records \\\n --args '{\"baseId\":\"base_xxx\",\"tableId\":\"tbl_xxx\",\"records\":[{\"cells\":{\"fld_name\":\"张三\"}}]}'","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4️⃣ 查询记录","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter call '\u003cDINGTALK_MCP_URL>' .query_records \\\n --args '{\"baseId\":\"base_xxx\",\"tableId\":\"tbl_xxx\",\"limit\":10}'","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5️⃣ 批量导入","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/import_records.py base_xxx tbl_xxx data.csv","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"核心概念","type":"text"}]},{"type":"paragraph","content":[{"text":"按 ","type":"text"},{"text":"新版 MCP schema","type":"text","marks":[{"type":"strong"}]},{"text":" 工作:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Base:","type":"text"},{"text":"baseId","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Table:","type":"text"},{"text":"tableId","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Field:","type":"text"},{"text":"fieldId","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Record:","type":"text"},{"text":"recordId","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"不要再用旧版 ","type":"text"},{"text":"dentryUuid / sheetIdOrName / fieldIdOrName","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]},{"type":"paragraph","content":[{"text":"推荐使用 ","type":"text"},{"text":"mcporter 0.8.1","type":"text","marks":[{"type":"code_inline"}]},{"text":" 及以上版本。","type":"text"}]},{"type":"paragraph","content":[{"text":"输出模式兼容说明:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"mcporter 0.8.1+","type":"text","marks":[{"type":"code_inline"}]},{"text":" 可直接调用","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"更低版本需要显式加 ","type":"text"},{"text":"--output text","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"AI 表格 MCP 无论使用哪种模式,返回体本身都是标准 JSON;差异主要在 ","type":"text"},{"text":"mcporter","type":"text","marks":[{"type":"code_inline"}]},{"text":" 的输出处理方式","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"版本守门规则(每个 MCP Server 地址只强制检查一次)","type":"text"}]},{"type":"paragraph","content":[{"text":"在真正开始任何 AI 表格操作前,必须先检查当前 ","type":"text"},{"text":"mcporter","type":"text","marks":[{"type":"code_inline"}]},{"text":" 注册的 ","type":"text"},{"text":"dingtalk-ai-table","type":"text","marks":[{"type":"code_inline"}]},{"text":" MCP server 实际返回的 tools schema。","type":"text"},{"text":"但这个检查不该每次都重复做;同一个 MCP Server 地址只需要强制检查一次。","type":"text","marks":[{"type":"strong"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"一次性检查策略","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"先读取当前 ","type":"text"},{"text":"mcporter","type":"text","marks":[{"type":"code_inline"}]},{"text":" 里 ","type":"text"},{"text":"dingtalk-ai-table","type":"text","marks":[{"type":"code_inline"}]},{"text":" 对应的 MCP Server 地址。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"用这个地址生成一个本地检查标记(例如基于完整 URL 或其 hash)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"在工作区保存检查结果,例如放到:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"~/.openclaw/workspace/.cache/dingtalk-ai-table/","type":"text"}]},{"type":"paragraph","content":[{"text":"建议文件名模式:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"schema-check-\u003curl-hash>.json","type":"text"}]},{"type":"ordered_list","attrs":{"order":4,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"如果当前地址对应的检查标记已经存在,并且结果是“已确认新版 schema”,则","type":"text"},{"text":"跳过重复检查","type":"text","marks":[{"type":"strong"}]},{"text":",直接继续后续 AI 表格操作。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"只有在以下情况才重新强制检查:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"第一次运行,没有检查标记","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"mcporter","type":"text","marks":[{"type":"code_inline"}]},{"text":" 里的 MCP Server 地址变了","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"之前检查结果是旧版 schema / 检查失败","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"用户明确要求重新验证","type":"text"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"强制检查时执行","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter list dingtalk-ai-table --schema","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"判断标准","type":"text"}]},{"type":"paragraph","content":[{"text":"如果返回的 tools 仍然是旧版这一套,例如出现:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"get_root_node_of_my_document","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_base_app","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"list_base_tables","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"add_base_record","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"search_base_record","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"list_base_field","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"或者整体仍然基于:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"dentryUuid","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sheetIdOrName","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"fieldIdOrName","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"那么说明:","type":"text"},{"text":"虽然 skill 文件已经是新版,但 mcporter 里注册的 MCP server 地址还是旧的,不能继续操作。","type":"text","marks":[{"type":"strong"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"遇到旧版 schema 时的强制提示","type":"text"}]},{"type":"paragraph","content":[{"text":"此时必须明确提示用户:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"打开这个页面: ","type":"text"},{"text":"https://mcp.dingtalk.com/#/detail?mcpId=9555&detailType=marketMcpDetail","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"点击右侧 ","type":"text"},{"text":"「获取 MCP Server 配置」","type":"text","marks":[{"type":"strong"}]},{"text":" 按钮","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"复制新的 MCP Server 地址","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"用新的地址替换 ","type":"text"},{"text":"mcporter","type":"text","marks":[{"type":"code_inline"}]},{"text":" 里已经注册的 ","type":"text"},{"text":"dingtalk-ai-table","type":"text","marks":[{"type":"code_inline"}]},{"text":" 地址","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"替换完成后,再重新执行:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter list dingtalk-ai-table --schema","type":"text"}]},{"type":"paragraph","content":[{"text":"只有当返回的 tools 已经变成新版 schema,例如出现:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"list_bases","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"get_base","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"get_tables","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"get_fields","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"query_records","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_records","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"update_records","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"delete_records","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"prepare_attachment_upload","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"才允许继续真正的 AI 表格操作。","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"通过检查后的处理","type":"text"}]},{"type":"paragraph","content":[{"text":"一旦确认当前 MCP Server 地址返回的是新版 schema,就把结果写入本地检查标记。后续只要 ","type":"text"},{"text":"mcporter","type":"text","marks":[{"type":"code_inline"}]},{"text":" 里的 ","type":"text"},{"text":"dingtalk-ai-table","type":"text","marks":[{"type":"code_inline"}]},{"text":" 地址没变,就不要再重复做这一步守门检查。","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"用户提示文案(可直接复用)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"当前 mcporter 里注册的 dingtalk-ai-table 还是旧版 MCP schema,暂时不能按新版技能操作。\n请打开 https://mcp.dingtalk.com/#/detail?mcpId=9555&detailType=marketMcpDetail ,点击右侧“获取 MCP Server 配置”按钮,复制新的 MCP Server 地址,并替换 mcporter 里已注册的 dingtalk-ai-table 地址。替换后重新检查 schema,确认出现 list_bases / get_base / create_records 等新版 tools 后,再继续操作 AI 表格。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"前置要求","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"安装 mcporter CLI","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"npm install -g mcporter\n# 或\nbun install -g mcporter","type":"text"}]},{"type":"paragraph","content":[{"text":"验证:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter --version","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"配置 MCP Server","type":"text"}]},{"type":"paragraph","content":[{"text":"在钉钉 MCP 广场 https://mcp.dingtalk.com/#/detail?mcpId=9555&detailType=marketMcpDetail 获取新版钉钉 AI 表格 MCP 的 ","type":"text"},{"text":"Streamable HTTP URL","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]},{"type":"paragraph","content":[{"text":"方式一:直接配置到 mcporter","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter config add dingtalk-ai-table --url \"\u003cStreamable_HTTP_URL>\"","type":"text"}]},{"type":"paragraph","content":[{"text":"方式二:使用环境变量","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"export DINGTALK_MCP_URL=\"\u003cStreamable_HTTP_URL>\"","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"这个 URL 带访问令牌,等同密码,不要泄露。","type":"text"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"工作区沙箱","type":"text"}]},{"type":"paragraph","content":[{"text":"脚本读取本地文件时,会优先使用 ","type":"text"},{"text":"OPENCLAW_WORKSPACE","type":"text","marks":[{"type":"code_inline"}]},{"text":" 作为允许根目录:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"export OPENCLAW_WORKSPACE=\"$HOME/.openclaw/workspace\"","type":"text"}]},{"type":"paragraph","content":[{"text":"未设置时默认使用当前工作目录。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"核心工具集","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Base 层","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"list_bases","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"search_bases","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"get_base","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_base","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"update_base","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"delete_base","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"search_templates","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Table 层","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"get_tables","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_table","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"update_table","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"delete_table","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Field 层","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"get_fields","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_fields","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"update_field","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"delete_field","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Record 层","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"query_records","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_records","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"update_records","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"delete_records","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"附件层","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"prepare_attachment_upload","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"推荐工作流","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. 先找 Base","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter call dingtalk-ai-table list_bases limit=10\nmcporter call dingtalk-ai-table search_bases query=\"销售\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. 再拿 Table 目录","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter call dingtalk-ai-table get_base baseId=\"base_xxx\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. 再展开表结构","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter call dingtalk-ai-table get_tables \\\n --args '{\"baseId\":\"base_xxx\",\"tableIds\":[\"tbl_xxx\"]}'","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. 字段复杂时读完整配置","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter call dingtalk-ai-table get_fields \\\n --args '{\"baseId\":\"base_xxx\",\"tableId\":\"tbl_xxx\",\"fieldIds\":[\"fld_xxx\"]}'","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. 再查 / 写记录","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter call dingtalk-ai-table query_records \\\n --args '{\"baseId\":\"base_xxx\",\"tableId\":\"tbl_xxx\",\"limit\":20}'\n\nmcporter call dingtalk-ai-table create_records \\\n --args '{\"baseId\":\"base_xxx\",\"tableId\":\"tbl_xxx\",\"records\":[{\"cells\":{\"fld_name\":\"张三\"}}]}'","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. 写入附件字段","type":"text"}]},{"type":"paragraph","content":[{"text":"attachment 字段支持三种写法:","type":"text"}]},{"type":"paragraph","content":[{"text":"方式一:先上传,再写 fileToken(推荐,可靠)","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Step 1:申请上传地址(返回 uploadUrl 和 fileToken)\nmcporter call dingtalk-ai-table prepare_attachment_upload \\\n --args '{\"baseId\":\"base_xxx\",\"fileName\":\"report.pdf\",\"size\":102400,\"mimeType\":\"application/pdf\"}'\n\n# Step 2:把文件 PUT 到 uploadUrl(必须带 Content-Type,值必须与 mimeType 完全一致)\ncurl -X PUT \"\u003cuploadUrl>\" \\\n -H \"Content-Type: application/pdf\" \\\n --data-binary @report.pdf\n\n# Step 3:把 fileToken 写入记录\nmcporter call dingtalk-ai-table create_records \\\n --args '{\"baseId\":\"base_xxx\",\"tableId\":\"tbl_xxx\",\"records\":[{\"cells\":{\"fld_attach\":[{\"fileToken\":\"ft_xxx\"}]}}]}'","type":"text"}]},{"type":"paragraph","content":[{"text":"方式二:直接传外链 URL(异步转存,best-effort)","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mcporter call dingtalk-ai-table create_records \\\n --args '{\"baseId\":\"base_xxx\",\"tableId\":\"tbl_xxx\",\"records\":[{\"cells\":{\"fld_attach\":[{\"url\":\"https://example.com/file.pdf\"}]}}]}'","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"URL 转存是 best-effort 异步链路,返回成功仅表示已受理,不保证立即可读。可靠写入请用 fileToken 方式。","type":"text"}]}]},{"type":"paragraph","content":[{"text":"方式三:原样回传已有附件数据(保留 / 追加已有附件时使用)","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"从 ","type":"text"},{"text":"query_records","type":"text","marks":[{"type":"code_inline"}]},{"text":" 读出的 attachment 单元格数据是完整对象数组,字段形状如下:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"[\n {\n \"filename\": \"a.xlsx\",\n \"size\": 92250,\n \"type\": \"xls\",\n \"resourceId\": \"\u003cid>\",\n \"resourceUrl\": \"\u003cresourceUrl>\"\n }\n]","type":"text"}]},{"type":"paragraph","content":[{"text":"其中 ","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":" 是文件类别枚举,常见值为 ","type":"text"},{"text":"\"xls\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"\"image\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等;","type":"text"},{"text":"resourceUrl","type":"text","marks":[{"type":"code_inline"}]},{"text":" 通常为有时效的下载链接。","type":"text"}]},{"type":"paragraph","content":[{"text":"如需保留已有附件,把读出的值原样塞回即可。如需追加新附件,把新的 ","type":"text"},{"text":"{\"fileToken\":\"ft_xxx\"}","type":"text","marks":[{"type":"code_inline"}]},{"text":" 与已有对象合并成一个数组一起传入。","type":"text"}]},{"type":"paragraph","content":[{"text":"update_records","type":"text","marks":[{"type":"code_inline"}]},{"text":" 的 attachment 字段格式相同,传入后会整体覆盖该字段。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"脚本","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"批量新增字段","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/bulk_add_fields.py \u003cbaseId> \u003ctableId> fields.json","type":"text"}]},{"type":"paragraph","content":[{"text":"fields.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" 示例:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"[\n {\"fieldName\":\"任务名\",\"type\":\"text\"},\n {\"fieldName\":\"优先级\",\"type\":\"singleSelect\",\"config\":{\"options\":[{\"name\":\"高\"},{\"name\":\"中\"},{\"name\":\"低\"}]}}\n]","type":"text"}]},{"type":"paragraph","content":[{"text":"兼容项:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":" 会自动映射为 ","type":"text"},{"text":"fieldName","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"phone","type":"text","marks":[{"type":"code_inline"}]},{"text":" 会自动映射为 ","type":"text"},{"text":"telephone","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"批量导入记录","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/import_records.py \u003cbaseId> \u003ctableId> data.csv\npython3 scripts/import_records.py \u003cbaseId> \u003ctableId> data.json 50","type":"text"}]},{"type":"paragraph","content":[{"text":"说明:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CSV 表头默认按 ","type":"text"},{"text":"fieldId","type":"text","marks":[{"type":"code_inline"}]},{"text":" 解释","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"JSON 支持:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"[{\"cells\": {...}}]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"[{\"fld_xxx\": \"value\"}]","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"安全规则","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"文件路径受 ","type":"text"},{"text":"OPENCLAW_WORKSPACE","type":"text","marks":[{"type":"code_inline"}]},{"text":" 沙箱限制","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"仅允许读取工作区内 ","type":"text"},{"text":".json","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":".csv","type":"text","marks":[{"type":"code_inline"}]},{"text":" 文件","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Base / Table / Field / Record ID 都做格式校验","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"批量上限按 MCP server 实际限制控制:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_fields","type":"text","marks":[{"type":"code_inline"}]},{"text":":最多 15","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"get_tables / get_fields","type":"text","marks":[{"type":"code_inline"}]},{"text":":最多 10","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create_records / update_records / delete_records","type":"text","marks":[{"type":"code_inline"}]},{"text":":最多 100","type":"text"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"调试原则","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"先 ","type":"text"},{"text":"get_base","type":"text","marks":[{"type":"code_inline"}]},{"text":",再 ","type":"text"},{"text":"get_tables","type":"text","marks":[{"type":"code_inline"}]},{"text":",必要时 ","type":"text"},{"text":"get_fields","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"不要猜 ","type":"text"},{"text":"fieldId","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"复杂参数一律用 ","type":"text"},{"text":"--args","type":"text","marks":[{"type":"code_inline"}]},{"text":" JSON","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"singleSelect / multipleSelect","type":"text","marks":[{"type":"code_inline"}]},{"text":" 过滤时必须传 option ID,不是 option name","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"参考","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"API 参考:","type":"text"},{"text":"references/api-reference.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"错误排查:","type":"text"},{"text":"references/error-codes.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"dingtalk-ai-table","author":"@skillopedia","source":{"stars":102,"repo_name":"dingtalk-ai-table","origin_url":"https://github.com/aliramw/dingtalk-ai-table/blob/HEAD/SKILL.md","repo_owner":"aliramw","body_sha256":"3e6a4e653c8a7c73a04ecb138d397b6b5e0132b8c6cfe7c53c4349325649d3f5","cluster_key":"aec00f160f603d7626e16035ddbe2cb210ca60e1a261ff7a2be9c37201937690","clean_bundle":{"format":"clean-skill-bundle-v1","source":"aliramw/dingtalk-ai-table/SKILL.md","attachments":[{"id":"46f9cbcd-e7b5-5b8f-9af5-2a264d4c9c66","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/46f9cbcd-e7b5-5b8f-9af5-2a264d4c9c66/attachment","path":".DS_Store","size":8196,"sha256":"7c934b94869497b78aff20e056db40561962d60f739db01925a9b50be47a73fc","contentType":"application/octet-stream"},{"id":"2fd7e115-9fb1-5904-9443-7a9a469d81ac","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2fd7e115-9fb1-5904-9443-7a9a469d81ac/attachment.md","path":"BENCHMARKS.md","size":2662,"sha256":"500d8ce0cf05160ee6dea0e1c2029259f34f462349064a40381e7a33a640c409","contentType":"text/markdown; charset=utf-8"},{"id":"bda95558-3765-5e14-88d0-f84cc251988d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bda95558-3765-5e14-88d0-f84cc251988d/attachment.md","path":"CHANGELOG.md","size":12370,"sha256":"468ba5a38c3b10954e14a70bf50c77cf4e701ca7eee2f9ca8c84fea3605a4920","contentType":"text/markdown; charset=utf-8"},{"id":"1151c1aa-4930-57f8-9a32-8776b9b4dca2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1151c1aa-4930-57f8-9a32-8776b9b4dca2/attachment.md","path":"GETTING_STARTED.md","size":2993,"sha256":"cff910df1b63dd73754e7d8aa8eba4fb309e1de5485b6e41ec0b7d7d7243eacf","contentType":"text/markdown; charset=utf-8"},{"id":"f6b18bd4-9f70-545d-87db-660e59b15475","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f6b18bd4-9f70-545d-87db-660e59b15475/attachment.md","path":"README.md","size":1811,"sha256":"ff876e22ed9324d930046578aa28163fe4d27fdb823e5dc48623eed1fb6a87df","contentType":"text/markdown; charset=utf-8"},{"id":"25a64326-e011-568e-8a71-734536c9ae77","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/25a64326-e011-568e-8a71-734536c9ae77/attachment.md","path":"SCORING.md","size":4534,"sha256":"0361899c0269352ab9eac86f5a7c00673e130d0bc4608a9f21b39511d5138ed3","contentType":"text/markdown; charset=utf-8"},{"id":"a66be420-3f48-56f0-bd9a-2533741e3469","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a66be420-3f48-56f0-bd9a-2533741e3469/attachment.sh","path":"examples/01-list-bases.sh","size":255,"sha256":"ecd4689948f54d3fcce15594a50da391c6e24788a149df9a43052a37e9d89486","contentType":"application/x-sh; charset=utf-8"},{"id":"0d577d58-a3fc-5d2a-beab-e346fb2e2da0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0d577d58-a3fc-5d2a-beab-e346fb2e2da0/attachment.sh","path":"examples/02-create-base.sh","size":289,"sha256":"8db8c26a811da4a87cb784431c7dd0dd8ec9a81526bb7b7861d1dfe90839b31e","contentType":"application/x-sh; charset=utf-8"},{"id":"856d45a6-3fcc-5618-864e-3dfe6912bdb7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/856d45a6-3fcc-5618-864e-3dfe6912bdb7/attachment.sh","path":"examples/03-get-base.sh","size":347,"sha256":"fbaa8273bc9c637165596ec600bc322431b677e0b9dc4ea77bd172df51016041","contentType":"application/x-sh; charset=utf-8"},{"id":"fd9cbee8-a7a1-55b4-b6f5-c9a9e994dc7e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fd9cbee8-a7a1-55b4-b6f5-c9a9e994dc7e/attachment.sh","path":"examples/04-query-records.sh","size":442,"sha256":"7980bca982b4a662659832ad4323c3fe6330f062ee1c9e79240868f48f64bcfc","contentType":"application/x-sh; charset=utf-8"},{"id":"67cdc559-2213-5b40-8811-5d1118088b5e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/67cdc559-2213-5b40-8811-5d1118088b5e/attachment.sh","path":"examples/05-create-records.sh","size":586,"sha256":"6960fbc139b8d2e20a8d4f6463cf5acb3cca2c19309982356f13edd87eafc78f","contentType":"application/x-sh; charset=utf-8"},{"id":"304de160-da56-5630-a4c1-3779911c9e5f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/304de160-da56-5630-a4c1-3779911c9e5f/attachment.sh","path":"examples/06-import-records.sh","size":454,"sha256":"616144757439f90b3f7c513c2b4539c840f7ff2b4967e7292aa5062ed5909bc1","contentType":"application/x-sh; charset=utf-8"},{"id":"7ef09884-561e-5e9f-a080-f4617b36e9a3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7ef09884-561e-5e9f-a080-f4617b36e9a3/attachment.sh","path":"examples/07-bulk-add-fields.sh","size":467,"sha256":"c95e58be6b3a02c8a02de473d2c80064a2e5f85f6ee2d1738983298ed283cb0e","contentType":"application/x-sh; charset=utf-8"},{"id":"10f0f6cf-9bd4-54ea-9109-9b9da19a3d6b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/10f0f6cf-9bd4-54ea-9109-9b9da19a3d6b/attachment.md","path":"examples/README.md","size":1596,"sha256":"52f9101db36331067771e41b5b8bd6f59861b75f6f91041d66f3c69ed942c19a","contentType":"text/markdown; charset=utf-8"},{"id":"bfa56c6b-b286-5625-9c16-017a65a5c098","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bfa56c6b-b286-5625-9c16-017a65a5c098/attachment.json","path":"package.json","size":1687,"sha256":"0f141e8b6068ae12e1d33a034bcb958e7e6289bfdc3cdbf99032a419dd0b4525","contentType":"application/json; charset=utf-8"},{"id":"21e4d3e7-36cb-5c1b-8ef5-23d4a07248b9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/21e4d3e7-36cb-5c1b-8ef5-23d4a07248b9/attachment.md","path":"references/api-reference.md","size":10811,"sha256":"637e4f60f9c45ed3a6ba9bcbe8fd55155c7ab9b890d00e28988992c799b2f66a","contentType":"text/markdown; charset=utf-8"},{"id":"6c266495-3f77-5803-b48b-a705f264a88e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6c266495-3f77-5803-b48b-a705f264a88e/attachment.md","path":"references/error-codes.md","size":4822,"sha256":"75db7b0081c12c7a199659ff83429bc22a4ae2cfa18ea1f2e2affcf0eb66f453","contentType":"text/markdown; charset=utf-8"},{"id":"16343047-6ffb-5a84-bd92-973848c43aa6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/16343047-6ffb-5a84-bd92-973848c43aa6/attachment.py","path":"scripts/bulk_add_fields.py","size":9288,"sha256":"c101c7b58d9c36b393eb37a5c425b63561f56ce9afa2b8f192fd6cd838ea03c6","contentType":"text/x-python; charset=utf-8"},{"id":"98cbff50-7abd-5ac0-9ee3-143f7b43f501","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/98cbff50-7abd-5ac0-9ee3-143f7b43f501/attachment.sh","path":"scripts/check-schema.sh","size":1563,"sha256":"921fc7a94690d2e5942e200a7ae3e02852937c19e8f7c925063b45c71ef3d127","contentType":"application/x-sh; charset=utf-8"},{"id":"913fe643-84b7-5efc-b7ca-e5243cb91702","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/913fe643-84b7-5efc-b7ca-e5243cb91702/attachment.py","path":"scripts/import_records.py","size":10552,"sha256":"0f7852ebf59a7c420d3618ba5fd5df7b5a44b41da36c05347f11a77fe4f58136","contentType":"text/x-python; charset=utf-8"},{"id":"452fb9f7-6531-598f-ab30-21a033df6d32","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/452fb9f7-6531-598f-ab30-21a033df6d32/attachment.md","path":"tests/TEST_REPORT.md","size":5005,"sha256":"e28f0a96c2fff1d447b91a6d9f25ba719145452b99fde3b80216fab9e5b2148c","contentType":"text/markdown; charset=utf-8"},{"id":"810c913c-6815-5d82-8658-992c3d756c15","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/810c913c-6815-5d82-8658-992c3d756c15/attachment.py","path":"tests/test_security.py","size":9079,"sha256":"fa8a5d58b0f51f61954259a8e26123e805b54e7f060ce8c3a117c709dbe43ecb","contentType":"text/x-python; charset=utf-8"},{"id":"36dc0e6a-4d56-5f51-91da-c4cf60476d51","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/36dc0e6a-4d56-5f51-91da-c4cf60476d51/attachment.py","path":"tests/test_triggering.py","size":1925,"sha256":"040b5e88bbff069063d326362a1336297936daf1cbf1fd410b20d5d11b620dba","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"70d04f52f66e322660a1e9ce711e682ba26f6a9a5e598909c999b34052ce88ce","attachment_count":23,"text_attachments":22,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":1,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"data-analytics","category_label":"Data"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"data-analytics","metadata":{"tags":["dingtalk","spreadsheet","mcp","automation","data-management"],"author":"Marila@Dingtalk","support":"https://github.com/aliramw/dingtalk-ai-table/issues","category":"productivity","openclaw":{"homepage":"https://github.com/aliramw/dingtalk-ai-table","requires":{"env":["DINGTALK_MCP_URL","OPENCLAW_WORKSPACE"],"bins":["mcporter","python3"]},"primaryEnv":"DINGTALK_MCP_URL"},"documentation":"https://github.com/aliramw/dingtalk-ai-table"},"import_tag":"clean-skills-v1","description":"钉钉 AI 表格(多维表)操作技能。使用 mcporter CLI 连接钉钉官方新版 AI 表格 MCP server,基于 baseId / tableId / fieldId / recordId 体系执行 Base、Table、Field、Record 的查询与增删改。适用于创建 AI 表格、搜索表格、读取表结构、批量增删改记录、批量建字段、更新字段配置、按模板建表等场景。需要配置 DINGTALK_MCP_URL 或直接使用 Streamable HTTP URL。"}},"renderedAt":1782982009371}

钉钉 AI 表格操作(新版 MCP) 🚀 5 分钟快速开始 1️⃣ 列出我的表格 2️⃣ 创建新表格 3️⃣ 添加记录 4️⃣ 查询记录 5️⃣ 批量导入 --- 核心概念 按 新版 MCP schema 工作: - Base: - Table: - Field: - Record: 不要再用旧版 。 推荐使用 及以上版本。 输出模式兼容说明: - 可直接调用 - 更低版本需要显式加 - AI 表格 MCP 无论使用哪种模式,返回体本身都是标准 JSON;差异主要在 的输出处理方式 版本守门规则(每个 MCP Server 地址只强制检查一次) 在真正开始任何 AI 表格操作前,必须先检查当前 注册的 MCP server 实际返回的 tools schema。 但这个检查不该每次都重复做;同一个 MCP Server 地址只需要强制检查一次。 一次性检查策略 1. 先读取当前 里 对应的 MCP Server 地址。 2. 用这个地址生成一个本地检查标记(例如基于完整 URL 或其 hash)。 3. 在工作区保存检查结果,例如放到: 建议文件名模式: 4. 如果当前地址对应的检查标记已经存在,并且结果是“已确认新版 schema”,则 跳过重复检查 ,直接继续后续 AI 表格操作。 5. 只有在以下情况才重新强制检查: - 第一次运行,没有检查标记 - 里的 MCP Ser…