From d857352e29d5428d2ac8d45ad276c2cf926b0967 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 21 Sep 2026 16:45:23 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E9=97=AD=E5=90=88=20harm?= =?UTF-8?q?ony-permission-history=20=E2=80=94=E2=80=94=20=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E6=A0=8F=E8=A1=A5=E4=B8=8A=E3=80=8C=E5=B7=B2=E5=86=B3?= =?UTF-8?q?=E7=AD=96=E7=9A=84=E5=8E=86=E5=8F=B2=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这是 `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、编译通过,但我不声称已看到它渲染。 --- .../electron/test/cross-client-logic.test.mjs | 10 +- .../entry/src/main/ets/model/MailGrouping.ts | 106 ++++++++++++++++++ .../entry/src/main/ets/pages/MainPage.ets | 83 +++++++++++++- docs/DEBTS.json | 8 +- 4 files changed, 201 insertions(+), 6 deletions(-) diff --git a/client/electron/test/cross-client-logic.test.mjs b/client/electron/test/cross-client-logic.test.mjs index 818107d..601a7bd 100644 --- a/client/electron/test/cross-client-logic.test.mjs +++ b/client/electron/test/cross-client-logic.test.mjs @@ -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, diff --git a/client/harmony/entry/src/main/ets/model/MailGrouping.ts b/client/harmony/entry/src/main/ets/model/MailGrouping.ts index 06766f1..f9cfdbd 100644 --- a/client/harmony/entry/src/main/ets/model/MailGrouping.ts +++ b/client/harmony/entry/src/main/ets/model/MailGrouping.ts @@ -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 = new Map(); + + 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[] = []; diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index 3f65128..aae0335 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -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) }) diff --git a/docs/DEBTS.json b/docs/DEBTS.json index cc30104..417afee 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -146,11 +146,11 @@ }, { "id": "harmony-permission-history", - "count": 1, - "due": "做「授权栏与 WebUI 对齐」时", + "count": 0, + "due": "**已完成 2026-09-21**(本轮「授权栏与 WebUI 对齐」)", "where": "client/harmony/entry/src/main/ets/pages/MainPage.ets 的 PermissionTab(现在只读 /permission/pending)", "kind": "scope", - "note": "2026-09-20 由 `cross-client-logic.test.mjs` 的**缺口判据**报出来的(它要求两侧导出缺口只减不增,`groupPermissions` 是新多出来的一个)。\n\n**两端拿授权栏数据的方式根本不同:**\n· WebUI:`PermissionList.tsx:27` 拿 inbox 自己分组 —— `const groups = groupPermissions(inbox)`,每组同时渲染两段:\n · `g.pending`(显示 `{n} 待决策`)\n · `g.settled`(显示 `历史 {n}`,见 `:182`)\n 即**待决 + 已决策历史**都在,且按会话分组。\n· 鸿蒙:`MainPage.ets` 的 `PermissionTab` 调专用端点 `GET /permission/pending`(服务端 `ListPendingPermissionsFor`,SQL 里带 `WHERE pr.result IS NULL`)—— **只拿得到待决的**,而且是**平铺一列**,不分组。\n\n⇒ 用户可见差异:**已决策的授权记录在鸿蒙上完全看不到**(WebUI 能看到每个会话的历史条数)。\n\n★ 为什么没当场改:这不是照抄一个函数能解决的 —— 要么改成读 inbox 并实现分组渲染(结构改动),要么另调一个含已决策的端点(服务端可能没有)。两条路都要想清楚哪边是权威(本仓方向:**electron 是唯一真实源泉**⇒ 倾向于改成读 inbox + 分组)。先登记,别假装对齐了。" + "note": "**已修(2026-09-21)**。原文:鸿蒙只调 `/permission/pending`(SQL `WHERE pr.result IS NULL`)⇒ 已决策的历史完全看不到。\n\n修法与**为什么不照抄 WebUI 的 inbox 分组**:\n· WebUI 从 inbox 分组(`groupPermissions`),但它的 `PermissionRow` 只渲染\n `subject`/`created_at`/`permission_result`/`permission_expires_at`(逐字段 grep 过);\n· 而**待决**那一段我们要显示 `question`/`options`/`context`/`kind` —— 那四个字段在\n `permission_requests` **表**里,inbox 回包(`models.Mail`)**没有**(模型逐条核过)。\n⇒ 待决继续走专用端点(信息更全、能直接决策),**历史**走 inbox 补上。\n 代价:每账号多一次请求。这是有意的取舍。\n\n落地:`model/MailGrouping.ts` 加 `groupPermissions` + `PermissionGroup` + `isPendingPermission`;\n`PermissionTab.load` 取 inbox 里 `mail_type=permission_request && permission_result!=空` 的,\n分组后渲染「历史 n 条」。`cross-client-logic.test.mjs` 的 `gaps` 已按提示清空。" } ] -} \ No newline at end of file +}