Hermes Desktop Office 空间交互解析:银行柜员/ATM 代表(Representative)系统与钱包面板实现
2026/9/22 18:35:56 网站建设 项目流程

Hermes Desktop Office 空间交互解析:银行柜员/ATM 代表(Representative)系统与钱包面板实现

【免费下载链接】hermes-desktopDesktop Companion for Hermes Agent项目地址: https://gitcode.com/gh_mirrors/her/hermes-desktop

本文围绕 Hermes Desktop(Hermes Agent 桌面伴侣)Office 标签页中的空间代表(Space Representative)系统展开:以银行柜员(bank teller)与自助 ATM 为第一个落地场景,完整剖析从 3D 场景的可交互对象(Interactable)、代表注册表(registry)、交互面板(RepInteractionPanel)到主进程后端钱包调用(wallet-actions)的整条调用链。读者将掌握该模块"一个注册表条目 + 一处 Interior 挂载 + 面板动作接线"的可扩展架构,以及账户作用域缓存、请求令牌防串号等关键实现细节。

背景:什么是"空间代表"系统

在 Office 标签页的 3D 城市中,银行是第一个"交易空间"(transaction space)。空间代表指的是空间中供 Agent 办理业务的 NPC——今天只有银行柜员,未来还有展厅销售(showroom sales)、建筑空间(building space)等。点击银行柜员或自助 ATM,会打开一个共享的动作模态框(balances、account status 等),针对所选 Agent 运行在 hermes-one 后端上的操作。

从源码结构看,新增一个空间代表只需四件事:

  1. 在 代表注册表 增加一条注册项;
  2. 在对应的 3D 室内场景(Interior)中用一个 Interactable 包裹代表模型;
  3. 在 RepInteractionPanel 中接线面板动作(仅当新空间需要新能力时);
  4. 补齐 i18n 字符串。

"即将上线"的动作(如 ATM 的 withdraw/deposit)通过disabled标志注册,在面板中渲染为弱化的"Soon" 芯片,直到对应流程落地。除点击外,面板还能由 Agent 自己打开:一条由聊天命令(chat command)驱动的"差事"(errand)会让 Agent 走到代表身边并自动打开面板执行指定动作——这一机制详见 office-world-actions。

代表注册表(Representative Registry)

注册表将每个代表的身份与菜单内容与 3D 场景解耦:注册项只描述 id、所属空间、i18n 标签键和有序动作列表,3D 场景通过 id 反查它。

核心类型与条目

registry.ts 定义了三个核心类型:

  • RepActionId:动作 id 的联合类型,含checkBalanceaccountStatuscreateAccountsendMoneywithdrawdeposit六种;
  • RepAction:动作描述,含id、i18n 键labelKey(位于office.命名空间)和可选disabled标志;
  • SpaceRepresentative:代表描述,含idspaceId、显示名labelKey、空间名spaceLabelKey和有序动作列表actions

当前注册的两个代表如下(完整源码见 registry.ts):

idspaceId动作说明
bank-tellerbankcheckBalance / accountStatus / createAccount银行柜员,全部可执行
atmbankcheckBalance / accountStatus / withdraw(disabled)/ deposit(disabled)自助 ATM,存取款为 coming-soon

值得注意的细节:

  • sendMoney("Send to agent")在类型层面存在、在注册表中被刻意省略。源码注释明确说明:在转账流程落地前,该动作不会出现在任何注册项中,但RepActionId类型与面板渲染已预留支持——这是"先扩展类型、后落地功能"的典型做法。
  • getRepresentative(id)负责按 id 解析代表,未知 id 或空值返回null,Office 屏幕据此决定是否渲染面板。
  • 面板校验保证"每个代表至少有 ≥1 个可执行动作",避免出现纯 coming-soon 的死菜单(见下文测试节)。

3D 交互物:Interactable 与银行场景挂载

Interactable 通用组件

Interactable.tsx 是把室内物体(ATM、展示车、办公桌)变为可交互对象的基础设施,行为如下:

  • 悬停:显示一个浮空的 Billboard 标签(深色半透明底板 + 浅色文字)和一个半透明的地面高亮环(ringGeometry),并切换指针光标(useCursor);
  • 点击:触发onActivate回调;
  • enabled关闭时:裸渲染子节点,不挂任何交互——这样在城市视角下,点击语义(选中 Agent、开发者建筑移动器)不会受室内交互影响。

组件还暴露positionindicatorPositionlabelHeightringRadius等参数,用于微调标签高度与高亮环半径(如 ATM 用labelHeight={1.9}ringRadius={0.75},柜员用labelHeight={2.1}ringRadius={0.55})。

银行室内:柜员与 ATM

Bank.tsx 是银行室内的实现,包含两个关键包装:

  • BankTellers:在柜台后排布 3 个StaffPerson(银行员工模型,制服色调比顾客更沉稳,见TELLER_TINTS),每个都用Interactable包裹;
  • BankATMs:按 4 个位置摆放atm.glb网格,同样用Interactable包裹。

两者都只在interactive(室内模式)下启用,onTellerActivate/onAtmActivate事件从 Bank → Office3D → Office.tsx 逐层冒泡,最终由 Office.tsx 设置当前激活的代表 id(bank-telleratm);进出建筑会清空该值。

一个值得注意的 i18n 边界:柜员的悬停标签tellerLabel是在 Office.tsx 中预先翻译后通过 props 层层下传的,因为 i18n 上下文无法跨越 r3f 的<Canvas>边界(useI18n只能在 React 树外使用);ATM 的标签则是静态字符串 "ATM"。

另外从代码演进看:ATM 之前打开的是个人资料模态框的钱包页,现已改为打开atm代表面板,与其他代表共用同一套模态机制。

交互面板:RepInteractionPanel

RepInteractionPanel.tsx 是所有代表(银行柜员、ATM)共用的居中模态框:半透明暗色背景 + 主题化卡片。面板采用纯扁平的主题 CSS 变量(var(--bg-tertiary)var(--border)等),不使用渐变。

Props 与打开方式

Prop类型说明
repSpaceRepresentative当前代表(由getRepresentative(activeRepId)解析)
agentsOfficeAgent[]可选 Agent 列表
initialAgentIdstring \| null初始选中 Agent
visiblebooleanOffice 标签页是否可见(用于账户重解析)
autoActionRepActionId \| null差事驱动的自动动作(chat world actions)
onClose() => void关闭回调

Office 传入的初始 Agent 为selectedId ?? defaultAgentIddefaultAgentId是当前激活 profile(按 agent id 匹配)或第一个 Agent——这样打开时选择器预选当前 profile,而不是空白的 "Choose an agent…"。

布局与交互细节

  • 背景层fixed inset-0,点击背景或按 Escape 关闭;卡片挂载时淡入/缩放(opacity+transform过渡),布局全部用内联样式(非 Tailwind class)以自包含、防定位破坏。
  • 头部身份区:按代表区分REP_ICONS图标(柜员 = 银行地标Landmark,ATM = 卡片CreditCard),下方是实时的状态行(带绿点):柜员显示 "Open · serving {agent}",ATM 显示 "Online · {agent}";未选 Agent 时显示 idle 文案。
  • Agent 选择芯片:头像 + 原生<select>组合,保证键盘可达性(appearance: none去掉默认样式,视觉上是胶囊芯片)。
  • Hero 卡片:随最近一次动作结果变化——
    • loading:骨架屏 + 旋转 Loader;
    • balance:总额数字 + 装饰性扁平 sparkline(BalanceSparkline 纯静态折线,不携带任何数据,因为后端不提供价格历史);
    • created:成功卡(地址缩写 · Base);
    • error:错误卡 +Retry按钮(runAction(activeAction)重跑上次动作);
    • hint:警告提示卡(signed-out / unlinked / foreign);
    • 空钱包卡:$0.00+ "no funds in this account yet"——注意不提供创建提示,因为能达到余额说明账户已存在(一个 Agent 只有一个账户)。
  • Token 行:余额加载后渲染圆形的 symbol 徽章(tokenBadge 按 symbol 着色:HD/H1/HERMES 系用主黄色,ETH/WETH 用主题强调色,其余用通用色)、名称、数量与 USD 估值。
  • 账户状态行:渲染 Transactable / Receive-only 徽章。
  • 数字字体:总额、Token 数量与 USD 均使用--font-numeric(Space Grotesk)渲染,通过自托管的@font-face引入src/renderer/src/assets/main.cssfonts/SpaceGrotesk-Variable.ttf),无 CDN

动作分发逻辑

动作渲染为flex-wrap 的芯片组visibleActions)而非固定网格,这样动作数量可变时永远整齐排布、不会出现空单元格:

  • check balance(主强调色芯片):优先读取缓存的可交易钱包 id(readBank),缺失时才走syncWallets查钱包列表,再渲染后端投资组合;
  • account status:通过syncWallets列出链接云端 Agent 的钱包;
  • create account(仅柜员):在后端创建钱包,后端幂等返回的 409 "already provisioned" 被映射为友好提示(见下节);
  • disabled 动作(ATM 的 withdraw/deposit):渲染为弱化芯片 + "Soon" 徽章。

因为一个 Agent 恰好只有一个账户,一旦确认该 Agent 已有账户,"Create account" 芯片会被直接移除rememberBank记录的hasAccount标志驱动visibleActions过滤)。signed-out、unlinked、foreign 三种状态渲染为提示而不是报错。

并发安全:每次请求都持有单调递增的requestSeq令牌。切换 Agent 选择器会使任何 in-flight 请求失效——晚到的响应永远不会把 A 的钱包渲染到 B 的上下文之下(apply只在requestSeq.current === request时生效)。

差事驱动的自动打开(autoAction)

来自 office-world-actions 编排的差事式打开会传入autoAction,面板在账户作用域解析完成后恰好执行一次该动作。这里的accountResolved门闩至关重要:账户 id 的解析会改变缓存键、递增requestSeq,若在挂载瞬间就触发动作,结果会被当作过期请求丢弃。autoRanRef保证同一动作只跑一次,切换autoAction时重置。

会话缓存:账户作用域 + 内存私有

缓存结构与键

每个 Agent 的银行状态(唯一的钱包 + 最近一次投资组合 +hasAccount)缓存在进程级内存Map(bankCache)中,键为`${signed-in account id}::${agent id}`。这样:

  • 重新打开面板可免请求瞬间渲染已知组合;
  • 缓存金融数据永不跨越登出或重新链接(不同账户产生不同键);
  • 纯内存存储,不落盘任何云端钱包数据,延续 wallet-token-balances 中 "never persist cloud wallets" 原则。

readBank/rememberBank是唯二访问入口。账户 id 通过window.hermesAPI.getAccount()(应用级)解析后拼入cacheKey;id 为 null(未解析或已登出)时,所有读都 miss、所有写都是 no-op——没有已知账户就既不提供也不存储任何金融数据。

关键安全语义

  • 同 profile 重链接到不同 Hermes 账户:键的账户半段变化,上一账户的投资组合与钱包 id 永远不会被读回;
  • 写按请求自身的账户 + Agent 键控:即使选择器已切换,晚到的结果仍会缓存到正确条目;
  • Office 标签页每次变为可见时重新解析账户,而非仅挂载时一次。原因:面板可以保持挂载(Office 只隐藏、从不卸载——见 office-3d-walk-mode),而用户可能在别处切换 Hermes 账户。visibleprop 把 Office 的显示状态传下来:隐藏时面板忘记已解析的账户accountId → null),防止返回时在重新校验前闪现过期余额;返回时重新运行getAccount,账户变化则产生新键与缓存 miss。

后端钱包动作:主进程对 hermes-one 的调用

面板的动作全部通过主进程调用 hermes-one 后端完成——桌面端不持有任何密钥、也不在本地读取链状态

IPC 通道与调用链

wallet-actions.ts 是主进程侧实现,通过 IPC 注册表 暴露两个通道:

IPC 通道主进程函数后端端点说明
wallet-portfoliogetWalletPortfolio(profile, walletId)GET /api/wallets/:id/portfolio读取钱包投资组合
wallet-provisionprovisionAgentWallet(profile)POST /api/wallets创建 Bankr 钱包

preload 层(src/preload/index.ts)把这两个通道封装为window.hermesAPI.getWalletPortfolio/provisionCloudWallet,结果类型WalletPortfolioResult/ProvisionWalletResult定义在 src/shared/wallets.ts。

getWalletPortfolio:读组合

  • 先经resolveLinkedAgent解析账户/令牌/链接 Agent id 前置信息;
  • 请求GET /api/wallets/:id/portfolio要求是可交易钱包(后端用钱包存储的密钥鉴权读取;receive-only 钱包会返回后端错误字符串);
  • 响应中的原始 token 行会被归一化:缺失 symbol 补"?"、缺失 name 补"Token"、非数字余额归零——畸形行有默认值兜底而不是崩溃
  • 网络失败时返回Couldn't reach ${apiUrl}: …错误信息。

provisionAgentWallet:创建钱包

  • 请求体为{ agentId, kind: "bankr" }
  • 后端幂等:重复创建返回 409,被映射为status: "exists",UI 据此提示"该 Agent 已存在账户"而非报错。

resolveLinkedAgent:公共前置

两个函数共用 resolveLinkedAgent(从钱包同步流程中提取的公共前奏),依次处理:

  1. signed-out:无账户或无令牌,直接短路(不发网络请求);
  2. 从未同步:先自动执行一次syncAgents()获得 Agent id;
  3. unlinked:同步后仍无 Agent id;
  4. foreign:链接的 Agent 归属另一个 Hermes 账户(或后端地址不匹配)——钱包动作不得作用于他人账户的 Agent(后端同样强制所有权,客户端提前显式拒绝);
  5. ok:返回{ apiUrl, token, agentId }

测试保障

模块配套三套 Vitest 测试:

  • registry.test.ts——校验每个代表都有标签且 ≥1 个可执行动作、id 唯一、银行柜员以 bank 空间注册了银行动作、未知 id 解析为 null;
  • wallet-actions.test.ts——portfolio:signed-out 短路(0 次网络调用)、token 映射、畸形行默认值、后端错误字符串透传、网络失败;provisioning:请求体、409 → exists、自动同步失败后 unlinked、HTTP 错误;
  • RepInteractionPanel.test.tsx——面板的四个关键行为保证:
    1. 面板跟随 Office 选择:选择变化时面板保持挂载,新非空选择被跟随,选择清空时保留面板自身选择,动作不会静默作用于 UI 已离开的 Agent;
    2. 丢弃过期动作结果:为 A 发起的动作在响应到达前切到 B,则结果被丢弃,B 的上下文永不显示 A 的钱包;重跑 B 的动作则正常渲染 B 的数据;
    3. 钱包缓存按账户作用域:同一 profile 重链接到另一账户后,之前账户下缓存的余额不再展示,重新打开渲染中性占位符(缓存键含账户 id);
    4. 账户作用域在重新显示时刷新:面板保持挂载期间账户变化(Office 隐藏再显示、从不卸载),重新显示会重新解析账户,旧账户缓存余额让位于占位符。

小结:可扩展的空间交互模式

银行是这套"空间代表"模式的第一个落地场景,其核心抽象——注册表驱动身份与菜单、Interactable 统一 3D 交互、面板按动作 id 分发、账户作用域缓存保证金融数据隔离、主进程统一走后端 API——为未来的展厅销售(showroom sales)与建筑空间(building space)提供了直接可复用的骨架。开发者若要新增一个空间,只需按文首的四步接入;而sendMoney类型预留与 ATM 的 disabled 存取款则展示了"先占位、后落地"的迭代路径。

进一步阅读:办公室 3D 室内与可交互对象总览见 office-3d-interiors,差事驱动的自动打开机制见 office-world-actions,钱包同步与余额机制见 wallet-token-balances。

【免费下载链接】hermes-desktopDesktop Companion for Hermes Agent项目地址: https://gitcode.com/gh_mirrors/her/hermes-desktop

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

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

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

立即咨询