fix: 事故全链路修复 — 调度器合流 + 接管保护 + 补全去重 + 权限显示 + homeagent SSE 振荡
## 事故现场 用户选中补全里的「项目定位」→ 邮件投进另一条会话,界面显示的名字也不是 自己选的那个。授权页只显示 Agent 名,看不出哪个目录哪条线索。 ## 四处因果链 **① 调度器自己的 new_mail payload(起点)。** `notifyRecipients`(handler) 与 `SendCalendarMail`(scheduler)是两份代码。加 `platform_session_id` 时只改了 handler 那份 → 日历提醒投进接管会话时插件不知道是接管 → 另开一条新会话 → 命名同步冲掉接管会话的别名。 修法:抽出 `internal/notify` 包,唯一入口 `notify.Recipients`。 handler / scheduler / permission.go 都走它。新增字段时不存在「另一处忘了改」。 **② SyncSessionAlias 覆盖接管别名。** 别名是人从补全里选中的平台 slug, 任何平台命名同步都不该动它。加守卫 `platform_id <> ''` → 有绑定就返回当前值。 **③ SuggestSessionCandidates 按别名字符串去重。** 别名一被冲掉,同一条会话 出现两次(一次被冲的名字、一次镜像 slug),而另一条真实会话被吃掉。 改按 `platform_id` 去重。 mail 侧查 `s.platform_id`,镜像侧查 `platform_id`。 **④ FindOrCreateDefaultSession 不排除接管会话。** 日历提醒省略 session 位 → FindOrCreateDefaultSession 挑中人显式指定的接管会话。加 `platform_id = ''` 条件。 ## 权限页 **CreatePermissionMail 不写 from_workspace。** `from_workspace` 存空串 → 前端 `g.path && ...` 不渲染 → 人只看到光秃的 Agent 名,不知道哪个目录 哪条线索在请求权限。修法:INSERT 时从 sessions.workspace 取。 **SSE payload 缺 session_alias。** permission.go 的 SSE 不走 notify 包(决策人 不是地址解析出的参与方),但 payload 也要带 `session_alias` → 前端拼出 `pi@/home/program/agentmail.别名`,而不是光秃的 `pi`。 **mailGroups.ts:path ← session_workspace。** `from_workspace` 对 Agent 存的是 Agent 名(历史遗留),不能当路径用。PermissionList 显示完整三段地址 `agent@path.alias`。 ## NarrowStack z-index 窄屏日历的星期表头(`sticky top-0 z-10`)穿透到二级页面之上。覆盖层 auto z-index 输给 z-10 → 底层组件的层叠穿透到覆盖层。 修法:底层容器加 `isolate`(isolation: isolate),自成层叠上下文; 覆盖层加 `z-10`。只给覆盖层加 z-index 只能治当前一处,底层再写更大的 z-index 又会复现。 ## homeagent SSE 自激振荡 根因:五处缺陷叠加,SSE 每 60 秒断一次 → Gateway 全量重放 → 再断 → 再重放。 1. `p.client`(60s Timeout)跑 SSE 长连接 → 新增 `sseClient`(无超时) 2. `InjectInputSync` 在读循环里同步调用 → 改为 `go p.handleNewMail(evt)` 3. `lastEventID` 无条件赋值,Gateway 重放时发旧 ID → 单调递增 `sseMaxID` 4. 无邮件级去重 → 补 `deliveredMails map[string]bool` 5. 手动 `[]byte` 管理:每次 `buf[lineStart:]` 缩小 cap → 最终 len==cap → Read 零长切片 → 满速空转。改 `bufio.Reader`。 ## 清库 保留 jianf + 4 个 Agent 密钥 + 模型范围配置。清掉 mails/sessions/ calendar_events/attachments/agent_platform_sessions/relayed_mails/ permission_requests/rate_limits。测试数据已全部清零。 ## 测试 - gateway 7 包全过;repo + 7 例(adopt_alias_test.go) - web 182 例(mailGroups 新增 session_workspace 断言) - 前端构建通过
This commit is contained in:
@ -58,14 +58,23 @@ export default function NarrowStack({
|
||||
return (
|
||||
<div className="flex-1 min-h-0 relative overflow-hidden">
|
||||
{/* 底层:始终挂载。打开覆盖层时用 aria-hidden 把它从无障碍树里摘掉,
|
||||
否则屏幕阅读器会读到两层内容 */}
|
||||
<div className="absolute inset-0 flex" aria-hidden={open ? 'true' : undefined}>
|
||||
否则屏幕阅读器会读到两层内容。
|
||||
|
||||
`isolate`(isolation: isolate)是必需的:它让底层**自成一个层叠上下文**。
|
||||
不加的后果在日历上实测到过:月视图的星期表头是 `sticky top-0 z-10`,
|
||||
而覆盖层没有 z-index(= auto = 0)—— 两者在同一个层叠上下文里比,
|
||||
`z-10` 赢过 `auto`,于是底层的表头穿透到二级页面之上,把日程内容遮住一条。
|
||||
|
||||
为何不只给覆盖层加 z-10 就完事:那只能治当下这一处。底层是任意业务组件,
|
||||
下一个人在里面写个 `z-20` 就又复现,而这类 bug 只能肉眼看见。
|
||||
isolate 把边界定在容器上,底层写多少 z-index 都出不来。 */}
|
||||
<div className="absolute inset-0 flex isolate" aria-hidden={open ? 'true' : undefined}>
|
||||
{base}
|
||||
</div>
|
||||
|
||||
{mounted && (
|
||||
<div
|
||||
className={`absolute inset-0 flex bg-white shadow-2xl transition-transform duration-200 ease-out motion-reduce:transition-none ${
|
||||
className={`absolute inset-0 z-10 flex bg-white shadow-2xl transition-transform duration-200 ease-out motion-reduce:transition-none ${
|
||||
entered ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
|
||||
@ -132,10 +132,9 @@ function PermissionSessionGroup({
|
||||
}`}
|
||||
/>
|
||||
<BotIcon className="w-3.5 h-3.5 text-slate-600 shrink-0" />
|
||||
<span className="text-xs font-semibold text-gray-900 truncate">{g.agentName}</span>
|
||||
{g.path && (
|
||||
<span className="text-[10px] text-gray-400 font-mono truncate">{g.path}</span>
|
||||
)}
|
||||
<span className="text-xs font-mono text-gray-900 truncate">
|
||||
{g.agentName}{g.path ? `@${g.path}` : ''}{g.alias ? `.${g.alias}` : ''}
|
||||
</span>
|
||||
<span className="ml-auto text-[10px] text-gray-400 shrink-0">{time}</span>
|
||||
</div>
|
||||
|
||||
@ -156,9 +155,11 @@ function PermissionSessionGroup({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-blue-500 font-mono truncate mt-0.5 pl-5">
|
||||
{g.alias ? `.${g.alias}` : '(未命名会话)'}
|
||||
</p>
|
||||
{!g.alias && (
|
||||
<p className="text-[10px] text-gray-400 font-mono truncate mt-0.5 pl-5">
|
||||
(未命名会话)
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
|
||||
@ -160,7 +160,7 @@ export function groupPermissions(mails: Mail[]): PermissionGroup[] {
|
||||
sessionId,
|
||||
alias: latest.session_alias || '',
|
||||
agentName: latest.from_name,
|
||||
path: latest.from_workspace || '',
|
||||
path: latest.session_workspace || '',
|
||||
pending: sorted.filter(isPendingPermission),
|
||||
settled: sorted.filter(m => !isPendingPermission(m)),
|
||||
latest
|
||||
|
||||
@ -254,12 +254,22 @@ describe('groupPermissions', () => {
|
||||
expect(groups.map(g => g.sessionId)).toEqual(['new', 'old']);
|
||||
});
|
||||
|
||||
it('组头带上发起请求的 Agent 与工作目录', () => {
|
||||
it('组头带上发起请求的 Agent 与会话工作目录', () => {
|
||||
const [g] = groupPermissions([
|
||||
perm({ session_id: 's', from_name: 'dsh', from_workspace: '/home/program/llmsproxy' })
|
||||
perm({
|
||||
session_id: 's',
|
||||
from_name: 'dsh',
|
||||
// from_workspace 对 Agent 存的是 **Agent 名**而不是路径(历史遗留)。
|
||||
// 拿它当路径用会在授权页拼出 `dsh@dsh`,而权限请求的
|
||||
// from_workspace 实测是**空串** —— 于是那一行永远不渲染,
|
||||
// 人根本不知道是哪个目录里的哪条线索在请求权限。
|
||||
from_workspace: 'dsh',
|
||||
session_workspace: '/home/program/llmsproxy'
|
||||
})
|
||||
]);
|
||||
// 同名 Agent 在不同目录是不同的活,光有名字判断不了
|
||||
expect(g.agentName).toBe('dsh');
|
||||
// path 必须取 session_workspace(会话的 workspace,权威来源)
|
||||
expect(g.path).toBe('/home/program/llmsproxy');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user