Craft Agents 表格数据呈现全解:datatable / spreadsheet 块与 transform_data 工具实战指南
2026/9/16 11:26:17 网站建设 项目流程

Craft Agents 表格数据呈现全解:datatable / spreadsheet 块与 transform_data 工具实战指南

【免费下载链接】craft-agents-oss项目地址: https://gitcode.com/GitHub_Trending/cr/craft-agents-oss

本篇指南围绕 Craft Agents 中结构化表格数据的呈现体系展开:如何在小数据量时内联datatable/spreadsheet块,以及在 20 行以上的大数据集场景下如何用transform_data工具将数据落盘为 JSON 文件并通过"src"字段引用,从而大幅降低 token 消耗。读完本文,你将掌握列类型(column type)的完整语义、transform_data的路径约定与脚本参数约定、五类常见数据转换配方,以及沙箱执行的安全边界与故障排查方法。

三种表格形态与选型原则

Craft Agents 支持三种方式展示表格数据,选型依据是数据规模、交互需求与导出需求:

格式适用场景交互能力
Markdown 表格小型简单数据(3-4 行)
datatable查询结果、对比数据、用户可能需要排序/过滤的数据排序、过滤、分组、搜索
spreadsheet财务报告、导出数据、用户可能需要下载为 .xlsx 的数据排序、导出 Excel/CSV

核心原则:对于 20 行以上(20+ rows)的数据集,使用transform_data工具将数据写入 JSON 文件,再通过"src"字段引用,而不是把所有行内联进块里。这能显著降低 token 使用量与成本(100 行数据内联约消耗 $1+ token)。

渲染侧的实现在 MarkdownDatatableBlock.tsx 与 MarkdownSpreadsheetBlock.tsx,Excel/CSV 导出能力由 table-export.ts 提供。

内联表格(小数据集)

对于少于 20 行的数据集,直接将数据内联在 markdown 块中。

Datatable 完整示例

```datatable { "title": "Top Users", "columns": [ { "key": "name", "label": "Name", "type": "text" }, { "key": "revenue", "label": "Revenue", "type": "currency" }, { "key": "growth", "label": "Growth", "type": "percent" }, { "key": "active", "label": "Active", "type": "boolean" }, { "key": "tier", "label": "Tier", "type": "badge" } ], "rows": [ { "name": "Acme Corp", "revenue": 4200000, "growth": 0.152, "active": true, "tier": "Enterprise" }, { "name": "StartupCo", "revenue": 85000, "growth": -0.03, "active": true, "tier": "Starter" } ] } ```

Spreadsheet 完整示例

```spreadsheet { "filename": "q4-revenue.xlsx", "sheetName": "Revenue", "columns": [ { "key": "month", "label": "Month", "type": "text" }, { "key": "revenue", "label": "Revenue", "type": "currency" } ], "rows": [ { "month": "October", "revenue": 125000 }, { "month": "November", "revenue": 142000 } ] } ```

列类型(Column Types)参考

类型输入格式渲染为示例输入示例输出
text任意字符串纯文本"John Doe"John Doe
number数字格式化数字15000001,500,000
currency原始数字(不要预格式化)美元金额4200000$4,200,000
percent小数(0-1 范围)带颜色百分比0.152+15.2%(绿色)
booleantrue/falseYes/NotrueYes
date日期字符串格式化日期"2025-01-15"Jan 15, 2025
badge字符串彩色状态徽章"Active"Active(徽章)

关键注意事项:

  • currency— 传原始数字,而不是格式化字符串。4200000渲染为$4,200,000
  • percent— 以小数形式传入。0.152渲染为+15.2%。正值为绿色,负值为红色。
  • boolean— 使用真实的true/false,而不是字符串。

这些语义由渲染层在 MarkdownDatatableBlock.tsx 中按type字段分发格式化,脚本侧无需(也不应)在转换脚本里预格式化数值——把渲染交给列类型即可。

文件支撑表格(大数据集)

适用场景

在以下情况应使用transform_data工具 +"src"字段:

  • 数据集有20 行以上——100 行内联约消耗 $1+ token;
  • 数据来自大型 API 响应或工具结果;
  • 需要在展示前对原始数据过滤、重塑或聚合
  • 数据是需要解析的 CSV、TSV 或非结构化文本
  • 需要联合多个来源的数据

transform_data 工具详解

transform_data在隔离的子进程中执行脚本,读取输入文件并写出结构化 JSON 输出。

参数:

参数类型说明
language"python3"|"node"|"bun"脚本运行时
scriptstring转换脚本源码
inputFilesstring[]相对于会话目录的输入文件路径
outputFilestring输出文件名(写入会话data/目录)

路径约定:

  • 输入文件相对于会话目录(session directory)。常见位置:
    • long_responses/tool_result_abc.txt— 保存的工具结果
    • data/previous_output.json— 前一次 transform 的输出
    • attachments/data.csv— 用户附带的文件
  • 输出文件相对于会话的data/目录。只需提供文件名(如"transactions.json")。
  • 从源码看,transform-data.ts 中的允许输入目录除会话目录外,还包含 skills 目录(用于读取 skill 资产),这一能力在测试用例 transform-data.test.ts 中得到验证。

脚本参数约定:

  • 输入文件路径作为位置命令行参数传入;
  • 最后一个参数始终是输出文件路径;
  • Python:sys.argv[1:-1]= 输入文件,sys.argv[-1]= 输出路径;
  • Node/Bun:process.argv.slice(2, -1)= 输入文件,process.argv.at(-1)= 输出路径。

这个约定直接对应实现中的参数拼装逻辑(transform-data.ts):

const spawnArgs = [...runtime.argsPrefix, tempScript, ...resolvedInputs, resolvedOutput];

脚本被写入临时文件后按「临时脚本 → 输入文件们 → 输出文件」的顺序作为位置参数传给子进程,因此输出路径永远位于参数列表末尾。

输出 JSON 格式

输出文件必须是有效 JSON,支持以下三种格式之一:

完整格式(推荐):

{ "title": "Recent Transactions", "columns": [ { "key": "date", "label": "Date", "type": "date" }, { "key": "amount", "label": "Amount", "type": "currency" }, { "key": "status", "label": "Status", "type": "badge" } ], "rows": [ { "date": "2025-01-15", "amount": 250.00, "status": "Completed" } ] }

仅行格式(Rows-only):

{ "rows": [ { "date": "2025-01-15", "amount": 250.00, "status": "Completed" } ] }

或裸数组(bare array):

[ { "date": "2025-01-15", "amount": 250.00, "status": "Completed" } ]

合并语义(Merge semantics):使用"src"时,markdown 块中内联的columnstitle优先于文件中的值。这允许你在块中定义列类型(列宽、格式化类型),同时从文件拉取行数据。渲染层对该优先级的实现见 MarkdownDatatableBlock.tsx:当块内columns非空时采用块内定义,否则回退到文件中的columns

引用输出文件

transform_data成功后会返回输出文件的绝对路径。将该路径原样用作 datatable 或 spreadsheet 块的"src"值:

```datatable { "src": "/absolute/path/returned/by/transform_data", "title": "Recent Transactions", "columns": [ { "key": "date", "label": "Date", "type": "date" }, { "key": "amount", "label": "Amount", "type": "currency" }, { "key": "status", "label": "Status", "type": "badge" } ] } ```

注意:始终使用transform_data工具结果中返回的绝对路径,不要手动构造相对路径。工具成功时的返回消息形如Output written to: <绝对路径>,并明确提示该路径可用作datatablespreadsheethtml-previewpdf-previewimage-preview块的"src"值(transform-data.ts)。

完整工作流示例

用户提问:"Show me all Stripe transactions from last month"(显示我上个月的所有 Stripe 交易)。

第 1 步:通过 MCP 工具调用 Stripe API——获得大型 JSON 响应。

第 2 步:调用transform_data提取并结构化数据:

transform_data({ language: "python3", script: "import json, sys\nwith open(sys.argv[1]) as f:\n data = json.load(f)\nrows = [{\n 'id': t['id'],\n 'date': t['created'],\n 'amount': t['amount'] / 100,\n 'status': t['status'].title(),\n 'customer': t.get('customer_email', 'N/A')\n} for t in data.get('data', data.get('transactions', []))]\nwith open(sys.argv[-1], 'w') as f:\n json.dump({'rows': rows}, f)", inputFiles: ["long_responses/stripe_result.txt"], outputFile: "transactions.json" })

第 3 步:使用transform_data结果中返回的绝对路径输出 datatable 块:

```datatable { "src": "/absolute/path/from/transform_data/result", "title": "Stripe Transactions — Last Month", "columns": [ { "key": "id", "label": "ID", "type": "text" }, { "key": "date", "label": "Date", "type": "date" }, { "key": "amount", "label": "Amount", "type": "currency" }, { "key": "status", "label": "Status", "type": "badge" }, { "key": "customer", "label": "Customer", "type": "text" } ] } ```

常见模式与配方

JSON API 响应 → Datatable

最常见的模式:从 JSON API 响应中提取字段。

Python:

import json, sys with open(sys.argv[1]) as f: data = json.load(f) # Handle common API response shapes items = data.get('data', data.get('items', data.get('results', data))) if not isinstance(items, list): items = [items] rows = [{ 'id': item['id'], 'name': item.get('name', ''), 'created': item.get('created_at', ''), } for item in items] with open(sys.argv[-1], 'w') as f: json.dump({'rows': rows}, f)

CSV/TSV → Spreadsheet

解析 CSV 数据为 spreadsheet 以便导出:

Python:

import csv, json, sys with open(sys.argv[1]) as f: reader = csv.DictReader(f) rows = list(reader) # Auto-detect columns from CSV headers columns = [{'key': k, 'label': k.replace('_', ' ').title(), 'type': 'text'} for k in rows[0].keys()] if rows else [] with open(sys.argv[-1], 'w') as f: json.dump({'columns': columns, 'rows': rows}, f)

多源数据联合(Multi-Source Join)

合并来自多个工具结果的数据:

Python:

import json, sys # sys.argv[1:-1] are input files, sys.argv[-1] is output with open(sys.argv[1]) as f: users = {u['id']: u for u in json.load(f)['data']} with open(sys.argv[2]) as f: orders = json.load(f)['data'] rows = [{ 'order_id': o['id'], 'customer': users.get(o['user_id'], {}).get('name', 'Unknown'), 'amount': o['total'], 'status': o['status'], } for o in orders] with open(sys.argv[-1], 'w') as f: json.dump({'rows': rows}, f)

调用方式(注意inputFiles顺序与脚本中sys.argv[1]sys.argv[2]一一对应):

transform_data({ language: "python3", script: "...", inputFiles: ["long_responses/users.txt", "long_responses/orders.txt"], outputFile: "orders-with-customers.json" })

过滤与聚合(Filtering & Aggregation)

展示前先汇总数据:

Python:

import json, sys from collections import defaultdict with open(sys.argv[1]) as f: data = json.load(f) # Group by category and sum totals = defaultdict(lambda: {'count': 0, 'total': 0}) for item in data['transactions']: cat = item.get('category', 'Other') totals[cat]['count'] += 1 totals[cat]['total'] += item['amount'] rows = [{'category': k, 'count': v['count'], 'total': v['total']} for k, v in sorted(totals.items(), key=lambda x: -x[1]['total'])] with open(sys.argv[-1], 'w') as f: json.dump({'rows': rows}, f)

Node.js 替代方案

当 Python 不可用或偏好 JavaScript 时:

Node:

const fs = require('fs'); const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf-8')); const rows = data.items.map(item => ({ id: item.id, title: item.title, status: item.state, created: item.created_at, })); fs.writeFileSync(process.argv.at(-1), JSON.stringify({ rows }));

安全与约束(源码级验证)

文档声明的五条安全约束均可在源码中得到印证:

  • 隔离子进程:脚本运行在子进程中,无法访问 API 密钥、凭据或敏感环境变量。环境变量清理逻辑在 sandbox-env.ts 的BLOCKED_ENV_VARS中维护,实际屏蔽的变量包括:ANTHROPIC_API_KEYCLAUDE_CODE_OAUTH_TOKENAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_SESSION_TOKENGITHUB_TOKENGH_TOKENOPENAI_API_KEYGOOGLE_API_KEYSTRIPE_SECRET_KEYNPM_TOKEN(与文档中的AWS_*等通配描述对应,源码采用的是逐项显式枚举)。
  • 30 秒超时:超时脚本会被强制终止。实现上(transform-data.ts、L116-L127)并不依赖spawn()内置的timeout选项——那只发送可被捕获的 SIGTERM——而是手动计时后直接发送SIGKILL,保证进程一定被杀掉。
  • 路径沙箱:输入文件必须位于会话目录(或 skills 目录)内,输出文件必须位于会话data/目录内,路径穿越(../)会被阻断。校验函数isPathWithinDirectory/isPathWithinDirectoryForCreation定义在 path-security.ts;transform-data.test.ts 中的测试覆盖了共享前缀的兄弟目录逃逸、符号链接逃逸(含 skills 目录下的符号链接)等攻击路径,并确认合法子孙路径正常写入。
  • 无网络访问(实践约定):脚本继承宿主进程环境(去掉密钥后),但不应在脚本内发起网络调用——数据获取请使用 MCP 工具,然后本地转换。
  • 运行时缓存重定向:从源码看,沙箱还会将TMPDIR/TMP/TEMP重定向到会话data/.tmp,Python 场景额外重定向UV_CACHE_DIRXDG_CACHE_HOMEPYTHONPYCACHEPREFIX到会话目录(sandbox-env.ts),使沙箱执行不依赖宿主 home 目录下的默认缓存位置。

运行时解析机制:language参数对应的可执行文件由 resolve-script-runtime.ts 按以下优先级解析:环境变量覆盖(CRAFT_UV/CRAFT_NODE/CRAFT_BUN)→ 打包内置二进制(bundled)→ PATH 查找(仅开发模式允许,打包模式下 PATH 回退被禁用)。Python 脚本实际通过uv run --python 3.12执行,因此脚本应只依赖 Python 标准库(json、csv 等),无需任何pip install

最佳实践

决策树

Is the data < 20 rows? → YES: Inline it directly in the datatable/spreadsheet block → NO: Use transform_data + "src" field Is the data already structured JSON? → YES: Write a simple extraction script → NO: Use Python's csv, json, or string parsing to structure it Does the user need to export/download? → YES: Use spreadsheet block (supports .xlsx export) → NO: Use datatable block (better sort/filter/group UX)

命名约定

  • 输出文件:描述性、kebab-case——stripe-transactions.jsonmonthly-revenue.json
  • 与上下文匹配——如果用户问的是 "Q4 sales",就命名为q4-sales.json

脚本中的错误处理

  • 处理前始终验证输入数据存在
  • 对 JSON 解析使用try/except(Python)或try/catch(Node)
  • 尽可能写出部分结果——有部分数据好过直接报错
  • 保持脚本简洁——复杂逻辑在 30 秒超时内更难调试

脚本编写技巧

  • 数据转换优先使用 Python——它是处理 JSON/CSV 最可靠的运行时
  • 保持脚本自包含——不要pip install或引入外部依赖
  • 使用json.dump默认序列化——不要在脚本里格式化数字,交给列类型处理渲染
  • 日期输出 ISO 格式字符串(YYYY-MM-DD)——date列类型会处理格式化

故障排查

"Script failed (exit code 1)"

  • 检查错误输出中的语法错误或缺失的 import
  • 确认输入文件在指定路径存在
  • 确保脚本正确地从sys.argv/process.argv读取参数

(对应实现:非零退出码会返回Script failed (exit code N):及截断的 stderr/stdout,见 transform-data.ts。)

"Output file was not created"

  • 确保脚本写入的是sys.argv[-1]/process.argv.at(-1)(最后一个参数)
  • 检查json.dump/fs.writeFileSync是否成功完成
  • 验证输出是合法 JSON

(实现中在脚本成功退出后还会显式检查输出文件是否存在,不存在则返回Script completed but output file was not created,见 transform-data.ts。)

"Input file not found"

  • 输入路径是相对于会话目录的
  • 对照产生该文件的工具结果,核对精确路径
  • 保存的工具结果使用long_responses/前缀,用户上传的文件使用attachments/前缀

表格中行为空或缺失

  • 验证 JSON 结构:必须有"rows"键且值为数组,或者是裸数组
  • 检查行键与列的"key"字段完全一致(大小写敏感)
  • 确保值类型匹配预期(currency/percent需要数字,而不是字符串)

表格一直显示 "Loading..."

  • "src"路径必须是transform_data返回的绝对路径——不要使用相对路径
  • 确认文件确实由transform_data创建(检查工具结果消息)

完整的官方文档位于 apps/electron/resources/docs/data-tables.md,transform_data工具的对外描述与参数 schema 定义在 tool-defs.ts 与同文件的工具注册处(第 561 行)。

【免费下载链接】craft-agents-oss项目地址: https://gitcode.com/GitHub_Trending/cr/craft-agents-oss

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询