1. 项目概述为什么我们需要一个生产级的 Attio CLI 工具如果你正在使用 Attio 这款现代化的 CRM并且已经不止一次地打开浏览器、登录后台、点击鼠标来批量修改客户状态或者为了导出一份数据而反复操作界面那么你很可能已经感受到了手动操作的效率瓶颈。尤其是在处理大量数据、自动化工作流或者将 CRM 数据与其他系统如内部 BI 工具、营销自动化平台集成时图形界面的局限性就暴露无遗。这正是attio-cli诞生的背景——它不是一个简单的 API 封装而是一个旨在将 Attio 的 API 能力无缝融入开发者命令行工作流的生产级 TypeScript CLI 工具。简单来说attio-cli让你能够用命令行直接管理你的 Attio 工作空间。无论是查询特定条件的客户、批量更新交易阶段还是以编程方式管理自定义属性和列表你都可以在终端里用一行命令完成。它的核心价值在于“可编程性”和“可集成性”。想象一下你可以写一个简单的 Shell 脚本每天定时运行自动将过去 24 小时新创建的“高意向”客户同步到你的内部通知系统或者在代码部署成功后自动在 Attio 中为相关客户记录创建一条备注。这些场景用 GUI 操作繁琐用原始的 HTTP 请求写起来又很麻烦而attio-cli提供了一个类型安全、开箱即用的中间层。我选择深度使用并推荐这个工具主要基于几个实际痛点首先Attio 的 API 虽然强大但直接调用涉及复杂的认证、错误处理和响应解析其次在脚本中处理嵌套的 JSON 过滤器和数据结构很容易出错最后缺乏一个统一的、类型友好的命令行界面使得自动化任务的门槛变高。attio-cli用 TypeScript 和 Zod 解决了类型安全问题用 Commander.js 提供了清晰的命令结构并且内置了智能重试、多种输出格式等生产级特性让它从一个“玩具”变成了一个可以信赖的“工程工具”。2. 核心设计思路与架构解析2.1 面向生产环境的设计哲学很多 CLI 工具只是 API 的简单映射但attio-cli在设计之初就考虑到了真实的生产环境需求。这体现在几个关键设计决策上1. 完备的类型安全TypeScript Zod工具完全使用 TypeScript 编写并开启严格模式。这不仅仅是“用了 TS”那么简单它意味着所有 API 的请求参数和响应数据都有精确的类型定义。你在编写调用attio-cli的脚本时IDE 能提供自动补全和类型检查极大减少了因字段名拼写错误或数据结构不符导致的运行时错误。更关键的是它引入了Zod进行运行时验证。即使 TypeScript 在编译时通过了Zod 还能在命令执行时校验输入数据的格式确保发送给 Attio API 的请求是有效的避免了因无效数据导致的 API 调用失败。2. 健壮的错误处理与自动重试直接与第三方 API 交互网络波动、速率限制Rate Limiting是家常便饭。attio-cli内置了基于指数退避Exponential Backoff的自动重试机制。当遇到 429请求过多或 5xx 服务器错误时CLI 不会立即报错退出而是会等待一段时间后重试。这对于执行批量操作的脚本至关重要你不需要自己再去实现复杂的重试逻辑。3. 开发者友好的输出与过滤工具提供了JSON、TableASCII 表格和CSV三种输出格式。JSON便于其他程序解析Table适合在终端快速查看CSV则能直接导入 Excel 或 Google Sheets 进行进一步分析。更重要的是其“紧凑输出格式化”功能。默认情况下查询记录或条目时它会自动剥离 API 返回的冗元数据如active_from,created_by_actor并根据属性类型如email-address、personal-name提取出最核心的值让结果清晰易读。当然你也可以通过--verbose标志查看原始完整响应。2.2 模块化架构与清晰的职责分离浏览其源码结构你能清晰地看到它如何组织代码以保持可维护性src/ ├── api/ # 底层 API 通信层 │ ├── client.ts # 配置了拦截器、重试逻辑的 HTTP 客户端 │ ├── types.ts # 所有 Zod Schema 和 TypeScript 类型定义 │ └── endpoints/ # 按资源分类的 API 端点封装 │ ├── records.ts │ ├── attributes.ts │ └── ... ├── commands/ # CLI 命令定义层 │ ├── record.ts │ ├── attribute.ts │ └── ... ├── formatters/ # 输出格式化层 │ ├── json.ts │ ├── table.ts │ └── csv.ts └── utils/ # 通用工具函数这种分层架构的好处非常明显api/层专注于与 Attio REST API 的对话处理 HTTP 细节、错误码和基础数据转换。commands/层利用 Commander.js 框架定义用户直接交互的命令行参数、选项并调用对应的api/层方法。formatters/层独立处理数据展示逻辑未来要增加新的输出格式如 YAML只需在此添加不会影响业务逻辑。这种分离使得单元测试更容易编写可以 Mockapi/层也使得代码库在面对 Attio API 更新时变更范围可以控制在api/types.ts和具体的 endpoint 文件中不会波及命令定义。实操心得理解“紧凑输出”的实现这个功能看似简单实则体现了对 API 响应的深度理解。例如一个“人名”属性在 API 返回中是一个包含first_name,last_name,full_name以及一堆元数据的复杂对象。attio-cli的格式化器会识别attribute_type: “personal-name”然后智能地提取full_name字段最终输出一个简单的字符串“Michael Fröhlich”。这省去了你在脚本中手动解析这些嵌套结构的麻烦。在写自动化脚本时这个特性极大地简化了后续的数据处理步骤。3. 从零开始安装、配置与快速验证3.1 多种安装方式及其适用场景官方推荐通过 npm 进行全局安装这也是最方便的方式npm install -g attio-cli安装后直接在终端输入attio --help就能看到所有命令概览。对于大多数希望将 CLI 集成到日常运维或脚本中的用户这是首选。但是这里有一个重要的细节如果你是在 CI/CD 流水线如 GitHub Actions, GitLab CI中使用全局安装可能不是最佳选择。因为 CI 环境通常是临时的更推荐在package.json的devDependencies中局部安装或者使用npx。使用npx可以确保每次运行都使用最新版本且不污染全局环境# 在 CI 脚本或一次性任务中 npx attio-cli workspace members list --format json从源码安装主要面向贡献者或需要尝鲜最新未发布功能的用户。步骤包括克隆仓库、安装依赖、构建和全局链接。需要注意的是构建项目需要 Node.js 环境和npm run build命令这会将 TypeScript 编译到dist/目录。git clone https://github.com/FroeMic/attio-cli.git cd attio-cli npm install npm run build npm link # 将当前包链接到全局 node_modules3.2 认证配置的实践与安全建议一切操作的前提是认证。attio-cli支持三种方式传递 API Key各有优劣1. 环境变量推荐用于日常开发这是最安全、最方便的方式避免了密钥硬编码在脚本或命令行历史中。export ATTIO_API_KEYattio_sk_your_actual_key_here # 将上述命令添加到 ~/.bashrc, ~/.zshrc 或 ~/.profile 中使其永久生效 echo export ATTIO_API_KEYattio_sk_your_key ~/.zshrc source ~/.zshrc2..env文件推荐用于项目如果你的脚本是某个项目的一部分使用.env文件是更规范的做法。attio-cli内部使用dotenv包自动加载当前工作目录下的.env文件。# 在项目根目录创建 .env 文件 ATTIO_API_KEYattio_sk_your_key_here注意务必在.gitignore中添加.env切勿将包含真实 API Key 的文件提交到版本控制系统。3. 命令行参数用于临时或覆盖适用于临时使用另一个工作空间的密钥或者在不方便设置环境变量的场景。attio --api-key attio_sk_a_different_key object list安全警告无论用哪种方式都要像保护密码一样保护你的 API Key。它拥有对应工作空间的访问权限。在 Attio 后台你可以创建多个 API Key并为它们设置不同的权限范围和描述便于管理和审计。对于生产环境建议使用具有最小必要权限的 Key。3.3 验证安装与初步探索配置好 API Key 后不要急于操作真实数据。先用几个只读命令验证安装并熟悉环境# 1. 测试连通性列出工作空间成员 attio workspace members list --format table # 这个命令几乎不会失败能快速确认认证和网络是否正常。 # 2. 查看数据模型列出所有对象Object attio object list --format table # 你会看到 people, companies, deals 等内置对象以及你创建的自定义对象。这帮助你理解当前工作空间的数据结构。 # 3. 查看具体对象的属性字段定义 attio object attributes people --format table # 这会列出“人员”对象的所有属性如 name, email_addresses 等包括它们的类型type和 API 标识slug。了解 slug 是后续使用过滤、创建数据的关键。如果这些命令都能成功返回数据恭喜你环境已经就绪。如果遇到Error: ATTIO_API_KEY not found in environment请回头检查你的环境变量是否设置正确或者尝试在命令前显式指定密钥ATTIO_API_KEYyour_key attio workspace members list。4. 核心功能深度实操与避坑指南4.1 对象Objects与记录Records管理增删改查的艺术记录是 CRM 的核心attio-cli提供了完整的 CRUD 操作。查询Read使用强大的过滤与排序基础的列表查询很简单attio record list people --limit 5。但其威力在于--filter和--sort参数。过滤语法是 JSON 格式支持丰富的操作符。例如查找所有邮箱域名是company.com的人员attio record list people \ --filter {email_addresses: {email_address: {$contains: company.com}}} \ --format table这里有几个关键点字段路径email_addresses.email_address。因为email_addresses本身是一个数组但过滤时可以直接用点号访问其内部属性。这是 Attio API 的约定CLI 保持了兼容。操作符$contains用于字符串的部分匹配。类似的还有$eq等于、$starts_with开头是、$gt大于用于数字或日期。复杂逻辑组合使用$and和$or实现多条件查询。例如查找名为“John”且来自“Acme”公司的人attio record list people \ --filter { $and: [ {name: {first_name: {$eq: John}}}, {company: {name: {$contains: Acme}}} ] } \ --format json避坑指南JSON 字符串转义在 Bash 中直接写复杂的 JSON 过滤器容易出错特别是引号嵌套。有两个解决方案使用单引号包裹整个 JSON内部的双引号无需转义如上例所示。将过滤器写在一个文件里如filter.json然后使用命令替换attio record list people --filter $(cat filter.json)创建Create与更新Update创建和更新记录都需要通过--data参数传递一个 JSON 对象其结构必须符合 Attio 的 API 规范。最可靠的方式是先通过attio object attributes-with-values people --format json命令获取“人员”对象的完整模式定义里面会包含每个属性的期望格式。# 创建一个新人员记录 attio record create people --data { values: { name: { first_name: Alice, last_name: Zhang, full_name: Alice Zhang }, email_addresses: [{ email_address: alice.zhangexample.com, is_primary: true }], twitter: [{ value: https://twitter.com/alicezhang }] } }创建成功后命令会返回新创建的记录详情其中包含系统生成的record_id如rec_xxx务必保存这个 ID 用于后续操作。更新操作类似需要指定记录 IDattio record update people rec_abc123 --data { values: { twitter: [{ value: https://x.com/newalicehandle }] } }断言式创建Assert / Upsert避免重复记录的利器这是非常实用的一个功能。你希望如果存在邮箱为aliceexample.com的记录则更新它如果不存在则创建它。用attio record assert可以一键完成attio record assert people \ --matching-attribute email_addresses \ --data { values: { email_addresses: [{ email_address: alice.zhangexample.com }], name: { first_name: Alice, last_name: Zhang } } }--matching-attribute指定了用于判断“唯一性”的属性。Attio 会尝试查找email_addresses属性值与提供数据匹配的记录。注意这依赖于该属性在 Attio 中被标记为“唯一”否则行为可能不符合预期。4.2 属性Attributes管理定义你的数据模型属性是对象的字段。attio-cli可以对属性和其下的选项Select Options、状态Statuses进行全方位管理。创建属性理解类型与约束创建一个新的属性本质上是定义数据模型。你需要决定它的类型--type、标题--title、API 标识--slug以及约束如--required。# 在“公司”对象上创建一个“行业”下拉选择属性 attio attribute create objects companies \ --title Industry \ --slug industry \ --type select \ --required创建后你需要为其添加选项attio attribute option-create objects companies industry --title Technology attio attribute option-create objects companies industry --title Healthcare attio attribute option-create objects companies industry --title Finance状态Status属性的特殊性与应用状态属性是一种特殊的属性常用于表示管线Pipeline阶段例如销售管线中的“潜在客户”、“已联系”、“谈判中”、“已成交”。关键限制状态属性只能创建在列表Lists或自定义对象上不能直接创建在 people, companies, deals 这些内置对象上。通常的用法是先创建一个列表如销售管线然后在该列表上创建状态属性。# 1. 创建一个销售管线列表父对象是“公司” attio list create \ --api-slug sales_pipeline \ --name Sales Pipeline \ --parent-object companies # 2. 在该列表上创建一个“交易阶段”状态属性 attio attribute create lists sales_pipeline \ --title Deal Stage \ --slug deal_stage \ --type status # 3. 为这个状态属性添加具体的阶段 attio attribute status-create lists sales_pipeline deal_stage --title Prospecting attio attribute status-create lists sales_pipeline deal_stage --title Qualified attio attribute status-create lists sales_pipeline deal_stage --title Proposal attio attribute status-create lists sales_pipeline deal_stage --title Closed Won --celebration注意--celebration标志它可以标记一个状态为“庆祝阶段”例如“已成交”在 Attio 的 UI 上可能会有特殊显示。重要警告关于删除与归档Attio API不支持直接删除属性。如果你创建了一个错误的属性或者想移除它标准的做法是将其“归档”Archive。对于普通属性可以通过attio attribute update命令在描述中标记为已归档例如--description “[Archived] Old field”。对于选择选项和状态则有专门的归档命令attio attribute option-archive和attio attribute status-archive。归档后的属性/选项/状态在默认查询中不会显示除非使用--show-archived标志并且通常无法再被用于新建或更新记录。这是一个不可逆的操作请谨慎执行。4.3 列表Lists与条目Entries组织你的视图列表是 Attio 中一个强大的概念它允许你基于某个对象父对象创建不同的视图或分组。例如你可以有一个“所有公司”的对象但同时创建“潜在客户列表”、“活跃客户列表”、“合作伙伴列表”等。每个列表可以有自己的属性和状态。列表条目的操作条目Entry是对象记录在特定列表中的具体实例。为一家公司创建一个销售管线条目意味着这家公司进入了你的销售流程。# 假设我们有一家公司记录 ID 为 rec_company123 # 将其加入到 sales_pipeline 列表中并设置初始阶段为“qualified” attio entry create sales_pipeline \ --parent-record rec_company123 \ --parent-object companies \ --data {entry_values: {deal_stage: qualified}}这里--data中的entry_values对应的是该列表上定义的属性如deal_stage而不是父对象公司的属性。列表 vs 对象过滤你可能会问既然能用attio record list companies --filter过滤出所有“行业科技”的公司为什么还需要列表两者的目的不同对象过滤是基于记录固有属性的临时查询。查询条件灵活但结果集是动态的。列表是一个静态的、有状态的成员集合。你可以手动或通过规则添加/移除记录并且可以为列表中的记录设置独立的属性如“销售优先级”、“预计成交日期”。列表更适合管理一个需要持续跟踪和操作的固定集合。4.4 笔记Notes与任务Tasks丰富客户互动笔记和任务是与记录关联的互动内容attio-cli也提供了完整的管理能力。创建富文本笔记笔记支持纯文本、Markdown 和 HTML 格式。attio note create \ --parent-object people \ --parent-record rec_person456 \ --title 产品演示反馈 \ --content ## 客户反馈\n- 对 **A功能** 非常感兴趣\n- 认为定价方案B更符合其预算\n- 约定下周进行技术细节沟通 \ --format markdown使用--format markdown可以让内容在 Attio UI 中正确渲染为富文本。管理跟进任务任务可以关联到记录并设置截止日期和负责人。# 创建一个跟进任务关联到某公司并指派给特定工作空间成员 attio task create \ --content 发送修订后的合同草案 \ --deadline 2024-06-15T17:00:00Z \ --assignee wm_member789 \ --linked-record rec_company123日期时间需要遵循 ISO 8601 格式YYYY-MM-DDTHH:mm:ssZ。--assignee的参数是工作空间成员的 ID可以通过attio workspace members list获取。5. 高级技巧与脚本集成实战5.1 利用输出格式实现自动化流水线attio-cli的三种输出格式是与外部工具集成的桥梁。场景一每日销售报告CSV 邮件你可以编写一个 Shell 脚本定时查询当天状态更新为“Closed Won”的销售条目并生成 CSV 报告。#!/bin/bash # generate_daily_wins.sh # 设置日期 TODAY$(date %Y-%m-%d) REPORT_FILEdaily_wins_${TODAY}.csv # 使用 CLI 查询并导出 CSV # 假设我们通过一个自定义的“closed_date”属性来筛选 attio entry list sales_pipeline \ --filter {\deal_stage\: {\\$eq\: \closed_won\}, \closed_date\: {\\$gte\: \${TODAY}T00:00:00Z\}} \ --format csv $REPORT_FILE # 检查文件是否非空 if [ -s $REPORT_FILE ]; then # 使用邮件命令发送报告这里以 mailx 为例 echo Please find todays sales wins attached. | mailx -s Daily Sales Wins Report - $TODAY -a $REPORT_FILE sales-teamcompany.com echo Report generated and sent: $REPORT_FILE else echo No wins today. No report generated. fi场景二数据同步到内部系统JSON jq假设你需要将 Attio 中所有“行业科技”且“最近有更新”的公司同步到内部系统。你可以用 JSON 格式获取数据并用jq这个强大的命令行 JSON 处理器进行转换。#!/bin/bash # sync_tech_companies.sh # 获取数据使用 jq 提取和转换所需字段 attio record list companies \ --filter {industry: {$eq: Technology}} \ --sort [{attribute: last_modified_at, direction: desc}] \ --format json | \ jq -c .[] | {id: .id.record_id, name: .values.name, website: .values.website, lastContact: .values.last_contact_date} tech_companies.ndjson # 现在 tech_companies.ndjson 是每行一个 JSON 对象的新行分隔 JSON 文件 # 可以方便地用 curl 发送到内部 API while IFS read -r company; do curl -X POST https://internal-api.company.com/sync/company \ -H Content-Type: application/json \ -H Internal-Auth-Token: $INTERNAL_API_TOKEN \ -d $company done tech_companies.ndjson5.2 错误处理与脚本健壮性在生产脚本中必须考虑命令执行失败的情况。attio-cli在遇到错误如网络问题、API 错误、无效参数时会返回非零的退出码并输出错误信息到标准错误流stderr。你的脚本应该检查这些状态。#!/bin/bash # robust_script.sh set -e # 遇到任何命令失败就退出 # 尝试执行一个可能失败的操作 if ! OUTPUT$(attio record list some_object --filter invalid json 21); then echo CLI command failed! 2 echo Error output: $OUTPUT 2 # 可以在这里添加报警逻辑如发送 Slack 通知 exit 1 fi # 如果成功继续处理 $OUTPUT echo Command succeeded. Processing output...对于批量操作你还可以利用--verbose标志来获取更详细的错误信息帮助调试。5.3 性能考量与速率限制当处理成千上万条记录时性能变得重要。attio-cli本身是轻量的但受限于 Attio API 的速率限制。分页查询attio record list命令默认返回一定数量的记录通常由 API 决定。使用--limit和--offset参数可以实现分页。在脚本中循环获取所有数据时需要合理设置limit例如 100并不断递增offset直到返回的记录数小于limit。批量操作目前attio-cli没有原生的批量创建/更新命令。如果你需要批量导入大量数据更高效的做法是直接使用 Attio 的批量导入功能通过 UI 或专门的批量导入 API。CLI 更适合中低频率的、精准的自动化操作。利用断言Assert在不确定记录是否存在时使用attio record assert代替“先查询再判断是创建还是更新”的逻辑可以减少一次 API 调用。6. 常见问题排查与实战心得在实际使用中你肯定会遇到一些问题。以下是我踩过的一些坑和解决方案。问题一Error: Invalid filter structure这是最常见的问题几乎总是因为 JSON 格式错误或过滤条件不符合 Attio 的语法。检查 JSON 格式使用在线的 JSON 校验工具或者用echo ‘你的过滤器’ | jq .来验证 JSON 是否有效。检查字段名Slug确保你使用的属性 slug 是正确的。最可靠的方法是先用attio object attributes object_slug --format json命令查看该对象所有可用的属性及其 slug。检查操作符确认你使用的操作符如$eq,$contains对该属性类型是有效的。例如$gt不能用于文本属性。问题二创建记录时返回模糊的错误例如Error: Invalid request body。这通常是因为--data中的 JSON 结构不正确。使用attributes-with-values命令在创建或更新前先运行attio object attributes-with-values people --format json person_schema.json然后打开这个 JSON 文件查看每个属性的完整结构示例。特别是对于数组类型的属性如email_addresses,phone_numbers其结构是[{“email_address”: “…”, “is_primary”: true}]而不是简单的字符串。从简单开始先尝试只设置一两个必填属性成功后再逐步添加其他属性。问题三attio record assert没有按预期更新记录断言操作依赖于--matching-attribute指定的属性。请确保该属性在 Attio 后台被配置为“唯一”或至少具有业务唯一性。你提供的--data中用于匹配的字段值如邮箱地址与目标记录中的值完全一致包括大小写和空格。如果匹配属性是数组如email_addresses断言逻辑可能会检查数组中的任意一项是否匹配。问题四集成测试的注意事项项目自带的 112 个集成测试非常全面但运行它们会真实地在你的工作空间中创建、修改和删除数据。务必使用测试工作空间不要在包含重要生产数据的工作空间中运行npm run test:integration。理解测试流程测试套件通常遵循“创建 - 测试 - 清理”的模式。但如果测试中途失败可能留下测试数据。你可以定期登录对应的 Attio 工作空间手动清理名称或描述中带有测试标记的数据。个人使用心得经过几个月的深度使用我认为attio-cli最大的优势在于它将 Attio 强大的 API 能力“平民化”了。我不再需要为了一个简单的数据导出任务去写几十行的 Node.js 脚本。现在很多日常运维工作都变成了一个简单的 cron 任务加几行 Shell 脚本。一个典型的场景是数据质量维护我写了一个每周运行的脚本用attio record list查找所有缺少“公司官网”属性的客户记录然后通过attio note create自动为这些记录添加一条备注提醒销售团队补充信息。整个过程完全自动化解放了人力。最后一个小建议虽然 CLI 功能强大但对于极其复杂的、一次性的数据迁移或清洗任务有时使用 Python 或 Node.js 直接调用 Attio 的官方 SDK配合更丰富的数据处理库如 pandas可能会更灵活。attio-cli是你的瑞士军刀适合集成和自动化而 SDK 更像是你的重型工具箱适合进行复杂的数据工程。根据任务性质选择合适的工具才能事半功倍。