Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/adventurex-live-matchmaking-implementation-spec.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# TOMEET AdventureX 现场组局技术实施规格

> 2026-07-25 冷启动修订:匹配采用在线贪心竞价,不再以固定用户总数 benchmark 决定是否启动。`waiting` 为实时高意愿,`watching` 为已授权主动推送的次优先状态,`push_consent` 为等待用户授权。30 秒仅是后台清算 tick;只有实际发送 1–3 个通过 `good | excellent` 门槛的候选后才开始 90 秒窗口。没有合格候选时本轮立即结束并如实说明池小或契合度不足,不刷新旧候选;用户同意后可在新用户或开放局出现时通过微信主动推送。虚拟用户池按所有者隔离并仅对白名单账号开放。本文后续旧段落中与本修订冲突的“三个固定候选 / 无候选也进入超时 / 固定池人数门槛”描述均以本修订为准。
> 2026-07-25 冷启动修订:匹配采用在线贪心竞价,不再以固定用户总数 benchmark 决定是否启动。`waiting` 为实时高意愿,`watching` 为已授权主动推送的次优先状态,`push_consent` 为等待用户授权。30 秒仅是后台清算 tick;候选按相对质量排序,`acceptable | good | excellent` 均可进入选择,只有 `bad` 作为无效组合排除。实际发送 1–3 个候选后才开始 90 秒窗口。没有有效候选时本轮立即结束并如实说明池小或没有可用活动,不刷新旧候选;用户同意后可在新用户或开放局出现时通过微信主动推送。虚拟用户池按所有者隔离并仅对白名单账号开放。本文后续旧段落中与本修订冲突的“三个固定候选 / 无候选也进入超时 / 固定池人数门槛 / good 绝对门槛”描述均以本修订为准。
>
> 2026-07-25 双向确认修订:用户已经选择但候选最终未成局时,系统必须确认其选择已收到,并以中性事实说明本次安排未完成成局;不得暴露或暗示具体是谁拒绝、未回复,也不得归因为该用户不够合适。当前尝试在窗口结束时明确结束,不自动刷新。未授权用户进入 `push_consent`,已授权用户回到 `watching`;其后续优先级高于普通 `watching`、低于实时 `waiting`,且仍须通过活动最低人数及 `good | excellent` 质量门槛。
> 2026-07-25 双向确认修订:用户已经选择但候选最终未成局时,系统必须确认其选择已收到,并以中性事实说明本次安排未完成成局;不得暴露或暗示具体是谁拒绝、未回复,也不得归因为该用户不够合适。当前尝试在窗口结束时明确结束,不自动刷新。未授权用户进入 `push_consent`,已授权用户回到 `watching`;其后续优先级高于普通 `watching`、低于实时 `waiting`,且仍须满足活动最低人数并排除 `bad` 无效组合。
>
> 2026-07-25 字符卡片修订:候选邀请和正式成局确认使用无右边框左框字符卡片。卡片保留顶部、底部、左侧竖线与必要分隔线;禁止右侧竖线、右上角、右下角和 Markdown 代码围栏。标题分别为 `TOMEET 组局邀请` 与 `TOMEET 成局确认函`。整张卡片最多使用 2 个克制、功能性的 emoji,例如人数或集合信息提示,不得逐行装饰。外框属于固定呈现协议,正文继续由 Hosted Agent 根据结构化事实和个人上下文生成。
> 2026-07-26 无边框文案修订:候选邀请和正式成局确认使用编排清晰的普通文本,不再使用字符画边框或 Markdown 代码围栏。候选按编号分段;成局信息按活动、人数、集合信息等事实自然分段。Hosted Agent 仍只能依据结构化事实和个人上下文生成措辞。
>
> 2026-07-25 成局退出修订:正式成局确认函发出后,用户退出必须在当前消息中给出任意非空理由;只说“退出”或“不去了”时不得执行,只追问一个简单理由。理由不做严格语义审查,仅私下保存,不向其他成员披露。退出后不得询问或自动启动重新匹配:已授权主动推送的原请求变为 `matching/watching`,未授权请求变为 `cancelled`。退出用户不得再次获得同一个开放局。本文后续与“确认函后退出可无理由、退出后询问/自动重新匹配”冲突的旧描述均以本修订为准。

Expand Down Expand Up @@ -171,7 +171,7 @@ stateDiagram-v2
- 用户明确要求英文时保存 `preferred_language=en` 并用四个英文气泡重新播放欢迎语,明确要求切回中文时同理;
- 用户已开始正常对话时不得再次补发;
- 初步了解结束时宽松地问一次有没有雷点、明确边界或不想遇到的情况,并记录 `boundary_prompted_at` 防止重复追问;
- 普通回复由微信发送端按一句话拆成气泡,内容较多时依次渐进发送;字符卡片始终保留为一个气泡。
- 普通回复由微信发送端按一句话拆成气泡,内容较多时依次渐进发送;候选和成局信息同样使用普通无边框文本。

### 3.2 图片拒绝

Expand Down Expand Up @@ -1610,7 +1610,7 @@ MemoryStore 和 SupabaseStore 必须同时实现,保证测试和 DEMO_MODE 可
- 用户要求直接匹配时立即输出 start_match;
- 图片拒绝后不再追问原因;
- 按 `preferredLanguage` 使用中文或英文,用户明确切换语言时输出对应 transition;
- 除字符卡片外,一句话一个段落,普通段落结尾不使用中文句号或英文句点;
- 一句话一个段落,普通段落结尾不使用中文句号或英文句点;
- 初步了解结束前宽松问一次雷点或边界,并用 `boundaryPromptedAt` 防止重复;
- 有歧义的具体事实可以顺势确认;
- 不确定事实不能写入 socialHooks。
Expand Down Expand Up @@ -1647,7 +1647,7 @@ MemoryStore 和 SupabaseStore 必须同时实现,保证测试和 DEMO_MODE 可
- 图片拒绝后 Agent 转文字且不追问原因;
- 中文与英文欢迎语内容完全一致且按四个气泡发送;
- 普通多句回复逐句发送,结尾句号移除,问号和感叹号保留;
- 字符卡片不拆分,最多接受 2 个克制、功能性的 emoji;
- 候选邀请按编号编排为普通无边框文本;
- 雷点问题只在初步了解结束时询问一次;
- 图片观察不直接生成 hook;
- “我们组过乐队”触发澄清而不保存;
Expand Down Expand Up @@ -1833,7 +1833,7 @@ ADVENTUREX_MATCHING_V1=true
- 用户拒绝图片后自然进入文字流程;
- Agent 追问具体、单问题、非问卷化;
- 初步了解结束时只问一次雷点或明确边界;
- 普通回复按一句话一个微信气泡渐进发送,字符卡片保持完整;
- 普通回复按一句话一个微信气泡渐进发送,候选和成局信息使用普通无边框文本;
- 图片观察不会直接写入社交钩子;
- 所有 hook 均可追溯到用户文字消息;
- 用户可随时要求直接匹配;
Expand Down
6 changes: 3 additions & 3 deletions docs/product-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ flowchart TB
- 用户因具体人物事实选择候选时,该人物未进入最终局则不自动改派;系统说明变化并结束本次请求,只有用户明确要求后才重新匹配。
- 没有生成真实候选时不启动 90 秒窗口,本轮立即结束。未授权用户进入 `push_consent`;同意后进入 `watching`,等待新用户或新开放局触发下一次清算。
- 已经收到候选但 90 秒内未选择时,本次具体尝试结束。未授权用户标记为 `expired`,Agent 告知如需继续可明确重新匹配;已授权用户回到 `watching`,不会把旧候选自动刷新成新一轮。
- 用户已经选择但候选最终未成局时,不按用户超时处理,也不向任何一方暴露谁拒绝或未确认。当前尝试明确结束;未授权用户进入 `push_consent`,已授权用户回到 `watching`。这类用户在后续 `watching` 候选中获得一次确认补位优先级,但仍低于当前实时 `waiting` 用户,且不会绕过 `good | excellent` 质量门槛。
- 用户已经选择但候选最终未成局时,不按用户超时处理,也不向任何一方暴露谁拒绝或未确认。当前尝试明确结束;未授权用户进入 `push_consent`,已授权用户回到 `watching`。这类用户在后续 `watching` 候选中获得一次确认补位优先级,但仍低于当前实时 `waiting` 用户,且不会绕过活动最低人数或 `bad` 无效组合限制。
- 正式成局确认函发出后,用户退出前必须给出任意非空理由;只说“退出”或“不去了”时,Agent 只追问一个简单理由,不执行退出。理由不做严格合理性判断,也不会发送给其他成员。退出后不询问重新匹配:已授权主动推送的请求回到 `matching/watching`,未授权请求变为 `cancelled`,两者都不会再次收到同一个开放局。

除首次 AdventureX 欢迎语外,候选、成局、超时、退出和变化通知都不在业务代码中拼固定回答。业务层只提交活动、人数、确认状态、人物事实和可用下一步等结构化事实;Hosted Agent 结合当前对话与记忆摘要生成最终措辞,再由发布前校验阶段删除无依据的标签、推断和状态升级。首次欢迎语是明确的硬编码产品例外,并通过语言级幂等键保证不会重复插入。新微信身份激活时只创建账号和加密保存一次性注册凭证;首次可确认投递的入站握手发送四个介绍气泡,再追加三个不写入聊天历史的 Web 注册气泡。触发这次投递的“你好”等首条消息只用于打开会话,不写入对话、不提交给 Agent;下一条消息才进入正常对话。完整发送后永久标记已交付,用户没有继续回复、Worker 重启或重新扫码都不会重放;中断重试复用每个气泡的稳定渠道幂等 ID。用户明确要求英文或切回中文时只重放四个介绍气泡,不生成新的注册凭证。微信默认中文;用户明确要求英文或切回中文时更新偏好并用对应语言重新播放欢迎语。

普通微信回复按一句话一个气泡发送,普通气泡结尾移除中文句号和英文句点,问号与感叹号保留。内容较多时按顺序逐个发送,并在气泡之间加入很短的可配置延迟。初步了解结束时,Agent 宽松地问一次用户有没有雷点或明确边界,不要求列清单,也不重复追问。

候选邀请与正式成局确认采用无右边框的左框字符卡片。邀请标题为 `TOMEET 组局邀请`,成局标题为 `TOMEET 成局确认函`;卡片保留顶部、分隔线、底部和左侧竖线,不使用右侧竖线、右上角或右下角,也不包 Markdown 代码围栏。整张卡片最多使用 2 个克制、功能性的 emoji,例如 `👥` 表示人数、`📍` 表示集合信息;不得逐行装饰。外框是固定呈现协议,卡片正文仍由 Hosted Agent 根据结构化事实和用户上下文生成,微信发送端将整张卡片保留为一个气泡。
候选邀请与正式成局确认采用编排清晰的普通无边框文本,不使用字符画外框或 Markdown 代码围栏。候选邀请保留 `TOMEET 组局邀请` 标题并按编号分段;成局确认按活动、人数、集合信息等结构化事实自然分段。Hosted Agent 仍根据结构化事实和用户上下文生成措辞,不能为了排版补充不存在的信息。

## 开放局与变化通知

Expand All @@ -83,7 +83,7 @@ flowchart TB
- 当前所有用户都在 AdventureX 现场,不使用时间、地点、距离、年龄、性别等用户硬约束。
- Agent 不临时生成线下活动,只使用人工维护的活动目录。
- 匹配同时考虑人与人、人与活动、整组人与活动,不以兴趣标签、人格分类、人口属性、关键词重合或永久兼容分数代替判断。
- 匹配采用在线贪心竞价:`waiting` 用户优先,`watching` 用户用于补足或形成明显更好的组合;没有固定总人数 benchmark,但活动最低人数与 `good | excellent` 质量门槛必须满足。
- 匹配采用在线贪心竞价:`waiting` 用户优先,`watching` 用户用于补足或形成明显更好的组合;没有固定总人数 benchmark。候选按相对质量排序,`acceptable | good | excellent` 均可参与,只有 `bad` 作为无效组合排除。
- 候选数量是 1–3 个真实选项,不为了凑满三个降低质量或编造候选。
- 虚拟测试用户池按所有者隔离,只能由配置账号通过受保护开关启用,永远不会进入真实全局池。
- 用户最多进入一个最终局,且只能进入自己看过并明确接受的活动。
Expand Down
2 changes: 1 addition & 1 deletion docs/railway-production.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ WECHAT_TURN_PROGRESS_INTERVAL_MS=30000
WECHAT_TURN_PROGRESS_MAX_NOTICES=1
```

`WECHAT_BUBBLE_DELAY_MS` 控制一句话气泡之间的渐进发送间隔,允许 `0–5000` 毫秒,生产建议约 `180–220` 毫秒,测试使用 `0`。组局邀请和成局确认函字符卡片不会被拆分。
`WECHAT_BUBBLE_DELAY_MS` 控制一句话气泡之间的渐进发送间隔,允许 `0–5000` 毫秒,生产建议约 `180–220` 毫秒,测试使用 `0`。组局邀请和成局确认函使用普通无边框文本,并按正常消息分段发送。

`WECHAT_TURN_PROGRESS_DELAY_MS` 控制首条“Agent 正在工作”提示出现前的等待时间,
`WECHAT_TURN_PROGRESS_INTERVAL_MS` 控制后续阶段提示间隔,`WECHAT_TURN_PROGRESS_MAX_NOTICES`
Expand Down
35 changes: 12 additions & 23 deletions packages/intelligence/src/hosted-llm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1144,8 +1144,9 @@ describe("hosted Agent product-event composition", () => {
expect(result.optionPreviews.map((option) => option.optionNumber)).toEqual([1, 2]);
expect(result.optionPreviews[1]?.text).toContain("共同散步");
expect(result.optionPreviews[1]?.text).toContain("边走边聊");
expect(result.content).toContain("┃ TOMEET 组局邀请");
expect(result.content).toContain("TOMEET 组局邀请");
expect(result.content).toContain("2|共同散步");
expect(result.content).not.toMatch(/[┏┓┗┛┣┫┃│]/u);
});

it("drops option previews on non-candidate product events", async () => {
Expand All @@ -1163,7 +1164,7 @@ describe("hosted Agent product-event composition", () => {
expect(result.optionPreviews).toEqual([]);
});

it("repairs invitation cards that include a right border", async () => {
it("normalizes a legacy framed invitation into organized plain text", async () => {
const invalidRightFrame = [
"┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓",
"┃ TOMEET 组局邀请 ┃",
Expand All @@ -1189,11 +1190,12 @@ describe("hosted Agent product-event composition", () => {
}
});

expect(result.content).toContain("┃ TOMEET 组局邀请");
expect(result.content).not.toMatch(/[┃│┫┤┓┐┛┘]\s*$/mu);
expect(result.content).toContain("TOMEET 组局邀请");
expect(result.content).toContain("1|故事交换桌");
expect(result.content).not.toMatch(/[┏┓┗┛┣┫┃│]/u);
});

it("accepts a left-frame-only formed-room confirmation", async () => {
it("normalizes a legacy framed room confirmation into plain text", async () => {
const content = leftFrame("TOMEET 成局确认函", [
"活动 故事交换桌",
"人数 4 人",
Expand All @@ -1214,11 +1216,12 @@ describe("hosted Agent product-event composition", () => {
}
});

expect(result.content).toContain("┃ TOMEET 成局确认函");
expect(result.content.split("\n").every((line) => !/[┃│┫┤┓┐┛┘]\s*$/u.test(line))).toBe(true);
expect(result.content).toContain("TOMEET 成局确认函");
expect(result.content).toContain("活动 故事交换桌");
expect(result.content).not.toMatch(/[┏┓┗┛┣┫┃│]/u);
});

it("allows at most two restrained emoji in invitation and confirmation cards", async () => {
it("keeps functional emoji in an organized plain room confirmation", async () => {
const content = leftFrame("TOMEET 成局确认函", [
"👥 人数 4 人",
"📍 集合 TOMEET 集合点"
Expand All @@ -1239,21 +1242,7 @@ describe("hosted Agent product-event composition", () => {
});
expect(result.content).toContain("👥");
expect(result.content).toContain("📍");

const crowded = leftFrame("TOMEET 成局确认函", ["👥 4 人", "📍 集合点", "✨ 已成局"]);
stubChatResponses(
{ content: crowded, optionPreviews: [] },
{ content: crowded, optionPreviews: [] }
);
await expect(hostedWithSearch().composeProductMessage(agentContext(), {
kind: "room_intro",
facts: {
activity: { name: "故事交换桌" },
playerCount: 4,
meetingPoint: "集合点",
confirmedFacts: []
}
})).rejects.toThrow("必须使用无右边框的左框字符卡片");
expect(result.content).not.toMatch(/[┏┓┗┛┣┫┃│]/u);
});

it("keeps unavailable messaging grounded in pool cause and consent state", async () => {
Expand Down
Loading
Loading