跨端: 闭合 harmony-permission-history —— 授权栏补上「已决策的历史」
这是 `docs/DEBTS.json` 里登记的一条,它的到期条件原文是
「做『授权栏与 WebUI 对齐』时」—— 就是现在这一轮。
## 原缺口
鸿蒙的 `PermissionTab` 只调 `GET /permission/pending`(服务端
`ListPendingPermissionsFor`,SQL 带 `WHERE pr.result IS NULL`)
⇒ **只拿得到待决的**,于是"这条会话批过哪些事"完全看不到;
而 WebUI 有(`PermissionList.tsx:182` 的「历史 {n}」)。
## 关键判断:**不照抄 WebUI 的 inbox 分组**
我先把 `PermissionTab.load` 整个改成读 inbox + `groupPermissions`,
**改到一半发现行不通**(编译报 `question`/`context`/`agent_name` 找不到):
· WebUI 从 inbox 分组,但它的 `PermissionRow` **只渲染**
`subject`/`created_at`/`permission_result`/`permission_expires_at`
(逐字段 grep 过,全文件没有 `question`/`options`/`context`);
· 而**待决**那一段我们要显示 `question`/`options`/`context`/`kind`
—— 那四个字段在 `permission_requests` **表**里,
inbox 回包(`models.Mail`)**没有它们**(模型逐条核过,只有 `permission_result`
与 `permission_options`)。
⇒ 两条来源各有各的信息量,不是二选一:
· **待决**继续走专用端点(信息更全、能直接决策);
· **历史**走 inbox 补上。
代价是每账号多一次请求 —— 这是**有意的取舍**,写在代码注释里。
(半成品已 `git checkout` 撤掉,没有把它留在提交里。
撤掉的原因如实记在注释里,免得下一个人以为"照着 WebUI 改"就行。)
## 落地
· `model/MailGrouping.ts`:加 `groupPermissions` + `PermissionGroup` +
`isPendingPermission`,逐条对齐 WebUI 的 `groupPermissions`,
含它那**三步排序**(有待决的先来 → 待决多的更靠前 → 最新一封倒序)。
· `PermissionTab`:从 inbox 取 `mail_type=permission_request &&
permission_result != ''` 的,分组后渲染「历史 n 条」(只读、不可操作)。
· 空态判据从 `requests.length === 0` 改成**两者都空**才显示 ——
否则"有待决的历史"会被误报成"没有待决策的请求"。
## 判据自己抓到了我
`cross-client-logic.test.mjs` 的「缺口只减不增」在我补上 `groupPermissions`
之后立刻变红,并给出准确指引:
减少(harmony 补上了功能)→ 请把 gaps 里对应的名字删掉
⇒ 已清空 `gaps`。**这条判据在这轮里三次发挥作用**:
第一次报出这个缺口(09-20),第二次在我半成品时红了,
第三次确认闭合。`pass=7 fail=0`。
`docs/DEBTS.json` 的 `count` 已改 0、`due` 记完成、`note` 写明修法与取舍。
## 设备验证(如实)
✓ 授权页正常渲染,进程存活(23343),无新 jscrash
✓ 空态文案正确(本机确实没有权限邮件)
✗ **"历史 n 条"真的显示出来**这条路径没能实测:
本机没有已决策的权限请求(服务端实测 `permission_request` 0 封)。
逻辑逐条对齐 WebUI、编译通过,但我不声称已看到它渲染。
This commit is contained in:
@ -135,7 +135,15 @@ const PAIRS = [
|
||||
* 登记在这里的意义:这条判据会让**下次再少一个导出**时红,
|
||||
* 而"这个缺口是已知的、有据可查的"与"悄悄少了一项比较"是两件事。
|
||||
*/
|
||||
gaps: ['groupPermissions'],
|
||||
/*
|
||||
* ★★ 2026-09-21 **缺口已闭合**:`groupPermissions` 原本登记在这里
|
||||
* (`harmony-permission-history`:鸿蒙只调 `/permission/pending`,
|
||||
* 拿不到已决策的历史)。本轮补上了:
|
||||
* · 纯逻辑层加 `groupPermissions`(与 WebUI 逐条对齐,含三步排序);
|
||||
* · `PermissionTab` 从 inbox 取**已决策**的权限请求并分组渲染「历史 n 条」。
|
||||
* ⇒ 按这条判据自己的提示「减少 → 请把 gaps 里对应的名字删掉」,删掉它。
|
||||
*/
|
||||
gaps: [],
|
||||
project: {
|
||||
groupMailsBySession: (v) => (v ?? []).map(g => ({
|
||||
sessionId: g.sessionId ?? g.session_id,
|
||||
|
||||
@ -170,6 +170,112 @@ export function sessionKey(m: MailLike): string {
|
||||
return m.source_account_id + '/' + sid;
|
||||
}
|
||||
|
||||
/** 一个会话下的权限请求分组(对齐 WebUI `PermissionGroup`) */
|
||||
export class PermissionGroup {
|
||||
/** 分组键:账号 + session_id(鸿蒙是多账号合并,见 `sessionKey`) */
|
||||
key: string = '';
|
||||
session_id: string = '';
|
||||
alias: string = '';
|
||||
/** 发起请求的 Agent 名(取最新一条) */
|
||||
agentName: string = '';
|
||||
/** 工作区路径(取最新一条) */
|
||||
path: string = '';
|
||||
/** 还没决策的 */
|
||||
pending: MailLike[] = [];
|
||||
/**
|
||||
* **已决策的历史**。
|
||||
*
|
||||
* ★★ 2026-09-21 新增 —— 这一段在鸿蒙侧**此前完全看不到**:
|
||||
* 旧实现只调 `GET /permission/pending`(SQL 里 `WHERE pr.result IS NULL`),
|
||||
* 于是"这个会话批过哪些事"无从查证。
|
||||
* 登记在 `docs/DEBTS.json` 的 `harmony-permission-history`,
|
||||
* 它的到期条件是「做『授权栏与 WebUI 对齐』时」—— 就是现在这一轮。
|
||||
*/
|
||||
settled: MailLike[] = [];
|
||||
latest: MailLike | undefined = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按会话把权限请求分组,每组**同时**给出待决与已决策两段。
|
||||
*
|
||||
* 逐条对齐 WebUI `mailGroups.ts:88-122` 的 `groupPermissions`:
|
||||
* · 只挑 `mail_type === 'permission_request'` 的邮件;
|
||||
* · 按会话桶化,组内按时间倒序,组头取最新一封;
|
||||
* · **排序**:有待决的先来 → 待决多的更靠前 → 其余按最新一封倒序。
|
||||
* 那三步的意图 WebUI 注释写明了:「那条会话卡得更久」优先。
|
||||
*
|
||||
* ★ 为什么放进纯逻辑层而不是写在 `PermissionTab` 里:
|
||||
* 它和 `groupMailsBySession` 是同一类东西(数据的组织方式),
|
||||
* 而 `cross-client-logic.test.mjs` 会把 `.ts` 模型与 electron 的同名函数**逐例比对**。
|
||||
* 写在组件闭包里就比对不到 —— 那种写法本仓已经栽过(`AddressSuggest` 那一轮)。
|
||||
*/
|
||||
export function groupPermissions(mails: MailLike[]): PermissionGroup[] {
|
||||
const groups: PermissionGroup[] = [];
|
||||
const index: Map<string, number> = new Map<string, number>();
|
||||
|
||||
for (let i = 0; i < mails.length; i++) {
|
||||
const m: MailLike = mails[i];
|
||||
if (m.mail_type !== 'permission_request') {
|
||||
continue;
|
||||
}
|
||||
const key: string = sessionKey(m);
|
||||
let g: PermissionGroup | undefined = undefined;
|
||||
const at: number | undefined = index.get(key);
|
||||
if (at !== undefined) {
|
||||
g = groups[at];
|
||||
}
|
||||
if (g === undefined) {
|
||||
g = new PermissionGroup();
|
||||
g.key = key;
|
||||
g.session_id = m.session_id;
|
||||
index.set(key, groups.length);
|
||||
groups.push(g);
|
||||
}
|
||||
if (isPendingPermission(m)) {
|
||||
g.pending.push(m);
|
||||
} else {
|
||||
g.settled.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const g: PermissionGroup = groups[i];
|
||||
g.pending.sort(byNewest);
|
||||
g.settled.sort(byNewest);
|
||||
/* 组头字段取"最新一封"(两段合起来看,与 WebUI 的 `sorted[0]` 同义) */
|
||||
const all: MailLike[] = g.pending.concat(g.settled);
|
||||
all.sort(byNewest);
|
||||
if (all.length > 0) {
|
||||
g.latest = all[0];
|
||||
g.alias = all[0].session_alias;
|
||||
g.agentName = all[0].from_name;
|
||||
/*
|
||||
* 路径取组内最新一封的。★ `MailLike` **没有** `session_workspace`(本仓
|
||||
* 为它踩过白屏:缺失时客户端拿到 `undefined`)⇒ 这里不做猜测,
|
||||
* 保持空串;授权页要显示路径的话得先给 `MailLike` 补字段并守 `omitempty`。
|
||||
* 本轮只做"待决/历史两段"这一件事,不顺手扩字段(顺手加是本仓反复出现的错法)。
|
||||
*/
|
||||
g.path = '';
|
||||
}
|
||||
}
|
||||
|
||||
groups.sort((a: PermissionGroup, b: PermissionGroup): number => {
|
||||
const aHas: boolean = a.pending.length > 0;
|
||||
const bHas: boolean = b.pending.length > 0;
|
||||
if (aHas !== bHas) {
|
||||
return aHas ? -1 : 1;
|
||||
}
|
||||
if (a.pending.length !== b.pending.length) {
|
||||
return b.pending.length - a.pending.length;
|
||||
}
|
||||
const al: MailLike | undefined = a.latest;
|
||||
const bl: MailLike | undefined = b.latest;
|
||||
return (al === undefined || bl === undefined) ? 0 : byNewest(al, bl);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** 按会话折叠;组头取组内**最新一封**,组间按最新一封时间倒序 */
|
||||
export function groupMailsBySession(mails: MailLike[]): SessionGroup[] {
|
||||
const groups: SessionGroup[] = [];
|
||||
|
||||
@ -29,6 +29,8 @@ import { BackgroundPlan, PresetLayer, TRANSPARENT, resolveBackground } from '../
|
||||
import { MailSummary, Contact, PermissionRequest, DecideResponse, SentResponse, PendingResponse } from '../model/Models';
|
||||
import {
|
||||
MailLike,
|
||||
PermissionGroup,
|
||||
groupPermissions,
|
||||
MailSplit,
|
||||
SessionGroup,
|
||||
groupMailsBySession,
|
||||
@ -1409,6 +1411,14 @@ struct PermissionTab {
|
||||
/** 底部悬浮条高度(窄屏非 0)——理由见 `InboxTab` 的 `navReserve` */
|
||||
@Prop navReserve: number = 0;
|
||||
@State requests: PermissionRequest[] = [];
|
||||
/**
|
||||
* 已决策的**历史**,按会话分组(对齐 WebUI `PermissionList.tsx:182` 的「历史 {n}」)。
|
||||
*
|
||||
* ★ 来源是 **inbox**(不是 `/permission/pending`)—— 后者 SQL 带
|
||||
* `WHERE pr.result IS NULL`,永远拿不到已决策的。
|
||||
* 详见 `load()` 里那段取舍说明。
|
||||
*/
|
||||
@State permGroups: PermissionGroup[] = [];
|
||||
@State loading: boolean = false;
|
||||
@State error: string = '';
|
||||
/** 正在填备注的那条(空串 = 没有) */
|
||||
@ -1432,6 +1442,8 @@ struct PermissionTab {
|
||||
await acctMgr.load();
|
||||
const accounts: AccountInfo[] = acctMgr.getAccounts();
|
||||
const all: PermissionRequest[] = [];
|
||||
/* 已决策的历史(从 inbox 取,见下面 `settled` 的注释) */
|
||||
const settled: MailLike[] = [];
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
const acct: AccountInfo = accounts[i];
|
||||
try {
|
||||
@ -1442,11 +1454,43 @@ struct PermissionTab {
|
||||
for (let j = 0; j < resp.requests.length; j++) {
|
||||
all.push(resp.requests[j]);
|
||||
}
|
||||
/*
|
||||
* ★★ 2026-09-21 补**已决策的历史**。
|
||||
*
|
||||
* 用户可见差异(登记在 `docs/DEBTS.json` 的 `harmony-permission-history`,
|
||||
* 其到期条件正是「做『授权栏与 WebUI 对齐』时」):
|
||||
* `GET /permission/pending` 的 SQL 带 `WHERE pr.result IS NULL`
|
||||
* ⇒ **只拿得到待决的**,于是"这条会话批过哪些事"在鸿蒙上完全看不到,
|
||||
* 而 WebUI 能看到(`PermissionList.tsx:182` 的「历史 {n}」)。
|
||||
*
|
||||
* ── 为什么不改走 WebUI 的 inbox 分组 ──
|
||||
*
|
||||
* 核实过:WebUI 从 inbox 分组(`groupPermissions`),但它的
|
||||
* `PermissionRow` **只渲染** `subject` / `created_at` / `permission_result`
|
||||
* / `permission_expires_at`(逐字段 grep 过)。
|
||||
* 而**待决**那一段我们要显示 `question`/`options`/`context`/`kind` ——
|
||||
* 那四个字段在 `permission_requests` **表**里,
|
||||
* inbox 回包(`models.Mail`)**没有它们**(模型里逐条核过)。
|
||||
*
|
||||
* ⇒ 待决继续走专用端点(信息更全、能直接决策),
|
||||
* 历史走 inbox 补上。两条来源合起来,与 WebUI 的可见信息量一致。
|
||||
* 代价:多一次请求/账号。这是**有意的取舍**,不是漏了优化。
|
||||
*/
|
||||
const inbox: InboxResponse = await new MailApi(c).inbox('all', INBOX_PAGE_SIZE);
|
||||
for (let j = 0; j < inbox.mails.length; j++) {
|
||||
const m: MailSummary = inbox.mails[j];
|
||||
/* 只要**已决策**的权限请求(待决的已由上面那个端点给了) */
|
||||
if (m.mail_type === 'permission_request' && m.permission_result.length > 0) {
|
||||
m.source_account_id = acct.id;
|
||||
settled.push(m);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 单账号失败不空整栏
|
||||
}
|
||||
}
|
||||
this.requests = all;
|
||||
this.permGroups = groupPermissions(settled);
|
||||
} catch (e) {
|
||||
const ae = e as ApiError;
|
||||
this.error = ae.message.length > 0 ? ae.message : '加载失败';
|
||||
@ -1624,7 +1668,7 @@ struct PermissionTab {
|
||||
} else if (this.error.length > 0) {
|
||||
Column() { Text(this.error).fontSize(13).fontColor(Theme.dangerFor()) }
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.requests.length === 0) {
|
||||
} else if (this.requests.length === 0 && this.permGroups.length === 0) {
|
||||
Column() {
|
||||
AmIcon({ iconName: 'shield', iconSize: 36, iconColor: Theme.textSubtleFor() }).margin({ bottom: 8 })
|
||||
Text(emptyTitle('permissions')).fontSize(15).fontColor(Theme.textMuted)
|
||||
@ -1633,12 +1677,49 @@ struct PermissionTab {
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
List({ space: 8 }) {
|
||||
/* 待决(可决策) */
|
||||
ForEach(this.requests, (req: PermissionRequest) => {
|
||||
ListItem() {
|
||||
this.RequestCard(req)
|
||||
}
|
||||
.width('100%')
|
||||
}, (req: PermissionRequest) => req.request_id)
|
||||
|
||||
/*
|
||||
* ── 已决策的历史(对齐 WebUI `PermissionList.tsx:182` 的「历史 {n}」)──
|
||||
*
|
||||
* ★★ 2026-09-21 新增。这一段在鸿蒙侧**此前完全看不到**
|
||||
* (`harmony-permission-history`,其到期条件就是"做授权栏对齐")。
|
||||
*
|
||||
* 只读、不可操作(决策已经发生,重放没有意义)—— 与 WebUI 一致:
|
||||
* 它把 `settled` 归到折叠的分组里,默认收起。
|
||||
* 这里是**平铺一行一行**的历史摘要(会话别名 + 决策 + 时间),
|
||||
* 不逐条列邮件正文:那是详情页的事。
|
||||
*/
|
||||
ForEach(this.permGroups, (g: PermissionGroup) => {
|
||||
ListItem() {
|
||||
Row() {
|
||||
Column() {
|
||||
Text(g.alias.length > 0 ? g.alias : '(未命名会话)')
|
||||
.fontSize(13)
|
||||
.fontFamily('monospace')
|
||||
.fontColor(Theme.textMuted)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
Text('历史 ' + g.settled.length.toString() + ' 条')
|
||||
.fontSize(11).fontColor(Theme.textSubtleFor())
|
||||
.margin({ top: 2 })
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(12)
|
||||
.attributeModifier(GlassCardModifier.of(this.bgActive, false))
|
||||
.borderRadius(Theme.radiusCard)
|
||||
}
|
||||
.width('100%')
|
||||
}, (g: PermissionGroup) => 'hist-' + g.key)
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||||
|
||||
Reference in New Issue
Block a user