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 | 数字 | 格式化数字 | 1500000 | 1,500,000 |
currency | 原始数字(不要预格式化) | 美元金额 | 4200000 | $4,200,000 |
percent | 小数(0-1 范围) | 带颜色百分比 | 0.152 | +15.2%(绿色) |
boolean | true/false | Yes/No | true | Yes |
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" | 脚本运行时 |
script | string | 转换脚本源码 |
inputFiles | string[] | 相对于会话目录的输入文件路径 |
outputFile | string | 输出文件名(写入会话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 块中内联的columns和title优先于文件中的值。这允许你在块中定义列类型(列宽、格式化类型),同时从文件拉取行数据。渲染层对该优先级的实现见 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: <绝对路径>,并明确提示该路径可用作datatable、spreadsheet、html-preview、pdf-preview、image-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_KEY、CLAUDE_CODE_OAUTH_TOKEN、AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_SESSION_TOKEN、GITHUB_TOKEN、GH_TOKEN、OPENAI_API_KEY、GOOGLE_API_KEY、STRIPE_SECRET_KEY、NPM_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_DIR、XDG_CACHE_HOME、PYTHONPYCACHEPREFIX到会话目录(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.json、monthly-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),仅供参考