Hermes Studio 移动端同意超时机制:日历与提醒授权统一五分钟后端实现解析
2026/9/24 1:39:46 网站建设 项目流程
  • AI 应用
  • 人工智能
  • AI Agent
  • 本地部署
  • 前端
  • 后端
  • 工作流自动化

【免费下载链接】ekko-studio

Ekko Studio is a local-first AI workspace for multi-agent chat, coding, and visual workflows, available on desktop and the web.

项目地址:https://gitcode.com/gh_mirrors/he/ekko-studio
点击查看免费下载

导读

本文围绕 Hermes Studio(Ekko Studio 的桌面/Web 本地优先 AI 工作区)中的移动端授权卡片超时机制展开,聚焦"移动日历与提醒(Mobile Calendar & Reminder)同意等待时长"这一变更:将原本 30 秒的设备端硬上限统一为与 App 通用同意卡片一致的5 分钟(300,000ms)默认值,并使服务端与设备端 deadline 保持一致。读完本文,你将理解 MCP/OpenAPI 对超时参数的约束、服务端如何对timeout_ms做边界钳制(clamp)、expires_at_ms如何在事件中随卡片刻画 deadline,以及过期删除保护与未过期响应放行的完整链路,并可通过仓库中的测试用例进行验证。

本文的技术依据为仓库文档 docs/chat-chain-changes/2026-09-05-mobile-consent-timeout.md(变更记录),以及服务端源码 chat-run.ts 与对应测试 run-chat-mobile-calendar.test.ts。

变更背景:30 秒设备端上限带来的不一致

此前,移动日历与提醒(calendar / reminder)类请求的设备端同意等待被限制在 30 秒内,而 App 中其他通用同意卡片(generic consent card)则采用 5 分钟等待时长。这种不一致会造成两类问题:

  • 用户体验不一致:用户在同一 App 内处理不同授权卡片时,等待规则完全不同,日历/提醒卡在 30 秒后即失效;
  • 服务端与设备端 deadline 错位:设备端按 30 秒兜底,而服务端任务可能仍在等待或按其他规则判定,导致"卡片已过期但后端仍挂起"或"后端已超时但卡片仍展示"的竞态。

本次变更的核心诉求(见文档 front-matter 的impact字段)即:默认改为 5 分钟,与 App 通用同意卡片保持一致,并让服务端与设备端 deadline 相匹配,取代原先 30 秒的设备端上限

超时参数的三重约束:MCP、OpenAPI 与服务端钳制

文档指出:"MCP and OpenAPI allow 3–300 seconds and advertise a five-minute default."这句话揭示了移动端同意超时的完整参数契约:

允许范围默认值说明
MCP 工具契约3–300 秒(即 3000–300000ms)300000ms(5 分钟)Agent 调用移动日历/提醒工具时的timeout_ms参数约束
OpenAPI 描述3–300 秒300000ms面向 App/第三方集成的接口文档同步声明
服务端实现3–300 秒(钳制)300000msboundedMobileCalendarTimeout兜底与边界钳制

在服务端源码 chat-run.ts 中,这一契约被落实为三个常量与一个钳制函数:

const MOBILE_CALENDAR_MIN_TIMEOUT_MS = 3_000 const MOBILE_CALENDAR_MAX_TIMEOUT_MS = 300_000 const MOBILE_CALENDAR_DEFAULT_TIMEOUT_MS = 300_000 function boundedMobileCalendarTimeout(value: unknown): number { const numeric = Math.round(Number(value)) if (value == null || !Number.isFinite(numeric) || numeric <= 0) return MOBILE_CALENDAR_DEFAULT_TIMEOUT_MS return Math.max(MOBILE_CALENDAR_MIN_TIMEOUT_MS, Math.min(MOBILE_CALENDAR_MAX_TIMEOUT_MS, numeric)) }

该函数的行为可以归纳为:

  1. 非法/缺省回退默认值timeout_msnullundefined、非有限数值或 ≤ 0 时,直接采用 5 分钟默认值;
  2. 上限钳制:超过 300000ms 的显式值会被压回 300000ms(例如调用方传入 600000ms 时,实际生效 300000ms);
  3. 下限保护:小于 3000ms 的值会被提升到 3000ms,避免过短的同意窗口导致用户来不及操作;
  4. 取整:非整数毫秒数会被Math.round归一。

文档特别强调"Explicit shorter waits remain supported"(显式更短的等待仍然受支持)——即 3–300 秒区间内的自定义值如 3000ms、15000ms 依然可以生效,只有越界与缺省才被钳制到边界或默认值。

服务端请求链路:timeout_ms 与 expires_at_ms 如何随事件下发

requestMobileCalendarrequestMobileHealth是服务端发起移动端同意等待的两条核心路径(chat-run.ts),二者共享同一套超时逻辑:

const requestId = randomUUID() const timeoutMs = boundedMobileCalendarTimeout(options.timeoutMs) const event = request.capability === 'reminder' ? 'reminder.requested' : 'calendar.requested' const idKey = request.capability === 'reminder' ? 'reminder_request_id' : 'calendar_request_id' return new Promise<MobileCalendarResponse>((resolve) => { const timer = setTimeout(() => { this.finishMobileCalendarRequest(requestId, { status: 'error', error: { code: 'calendar_failed' }, }) }, timeoutMs) timer.unref?.() this.pendingMobileCalendar.set(requestId, { sessionId, profile, target, request, resolve, timer }) this.emitMobileCalendarEvent(profile, sessionId, event, { event, [idKey]: requestId, ...request, target_device_id: target.deviceCode, target_user_id: target.userId, target_profile: target.profile, timeout_ms: timeoutMs, expires_at_ms: Date.now() + timeoutMs, }) })

关键设计点:

  • 单一超时源timeoutMs只计算一次,同时驱动服务端定时器、事件里的timeout_msexpires_at_ms,从源头杜绝三者不一致;
  • 绝对 deadline 优先expires_at_ms = Date.now() + timeoutMs以绝对时间戳形式下发,设备端/App 只需与本地时钟比对即可判定过期,无需自行换算相对时长;
  • 请求去重:同一会话已存在 pending 的日历/提醒请求时,新请求会被拒绝("A mobile calendar or reminder request is already pending"),配合单会话单卡片语义避免同意风暴;
  • 定时器不阻塞退出timer.unref?.()使该定时器不阻止进程退出,属于后台等待型定时器;
  • 设备路由隔离:事件通过target_device_id / target_user_id / target_profile精确投递到对应移动设备房间(见 mobile-device-target.ts 的mobileDeviceRoom命名空间mobile-consent:<deviceId>:<profile>),非目标设备的迟到响应会被mobileEventAllowed拒绝。

移动健康(health)请求路径requestMobileHealth采用完全相同的boundedMobileCalendarTimeout钳制逻辑,超时时返回status: 'error'error.code: 'health_timeout',仅额外要求目标平台为 iOS(iPhone/iPad)。

过期删除保护与未过期响应的放行语义

文档强调 "Expired delete protection remains"(过期删除保护保持不变)。这在 run-chat-mobile-calendar.test.ts 的用例 "requires fresh deletion confirmation and rejects late responses" 中得到了完整验证:

1. 发起 reminder delete 请求(timeoutMs: 3000),事件携带 expires_at_ms = now + 3000 2. 设备在过期前回复 status: 'denied' → 请求正常 resolve 为 denied 3. 同一会话再次发起删除请求,推进 3001ms 后 → 请求 resolve 为 status: 'error' 4. pending 表清空(pendingMobileCalendar.size === 0) 5. 此时设备端再以第一次的 reminder_request_id 回复 success → 因请求已过期删除,响应被丢弃,pending 表仍为 0

这说明删除类敏感操作要求"新鲜确认":一旦卡片超时,服务端立即清理挂起状态,迟到响应无法再提交删除动作——这是对删除等不可逆操作的关键安全护栏。与之相对,未过期的响应(无论 accept 还是 denied)都可在剩余窗口内正常放行。

默认值与边界的可验证行为:测试用例视角

测试 run-chat-mobile-calendar.test.ts 直接对应本次变更的验收标准,覆盖了文档描述的"五分钟后端默认":

it.each([undefined, null, 0, 600000, 300000])('uses a matching five-minute card/server deadline for %s', async timeoutMs => { const promise = server.requestMobileCalendar({ sessionId, profile, capability: 'reminder', action: 'list', purpose: 'test', timeoutMs }) const event = emitted.find(entry => entry.event === 'reminder.requested')!.payload expect(event.timeout_ms).toBe(300000) expect(event.expires_at_ms).toBe(Date.now() + 300000) await vi.advanceTimersByTimeAsync(60001) expect((server as any).pendingMobileCalendar.size).toBe(1) // 60s 后仍存活 await vi.advanceTimersByTimeAsync(239999) await expect(promise).resolves.toMatchObject({ status: 'error' }) expect((server as any).pendingMobileCalendar.size).toBe(0) // 300s 整到期清理 })

该用例用参数化方式断言:无论调用方传入undefinednull0600000还是300000,服务端最终下发的timeout_ms一律是300000expires_at_ms一律是now + 300000。这正对应boundedMobileCalendarTimeout的三类行为——缺省/非法回退默认、上限钳制、默认值直通。时间推进验证还精确刻画了生命周期:60 秒时挂起请求仍在(印证从 30 秒放宽到 5 分钟后的存活区间),推进满 300 秒后请求以error收尾并清理。

App / 原生侧兼容性:无需重建

文档明确指出:"Current App already consumes timeout_ms/expires_at_ms so no App/native rebuild is required."即服务端下发协议字段timeout_ms/expires_at_ms早已被现有 App 消费,本次变更纯粹是服务端默认值与钳制逻辑的调整:

  • 无协议破坏:字段名与语义未变,App 无需改版即可识别新的 5 分钟窗口;
  • 无原生重建成本:iOS/Android 端只需按其既有逻辑渲染卡片倒计时(依据expires_at_ms计算剩余时间),即可无缝获得更宽松、与通用卡片一致的等待体验;
  • 兼容旧行为:显式传入更短超时的调用方(如 Agent 侧指定 3000ms)依然按原样工作,符合 "Explicit shorter waits remain supported"。

与同类等待机制的横向对照

移动端同意超时并非孤立设计,仓库中还存在多个共享"5 分钟"语义的等待/授权机制,可作为理解本机制的参照:

机制常量/默认值位置
移动日历/提醒同意MOBILE_CALENDAR_DEFAULT_TIMEOUT_MS = 300_000chat-run.ts
Agent 澄清请求CLARIFICATION_TIMEOUT_MS = 300_000clarification-runs.ts
设备配对请求 TTLREQUEST_TTL_MS = 5 * 60 * 1000devices.ts
App 授权码 TTLAPP_AUTHORIZATION_CODE_TTL_SECONDS = 5 * 60app-connections-store.ts
群聊上传会话 TTLGROUP_CHAT_UPLOAD_SESSION_TTL_MS = 5 * 60 * 1000chunked-upload.ts

从源码结构看,这些机制普遍采用"服务端定时器 + 绝对过期时间戳 + 挂起表清理"的同一模式,本变更把移动日历/提醒对齐到这一既有节奏,属于一致性收敛而非引入新范式。可以推断,统一 5 分钟窗口也有助于用户在锁屏/切后台后仍有余裕完成授权,同时绝对时间戳语义天然免疫相对计时的漂移误差。

总结与排查建议

本次变更最终落地为三点:

  1. 默认统一:移动日历/提醒同意等待默认 5 分钟,与 App 通用同意卡片一致,取代旧的 30 秒设备端上限;
  2. 边界钳制timeout_ms允许 3000–300000ms,越界钳制、缺省回退默认值,显式短等待仍受支持;
  3. 协议兼容:服务端下发timeout_msexpires_at_ms供卡片渲染 deadline,App 无需重建,过期删除保护语义不变。

实际排查移动端同意失效问题时,建议按以下顺序核对:

  • 事件负载中的expires_at_ms是否等于Date.now() + timeout_ms(单点计算,正常应恒等);
  • timeout_ms是否落回 300000ms(先确认调用方是否传入了越界值如 600000ms,其会被钳制为 300000ms 而非直接生效);
  • 设备端是否基于绝对时间戳而非相对时长计算剩余时间,避免时钟偏差导致提前过期;
  • 删除类操作的响应是否在超时后到达——按设计会被静默丢弃,属预期安全行为。

如需深入验证或复现,可直接阅读服务端实现 chat-run.ts 与 L560-L676,并运行测试 run-chat-mobile-calendar.test.ts(关键用例位于 L124-L166)。

  • AI 应用
  • 人工智能
  • AI Agent
  • 本地部署
  • 前端
  • 后端
  • 工作流自动化

【免费下载链接】ekko-studio

Ekko Studio is a local-first AI workspace for multi-agent chat, coding, and visual workflows, available on desktop and the web.

项目地址:https://gitcode.com/gh_mirrors/he/ekko-studio
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询