# 私聊操作菜单

本次实现基线：official/dev `f7a9f1f`。开发前同步 `0bd6b9b`，交付前再次 rebase，纳入会话策略、输入框聚焦、个人快记主题和成员统计更新。合并前审核期间继续合入 `dev@3acb791` 的图片预览与快记背景音更新，保留已推送任务历史并重新回归。一次性替换菜单客户端状态管理，不新增配置、灰度或业务接口。

## 能力边界

| 消费面 | 本次范围 | 验证 |
| --- | --- | --- |
| UI | 统一菜单、共享查询快照、明确操作意图、身份隔离及失败恢复 | 资源/业务/组件/Sidebar 回归及页面验收 |
| Host owner | 沿用现有服务；补齐相关录音取消信号，封禁解析与提交绑定同一登录上下文 | 分发、服务及真实 HTTP 连通测试 |
| SDK | 不新增业务能力；沿用现有查询、写入与取消合同 | 既有 SDK 合同回归 |
| Tools | 不新增业务能力；沿用权限、确认及版本校验 | 既有工具合同回归 |

公共资源层不解释员工、拒收或封禁，只管理请求合并、快照、失效、操作屏障和有界缓存。业务通过 Interface 接入，不使用动态工作流引擎。

展示默认值不写成真实事实。拒收未知时不伪造 revision/canSend；已知拒收仍约束编辑器。菜单后台刷新不禁用其他操作；员工入口只使用当前账号本次登录经网络核验且有效的身份，服务端仍实时校验权限。

身份展示新鲜度采用内部 60 秒上限，不持久化授予资格；这不是远端角色撤销零延迟的保证。未知/过期身份隐藏受限项，仍有效身份刷新时不闪烁。

持久化拒收沿用现有存储协议，不增加双读迁移路径；共享内存快照按稳定会话身份管理，引用变化更新请求绑定，不丢弃已知事实。

查询失败保留事实；修改失败保留对应业务的恢复语义。拒收冲突不自动重放；封禁未确认时明确重试原命令，不把反向动作当恢复。默认缓存不替代服务端权限、目标或版本校验。

查询重试开始不清除已知错误，只有成功结果才能解除已知的权限拒绝。身份查询失败后，用户重新打开菜单会重新核验；仍有效身份无需等待后台刷新。封禁 Host owner 在解析私聊对端前后校验账号和登录凭据，并将同一会话传给 Backend；账号变化不能借用新账号提交旧操作。

## 资源与扩展

| 资源 | 键与策略 | 默认展示 |
| --- | --- | --- |
| 当前身份 | 当前环境与账号；每次登录重新确认，登录预热、前台恢复与过期核验 | 未确认不显示员工项 |
| 相关录音资格 | 当前环境与账号；只共享 able-func 资格，每次打开仍以当前私聊引用刷新 | 无缓存隐藏 |
| 拒收准入 | 账号 + 稳定会话键；保留完整投影，两个版本轴均不得倒退 | 未勾选，不伪造发送权限或版本 |
| 对端封禁 | 账号 + 稳定会话键；只有已核验员工打开菜单才读取 | 员工显示“封禁用户” |

相关录音列表、私聊目标解析和最终写入权限仍由原 Host/服务端 owner 校验。账号级资格缓存不授权访问另一个私聊或预加载录音列表。引用更新不会清空同一会话的已知事实。

新增选项在 `privateChatActionItems` 中显式声明可见条件、标签、固定 id 和动作；需要查询时在 `PrivateChatActionsPort` 下接入独立业务资源。无需修改公共 Store 或菜单渲染器，不引入动态注册框架、依赖图、轮询配置或兼容双读分支。

查询刷新不禁用按钮；仅对应项目执行修改时禁用。指针按下时捕获动作意图，行集合变化则取消当前手势；支持方向键、Home/End、Escape、Tab 和焦点恢复。使用实际菜单尺寸定位。

## 验证记录（2026-09-09）

- Node 24.19.0 类型构建、全量测试及 bundle 构建通过：460 个测试文件通过、4 个跳过；5,124 项测试通过、6 项跳过。仓库已有跳过项未改为已验收。
- 资源、业务、DOM 与 Sidebar 测试覆盖无缓存/离线、存量持久化数据、双版本冲突、读写竞态、同帧重复点击、取消确认、换账号/同账号重新登录、Provider 失效、权限过期、封禁不确定结果及第四项扩展。
- 通过官方 DSH CLI 将不可变 `.tgz` 安装到全新临时 Home、带空格路径的独立 Profile，插件版本仍为 `0.1.49`。实际运行时为未修改的官方 `0.1.0-rc.8`；原有常驻实例未改动。
- 制品页面通过本地 Host 响应夹具验收：30 秒慢请求下无加载占位；热打开重新请求；Escape 恢复焦点；普通账号无封禁入口且封禁查询为 0；资格失败可独立重试；拒收/解除恢复原草稿，消息发送次数为 0。
- 模拟封禁已写事实但投影失败：读取已封禁仍显示“重试封禁用户”；两次提交均为封禁，没有误发解封，成功回执后才显示解封。
- 手动页面夹具：`node tests/fixtures/private-chat-actions-browser.mjs <隔离 DSH 的 loopback 地址>`。所有 Arkme API 路径均由夹具处理，不转发业务写入；只适用于无真实凭据的隔离实例。

## 明确边界

- 角色展示最多复用 60 秒的当前登录已验证身份；没有实时角色撤销推送，不宣称远端撤销零延迟消失。未知、过期、账号切换和已知权限拒绝均保守隐藏；Backend 仍实时鉴权。
- 封禁没有公开 CAS 版本，不能宣称解决多管理员同时修改。未确认操作的同向重试保留在当前客户端资源生命周期内，不新增跨重启持久化命令队列。
- 页面验收使用模拟业务数据；没有对生产或测试服真实用户执行拒收/封禁。Windows/Linux 未进行真实系统运行验收；本次没有改动平台、进程、路径或凭据实现。
- 缓存不保证减少请求：每次打开刷新比旧版“成功后永久复用”请求更多；完整身份查询沿用 Profile 服务及其头像补全，不新增轻量身份接口。
- 本次不修改服务端业务合同、插件版本或根 README。按后续明确授权提交并推送任务分支；不合并、发布或替换常驻插件。

后续合并前审核、修复循环及分层验证记录位于仓库 `docs/changes/private-chat-actions-premerge-review.md`（审核资料不随运行制品发布）。

## 转发快记弹窗里的发起私聊

2026-09-20 新增：点击聊天记录（无论是转发过来的还是通过分享链接打开的）会打开转发快记弹窗，弹窗里每条记录的发送人头像此前没有入口。现在把指针移到**他人**的头像上会出现操作卡片，里面是「发起私聊」，选择后直接打开与该发送人的私聊会话。键盘聚焦头像同样能打开卡片；触屏不因悬浮触发，卡片本身沿用既有菜单的定位、外部点击与 Escape 收起。

身份来源：转发快照此前只有发送人昵称和不透明的 `avatarRef`，无法定位账号。`ArkmeForwardRecordPreviewItem` 因此新增 `senderUserId`，由 Provider 在已有解析结果上补齐——该字段**只在解析到真实账号且不是查看者本人时**下发，查看者自己的 id 和未解析的发送人都不上链路；发送人的头像封印仍照旧独立生成，两者互不替代。弹窗侧只消费这个字段，不按昵称反查账号。

边界：转写说话人标签从来不是账号身份（快照可能复用发送人的昵称与头像），因此说话人行永远不出现入口，即使调用方误传了 id 也会被 `arkmeForwardSenderChatPeerId` 拦掉；无法导航到会话的宿主（未传 `onPrivateChatOpened`）同样不显示入口。打开失败时保留卡片并显示原因，不伪造成功。所有入口路径（会话内详情、客户端日历、发给自己日历）共用同一个组件与同一套判定。

验证：`tests/forward-sender-identity.test.ts` 断言只有他人的已解析账号会出现在投影里，本人和未解析发送人都不下发；`tests/forward-sender-private-chat.test.tsx` 覆盖悬浮出现卡片、点击调用私聊接口并回调导航、失败保留卡片、无身份与不可导航时不出现入口、说话人行永不出现入口；并确认分别去掉身份判断、说话人判断、导航判断和本人排除后，对应用例都会失败。类型检查、完整测试与打包通过。
