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状态 |
| API | NestJS与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/app | Next.js前端 · :3000 |
| apps/api | NestJS API——HTTP、认证、tRPC、Google同步 · :3001 |
| packages/db | Prisma模式、迁移、共享Postgres客户端 |
| packages/auth | Better Auth配置和登录允许列表 |
| packages/ui | shadcn/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_SECRET | openssl rand -base64 32 |
| ALLOWED_SIGN_IN | 您的电子邮件域名,例如acme.com。或一个地址,例如you@gmail.com。 |
| GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | Google OAuth客户端——2分钟,如下。 |
DATABASE_URL已经匹配docker compose Postgres,因此除非您自带,否则保持原样。
获取Google OAuth客户端
- Google Cloud console → 凭据 → 创建凭据 → OAuth客户端ID → Web应用程序。
- 在授权重定向URI下,添加http://localhost:3001/api/auth/callback/google。
- 为项目启用Gmail API和Calendar API。
- 将客户端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 / format | Biome |
| bun run db:migrate | 创建并应用迁移 |
| bun run db:seed | 补充演示管道(幂等) |
| bun run db:studio | Prisma 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。