跨端: 会话改名建议的接口层(详情页缺的第二块,服务端三个端点齐)
审计发现详情页缺三块功能之二:**Agent 的改名建议条**(WebUI `MailView.tsx:324`
的 `RenameProposalBar`)。这一批做**接口层**,UI 接线下一批。
## 为什么这件事不只是"少个提示条"
WebUI 那段的注释写得很清楚:
> Agent 干到一半自己改掉,人上一秒记住的地址下一秒就失效。
> 提议 + 人确认,既让 Agent 表达意图,又保证寻址稳定性由人掌握。
也就是**寻址稳定性**的设计 —— 别名是人的寻址入口,Agent 只能**提议**。
## 做了什么
1. `model/SessionRename.ets` —— `RenameProposal`(`alias` / `reason`,
字段名与服务端 json tag **逐字对齐**)
2. `api/SessionApi.ets` —— 三个动作:
· `getRenameProposal(sessionId)` → `{"proposal": {...}}` 或 `{"proposal": null}`
· `acceptRename(id, alias)` → **`PUT /sessions/{id}/alias`**
· `dismissRename(id)` → `POST /sessions/{id}/rename-proposal/dismiss`
3. 判据(`harmony-logic`,+1 条):字段名、响应外壳容忍 `null`、
接受走别名端点、驳回走专用端点、方法名与 WebUI store 同名、+ 变异自检。
★ **接受为什么不另开端点**(服务端注释原话):
「那条路径已经有唯一性校验与 409 处理,复制一遍只会多一个出错的地方」。
所以"接受建议"与"人手改别名"是**同一条路**,而"驳回"是另一条
(它不是改别名,是"别再问了"——服务端记下被驳回的别名,
否则每次打开会话都要重新点一次「忽略」)。
## 端到端验证(真数据,不是形态检查)
造了一封带标记的真邮件投进一个真会话:
POST /mail/send {reply_to: …,
body: "内容正文。<!-- agentmail:rename-session alias=\"rename-verify\" reason=\"验证改名建议链路\" -->"}
→ 200 {"rename_proposed":"rename-verify", …}
然后:
GET /sessions/{id}/rename-proposal
→ {"proposal":{"alias":"rename-verify","reason":"验证改名建议链路"}} ✓ 形状与我的一致
SELECT body FROM mails …
→ "内容正文。" ✓ 标记被剥掉
两件事都验到了:**服务端识别建议**,且**标记从人读的正文里剥离**
(HTML 注释在 Markdown 渲染器里会变成可见文本,所以必须剥,不能指望渲染器吞掉)。
## 判据
`run-all.mjs` → `checks=510 pass=510 fail=0 skip=0 red=0 broken=0 unreported=0`。
`harmony-logic` 30 → 31。`hvigorw assembleHap` 成功;前端重建 + 重打包。
**未做**:UI 接线(详情页的提示条)。接口层已完成并验证,
但"页面上真的显示建议条并能点接受/驳回"要下一批。
This commit is contained in:
59
client/harmony/entry/src/main/ets/api/SessionApi.ets
Normal file
59
client/harmony/entry/src/main/ets/api/SessionApi.ets
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 会话级接口(`/sessions/{id}/...`)。
|
||||
*
|
||||
* 为什么单独一个文件:会话与**邮件**是两个生命周期不同的东西 ——
|
||||
* 邮件是内容,会话是"一条任务"(有别名、有往返预算、有权限档)。
|
||||
* 混进 `MailApi` 会让"这条路径属于谁"变得含糊(本仓已有一次
|
||||
* `MeApi` 被塞进 `AdminApi.ets` 的教训,那是历史原因,不重复)。
|
||||
*/
|
||||
import { ApiClient } from './ApiClient';
|
||||
import { RenameProposal } from '../model/SessionRename';
|
||||
|
||||
/** `GET /sessions/{id}/rename-proposal` 的响应外壳 */
|
||||
class RenameProposalEnvelope {
|
||||
/** `null` = 没有待处理的建议(服务端用 null 而不是 404,是"正常状态") */
|
||||
proposal: RenameProposal | null = null;
|
||||
}
|
||||
|
||||
export class SessionApi {
|
||||
private client: ApiClient;
|
||||
|
||||
constructor(client: ApiClient) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读一条会话的**改名建议**(Agent 在正文里提的)。
|
||||
*
|
||||
* 服务端从正文里解析 `<!-- agentmail:rename-session alias="…" reason="…" -->`
|
||||
* 标记(`rename_proposal.go`),并把标记从正文剥掉 —— 所以页面拿到的是
|
||||
* "建议"与"干净正文"两件事。
|
||||
*/
|
||||
async getRenameProposal(sessionId: string): Promise<RenameProposal | null> {
|
||||
const resp: RenameProposalEnvelope =
|
||||
await this.client.get<RenameProposalEnvelope>('/sessions/' + sessionId + '/rename-proposal');
|
||||
return resp.proposal;
|
||||
}
|
||||
|
||||
/**
|
||||
* **接受**建议。
|
||||
*
|
||||
* ★ 接受走的是 `PUT /sessions/{id}/alias`(与"人手改别名"**同一个端点**)——
|
||||
* WebUI 侧也是这么做的(`sessionStore.acceptRename` 调 `updateSessionAlias`)。
|
||||
* 不为"接受建议"单开一个端点:那会让同一件事有两条实现,
|
||||
* 而别名冲突(409)的判定只需一处。
|
||||
*/
|
||||
async acceptRename(sessionId: string, alias: string): Promise<void> {
|
||||
await this.client.put('/sessions/' + sessionId + '/alias', { session_alias: alias });
|
||||
}
|
||||
|
||||
/**
|
||||
* **驳回**建议。服务端记下来,不再反复弹同一个。
|
||||
*
|
||||
* 与"接受"不同,这条有**专用端点**(`/rename-proposal/dismiss`)——
|
||||
* 因为"驳回"这件事在别名端点里没有对应物(它不是改别名,是"别再问了")。
|
||||
*/
|
||||
async dismissRename(sessionId: string): Promise<void> {
|
||||
await this.client.post('/sessions/' + sessionId + '/rename-proposal/dismiss', {});
|
||||
}
|
||||
}
|
||||
19
client/harmony/entry/src/main/ets/model/SessionRename.ets
Normal file
19
client/harmony/entry/src/main/ets/model/SessionRename.ets
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 会话改名建议(Agent 在正文里提的)。
|
||||
*
|
||||
* 服务端形状见 `server/internal/handler/rename_proposal.go`:
|
||||
* 正文里的 HTML 注释标记
|
||||
* `<!-- agentmail:rename-session alias="fix-login-leak" reason="…" -->`
|
||||
* 被解析成这个结构,标记本身从正文剥掉。
|
||||
*
|
||||
* ★ 为什么这件事重要(WebUI 注释里的原话):
|
||||
* 「Agent 干到一半自己改掉,人上一秒记住的地址下一秒就失效」——
|
||||
* 所以是**提议 + 人确认**,而不是 Agent 单方面改。
|
||||
* 人的确认是**寻址稳定性**的最后一道。
|
||||
*/
|
||||
export class RenameProposal {
|
||||
/** 建议的新别名(不带点;页面上显示成 `.xxx`) */
|
||||
alias: string = '';
|
||||
/** Agent 给的理由(可空) */
|
||||
reason: string = '';
|
||||
}
|
||||
Reference in New Issue
Block a user