fix(dsh): session_update 在插件重启后仍能定位活会话(权限热更新不再静默失效)

sessionMap 是纯内存表,插件重启后为空。而 session_update(人在 WebUI 改权限
档位)原来只查这张表 —— 于是「插件刚重启 + 那条会话还没收到新邮件」时,
那条更新被静默忽略:人在界面上把 full 改回 workspace,DSH 运行时仍按 full 执行。
人以为自己收紧了权限,实际上没有。

邮件新建的会话 id 是确定性的(mail-<邮件会话 id>,模型降级重试时带 -r<i>),
所以重启后可以按前缀在活会话里定位,并顺手把 sessionMap/reverseMap 补回去
—— 不补的话这条会话后续的自动转发也会一起失效。

- lib/mail-session-id.js(dsh 专用,9 例测试):id 派生与匹配的纯函数。
  放宽成前缀匹配会把 mail-abcdef 误判成 mail-abc 的会话;非数字后缀
  (-retry / -rx)也不匹配,避免误改别的会话的档位。
- 接管会话(adopted)仍是例外:其 DSH 会话 id 由平台生成、推不出来,
  重启后无法热更新 —— 已知取舍;下次投递会按邮件里的 permission_mode 重设。
- tsconfig 清理:allowJs 原先写在根层(无效位置),移入 compilerOptions 后
  会让 lib/*.js 进入 TS 程序并违反 rootDir;这些模块已有 .d.ts 提供类型,
  该选项本就不需要,直接移除。

测试:dsh 358(新增 9)/ opencode 316 / pi 405 全绿。
This commit is contained in:
2026-09-11 10:47:30 +08:00
parent f91efd2d8d
commit 1692c615c6
5 changed files with 197 additions and 11 deletions

View File

@ -0,0 +1,64 @@
/**
* 邮件会话 → DSH 会话 id 的派生约定。
*
* 这组函数存在的原因是一个具体的静默失效:插件重启后 `sessionMap` 为空,
* 人在 WebUI 改档位的 `session_update` 找不到目标会话就被忽略,
* 运行时仍按旧档位执行 —— 人以为自己收紧了权限。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
dshSessionIdForMail,
matchesMailSession,
pickMailSession,
} from '../lib/mail-session-id.js';
test('首次尝试的 id 是 mail-<邮件会话 id>', () => {
assert.equal(dshSessionIdForMail('abc-123'), 'mail-abc-123');
});
test('匹配首次尝试的 id', () => {
assert.equal(matchesMailSession('mail-abc', 'abc'), true);
});
test('匹配模型降级重试的 -r<i>', () => {
assert.equal(matchesMailSession('mail-abc-r1', 'abc'), true);
assert.equal(matchesMailSession('mail-abc-r12', 'abc'), true);
});
test('不匹配别的会话(前缀相同也不行)', () => {
assert.equal(matchesMailSession('mail-abcdef', 'abc'), false, 'mail-abc 的前缀不能吃掉 mail-abcdef');
assert.equal(matchesMailSession('mail-abd', 'abc'), false);
assert.equal(matchesMailSession('other-abc', 'abc'), false);
assert.equal(matchesMailSession('', 'abc'), false);
});
test('不匹配非数字后缀(避免误吞其它会话)', () => {
assert.equal(matchesMailSession('mail-abc-rx', 'abc'), false);
assert.equal(matchesMailSession('mail-abc-r', 'abc'), false);
assert.equal(matchesMailSession('mail-abc-retry', 'abc'), false);
});
test('pickMailSession 优先首次尝试,而不是数组顺序', () => {
// 数组顺序可能来自 ctx.agents.list(),与尝试顺序无关
assert.equal(pickMailSession(['mail-abc-r2', 'mail-abc-r1', 'mail-abc'], 'abc'), 'mail-abc');
});
test('pickMailSession 没有首次尝试时取序号最小的重试', () => {
assert.equal(pickMailSession(['mail-abc-r3', 'mail-abc-r1', 'mail-abc-r2'], 'abc'), 'mail-abc-r1');
});
test('pickMailSession 找不到时返回 undefined调用方据此跳过', () => {
assert.equal(pickMailSession(['other-1', 'mail-def'], 'abc'), undefined);
assert.equal(pickMailSession([], 'abc'), undefined);
assert.equal(pickMailSession(undefined, 'abc'), undefined);
});
test('接管的平台会话推不出 id不能被误判成邮件会话', () => {
// 平台自己生成的 id如 DSH 界面里开的会话)与邮件会话无关,
// 匹配函数必须说「不是」,否则会给错误的会话改档位。
assert.equal(matchesMailSession('session-7f3a91', 'abc'), false);
assert.equal(pickMailSession(['session-7f3a91'], 'abc'), undefined);
});