TOMEET 是一个可以长期对话的社交 Agent。
Agent 通过用户主动提供的文本、图片、短录音和持续对话认识用户。当用户想社交时,系统先使用 LLM 贪心选择当前最高匹配的一位用户;双方接受后建立房间,再逐位邀请新用户直至满员或成员明确停止匹配。活动结束后,用户向 Agent 反馈感受,Agent 据此持续对齐用户意图。
仓库已经包含可运行的纯后端框架;可视化前端位于仓库之外:
apps/api:Fastify API,部署到 Railway。apps/intelligence-worker:支持多并发槽位的智能任务 Worker,部署到 Railway。apps/wechat-ilink-worker:微信 iLink 长轮询、收发消息与连接生命周期 Worker,部署到 Railway。apps/relationship-worker:把双方确认的关系凭证锚定到 Injective EVM,并处理可重试撤销。contracts/RelationshipRegistry.sol:隐私承诺式、不可转让的链上关系证明注册表。packages/*:契约、Agent、用户模型、匹配、游戏目录、房间、反馈、数据访问和任务编排。supabase/migrations:业务表、索引、私有 Storage Bucket 和并发安全 RPC。- Agent Memory V2:独立证据记忆、隐藏 profile、token-budgeted context 与同用户任务 FIFO;详见
docs/agent-memory-context.md。
Supabase 是正式持久化路径。内存 Store 只用于自动测试和 DEMO_MODE 本地预览。
仓库要求 Node.js 22 或更高版本,并通过 packageManager 固定使用 pnpm 10.14.0。仓库根目录的 .nvmrc 可用于切换 Node.js 主版本;首次运行前启用 Corepack:
nvm use 22
corepack enable
pnpm --versionpnpm --version 应输出 10.14.0。不要使用 Node.js 20 或未锁定版本的包管理器生成 lockfile。
无需 Supabase 凭据即可先验证完整流程:
pnpm install --frozen-lockfile
cp .env.example .env将 .env 中的 DEMO_MODE 改为 true,然后运行:
pnpm dev- API:
http://localhost:4000
本仓库不再提供本地页面。接口联调使用自动测试、docs/openapi.yaml 或仓库外部客户端。
当前 .env.example 默认使用真实硅基流动模型。填写 LLM_API_KEY 后,使用以下组合可以在不接 Supabase 的情况下做真实模型场景测试:
DEMO_MODE=true
LLM_API_BASE_URL=https://api.siliconflow.cn/v1
LLM_TEXT_MODEL=Qwen/Qwen3-Omni-30B-A3B-Instruct
LLM_VISION_MODEL=Qwen/Qwen3-Omni-30B-A3B-Instruct
TAVILY_API_KEY=...
这时只有业务数据和测试成员保存在内存中,Agent 理解、图片理解、动作识别、组人和游戏选择全部由真实模型完成。运行时不提供 Mock 模型;Mock 只存在于自动测试代码中。
- 创建 Supabase 项目。
- 使用 Supabase CLI 将迁移推送到该项目:
supabase login
supabase link --project-ref <project-ref>
supabase db push- 如需单人走通匹配,在开发项目的 SQL Editor 执行
supabase/seed.sql。这些种子成员只用于测试,会自动确认房间。 - 配置 API 和 Worker 的
SUPABASE_URL、SUPABASE_SERVICE_ROLE_KEY。 - API 使用
DEMO_MODE=false,Worker 必须配置真实模型。
使用真实托管模型时设置:
LLM_API_KEY=...
LLM_API_BASE_URL=https://api.siliconflow.cn/v1
LLM_TEXT_MODEL=Qwen/Qwen3-Omni-30B-A3B-Instruct
LLM_VISION_MODEL=Qwen/Qwen3-Omni-30B-A3B-Instruct
LLM_AUDIO_MODEL=FunAudioLLM/SenseVoiceSmall
TAVILY_API_KEY=...
TAVILY_API_BASE_URL=https://api.tavily.com
LLM 适配器采用 OpenAI 兼容的 Chat Completions HTTP 边界,当前已通过硅基流动 Qwen/Qwen3-Omni-30B-A3B-Instruct 的文本、图片和 JSON 输出验证。
Agent 会先生成受约束的搜索计划。明确要求联网、询问实时信息或出现无法可靠识别的专名时,Worker 使用 Tavily 搜索,再让模型只依据搜索证据生成候选回复,并在发布前独立核验活动名称、地点、日期和日程。来源保留在 webSearch.sources 元数据中,不拼接到 Agent 消息正文。普通聊天、稳定常识和单纯的社交意图不会调用搜索。未配置 TAVILY_API_KEY 时服务仍可启动,但 Agent 会明确说明无法联网核实,不会根据模型记忆猜测实时事实。
真实模型会输出受约束的结构化动作,包括开始/取消/重开匹配、选择/刷新候选、授权或停止主动推送、把 watching 重新激活为实时匹配、退出/确认/完成房间和提交反馈。Worker 执行动作前仍会通过领域规则和 Supabase RPC 校验,模型不能绕过房间状态或匹配约束。
AdventureX 冷启动匹配使用在线贪心竞价:当前 waiting 用户优先,已授权主动推送的 watching 用户次之;活动最低人数是硬约束,只有 Agent 判定为 good 或 excellent 的人与活动组合才会发送。30 秒只是后台清算 tick,90 秒只在 1–3 个真实候选实际生成后开始。没有合格候选时系统如实说明池小或契合度不足,并询问是否以后通过微信主动推送。
业务流程产生候选、成局、超时、房间变化或渠道能力边界时,只向 Hosted Agent 提交结构化事实,不在生产代码中拼接固定回复。Agent 先结合用户当前对话和记忆摘要生成措辞,再进行一次基于同一事实载荷的发布前校验;选项编号必须完整覆盖,确认成员与可能成员不能混写,也不能新增人格或兴趣标签。唯一明确保留的硬编码产品话术是首次 AdventureX 欢迎语。
生产上线的完整变量表、部署顺序、Supabase Auth 和冒烟验收步骤见 docs/railway-production.md。前端对接同时参考 docs/api.md 和机器可读的 docs/openapi.yaml。
正式发布在同一个 Railway Project 中保留 Intelligence Worker、Web API、WeChat API、WeChat Worker 和 Relationship Worker,并统一从同一个 main commit 发布。详细变量和发布顺序见 docs/agent-layer-release.md。
- Config file:
/railway.api.toml - 环境变量:
NODE_ENV=production、SUPABASE_URL、SUPABASE_SERVICE_ROLE_KEY、FRONTEND_ORIGIN、DEMO_MODE=false、ADVENTUREX_MATCHING_V1=true FRONTEND_ORIGIN支持逗号分隔多个来源,例如本地测试台和现有 Vercel 域名。- Agent 等用户接口要求
Authorization: Bearer <Supabase access token>;公开微信扫码接口改用一次性X-WeChat-Session-Token,详见docs/wechat-qr-api.md。 - 新微信用户会收到一次性 Web 注册链接;注册页领取同一个匿名 Supabase 用户后,可升级为邮箱+密码、手机号+密码或 Google 登录,详见
docs/wechat-web-registration.md。 - Railway 通过
/health做存活检查,通过/ready检查 Supabase 连接。
- Config file:
/railway.worker.toml - 环境变量:
SUPABASE_URL、SUPABASE_SERVICE_ROLE_KEY、真实模型配置、用于联网搜索的TAVILY_API_KEY,以及与 API 一致的ADVENTUREX_MATCHING_V1=true。 WORKER_CONCURRENCY默认8,单实例最大允许32;也可以在 Railway 横向增加副本。
Worker 使用 Supabase PostgreSQL 的 FOR UPDATE SKIP LOCKED 领取任务。多槽位和多副本不会重复领取同一任务;交互回复使用 partition_key=user:{userId},后台记忆与反馈使用 partition_key=memory:{userId},避免较慢的记忆任务阻塞用户当前回复,同时各自保持 FIFO。失败任务使用指数退避,处于未来退避时间的旧任务不会占住分区,进程中断后的锁会自动回收。
- Config file:
/railway.relationship.toml - 环境变量:Supabase 服务端凭据、Injective EVM RPC/Chain ID、已部署的 Registry 地址和专用 relayer 私钥。
- 合约通过
pnpm contracts:test验证;部署前为 Ignition 明确传入冷钱包admin和专用热钱包attester,不要在生产沿用默认同一账户。
当前外部前端只负责微信扫码连接,不承载 Agent Layer。它将 API Base URL 指向 Railway Web API,并仅调用 docs/wechat-qr-api.md 定义的四个 /wechat/connect/sessions* 接口;完整机器可读契约见 docs/openapi.yaml。
- 一个用户只能存在一个活跃匹配请求,由部分唯一索引保证。
- LLM 任务通过唯一幂等键去重。
- Worker 使用
SKIP LOCKED并发领取任务。 - 同一用户的交互回复与后台记忆分别按
user:*、memory:*FIFO;两条队列互不阻塞。 - 建房在单个数据库事务内锁定全部匹配请求,并再次校验等待状态、成员对应关系、重复成员和游戏人数范围。
- 重叠匹配并发发生时,只有第一个事务能成功分配成员。
- 建房记录
source_job_id,Worker 在建房后异常重试不会创建重复房间。 - 用户模型采用版本号乐观锁,冲突时 Worker 自动重新读取并重试。
- 房间确认、活动完成和反馈写入均在 Supabase RPC 内校验状态并原子提交。
pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm checkpnpm check 按 lint、typecheck、test、build 的顺序执行,是本地和后续 CI 的统一质量门禁。
当前测试覆盖完整核心流程、同一用户 50 个并发匹配请求去重、32 个 Worker 槽位任务领取不重复,并会在 PGlite 轻量 PostgreSQL 内核中执行 Supabase migrations 和关键 RPC。PGlite 测试属于 migration/RPC smoke,不等同于本地 Supabase Auth、RLS、Storage 或 Realtime 集成测试;详细基线和缺口见 docs/qa-baseline.md。
验证真实模型的结构化动作和匹配约束:
pnpm --filter @tomeet/intelligence-worker smoke:llm该检查会真实调用配置的模型,验证发起匹配、确认房间、完成活动、反馈整理,以及匹配结果必须包含触发用户。
同时配置 LLM_API_KEY 和 TAVILY_API_KEY 后,可以验证真实联网搜索、证据引用和 AdventureX 官方来源:
pnpm --filter @tomeet/intelligence-worker smoke:web-search安装 k6 后可运行基础 API 压测:
API_BASE_URL=https://your-api.up.railway.app k6 run tests/load/k6-api.js产品流程描述用户如何使用 TOMEET,不代表系统架构。
flowchart LR
Conversation["与 Agent 长期对话"]
Understanding["持续认识用户"]
Intent["用户表达社交意图"]
Match["LLM 贪心选择下一位用户"]
Room["双边接受后建房并逐位扩充"]
Offline["线下参与游戏"]
Feedback["向 Agent 反馈感受"]
Alignment["更新用户理解和意图"]
Conversation --> Understanding
Understanding --> Intent
Intent --> Match
Match --> Room
Room --> Offline
Offline --> Feedback
Feedback --> Alignment
Alignment --> Conversation
flowchart TB
DNS["Cloudflare DNS"]
Web["Next.js Web · Vercel"]
API["Application API · Railway"]
Worker["Intelligence Worker · Railway"]
DB["PostgreSQL · Supabase"]
Storage["Multimodal Storage · Supabase"]
LLM["Multimodal LLM / Text LLM"]
Search["Tavily Web Search"]
DNS --> Web
DNS --> API
Web --> API
Web --> Storage
API --> DB
API --> Storage
API --> Worker
Worker --> DB
Worker --> Storage
Worker --> LLM
Worker --> Search
采用单仓库、模块化单体和独立智能任务 Worker:
- Vercel 运行前端。
- Railway 运行 API 服务和 Intelligence Worker。
- Supabase 保存业务数据和多模态原文件。
- Cloudflare 只负责域名 DNS 解析。
- LLM 负责用户理解、长期记忆更新、组人和游戏选择。
- Tavily 只接收模型生成的短搜索查询,为实时外部事实提供网页证据。
- 维护用户与 Agent 的长期对话。
- 按 token 预算组装最近对话、短 checkpoint、隐藏 profile 和按需检索的详细记忆。
- 先冻结回复计划与业务 action,再由只读证据 finalizer 生成最终回复。
- 通过对话确认用户当前社交意图。
user_models 保留兼容字段、当前意图和业务历史;新的长期理解使用独立的证据层与摘要层:
UserMemory:带来源、状态、确认/使用次数和 TTL 的低敏感自然语言证据。UserMemoryProfile:可从 active memory 重建的隐藏profileNarrative与matchingNarrative。CurrentIntent:用户当前的社交意图。SocialHistory:历史匹配和线下游戏。FeedbackMemory:兼容保留的活动反馈摘要。MultimodalUnderstanding:有界的图片/短录音近期理解;不会自动成为稳定事实。
默认上下文不再注入完整 user_models。详细策略见 Agent Memory / Context V2。
只有当用户明确表达想社交时,系统才创建 MatchRequest。
LLM 匹配任务只读取受治理的自然语言输入:
- 当前等待中的匹配请求。
- 每位用户本次表达社交意图时的原话。
- 由明确低敏感记忆整合出的连续自然语言
matchingNarrative。 - 人工策划游戏的说明、人数、条件和执行过程。
原始 profile、详细记忆、多模态原文、兴趣标签、intentTags、traits、性格类型、人口属性、关键词计数和标签分数都不会进入匹配模型输入。游戏目录可以保留运营元数据,但匹配只看到自然语言体验说明与硬性人数条件。
初始 LLM 输出结构化 MatchDecision:
- 触发用户和当前最高匹配的另一位用户,共 2 人。
- 选中的线下游戏。
- 匹配判断摘要。
系统先创建双边邀请,双方接受后才创建房间。房间处于 active 时,每次只为当前最高匹配的一位等待用户创建入房邀请;接受后原子加入并继续下一轮。达到 capacity 自动变为 full,任一成员明确说“停止匹配”等同义指令后变为 stopped。
线下游戏由策划人员提前录入,Agent 和 LLM 只能选择已有游戏。
每个游戏保存:
- 游戏名称和说明。
- 最少与最多参与人数。
- 适合的社交意图。
- 体验特点和参与条件。
- 线下执行说明。
匹配房间只保存:
- 2–10 名成员。
- 选中的线下游戏。
- 成员确认状态。
- 持续匹配状态和房间人数上限。
- 活动完成状态。
房间不承载在线游戏过程。
活动结束后,用户通过 Agent 表达:
- 对本次人群的感受。
- 对线下游戏的感受。
- 是否建立了想继续发展的连接。
- 下一次希望保持或改变什么。
LLM 独立整理 CurrentIntent;反馈原话进入受隐私策略约束的异步记忆提取与 profile 重建流程。
Railway 上的 Intelligence Worker 处理:
- 文本、图片和短录音理解。
- Agent 回复生成。
- 独立记忆提取和 profile 整合。
- LLM 组人和线下游戏选择。
- 活动后反馈整理。
任务写入 Supabase 的 llm_jobs 表。Worker 使用数据库锁领取任务,执行完成后写回结果和状态,不增加额外队列服务。
users
conversations
messages
user_models
user_memories
user_memory_profiles
multimodal_inputs
match_requests
match_rooms
room_members
offline_games
post_event_feedback
llm_jobs
关键关系:
User
├── Conversation → Messages
├── UserModel
├── MultimodalInputs
├── MatchRequests
└── PostEventFeedback
MatchRequest
└── MatchRoom
├── RoomMembers
├── OfflineGame
└── PostEventFeedback
export interface UserModel {
userId: string;
vibeNarrative: string;
longTermProfile: Record<string, unknown>;
currentIntent: Record<string, unknown>;
socialHistory: string[];
feedbackMemory: string[];
multimodalUnderstanding: Record<string, unknown>;
version: number;
}
export interface MatchRequest {
requestId: string;
userId: string;
intentSnapshot: Record<string, unknown>;
status: "matching" | "invited" | "matched" | "cancelled" | "expired";
roomId: string | null;
inviteId: string | null;
}
export interface MatchDecision {
memberIds: string[];
offlineGameId: string;
summary: string;
}
export interface MatchRoom {
roomId: string;
memberIds: string[];
offlineGameId: string;
status: "confirming" | "confirmed" | "completed";
matchingStatus: "active" | "stopped" | "full";
capacity: number;
}
export interface PostEventFeedback {
userId: string;
roomId: string;
peopleFeedback: string;
gameFeedback: string;
connectionUserIds: string[];
nextIntent: string;
}POST /agent/messages
POST /agent/multimodal-inputs
POST /match-requests
GET /match-requests/:id
POST /match-requests/:id/cancel
GET /match-invites/:id
POST /match-invites/:id/accept
POST /match-invites/:id/decline
GET /rooms/:id
POST /rooms/:id/confirm
POST /rooms/:id/stop-match
POST /rooms/:id/complete
POST /rooms/:id/feedback
前端通过轮询获取匹配状态。Agent 回复可以使用普通请求或 SSE 流式返回。
tomeet/
├── apps/
│ ├── api/ # API 服务,部署到 Railway
│ ├── intelligence-worker/ # 智能任务,部署到 Railway
│ └── wechat-ilink-worker/ # 微信 iLink Worker,部署到 Railway
├── packages/
│ ├── contracts/
│ ├── agent-core/
│ ├── user-model/
│ ├── matchmaking/
│ ├── game-catalog/
│ ├── room/
│ ├── feedback/
│ ├── data/ # Supabase / 内存数据适配器
│ └── intelligence/ # 后台任务编排
├── supabase/
│ └── migrations/
├── docs/
│ ├── product-flow.md
│ └── architecture.md
├── tests/
│ └── core-flow/
├── .env.example
├── package.json
├── pnpm-workspace.yaml
└── README.md
- 创建 PostgreSQL 项目。
- 执行
supabase/migrations。 - 创建保存图片和短录音的 Storage Bucket。
- Railway 使用服务端数据库连接和 Service Role Key。
- Vercel 只使用公开配置或后端生成的上传授权。
在同一个 Railway Project 中保留四个 Service:
- Web API 和 WeChat API:均运行
apps/api的同一个maincommit。 - Intelligence Worker:运行
apps/intelligence-worker。 - WeChat Worker:运行
apps/wechat-ilink-worker。
四个 Service 连接同一套受控环境配置和 Supabase 项目。
- 继续使用已经部署的正式前端项目。
- 将
NEXT_PUBLIC_API_BASE_URL指向 Railway API 域名。 - 图片和短录音上传需要 Supabase 公开配置,但 Service Role Key 只能留在 Railway。
- 本仓库不包含前端源码或可部署前端。
Cloudflare 管理域名解析:
@ / www / app → Vercel 提供的域名记录
api → Railway 提供的域名记录
初始使用 DNS only,HTTPS 证书由 Vercel 和 Railway 自动签发。
NEXT_PUBLIC_API_BASE_URL
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY
DATABASE_URL
SUPABASE_URL
SUPABASE_SERVICE_ROLE_KEY
LLM_API_KEY
FRONTEND_ORIGIN
ADVENTUREX_MATCHING_V1
DATABASE_URL
SUPABASE_URL
SUPABASE_SERVICE_ROLE_KEY
LLM_API_KEY
TAVILY_API_KEY
ADVENTUREX_MATCHING_V1
- Agent 能通过长期对话和多模态输入持续认识用户。
- 用户表达社交意图后,系统能创建匹配请求。
- 用户能通过 Agent 收到并自然语言选择 1–3 个现场活动候选。
- 系统只用明确接受的候选原子创建 3–10 人确认房间,并支持合适的开放局补位。
- 成员、集合信息和招募状态变化能通过幂等 Agent 消息主动通知。
- LLM 能按贪心机制逐位选择候选用户并选择一款已有线下游戏。
- 系统能在双边接受后建房,持续邀请新成员,并在满员或收到停止指令后终止匹配。
- 活动结束后,用户能向 Agent 反馈感受和连接结果。
- 反馈能更新用户意图,并影响下一次匹配和游戏选择。