Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/commands.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,14 +628,17 @@ mailly agents update <agent-id> --skills-file ./skills.json --json
mailly agents update <agent-id> --skills-file - --json < ./skills.json
mailly agents update <agent-id> --unbind-address --json
mailly agents update <agent-id> --clear-skills --json
mailly agents update <agent-id> --summary-prompt "用中文写一句话摘要,写清截止时间。" --json
mailly agents update <agent-id> --reset-summary-prompt --json
```

创建时只要求 `--name`;省略模式默认为 `off`,Skills 默认为空。更新只提交明确提供的字段,省略 Skills、人设、白名单或地址时保留原值。`--mode` 支持 `off / summarize / draft`。

- `--persona <text>` 或 `--persona-file <path>`:人设与业务说明,文件为 UTF-8,路径 `-` 读 stdin;`--persona ""` 清空。
- `--address-id <id>`:绑定收件地址;更新时 `--unbind-address` 解除绑定。
- `--allowed-senders <senders...>`:替换白名单,支持邮箱或 `@域名`;更新时 `--clear-allowed-senders` 清空,允许全部发件人。
- `--skills-file <path>`:读取 JSON 数组,**整体替换**该 Agent 的 Skills;`[]` 或更新时 `--clear-skills` 删除全部。修改单个技能时应保留文件中的其他技能及其 ID。两个文件选项不能同时读 stdin。
- `--summary-prompt <text>` 或 `--summary-prompt-file <path>`:每封来信的总结提示词,最多 4000 字符,路径 `-` 读 stdin。不设置时使用默认提示词,默认提示词随 Web 工作台的界面语言切换;设置后以自定义为准,切换语言不会改动。更新时 `--reset-summary-prompt` 恢复默认。`agents show` 显示当前实际使用的提示词。
- `--skills-file <path>`:读取 JSON 数组,**整体替换**该 Agent 的 Skills;`[]` 或更新时 `--clear-skills` 删除全部。修改单个技能时应保留文件中的其他技能及其 ID。多个文件选项中只能有一个读 stdin。

技能全部由用户录入。文件中每项字段为 `id / name / description / instructions / enabled`,例如以下格式示意(请填写自己的内容):

Expand Down
5 changes: 4 additions & 1 deletion skills/mailly/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ mailly agents update <agent-id> --skills-file ./skills.json --json
mailly agents update <agent-id> --skills-file - --json < ./skills.json
mailly agents update <agent-id> --unbind-address --json
mailly agents update <agent-id> --clear-skills --json
mailly agents update <agent-id> --summary-prompt "用中文写一句话摘要,写清截止时间。" --json
mailly agents update <agent-id> --reset-summary-prompt --json
```

创建时只要求 `--name`;省略模式默认为 `off`,Skills 默认为空。更新只提交明确提供的字段,省略 Skills、人设、白名单或地址时保留原值。`--mode` 支持 `off / summarize / draft`。

- `--persona <text>` 或 `--persona-file <path>`:人设与业务说明,文件为 UTF-8,路径 `-` 读 stdin;`--persona ""` 清空。
- `--address-id <id>`:绑定收件地址;更新时 `--unbind-address` 解除绑定。
- `--allowed-senders <senders...>`:替换白名单,支持邮箱或 `@域名`;更新时 `--clear-allowed-senders` 清空,允许全部发件人。
- `--skills-file <path>`:读取 JSON 数组,**整体替换**该 Agent 的 Skills;`[]` 或更新时 `--clear-skills` 删除全部。修改单个技能时应保留文件中的其他技能及其 ID。两个文件选项不能同时读 stdin。
- `--summary-prompt <text>` 或 `--summary-prompt-file <path>`:每封来信的总结提示词,最多 4000 字符,路径 `-` 读 stdin。不设置时使用默认提示词,默认随 Web 工作台的界面语言切换;设置后以自定义为准,切换语言不会改动。更新时 `--reset-summary-prompt` 恢复默认。`agents show` 显示当前实际使用的提示词。
- `--skills-file <path>`:读取 JSON 数组,**整体替换**该 Agent 的 Skills;`[]` 或更新时 `--clear-skills` 删除全部。修改单个技能时应保留文件中的其他技能及其 ID。多个文件选项中只能有一个读 stdin。

技能全部由用户录入。文件中每项字段为 `id / name / description / instructions / enabled`,例如以下格式示意(请填写自己的内容):

Expand Down
5 changes: 4 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface AgentInput {
address_id?: string | null;
allowed_senders?: string[];
skills?: unknown[];
summary_prompt?: string | null;
}

export interface AgentProfile {
Expand All @@ -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';
Expand Down Expand Up @@ -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 }) {
Expand Down
28 changes: 24 additions & 4 deletions src/commands/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MaillyClient> {
let root = command;
while (root.parent) root = root.parent;
Expand All @@ -32,12 +37,14 @@ function addProfileOptions(command: Command) {
.addOption(new Option('--persona-file <path>', 'Read a UTF-8 persona file; use - to read from stdin').conflicts('persona'))
.option('--address-id <id>', 'Inbox address ID to bind')
.option('--allowed-senders <senders...>', 'Replace the sender allowlist; email addresses or @domains')
.option('--skills-file <path>', 'JSON array that replaces all skills; use - to read from stdin, [] clears them');
.option('--skills-file <path>', 'JSON array that replaces all skills; use - to read from stdin, [] clears them')
.addOption(new Option('--summary-prompt <text>', 'Prompt for the summary of each incoming email; without it the default follows the workspace language').conflicts('summaryPromptFile'))
.addOption(new Option('--summary-prompt-file <path>', 'Read a UTF-8 summary prompt file; use - to read from stdin').conflicts('summaryPrompt'));
}

async function profileInput(opts: AgentOptions): Promise<AgentInput> {
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) {
Expand Down Expand Up @@ -66,6 +73,14 @@ async function profileInput(opts: AgentOptions): Promise<AgentInput> {
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;
}

Expand All @@ -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 <name>', 'Agent name'))
Expand All @@ -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');
Expand Down
10 changes: 10 additions & 0 deletions tests/agents.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) => {
Expand Down
Loading