From 67f87297d42c24418e844b43ec4b3524a6554254 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 15:22:37 +0000 Subject: [PATCH 1/2] agents: add --summary-prompt, --summary-prompt-file and --reset-summary-prompt Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0193QCQU3b16BWm6tKWmAMbi --- docs/commands.zh-CN.md | 5 ++++- src/client.ts | 5 ++++- src/commands/agents.ts | 28 ++++++++++++++++++++++++---- tests/agents.test.mjs | 10 ++++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/docs/commands.zh-CN.md b/docs/commands.zh-CN.md index 6d50e35..fae0b9d 100644 --- a/docs/commands.zh-CN.md +++ b/docs/commands.zh-CN.md @@ -628,6 +628,8 @@ mailly agents update --skills-file ./skills.json --json mailly agents update --skills-file - --json < ./skills.json mailly agents update --unbind-address --json mailly agents update --clear-skills --json +mailly agents update --summary-prompt "用中文写一句话摘要,写清截止时间。" --json +mailly agents update --reset-summary-prompt --json ``` 创建时只要求 `--name`;省略模式默认为 `off`,Skills 默认为空。更新只提交明确提供的字段,省略 Skills、人设、白名单或地址时保留原值。`--mode` 支持 `off / summarize / draft`。 @@ -635,7 +637,8 @@ mailly agents update --clear-skills --json - `--persona ` 或 `--persona-file `:人设与业务说明,文件为 UTF-8,路径 `-` 读 stdin;`--persona ""` 清空。 - `--address-id `:绑定收件地址;更新时 `--unbind-address` 解除绑定。 - `--allowed-senders `:替换白名单,支持邮箱或 `@域名`;更新时 `--clear-allowed-senders` 清空,允许全部发件人。 -- `--skills-file `:读取 JSON 数组,**整体替换**该 Agent 的 Skills;`[]` 或更新时 `--clear-skills` 删除全部。修改单个技能时应保留文件中的其他技能及其 ID。两个文件选项不能同时读 stdin。 +- `--summary-prompt ` 或 `--summary-prompt-file `:每封来信的总结提示词,最多 4000 字符,路径 `-` 读 stdin。不设置时使用默认提示词,默认提示词随 Web 工作台的界面语言切换;设置后以自定义为准,切换语言不会改动。更新时 `--reset-summary-prompt` 恢复默认。`agents show` 显示当前实际使用的提示词。 +- `--skills-file `:读取 JSON 数组,**整体替换**该 Agent 的 Skills;`[]` 或更新时 `--clear-skills` 删除全部。修改单个技能时应保留文件中的其他技能及其 ID。多个文件选项中只能有一个读 stdin。 技能全部由用户录入。文件中每项字段为 `id / name / description / instructions / enabled`,例如以下格式示意(请填写自己的内容): diff --git a/src/client.ts b/src/client.ts index 5acc91b..df3bb15 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,6 +11,7 @@ export interface AgentInput { address_id?: string | null; allowed_senders?: string[]; skills?: unknown[]; + summary_prompt?: string | null; } export interface AgentProfile { @@ -22,6 +23,8 @@ export interface AgentProfile { bound_address: string | null; allowed_senders_json: string | null; skills_json?: string; + summary_prompt?: string | null; + effective_summary_prompt?: string; } export type InboxBodyFormat = 'text' | 'markdown' | 'html'; @@ -771,7 +774,7 @@ export class MaillyClient { } listAgents() { - return this.request<{ success: boolean; agents: AgentProfile[] }>('GET', '/agents'); + return this.request<{ success: boolean; summary_locale?: string; agents: AgentProfile[] }>('GET', '/agents'); } createAgent(input: AgentInput & { name: string }) { diff --git a/src/commands/agents.ts b/src/commands/agents.ts index 834a9c8..296dfd9 100644 --- a/src/commands/agents.ts +++ b/src/commands/agents.ts @@ -17,8 +17,13 @@ interface AgentOptions { clearAllowedSenders?: boolean; skillsFile?: string; clearSkills?: boolean; + summaryPrompt?: string; + summaryPromptFile?: string; + resetSummaryPrompt?: boolean; } +const MAX_SUMMARY_PROMPT = 4000; + async function clientFor(command: Command): Promise { let root = command; while (root.parent) root = root.parent; @@ -32,12 +37,14 @@ function addProfileOptions(command: Command) { .addOption(new Option('--persona-file ', 'Read a UTF-8 persona file; use - to read from stdin').conflicts('persona')) .option('--address-id ', 'Inbox address ID to bind') .option('--allowed-senders ', 'Replace the sender allowlist; email addresses or @domains') - .option('--skills-file ', 'JSON array that replaces all skills; use - to read from stdin, [] clears them'); + .option('--skills-file ', 'JSON array that replaces all skills; use - to read from stdin, [] clears them') + .addOption(new Option('--summary-prompt ', 'Prompt for the summary of each incoming email; without it the default follows the workspace language').conflicts('summaryPromptFile')) + .addOption(new Option('--summary-prompt-file ', 'Read a UTF-8 summary prompt file; use - to read from stdin').conflicts('summaryPrompt')); } async function profileInput(opts: AgentOptions): Promise { - if (opts.personaFile === '-' && opts.skillsFile === '-') { - throw new Error('--persona-file and --skills-file cannot both read from stdin'); + if ([opts.personaFile, opts.skillsFile, opts.summaryPromptFile].filter((path) => path === '-').length > 1) { + throw new Error('Only one of --persona-file, --skills-file and --summary-prompt-file can read from stdin'); } const body: AgentInput = {}; if (opts.name !== undefined) { @@ -66,6 +73,14 @@ async function profileInput(opts: AgentOptions): Promise { body.skills = skills; } if (opts.clearSkills) body.skills = []; + if (opts.summaryPrompt !== undefined) body.summary_prompt = opts.summaryPrompt; + if (opts.summaryPromptFile !== undefined) { + body.summary_prompt = opts.summaryPromptFile === '-' ? await readStdin() : await readFile(opts.summaryPromptFile, 'utf8'); + } + if (typeof body.summary_prompt === 'string' && body.summary_prompt.length > MAX_SUMMARY_PROMPT) { + throw new Error(`The summary prompt can be at most ${MAX_SUMMARY_PROMPT} characters`); + } + if (opts.resetSummaryPrompt) body.summary_prompt = null; return body; } @@ -85,13 +100,17 @@ export function registerAgents(program: Command) { const res = await (await clientFor(command)).listAgents(); const agent = res.agents.find((item) => item.id === id); if (!agent) throw new Error(`Agent ${id} not found`); - if (getOutputFormat() === 'json') return printJSON({ success: true, agent }); + if (getOutputFormat() === 'json') return printJSON({ success: true, summary_locale: res.summary_locale, agent }); info(`${agent.name} (${agent.id})`); info(`Mode: ${agent.mode}`); info(`Inbox address: ${agent.bound_address ?? agent.address_id ?? 'not bound'}`); info(`Persona and business instructions:\n${agent.persona_prompt || 'not set'}`); info(`Sender allowlist: ${agent.allowed_senders_json ?? '[]'}`); info(`Skills:\n${JSON.stringify(JSON.parse(agent.skills_json ?? '[]'), null, 2)}`); + if (agent.effective_summary_prompt !== undefined) { + const source = agent.summary_prompt ? 'custom' : `default for ${res.summary_locale ?? 'en-US'}, follows the workspace language`; + info(`Summary prompt (${source}):\n${agent.effective_summary_prompt}`); + } }); addProfileOptions(agents.command('create').requiredOption('--name ', 'Agent name')) @@ -106,6 +125,7 @@ export function registerAgents(program: Command) { .addOption(new Option('--unbind-address', 'Unbind the inbox address').conflicts('addressId')) .addOption(new Option('--clear-allowed-senders', 'Clear the allowlist and accept all senders').conflicts('allowedSenders')) .addOption(new Option('--clear-skills', 'Delete all skills').conflicts('skillsFile')) + .addOption(new Option('--reset-summary-prompt', 'Go back to the default summary prompt, which follows the workspace language').conflicts(['summaryPrompt', 'summaryPromptFile'])) .action(async (id, opts: AgentOptions, command) => { const body = await profileInput(opts); if (!Object.keys(body).length) throw new Error('No fields to update'); diff --git a/tests/agents.test.mjs b/tests/agents.test.mjs index a6c26b9..45da2d8 100644 --- a/tests/agents.test.mjs +++ b/tests/agents.test.mjs @@ -64,6 +64,12 @@ test('update preserves omitted fields, escapes IDs, reads stdin and explicitly c assert.deepEqual(requests[3].body, { persona_prompt: '', address_id: null, allowed_senders: [], skills: [] }); await run(['update', 'agent-1', '--skills-file', '-'], '[]'); assert.deepEqual(requests[4].body, { skills: [] }); + await run(['update', 'agent-1', '--summary-prompt', '只写一句话。']); + assert.deepEqual(requests[5].body, { summary_prompt: '只写一句话。' }); + await run(['update', 'agent-1', '--summary-prompt-file', '-'], '# 总结\n写清截止时间。\n'); + assert.deepEqual(requests[6].body, { summary_prompt: '# 总结\n写清截止时间。\n' }); + await run(['update', 'agent-1', '--reset-summary-prompt']); + assert.deepEqual(requests[7].body, { summary_prompt: null }); }); test('invalid and conflicting inputs fail with JSON before contacting the API', async (t) => { @@ -76,6 +82,10 @@ test('invalid and conflicting inputs fail with JSON before contacting the API', ['update', 'a', '--skills-file', '-', '--clear-skills'], ['update', 'a', '--persona', 'text', '--persona-file', '-'], ['update', 'a', '--persona-file', '-', '--skills-file', '-'], + ['update', 'a', '--persona-file', '-', '--summary-prompt-file', '-'], + ['update', 'a', '--summary-prompt', 'x', '--summary-prompt-file', '-'], + ['update', 'a', '--summary-prompt', 'x', '--reset-summary-prompt'], + ['update', 'a', '--summary-prompt', 'x'.repeat(4001)], ['update', 'a', '--skills-file', join(dir, 'missing.json')], ]) { await assert.rejects(run(args), (error) => { From dc1d1d7d471c7d4d0ccf8f0ba9c14d7c85f8f463 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 16:14:27 +0000 Subject: [PATCH 2/2] skill: document the agent summary prompt options Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0193QCQU3b16BWm6tKWmAMbi --- skills/mailly/reference/commands.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skills/mailly/reference/commands.md b/skills/mailly/reference/commands.md index 7485ea9..3f04c78 100644 --- a/skills/mailly/reference/commands.md +++ b/skills/mailly/reference/commands.md @@ -30,6 +30,8 @@ mailly agents update --skills-file ./skills.json --json mailly agents update --skills-file - --json < ./skills.json mailly agents update --unbind-address --json mailly agents update --clear-skills --json +mailly agents update --summary-prompt "用中文写一句话摘要,写清截止时间。" --json +mailly agents update --reset-summary-prompt --json ``` 创建时只要求 `--name`;省略模式默认为 `off`,Skills 默认为空。更新只提交明确提供的字段,省略 Skills、人设、白名单或地址时保留原值。`--mode` 支持 `off / summarize / draft`。 @@ -37,7 +39,8 @@ mailly agents update --clear-skills --json - `--persona ` 或 `--persona-file `:人设与业务说明,文件为 UTF-8,路径 `-` 读 stdin;`--persona ""` 清空。 - `--address-id `:绑定收件地址;更新时 `--unbind-address` 解除绑定。 - `--allowed-senders `:替换白名单,支持邮箱或 `@域名`;更新时 `--clear-allowed-senders` 清空,允许全部发件人。 -- `--skills-file `:读取 JSON 数组,**整体替换**该 Agent 的 Skills;`[]` 或更新时 `--clear-skills` 删除全部。修改单个技能时应保留文件中的其他技能及其 ID。两个文件选项不能同时读 stdin。 +- `--summary-prompt ` 或 `--summary-prompt-file `:每封来信的总结提示词,最多 4000 字符,路径 `-` 读 stdin。不设置时使用默认提示词,默认随 Web 工作台的界面语言切换;设置后以自定义为准,切换语言不会改动。更新时 `--reset-summary-prompt` 恢复默认。`agents show` 显示当前实际使用的提示词。 +- `--skills-file `:读取 JSON 数组,**整体替换**该 Agent 的 Skills;`[]` 或更新时 `--clear-skills` 删除全部。修改单个技能时应保留文件中的其他技能及其 ID。多个文件选项中只能有一个读 stdin。 技能全部由用户录入。文件中每项字段为 `id / name / description / instructions / enabled`,例如以下格式示意(请填写自己的内容):