ESC
AI 2 分钟阅读

CRM

这是一个开源CRM项目,采用代理优先架构。AI代理是核心,负责研究和记录,数据库仅用于存储。代理自主运行,基于预算和工作队列,不猜测用户信息,确保数据准确性。系统设计为单租户,集成Google登录,使用eve框架构建,支持自部署。

来源:GitHub

CRM

一个开源、代理优先的CRM。

持久的研究代理是产品。数据库只是它记录信息的地方。

代理 · 技术栈 · 快速开始 · 配置 · 部署 · 贡献

MIT许可证 · 基于eve构建 · 基于Bun运行时 · Postgres数据库

这是什么

大多数CRM是一个数据库前有一个表单。AI的在表单旁边添加聊天框。两者都将实际工作——找出真相并记录下来——留给有更好事情要做的人。

这个项目是反向构建的。代理不是CRM的功能;CRM是代理保存笔记的地方。它在自己的部署上运行,按自己的时间表,针对自己的工作队列。它决定接下来查看什么,安排自己的跟进,花费研究预算,预算用尽时停止。它的任何部分都不是请求-响应:关闭浏览器它继续运行。

API故意没有任何智能。NestJS报告某事发生了——线程被摄入,公司被创建,参与者未知——通过向队列写入一行。代理租用该行并决定其含义。调用充实API的Nest服务被视为错误,docs/api.md解释了导致该规则的故障。

代理本身从不打破的规则:绝不猜测关于人的信息。没有工具接受置信度分数,因为要求模型评估自己的确定性,它会错误地朝着看起来有用的方向。工具报告它们观察到的内容——crm.signature-block、github.account-identity——而账本对证据定价。强证据写入记录。弱证据成为人类解决的建议。关于客户的错误事实比空白字段更糟,因为没人能分辨它是错的。

它设计为单租户和内部使用。登录是Google,允许列表是一个环境变量,每个进入的人都能看到一切。这是整个授权模型——在指向真实客户数据之前,参见SECURITY.md。

截图

(截图部分,保留描述)

代理

apps/agent是其自己的部署,基于eve构建——Vercel的文件系统优先框架,用于持久代理。工具是文件,技能是markdown文件,调度是文件,运行时处理持久部分:在重新部署后存活的会话,从停止处恢复的工作。

18个自定义工具read_crm_history、search_crm、identify_contact、research_person、enrich_company、record_fact、schedule_recheck…
4个技能evidence.md、identity-matching.md、data-boundaries.md、writing-a-brief.md——代理读取的散文,像代码一样版本化
1个调度dispatch.ts,它什么也不决定。它租用到期的行,并为每行启动一个会话。
沙箱bash、grep、glob和/workspace,具有deny-all出口

它自己运行。lib/tasks.ts是工作队列:claimDue使用FOR UPDATE SKIP LOCKED租用行,因此两个调度器取不相交的工作,运行死亡时释放其行当租用过期时。任何看起来像“每N分钟,最旧的十个联系人”的内容属于任务的dueAt,而不是cron表达式。当代理想再次查看某人时,它调用schedule_recheck并说明原因——原因显示给代表,因为一个不能说为什么它将在十四天后返回的代理没有理由,它有一个默认值。

每个外部源都是可选的,并设计为不使用它们运行。没有API密钥它仍然工作:read_crm_history读取您自己的线程、会议和签名块,这是免费的,并且是最佳证据——没有数据供应商可以卖给您来自个人自己地址的回复。每个密钥打开一个更多查看的地方。在每个会话开始时告知它此安装拥有哪些,因此它围绕实际拥有的进行计划,而不是通过一次失败调用发现差距,并在启动时打印列表:

[agent] on   LinkedIn (RAPIDAPI_KEY)
[agent] off  Web research (PERPLEXITY_API_KEY)
[agent] off  Company brand data (CONTEXT_DEV_API_KEY)

沙箱没有网络也没有数据库。打开它为模型提供shell——工具调用者和可以保存文件、diff本月配置文件与上月、grep线程获取签名块之间区别。deny-all出口零成本,因为web_fetch在应用程序运行时运行,web_search在模型提供者处。它移除的是唯一客户电子邮件正文可能通过shell命令离开的路径。该规则的另一半是缺失:沙箱永远不会获得DATABASE_URL。带有凭据和出口的shell在内部工具中也是数据外泄形状;没有两者的shell是文本处理器。

您可以与它对话,并观察它工作。每个联系人、公司和交易都有代理标签——步骤采取它时,它丢弃的潜在客户及原因,以及它无法决定两个人时回答的问题。对话是持久的,重新加载后存活;记录在签名令牌中旅行,而不是附加到您的消息前。在两个进程中设置AGENT_BRIDGE_SECRET为相同值以启用它。没有它,标签报告未配置,代理继续运行自己的计划。

docs/agent.md是完整文档。

技术栈

基于Bun的Turborepo monorepo,部署在Vercel上。

代理eve——持久会话、工具、技能、调度、沙箱
模型Vercel AI Gateway——没有提供者SDK,Vercel上的OIDC意味着没有密钥管理
沙箱生产中Vercel Sandbox,本地Docker或microsandbox
前端Next.js App Router · shadcn/ui · nuqs用于URL状态
APINestJS与nestjs-trpc——HTTP、认证、tRPC、Google同步
数据Prisma · Postgres (Neon) · 可选Redis (Upstash)
认证Better Auth,仅Google,一个允许列表
文件Vercel Blob——镜像头像图片,即使源消失也能存活
工具Biome · 全TypeScript

应用程序通过tRPC与API通信,路由器类型从NestJS路由器生成——因此前端从Prisma行到表格单元格是类型安全的。列表状态(过滤、排序、分页)位于URL中,因此复制地址栏重现视图。

布局

路径
apps/agent研究代理——工具、技能、调度、沙箱
apps/appNext.js前端 · :3000
apps/apiNestJS API——HTTP、认证、tRPC、Google同步 · :3001
packages/dbPrisma模式、迁移、共享Postgres客户端
packages/authBetter Auth配置和登录允许列表
packages/uishadcn/ui组件、Tailwind主题
packages/env查找并加载根.env

代码库坚持的三个规则

写在发生工作的地方,不在风格指南中:

  • 智能永不生活在API中 (docs/api.md)。Nest报告某事发生了;代理决定其含义。两份身份匹配器曾漂移,直到一个匹配地球上的每个雇主。
  • packages/ui是唯一的UI源 (docs/design.md)。没有在调用站点覆盖样式。
  • 没有组织。单租户,故意。一个总是相同值的organizationId是列、索引和权限检查,不购买任何东西,在审查时读起来像真实的。

快速开始

您需要Bun和Docker。

git clone https://github.com/trycompai/crm.git && cd crm
bun install

docker compose up -d          # Postgres on :5432
cp .env.example .env          # then fill in the four values below

bun run db:deploy             # apply migrations
bun run db:seed               # optional: a believable pipeline to look at
bun run dev

应用程序在localhost:3000,API在localhost:3001。

四个值

打开.env并设置这些。文件中的其他所有内容都是可选的,并注释掉。

变量填入什么
BETTER_AUTH_SECRETopenssl rand -base64 32
ALLOWED_SIGN_IN您的电子邮件域名,例如acme.com。或一个地址,例如you@gmail.com。
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETGoogle OAuth客户端——2分钟,如下。

DATABASE_URL已经匹配docker compose Postgres,因此除非您自带,否则保持原样。

获取Google OAuth客户端
  1. Google Cloud console → 凭据 → 创建凭据 → OAuth客户端ID → Web应用程序。
  2. 在授权重定向URI下,添加http://localhost:3001/api/auth/callback/google。
  3. 为项目启用Gmail API和Calendar API。
  4. 将客户端ID和密钥复制到.env。

Google登录是唯一进入方式,因此API在没有这些的情况下无法启动。如果您的账户在Google Workspace域名上,将同意屏幕设置为内部,您组织外的任何人甚至无法达到提示。

ALLOWED_SIGN_IN是整个授权模型——未设置值意味着无人可以登录,这是安全的失败方向。它接受整个域名、单个地址或混合:

ALLOWED_SIGN_IN="acme.com"                       # everyone at your company
ALLOWED_SIGN_IN="acme.com,contractor@gmail.com"  # …plus one outsider
ALLOWED_SIGN_IN="you@gmail.com"                  # a one-person install

配置

有一个.env在仓库根目录,由所有三个进程读取。真实环境变量总是赢,因此在托管平台上您在那里配置,文件纯粹是本地便利。

除了四个必需值,一切都是可选的,应用程序在没有它的情况下运行。.env.example是完整列表,每个都有注释;简短版本:

API_URL / APP_URL两个部分被服务的位置。仅在非localhost时需要。
PERPLEXITY_API_KEY让代理搜索开放网络,带引用。
RAPIDAPI_KEY让代理读取LinkedIn配置文件进行身份识别。
CONTEXT_DEV_API_KEY从域名获取公司logo、行业和社交信息。
AGENT_BRIDGE_SECRET让代表从联系人代理标签与代理对话。
REDIS_URL共享缓存。没有它,按实例和内存。
CRON_SECRET保护Gmail/Calendar同步路由。使用它时必需。

任务

命令
bun run dev一切,在监视模式
bun run build构建所有应用程序和包
bun run test运行测试套件
bun run check-types到处tsc –noEmit
bun run lint / formatBiome
bun run db:migrate创建并应用迁移
bun run db:seed补充演示管道(幂等)
bun run db:studioPrisma Studio
bun run –filter=api trpc:generate重新生成AppRouter类型
bun run –filter=api dev:session为本地用户打印会话cookie

使用Turborepo过滤器限定任何:bun run dev –filter=api。

因为Google是唯一的门,没有办法从终端获取会话——dev:session写入Better Auth本应写的行并打印它本应设置的cookie。它拒绝在NODE_ENV=production下运行。

部署

三个部署和一个Postgres:Next.js应用程序、NestJS API和代理。它们是独立的,唯一必须一致的是DATABASE_URL和BETTER_AUTH_SECRET——API生成会话cookie,应用程序验证它,因此不匹配是重定向循环而不是错误。

设置API_URL和APP_URL为真实源,如果两者在一个父级的不同子域上,设置AUTH_COOKIE_DOMAIN为父级,使一个cookie覆盖两者。将http://your-api-host/api/auth/callback/google添加到OAuth客户端的重定向URI。设置CRON_SECRET并指向调度器到POST /internal/sync/google以保持邮箱同步运行。

apps/api/src/generated/server.ts已提交,build永远不应重新生成它——生成器需要比大多数构建镜像更新的GLIBC。在本地重新生成并与导致它的路由器更改一起提交。

贡献

我们宁愿有一段您写的段落,而不是代理写的拉取请求。参见CONTRIBUTING.md。

安全问题通过SECURITY.md私下进行,不是公共问题。

许可证

MIT。