feat(opencode桥): withScope 在非邮件轮次回落到 /tmp 默认会话 —— 配套 15e4fe9 的收严
与 dsh 同一形状:`reverseMap` 只在邮件投递时填 ⇒ 用户在界面里新开一轮对话时 查表必空 ⇒ withScope「取不到就原样返回」⇒ 不带 session_id ⇒ 服务端 403。 实测它至今只裸奔过 1 次(2026-09-28),但**形状在**,且旧兜底的服务端放行已作废。 一致性比实测频率重要:留着一个已知失效的分支,下次有人读它会以为那是可用的。 回落答案只能问服务端 —— opencode 自己的 session id 与 AgentMail 会话 uuid 没有映射。★ 这与 `workspace` 那一维**不同**:那一维可以从 `Session.directory` 问出来(见 workspace-probe.test.mjs,同一天修的),所以这一维只能走默认会话。 装配期 fire-and-forget 问一次(不 await):它的失败后果只是那次读信 403, 不该拖住注册流程(而 dsh 那边是 await,因为 dsh 的注册路径本来就顺序执行)。 这个差异是有意的,不是抄漏。 判据 5 格。三个变异各红 3 格(交叉命中,说明各自在钉不同东西)。 全量 364/364 绿(359 + 5)。
This commit is contained in:
@ -457,7 +457,17 @@ const downloadAttachmentTool = {
|
||||
* (服务端会记警告),也不猜一个。
|
||||
*/
|
||||
function withScope(path, context) {
|
||||
const mailSessionID = reverseMap.get(String(context?.sessionID ?? "")) || "";
|
||||
// ★ 2026-10-02:取不到邮件会话时回落到**默认会话**(/tmp),不再原样返回。
|
||||
//
|
||||
// 旧写法「原样返回」是 2026-09-15 之前的迁移期兜底;那个放行已作废
|
||||
// (AgentMayReadSession 对未声明 session_id 一律 403)。
|
||||
// `reverseMap` 只在邮件投递时填 ⇒ 用户在 opencode 界面里新开一轮对话时
|
||||
// 查表必空 ⇒ 读信 403。
|
||||
//
|
||||
// 回落答案只能问服务端:opencode 侧的 session id 是它自己的,
|
||||
// 与 AgentMail 会话 uuid 没有映射(不像 workspace 能从 Session.directory 问出来)。
|
||||
const mailSessionID =
|
||||
reverseMap.get(String(context?.sessionID ?? "")) || defaultSessionId || "";
|
||||
if (!mailSessionID) return path;
|
||||
const sep = path.includes("?") ? "&" : "?";
|
||||
return `${path}${sep}session_id=${encodeURIComponent(mailSessionID)}`;
|
||||
@ -718,6 +728,42 @@ const sessionWorkspace = new BoundedMap(MAX_TRACKED_SESSIONS); // opencode sessi
|
||||
还有宿主句柄 —— 以前没这个需求,所以没有通道。
|
||||
*/
|
||||
let hostClient = null;
|
||||
|
||||
/*
|
||||
★ 2026-10-02:非邮件轮次的 session_id 兜底(/tmp 默认会话)。
|
||||
|
||||
`reverseMap` 只在邮件投递时填 ⇒ 用户在界面里直接对话时查表为空。
|
||||
withScope 旧写法是「取不到就原样返回」,那是 2026-09-15 前的迁移期放行;
|
||||
服务端已收严为「未声明 session_id 一律 403」⇒ 不加兜底就是界面里读不到信。
|
||||
|
||||
端点是**纯只读**的(默认工作区没通信过就返回 session_id=null,不建会话),
|
||||
所以这里拿到空是正常形状;后果只是那次读信 403,而非静默放行成越权。
|
||||
|
||||
装配期问一次而不是每次 withScope 都问:后者会给服务端添无谓压力。
|
||||
*/
|
||||
const DEFAULT_FALLBACK_WORKSPACE = "/tmp"; // 必须与 handler.DefaultFallbackWorkspace 一致
|
||||
let defaultSessionId = "";
|
||||
let defaultSessionProbed = false;
|
||||
|
||||
async function loadDefaultSession(log) {
|
||||
if (defaultSessionProbed || !hostClient?.session?.get) return;
|
||||
defaultSessionProbed = true;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${GATEWAY_URL}/api/v1/agent/session/default?workspace=${encodeURIComponent(DEFAULT_FALLBACK_WORKSPACE)}`,
|
||||
{ headers: authHeaders() },
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
||||
defaultSessionId = String(data?.session_id ?? "");
|
||||
if (defaultSessionId) {
|
||||
log?.(`[mail-bridge] 默认会话(/tmp): ${defaultSessionId}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// 问不到就当没有:不带 session_id ⇒ 服务端 403(可见)而非静默放行。
|
||||
log?.(`[mail-bridge] 取默认会话失败(本次不带 session_id): ${e?.message || e}`);
|
||||
}
|
||||
}
|
||||
const syncedTitles = new BoundedMap(MAX_TRACKED_SESSIONS); // opencode session id -> 已回写过的标题(去重,避免 session.updated 刷屏)
|
||||
|
||||
// 权限询问的双向定位。
|
||||
@ -1258,6 +1304,10 @@ export default async function mailBridge(input) {
|
||||
// 供模块级工具对象(read_inbox)问工作区用,见 hostClient 的注释。
|
||||
if (client) hostClient = client;
|
||||
|
||||
// ★ 2026-10-02:装配期问一次默认会话(非邮件轮次的 session_id 兜底)。
|
||||
// 不 await:注册流程不该被它拖住(问不到的后果只是那次读信 403)。
|
||||
loadDefaultSession((m) => console.log(m));
|
||||
|
||||
// 注册 Agent。无密钥也无 secret 时先本地生成一把密钥,
|
||||
// 等管理员在后台登记后即可接入(无需重装插件)。
|
||||
if (!AGENT_KEY && !AGENT_SECRET) {
|
||||
|
||||
Reference in New Issue
Block a user