feat(pi): 交互式 pi 会话接入邮件工具(send_mail/read_inbox 等 10 个)
问题(⑧):守护进程用 noExtensions:true 起会话,它的邮件工具只给模型在邮件
会话里用;人在 TUI 里敲的 pi 拿不到。结果是平台的建设者自己收不到邮件 ——
一个「邮件驱动」的平台,维护者只能绕到 curl + 密钥直连 Gateway 才能看收件箱。
新增 plugins/pi-mail-bridge/extension/index.ts:把同一套工具(createMailTools)
注册到交互式会话。两者是同一条 AgentMail 身份(agent pi)的两个入口,与 DSH 的
「TUI + 邮箱是同一个 Agent」一致。
密钥解析顺序(交互式 pi 的环境里没有 AGENTMAIL_*):
1. 进程环境
2. AGENTMAIL_ENV_FILE(默认 /etc/agentmail/pi.env)—— 与守护进程同一把密钥,
因此身份一致
3. AGENTMAIL_CONFIG_DIR/agent.key 或 ~/.agentmail/agent.key
(兼容 key 与 key_token 两种字段名;实测本机文件用的是 key_token,
只认 key 会静默读不到)
拿不到密钥时不注册任何工具并明确告知 —— 挂一组永远 401 的工具比没有更糟。
不注册 connect_to_server:它会重写 Gateway 坐标并重新登记密钥,而交互式会话与
守护进程共用同一身份,一次 TUI 对话不该改到守护进程的配置。
为什么不会重复注册(读 SDK 实现确认,并用探针实测):
resource-loader.js 里 noExtensions 为真时只用 cliEnabledExtensions,
settings.json 的 extensions 数组被排除 —— 即 noExtensions:true 只加载
命令行 -e 传入的扩展。
探针:noExtensions=true → 扩展数=0;false → 16 个且含 pi-mail-bridge。
deploy/install.sh 增加幂等的扩展注册步骤(写入 settings.json 的 extensions)。
验证:headless pi 实际调用 read_inbox 返回真实邮件主题;工具清单含
send_mail/read_inbox/read_mail/forward_mail/upload_attachment/download_attachment/
suggest_address/list_contacts/session_participants/read_thread(10 个),
connect_to_server 按设计排除。
This commit is contained in:
10
plugins/dsh-mail-bridge/lib/mail-session-id.d.ts
vendored
10
plugins/dsh-mail-bridge/lib/mail-session-id.d.ts
vendored
@ -1,3 +1,9 @@
|
||||
export declare function dshSessionIdForMail(mailSessionID: any): string;
|
||||
export declare function matchesMailSession(dshSessionId: any, mailSessionID: any): boolean;
|
||||
export declare function pickMailSession(dshSessionIds: any[] | undefined, mailSessionID: any): string | undefined;
|
||||
export declare function matchesMailSession(
|
||||
dshSessionId: any,
|
||||
mailSessionID: any,
|
||||
): boolean;
|
||||
export declare function pickMailSession(
|
||||
dshSessionIds: any[] | undefined,
|
||||
mailSessionID: any,
|
||||
): string | undefined;
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
|
||||
/** 首次尝试使用的 DSH 会话 id。 */
|
||||
export function dshSessionIdForMail(mailSessionID) {
|
||||
return `mail-${String(mailSessionID ?? '')}`;
|
||||
return `mail-${String(mailSessionID ?? "")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,12 +35,12 @@ export function dshSessionIdForMail(mailSessionID) {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function matchesMailSession(dshSessionId, mailSessionID) {
|
||||
const id = String(dshSessionId ?? '');
|
||||
const base = dshSessionIdForMail(mailSessionID);
|
||||
if (id === base) return true;
|
||||
// 模型降级重试:mail-<id>-r1 / -r2 / …
|
||||
const suffix = id.startsWith(`${base}-r`) ? id.slice(base.length + 2) : '';
|
||||
return suffix.length > 0 && /^\d+$/.test(suffix);
|
||||
const id = String(dshSessionId ?? "");
|
||||
const base = dshSessionIdForMail(mailSessionID);
|
||||
if (id === base) return true;
|
||||
// 模型降级重试:mail-<id>-r1 / -r2 / …
|
||||
const suffix = id.startsWith(`${base}-r`) ? id.slice(base.length + 2) : "";
|
||||
return suffix.length > 0 && /^\d+$/.test(suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,19 +54,19 @@ export function matchesMailSession(dshSessionId, mailSessionID) {
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function pickMailSession(dshSessionIds, mailSessionID) {
|
||||
const base = dshSessionIdForMail(mailSessionID);
|
||||
const list = Array.isArray(dshSessionIds) ? dshSessionIds.map(String) : [];
|
||||
if (list.includes(base)) return base;
|
||||
const base = dshSessionIdForMail(mailSessionID);
|
||||
const list = Array.isArray(dshSessionIds) ? dshSessionIds.map(String) : [];
|
||||
if (list.includes(base)) return base;
|
||||
|
||||
let best;
|
||||
let bestIndex = Infinity;
|
||||
for (const id of list) {
|
||||
if (!matchesMailSession(id, mailSessionID)) continue;
|
||||
const idx = Number(id.slice(base.length + 2));
|
||||
if (idx < bestIndex) {
|
||||
bestIndex = idx;
|
||||
best = id;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
let best;
|
||||
let bestIndex = Infinity;
|
||||
for (const id of list) {
|
||||
if (!matchesMailSession(id, mailSessionID)) continue;
|
||||
const idx = Number(id.slice(base.length + 2));
|
||||
if (idx < bestIndex) {
|
||||
bestIndex = idx;
|
||||
best = id;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
22
plugins/dsh-mail-bridge/lib/sse-client.d.ts
vendored
22
plugins/dsh-mail-bridge/lib/sse-client.d.ts
vendored
@ -1,17 +1,17 @@
|
||||
export interface SseClientOptions {
|
||||
authHeaders: () => Record<string, string>;
|
||||
baseURL: string;
|
||||
path: string;
|
||||
onEvent: (evt: string, data: any) => void;
|
||||
log?: (msg: string) => void;
|
||||
authHeaders: () => Record<string, string>;
|
||||
baseURL: string;
|
||||
path: string;
|
||||
onEvent: (evt: string, data: any) => void;
|
||||
log?: (msg: string) => void;
|
||||
}
|
||||
export declare function createSSEClient(options: SseClientOptions): {
|
||||
stop: () => void;
|
||||
reset: () => void;
|
||||
stop: () => void;
|
||||
reset: () => void;
|
||||
};
|
||||
export declare function createFrameParser(): {
|
||||
push: (chunk: string) => Array<{ event: string; data: string; id: string }>;
|
||||
reset: () => void;
|
||||
lastEventId: () => string;
|
||||
setLastEventId: (id: string) => void;
|
||||
push: (chunk: string) => Array<{ event: string; data: string; id: string }>;
|
||||
reset: () => void;
|
||||
lastEventId: () => string;
|
||||
setLastEventId: (id: string) => void;
|
||||
};
|
||||
|
||||
22
plugins/dsh-mail-bridge/lib/user-question.d.ts
vendored
22
plugins/dsh-mail-bridge/lib/user-question.d.ts
vendored
@ -2,12 +2,20 @@ export declare function hasOptions(question: any): boolean;
|
||||
export declare function optionLabels(question: any): string[];
|
||||
export declare function questionTitle(question: any): string;
|
||||
export declare function flattenQuestions(questions: any[]): {
|
||||
question: string;
|
||||
options: string[];
|
||||
context: string;
|
||||
multiSelect: boolean;
|
||||
question: string;
|
||||
options: string[];
|
||||
context: string;
|
||||
multiSelect: boolean;
|
||||
};
|
||||
export declare function answersFromDecision(questions: any[], decision: string, note?: string): {
|
||||
answers: Array<{ id: string; selected: string[]; custom?: string }>;
|
||||
export declare function answersFromDecision(
|
||||
questions: any[],
|
||||
decision: string,
|
||||
note?: string,
|
||||
): {
|
||||
answers: Array<{ id: string; selected: string[]; custom?: string }>;
|
||||
};
|
||||
export declare function isBlankAnswer(questions: any[], decision: string, note?: string): boolean;
|
||||
export declare function isBlankAnswer(
|
||||
questions: any[],
|
||||
decision: string,
|
||||
note?: string,
|
||||
): boolean;
|
||||
|
||||
Reference in New Issue
Block a user