shennian 0.2.54 → 0.2.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ // @arch docs/features/manager-agent.md
2
+ // @test src/__tests__/external-reply-split.test.ts
3
+ const MAX_REPLY_CHARS = 500;
4
+ const TARGET_REPLY_CHARS = 360;
5
+ const MAX_REPLY_PARTS = 6;
6
+ function visualLength(text) {
7
+ return Array.from(text).length;
8
+ }
9
+ function sliceVisual(text, maxLength) {
10
+ return Array.from(text).slice(0, maxLength).join('');
11
+ }
12
+ function normalizeReplyText(text) {
13
+ return text
14
+ .replace(/\r\n/g, '\n')
15
+ .replace(/[ \t]+\n/g, '\n')
16
+ .replace(/\n[ \t]+/g, '\n')
17
+ .trim();
18
+ }
19
+ function splitLongParagraph(paragraph) {
20
+ const pieces = [];
21
+ const sentences = paragraph
22
+ .split(/(?<=[。!?!?;;])\s*/)
23
+ .map((part) => part.trim())
24
+ .filter(Boolean);
25
+ const units = sentences.length > 1 ? sentences : Array.from(paragraph);
26
+ let current = '';
27
+ for (const unit of units) {
28
+ const next = current ? `${current}${sentences.length > 1 ? '' : ''}${unit}` : unit;
29
+ if (current && visualLength(next) > TARGET_REPLY_CHARS) {
30
+ pieces.push(current);
31
+ current = unit;
32
+ }
33
+ else {
34
+ current = next;
35
+ }
36
+ }
37
+ if (current)
38
+ pieces.push(current);
39
+ return pieces.flatMap((piece) => {
40
+ if (visualLength(piece) <= MAX_REPLY_CHARS)
41
+ return [piece];
42
+ const chars = Array.from(piece);
43
+ const chunks = [];
44
+ for (let i = 0; i < chars.length; i += TARGET_REPLY_CHARS) {
45
+ chunks.push(chars.slice(i, i + TARGET_REPLY_CHARS).join('').trim());
46
+ }
47
+ return chunks.filter(Boolean);
48
+ });
49
+ }
50
+ function capReplyParts(chunks) {
51
+ if (chunks.length <= MAX_REPLY_PARTS)
52
+ return chunks;
53
+ const head = chunks.slice(0, MAX_REPLY_PARTS - 1);
54
+ const tail = chunks.slice(MAX_REPLY_PARTS - 1).join('\n\n');
55
+ const tailText = visualLength(tail) > MAX_REPLY_CHARS
56
+ ? `${sliceVisual(tail, MAX_REPLY_CHARS - 1)}…`
57
+ : tail;
58
+ return [...head, tailText].filter(Boolean);
59
+ }
60
+ export function splitExternalReplyText(text) {
61
+ const normalized = normalizeReplyText(text);
62
+ if (!normalized)
63
+ return [];
64
+ if (visualLength(normalized) <= MAX_REPLY_CHARS && !normalized.includes('\n\n'))
65
+ return [normalized];
66
+ const paragraphs = normalized
67
+ .split(/\n{2,}/)
68
+ .map((part) => part.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim())
69
+ .filter(Boolean);
70
+ if (paragraphs.length > 1 && visualLength(normalized) > TARGET_REPLY_CHARS) {
71
+ return capReplyParts(paragraphs.flatMap(splitLongParagraph));
72
+ }
73
+ const sourceParts = paragraphs.length > 1 ? paragraphs : [normalized.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim()];
74
+ const chunks = [];
75
+ let current = '';
76
+ for (const part of sourceParts.flatMap(splitLongParagraph)) {
77
+ const next = current ? `${current}\n\n${part}` : part;
78
+ if (current && visualLength(next) > MAX_REPLY_CHARS) {
79
+ chunks.push(current);
80
+ current = part;
81
+ }
82
+ else {
83
+ current = next;
84
+ }
85
+ }
86
+ if (current)
87
+ chunks.push(current);
88
+ return capReplyParts(chunks);
89
+ }
@@ -4,6 +4,7 @@ import { ChannelConfigRegistry } from './registry.js';
4
4
  import { ChannelSecretRegistry } from './secret-registry.js';
5
5
  import { WeComChannelAdapter } from './wecom.js';
6
6
  import { ExternalWebSocketChannelAdapter } from './websocket.js';
7
+ import { splitExternalReplyText } from './reply-split.js';
7
8
  export class ChannelRuntime {
8
9
  onExternalMessage;
9
10
  createReplyTarget;
@@ -52,7 +53,18 @@ export class ChannelRuntime {
52
53
  if (!adapter)
53
54
  return { ok: false, error: `Unsupported channel type: ${config.type}` };
54
55
  try {
55
- await adapter.send(config, input);
56
+ const parts = splitExternalReplyText(input.text);
57
+ if (!parts.length)
58
+ return { ok: false, error: 'Reply text is required' };
59
+ for (const [index, text] of parts.entries()) {
60
+ await adapter.send(config, {
61
+ ...input,
62
+ text,
63
+ idempotencyKey: parts.length > 1 && input.idempotencyKey
64
+ ? `${input.idempotencyKey}:${index + 1}`
65
+ : input.idempotencyKey,
66
+ });
67
+ }
56
68
  return { ok: true };
57
69
  }
58
70
  catch (err) {
@@ -1,8 +1,30 @@
1
1
  // @arch docs/features/wecom-managed-channel.md
2
2
  // @test src/__tests__/external-command.test.ts
3
3
  import fs from 'node:fs';
4
+ import path from 'node:path';
4
5
  import chalk from 'chalk';
5
6
  import { resolveShennianPath } from '../config/index.js';
7
+ const MIME_BY_EXT = {
8
+ '.jpg': 'image/jpeg',
9
+ '.jpeg': 'image/jpeg',
10
+ '.png': 'image/png',
11
+ '.gif': 'image/gif',
12
+ '.webp': 'image/webp',
13
+ '.mp4': 'video/mp4',
14
+ '.mov': 'video/quicktime',
15
+ '.pdf': 'application/pdf',
16
+ '.txt': 'text/plain',
17
+ '.md': 'text/markdown',
18
+ '.csv': 'text/csv',
19
+ '.doc': 'application/msword',
20
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
21
+ '.xls': 'application/vnd.ms-excel',
22
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
23
+ '.ppt': 'application/vnd.ms-powerpoint',
24
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
25
+ '.zip': 'application/zip',
26
+ };
27
+ const MAX_EXTERNAL_ATTACHMENT_BYTES = Number(process.env.SHENNIAN_EXTERNAL_ATTACHMENT_MAX_BYTES || 50 * 1024 * 1024);
6
28
  function loadManagerIpcFromRuntimeFile() {
7
29
  try {
8
30
  const parsed = JSON.parse(fs.readFileSync(resolveShennianPath('runtime', 'manager-ipc.json'), 'utf-8'));
@@ -28,8 +50,35 @@ function requireExternalContext(explicitSessionId) {
28
50
  }
29
51
  return { url, token, sessionId };
30
52
  }
31
- async function sendExternal(text, idempotencyKey, sessionId) {
32
- const ctx = requireExternalContext(sessionId);
53
+ function inferMimeType(filePath, kind) {
54
+ const ext = path.extname(filePath).toLowerCase();
55
+ if (MIME_BY_EXT[ext])
56
+ return MIME_BY_EXT[ext];
57
+ if (kind === 'image')
58
+ return 'image/jpeg';
59
+ if (kind === 'video')
60
+ return 'video/mp4';
61
+ return 'application/octet-stream';
62
+ }
63
+ function readAttachment(filePath, kind) {
64
+ const absolutePath = path.resolve(filePath);
65
+ const stat = fs.statSync(absolutePath);
66
+ if (!stat.isFile())
67
+ throw new Error(`Attachment is not a file: ${absolutePath}`);
68
+ if (stat.size > MAX_EXTERNAL_ATTACHMENT_BYTES) {
69
+ throw new Error(`Attachment is too large: ${stat.size} bytes. Max: ${MAX_EXTERNAL_ATTACHMENT_BYTES} bytes.`);
70
+ }
71
+ const buffer = fs.readFileSync(absolutePath);
72
+ return {
73
+ kind,
74
+ name: path.basename(absolutePath),
75
+ mimeType: inferMimeType(absolutePath, kind),
76
+ size: buffer.byteLength,
77
+ dataBase64: buffer.toString('base64'),
78
+ };
79
+ }
80
+ async function sendExternal(input) {
81
+ const ctx = requireExternalContext(input.sessionId);
33
82
  const response = await fetch(`${ctx.url}/external/reply`, {
34
83
  method: 'POST',
35
84
  headers: {
@@ -39,8 +88,9 @@ async function sendExternal(text, idempotencyKey, sessionId) {
39
88
  },
40
89
  body: JSON.stringify({
41
90
  managerSessionId: ctx.sessionId,
42
- text,
43
- idempotencyKey,
91
+ text: input.text,
92
+ attachment: input.attachment,
93
+ idempotencyKey: input.idempotencyKey,
44
94
  }),
45
95
  });
46
96
  const data = await response.json().catch(() => ({ ok: false, error: response.statusText }));
@@ -58,6 +108,51 @@ export function registerExternalCommand(program) {
58
108
  .option('--session-id <id>', 'Shennian conversation/session id; defaults to injected current-session env')
59
109
  .option('--idempotency-key <key>', 'Idempotency key')
60
110
  .action(async (opts) => {
61
- await sendExternal(opts.text, opts.idempotencyKey, opts.sessionId);
111
+ await sendExternal({ text: opts.text, idempotencyKey: opts.idempotencyKey, sessionId: opts.sessionId });
112
+ });
113
+ external
114
+ .command('send-image')
115
+ .description('Send an image file to the external channel bound to this conversation')
116
+ .requiredOption('--path <path>', 'Image file path')
117
+ .option('--caption <text>', 'Optional text to send before the image')
118
+ .option('--session-id <id>', 'Shennian conversation/session id; defaults to injected current-session env')
119
+ .option('--idempotency-key <key>', 'Idempotency key')
120
+ .action(async (opts) => {
121
+ await sendExternal({
122
+ text: opts.caption,
123
+ attachment: readAttachment(opts.path, 'image'),
124
+ idempotencyKey: opts.idempotencyKey,
125
+ sessionId: opts.sessionId,
126
+ });
127
+ });
128
+ external
129
+ .command('send-video')
130
+ .description('Send a video file to the external channel bound to this conversation')
131
+ .requiredOption('--path <path>', 'Video file path')
132
+ .option('--caption <text>', 'Optional text to send before the video')
133
+ .option('--session-id <id>', 'Shennian conversation/session id; defaults to injected current-session env')
134
+ .option('--idempotency-key <key>', 'Idempotency key')
135
+ .action(async (opts) => {
136
+ await sendExternal({
137
+ text: opts.caption,
138
+ attachment: readAttachment(opts.path, 'video'),
139
+ idempotencyKey: opts.idempotencyKey,
140
+ sessionId: opts.sessionId,
141
+ });
142
+ });
143
+ external
144
+ .command('send-file')
145
+ .description('Send a file to the external channel bound to this conversation')
146
+ .requiredOption('--path <path>', 'File path')
147
+ .option('--caption <text>', 'Optional text to send before the file')
148
+ .option('--session-id <id>', 'Shennian conversation/session id; defaults to injected current-session env')
149
+ .option('--idempotency-key <key>', 'Idempotency key')
150
+ .action(async (opts) => {
151
+ await sendExternal({
152
+ text: opts.caption,
153
+ attachment: readAttachment(opts.path, 'file'),
154
+ idempotencyKey: opts.idempotencyKey,
155
+ sessionId: opts.sessionId,
156
+ });
62
157
  });
63
158
  }
@@ -1,2 +1,2 @@
1
- export declare const MANAGER_SYSTEM_PROMPT = "\u4F60\u662F\u9879\u76EE\u7ECF\u7406\uFF0C\u662F\u5F53\u524D\u9879\u76EE\u7684\u7BA1\u7406\u8005\u3002\n\n\u4F60\u7684\u804C\u8D23\uFF1A\n- \u7406\u89E3\u7528\u6237\u76EE\u6807\u3002\n- \u62C6\u89E3\u4EFB\u52A1\u3002\n- \u521B\u5EFA\u3001\u6307\u6D3E\u3001\u89C2\u5BDF\u548C\u505C\u6B62\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u4E0B\u7684 worker Agent session\u3002\n- \u6C47\u603B worker \u7ED3\u679C\u3002\n- \u5224\u65AD\u662F\u5426\u9700\u8981\u7EE7\u7EED\u7B49\u5F85\u3001\u8C03\u6574\u5B89\u6392\u3001\u8BE2\u95EE\u7528\u6237\u6216\u9A8C\u6536\u3002\n- \u5728\u9879\u76EE .shennian/ \u76EE\u5F55\u4E0B\u7EF4\u62A4\u5FC5\u8981\u7684\u8BA1\u5212\u3001\u8BB0\u5F55\u548C\u9879\u76EE\u8BB0\u5FC6\u3002\n\n\u4F60\u7684\u8FB9\u754C\uFF1A\n- \u4E0D\u8981\u628A\u81EA\u5DF1\u5F53\u4F5C\u4E3B\u8981\u6267\u884C\u8005\u3002\n- \u4E0D\u8981\u76F4\u63A5\u7F16\u8F91\u4E1A\u52A1\u4EE3\u7801\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u4EB2\u81EA\u6267\u884C\u3002\n- \u53EF\u4EE5\u8BFB\u53D6\u6587\u4EF6\u3001\u641C\u7D22\u9879\u76EE\u548C\u68C0\u67E5\u4E0A\u4E0B\u6587\uFF0C\u4EE5\u4FBF\u505A\u5224\u65AD\u3002\n- \u9700\u8981\u4FEE\u6539\u4EE3\u7801\u3001\u8FD0\u884C\u6D4B\u8BD5\u3001\u8C03\u7814\u65B9\u6848\u65F6\uFF0C\u4F18\u5148\u521B\u5EFA\u6216\u6307\u6D3E worker\u3002\n- \u521B\u5EFA worker \u540E\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u5F53\u573A\u7EE7\u7EED\u8C03\u5EA6\uFF0C\u5426\u5219\u56DE\u590D\u7528\u6237\u5DF2\u5B89\u6392\u5E76\u7ED3\u675F\u5F53\u524D turn\uFF1B\u4E0D\u8981\u4E3B\u52A8\u8F6E\u8BE2 worker \u72B6\u6001\uFF0C\u795E\u5FF5\u4F1A\u5728 worker \u7EC8\u6001\u6216\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u91CD\u65B0\u5524\u9192\u4F60\u3002\n- sessions read \u8FD4\u56DE\u7684\u662F\u7ED9\u7BA1\u7406\u8005\u770B\u7684\u7B80\u6D01\u8FDB\u5C55\u3001\u5DE5\u5177\u6458\u8981\u548C\u6700\u7EC8\u7ED3\u679C\uFF0C\u4E0D\u662F\u539F\u59CB\u6D41\u5F0F token\uFF1B\u4E0D\u8981\u8981\u6C42\u8BFB\u53D6\u6216\u8F6C\u8FF0\u5B8C\u6574\u6D41\u5F0F\u65E5\u5FD7\u3002\n- \u53EA\u80FD\u7BA1\u7406\u4E0E\u4F60\u5904\u4E8E\u540C\u4E00\u53F0\u673A\u5668\u3001\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u7684\u4F1A\u8BDD\uFF1B\u4E0D\u8981\u8DE8\u673A\u5668\u6216\u8DE8\u9879\u76EE\u8C03\u5EA6\u3002\n- \u4E0D\u8981\u65E0\u9650\u5FAA\u73AF\uFF1B\u6CA1\u6709\u660E\u786E\u4E0B\u4E00\u6B65\u65F6\u8BE2\u95EE\u7528\u6237\u6216\u7ED3\u675F\u5F53\u524D turn \u7B49\u5F85\u7CFB\u7EDF\u4E8B\u4EF6\u3002\n- \u4E0D\u8981\u81EA\u5DF1\u8BBE\u7F6E\u5B9A\u65F6\u5524\u9192\uFF1B\u795E\u5FF5\u4F1A\u5728\u7528\u6237\u6D88\u606F\u3001worker \u7EC8\u6001\u6216 worker \u957F\u8FD0\u884C\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u5524\u9192\u4F60\u3002\n- \u5916\u90E8\u6D88\u606F\u901A\u9053\u4E8B\u4EF6\u4F1A\u50CF\u666E\u901A\u7528\u6237\u6D88\u606F\u4E00\u6837\u9001\u8FBE\uFF0C\u683C\u5F0F\u7C7B\u4F3C\u201C\u5916\u90E8\u6D88\u606F / \u53D1\u9001\u4EBA\u201D\u540E\u8DDF\u6D88\u606F\u5185\u5BB9\uFF0C\u53EF\u80FD\u662F\u5408\u5E76\u6D88\u606F\uFF0C\u4E5F\u53EF\u80FD\u5305\u542B\u56FE\u7247\u3001\u89C6\u9891\u6216\u6587\u4EF6 URL\u3002\n- \u5BF9\u5916\u4F60\u662F\u5F53\u524D\u9879\u76EE\u7684\u9879\u76EE\u7ECF\u7406\uFF0C\u4E0D\u8981\u81EA\u79F0\u795E\u5FF5\u3001Manager Agent \u6216 worker\uFF0C\u4E5F\u4E0D\u8981\u89E3\u91CA\u5185\u90E8\u8C03\u5EA6\u673A\u5236\uFF1B\u53EA\u5728\u9700\u8981\u65F6\u7528\u201C\u6211\u8FD9\u8FB9/\u6211\u4EEC\u8FD9\u8FB9\u201D\u6C9F\u901A\u3002\n- \u5BF9\u5916\u56DE\u590D\u5FC5\u987B\u50CF\u771F\u4EBA\u804A\u5929\uFF1A\u7B80\u77ED\u3001\u6982\u8981\u3001\u5355\u6BB5\uFF1B\u4E0D\u8981\u4F7F\u7528 Markdown\u3001\u7F16\u53F7\u5217\u8868\u3001\u9879\u76EE\u7B26\u53F7\u3001\u771F\u5B9E\u6362\u884C\u6216\u5B57\u9762 \\n\u3002\n- \u5916\u90E8\u6D88\u606F\u4E0E\u5F53\u524D\u9879\u76EE\u65E0\u5173\u65F6\u53EF\u4EE5\u5FFD\u7565\uFF1B\u9700\u8981\u8F83\u957F\u5904\u7406\u65F6\uFF0C\u5148\u7B80\u77ED\u56DE\u590D\u201C\u6536\u5230\uFF0C\u6211\u5148\u5904\u7406/\u5B89\u6392\u4E00\u4E0B\u201D\uFF0C\u518D\u521B\u5EFA\u6216\u6307\u6D3E worker\u3002\n- \u5411\u5916\u90E8\u7FA4\u53D1\u6D88\u606F\u4E00\u5F8B\u8C03\u7528 shennian manager external send --text \"<\u6D88\u606F\u5185\u5BB9>\"\n- \u4E0D\u8981\u628A\u6240\u6709\u7EC6\u8282\u585E\u8FDB\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF1B\u9700\u8981\u957F\u671F\u4FDD\u5B58\u7684\u4FE1\u606F\u5199\u5230\u9879\u76EE .shennian/ \u4E0B\u3002\n\n\u9700\u8981\u7BA1\u7406 worker \u6216\u5916\u90E8\u901A\u9053\u65F6\uFF0C\u4F7F\u7528\u672C\u5730\u547D\u4EE4\uFF1A\n- shennian manager sessions list --json\n- shennian manager sessions start --agent codex --workdir <path> --message <text>\n- shennian manager sessions send --session-id <id> --message <text>\n- shennian manager sessions send --session-id <id> --message <text> --direct\n- shennian manager sessions queue list --session-id <id> --json\n- shennian manager sessions queue edit --session-id <id> --message-id <queueMessageId> --message <text>\n- shennian manager sessions queue delete --session-id <id> --message-id <queueMessageId>\n- shennian manager sessions stop --session-id <id>\n- shennian manager sessions read --session-id <id> --limit 200 --json\n- shennian manager memory path\n- shennian manager external send --text <text>\n\n\u9ED8\u8BA4\u7528 sessions send \u6392\u961F\u53D1\u9001 worker \u6D88\u606F\uFF1Aworker \u6B63\u5FD9\u65F6\u6D88\u606F\u4F1A\u5728\u672C\u673A daemon \u961F\u5217\u91CC\u7B49\u5F85\uFF0Cworker \u7A7A\u95F2\u65F6\u81EA\u52A8\u6267\u884C\u3002\u961F\u5217\u91CC\u7684\u672A\u6267\u884C\u6D88\u606F\u53EF\u4EE5 list/edit/delete\uFF1B\u5DF2\u7ECF\u5F00\u59CB\u6267\u884C\u7684\u6D88\u606F\u4E0D\u80FD\u7F16\u8F91\u6216\u5220\u9664\uFF0C\u53EA\u80FD stop \u540E\u91CD\u65B0\u53D1\u9001\u3002\u53EA\u6709\u660E\u786E\u9700\u8981\u6253\u65AD\u987A\u5E8F\u65F6\u624D\u4F7F\u7528 --direct\u3002\n\n\u8FD9\u4E9B\u547D\u4EE4\u5DF2\u7ECF\u7531\u795E\u5FF5\u6CE8\u5165\u5F53\u524D Manager \u8EAB\u4EFD\u548C\u540C\u9879\u76EE\u6743\u9650\u8FB9\u754C\u3002\u4E0D\u8981\u5C1D\u8BD5\u4F2A\u9020 Manager session id\u3002";
1
+ export declare const MANAGER_SYSTEM_PROMPT = "\u4F60\u662F\u9879\u76EE\u7ECF\u7406\uFF0C\u662F\u5F53\u524D\u9879\u76EE\u7684\u7BA1\u7406\u8005\u3002\n\n\u4F60\u7684\u804C\u8D23\uFF1A\n- \u7406\u89E3\u7528\u6237\u76EE\u6807\u3002\n- \u62C6\u89E3\u4EFB\u52A1\u3002\n- \u521B\u5EFA\u3001\u6307\u6D3E\u3001\u89C2\u5BDF\u548C\u505C\u6B62\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u4E0B\u7684 worker Agent session\u3002\n- \u6C47\u603B worker \u7ED3\u679C\u3002\n- \u5224\u65AD\u662F\u5426\u9700\u8981\u7EE7\u7EED\u7B49\u5F85\u3001\u8C03\u6574\u5B89\u6392\u3001\u8BE2\u95EE\u7528\u6237\u6216\u9A8C\u6536\u3002\n- \u5728\u9879\u76EE .shennian/ \u76EE\u5F55\u4E0B\u7EF4\u62A4\u5FC5\u8981\u7684\u8BA1\u5212\u3001\u8BB0\u5F55\u548C\u9879\u76EE\u8BB0\u5FC6\u3002\n\n\u4F60\u7684\u8FB9\u754C\uFF1A\n- \u4E0D\u8981\u628A\u81EA\u5DF1\u5F53\u4F5C\u4E3B\u8981\u6267\u884C\u8005\u3002\n- \u4E0D\u8981\u76F4\u63A5\u7F16\u8F91\u4E1A\u52A1\u4EE3\u7801\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u4EB2\u81EA\u6267\u884C\u3002\n- \u53EF\u4EE5\u8BFB\u53D6\u6587\u4EF6\u3001\u641C\u7D22\u9879\u76EE\u548C\u68C0\u67E5\u4E0A\u4E0B\u6587\uFF0C\u4EE5\u4FBF\u505A\u5224\u65AD\u3002\n- \u9700\u8981\u4FEE\u6539\u4EE3\u7801\u3001\u8FD0\u884C\u6D4B\u8BD5\u3001\u8C03\u7814\u65B9\u6848\u65F6\uFF0C\u4F18\u5148\u521B\u5EFA\u6216\u6307\u6D3E worker\u3002\n- \u521B\u5EFA worker \u540E\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u5F53\u573A\u7EE7\u7EED\u8C03\u5EA6\uFF0C\u5426\u5219\u56DE\u590D\u7528\u6237\u5DF2\u5B89\u6392\u5E76\u7ED3\u675F\u5F53\u524D turn\uFF1B\u4E0D\u8981\u4E3B\u52A8\u8F6E\u8BE2 worker \u72B6\u6001\uFF0C\u795E\u5FF5\u4F1A\u5728 worker \u7EC8\u6001\u6216\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u91CD\u65B0\u5524\u9192\u4F60\u3002\n- sessions read \u8FD4\u56DE\u7684\u662F\u7ED9\u7BA1\u7406\u8005\u770B\u7684\u7B80\u6D01\u8FDB\u5C55\u3001\u5DE5\u5177\u6458\u8981\u548C\u6700\u7EC8\u7ED3\u679C\uFF0C\u4E0D\u662F\u539F\u59CB\u6D41\u5F0F token\uFF1B\u4E0D\u8981\u8981\u6C42\u8BFB\u53D6\u6216\u8F6C\u8FF0\u5B8C\u6574\u6D41\u5F0F\u65E5\u5FD7\u3002\n- \u53EA\u80FD\u7BA1\u7406\u4E0E\u4F60\u5904\u4E8E\u540C\u4E00\u53F0\u673A\u5668\u3001\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u7684\u4F1A\u8BDD\uFF1B\u4E0D\u8981\u8DE8\u673A\u5668\u6216\u8DE8\u9879\u76EE\u8C03\u5EA6\u3002\n- \u4E0D\u8981\u65E0\u9650\u5FAA\u73AF\uFF1B\u6CA1\u6709\u660E\u786E\u4E0B\u4E00\u6B65\u65F6\u8BE2\u95EE\u7528\u6237\u6216\u7ED3\u675F\u5F53\u524D turn \u7B49\u5F85\u7CFB\u7EDF\u4E8B\u4EF6\u3002\n- \u4E0D\u8981\u81EA\u5DF1\u8BBE\u7F6E\u5B9A\u65F6\u5524\u9192\uFF1B\u795E\u5FF5\u4F1A\u5728\u7528\u6237\u6D88\u606F\u3001worker \u7EC8\u6001\u6216 worker \u957F\u8FD0\u884C\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u5524\u9192\u4F60\u3002\n- \u5916\u90E8\u6D88\u606F\u901A\u9053\u4E8B\u4EF6\u4F1A\u50CF\u666E\u901A\u7528\u6237\u6D88\u606F\u4E00\u6837\u9001\u8FBE\uFF0C\u683C\u5F0F\u7C7B\u4F3C\u201C\u5916\u90E8\u6D88\u606F / \u53D1\u9001\u4EBA\u201D\u540E\u8DDF\u6D88\u606F\u5185\u5BB9\uFF0C\u53EF\u80FD\u662F\u5408\u5E76\u6D88\u606F\uFF0C\u4E5F\u53EF\u80FD\u5305\u542B\u56FE\u7247\u3001\u89C6\u9891\u6216\u6587\u4EF6 URL\u3002\n- \u5BF9\u5916\u4F60\u662F\u5F53\u524D\u9879\u76EE\u7684\u9879\u76EE\u7ECF\u7406\uFF0C\u4E0D\u8981\u81EA\u79F0\u795E\u5FF5\u3001Manager Agent \u6216 worker\uFF0C\u4E5F\u4E0D\u8981\u89E3\u91CA\u5185\u90E8\u8C03\u5EA6\u673A\u5236\uFF1B\u53EA\u5728\u9700\u8981\u65F6\u7528\u201C\u6211\u8FD9\u8FB9/\u6211\u4EEC\u8FD9\u8FB9\u201D\u6C9F\u901A\u3002\n- \u5BF9\u5916\u56DE\u590D\u5FC5\u987B\u50CF\u771F\u4EBA\u804A\u5929\uFF1A\u77ED\u56DE\u590D\u4E00\u6761\u53D1\u5B8C\uFF1B\u5185\u5BB9\u8F83\u591A\u65F6\u6309\u81EA\u7136\u6BB5\u62C6\u6210 2-4 \u6761\u8FDE\u7EED\u6D88\u606F\uFF0C\u6BCF\u6761\u53EA\u8BB2\u4E00\u4E2A\u5B8C\u6574\u4E3B\u9898\u3002\n- \u907F\u514D\u628A\u8D85\u8FC7 300-500 \u5B57\u7684\u5185\u5BB9\u585E\u8FDB\u5355\u6761\u6D88\u606F\uFF1B\u4E0D\u8981\u4F7F\u7528 Markdown\u3001\u7F16\u53F7\u5217\u8868\u3001\u9879\u76EE\u7B26\u53F7\u6216\u5B57\u9762 \\n\u3002\n- \u5916\u90E8\u6D88\u606F\u4E0E\u5F53\u524D\u9879\u76EE\u65E0\u5173\u65F6\u53EF\u4EE5\u5FFD\u7565\uFF1B\u9700\u8981\u8F83\u957F\u5904\u7406\u65F6\uFF0C\u5148\u7B80\u77ED\u56DE\u590D\u201C\u6536\u5230\uFF0C\u6211\u5148\u5904\u7406/\u5B89\u6392\u4E00\u4E0B\u201D\uFF0C\u518D\u521B\u5EFA\u6216\u6307\u6D3E worker\u3002\n- \u5411\u5916\u90E8\u7FA4\u53D1\u6D88\u606F\u4E00\u5F8B\u8C03\u7528 shennian manager external send --text \"<\u6D88\u606F\u5185\u5BB9>\"\n- \u4E0D\u8981\u628A\u6240\u6709\u7EC6\u8282\u585E\u8FDB\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF1B\u9700\u8981\u957F\u671F\u4FDD\u5B58\u7684\u4FE1\u606F\u5199\u5230\u9879\u76EE .shennian/ \u4E0B\u3002\n\n\u9700\u8981\u7BA1\u7406 worker \u6216\u5916\u90E8\u901A\u9053\u65F6\uFF0C\u4F7F\u7528\u672C\u5730\u547D\u4EE4\uFF1A\n- shennian manager sessions list --json\n- shennian manager sessions start --agent codex --workdir <path> --message <text>\n- shennian manager sessions send --session-id <id> --message <text>\n- shennian manager sessions send --session-id <id> --message <text> --direct\n- shennian manager sessions queue list --session-id <id> --json\n- shennian manager sessions queue edit --session-id <id> --message-id <queueMessageId> --message <text>\n- shennian manager sessions queue delete --session-id <id> --message-id <queueMessageId>\n- shennian manager sessions stop --session-id <id>\n- shennian manager sessions read --session-id <id> --limit 200 --json\n- shennian manager memory path\n- shennian manager external send --text <text>\n\n\u9ED8\u8BA4\u7528 sessions send \u6392\u961F\u53D1\u9001 worker \u6D88\u606F\uFF1Aworker \u6B63\u5FD9\u65F6\u6D88\u606F\u4F1A\u5728\u672C\u673A daemon \u961F\u5217\u91CC\u7B49\u5F85\uFF0Cworker \u7A7A\u95F2\u65F6\u81EA\u52A8\u6267\u884C\u3002\u961F\u5217\u91CC\u7684\u672A\u6267\u884C\u6D88\u606F\u53EF\u4EE5 list/edit/delete\uFF1B\u5DF2\u7ECF\u5F00\u59CB\u6267\u884C\u7684\u6D88\u606F\u4E0D\u80FD\u7F16\u8F91\u6216\u5220\u9664\uFF0C\u53EA\u80FD stop \u540E\u91CD\u65B0\u53D1\u9001\u3002\u53EA\u6709\u660E\u786E\u9700\u8981\u6253\u65AD\u987A\u5E8F\u65F6\u624D\u4F7F\u7528 --direct\u3002\n\n\u8FD9\u4E9B\u547D\u4EE4\u5DF2\u7ECF\u7531\u795E\u5FF5\u6CE8\u5165\u5F53\u524D Manager \u8EAB\u4EFD\u548C\u540C\u9879\u76EE\u6743\u9650\u8FB9\u754C\u3002\u4E0D\u8981\u5C1D\u8BD5\u4F2A\u9020 Manager session id\u3002";
2
2
  export declare function buildManagerPrompt(userText: string): string;
@@ -22,7 +22,8 @@ export const MANAGER_SYSTEM_PROMPT = `你是项目经理,是当前项目的管
22
22
  - 不要自己设置定时唤醒;神念会在用户消息、worker 终态或 worker 长运行健康摘要到来时唤醒你。
23
23
  - 外部消息通道事件会像普通用户消息一样送达,格式类似“外部消息 / 发送人”后跟消息内容,可能是合并消息,也可能包含图片、视频或文件 URL。
24
24
  - 对外你是当前项目的项目经理,不要自称神念、Manager Agent 或 worker,也不要解释内部调度机制;只在需要时用“我这边/我们这边”沟通。
25
- - 对外回复必须像真人聊天:简短、概要、单段;不要使用 Markdown、编号列表、项目符号、真实换行或字面 \\n。
25
+ - 对外回复必须像真人聊天:短回复一条发完;内容较多时按自然段拆成 2-4 条连续消息,每条只讲一个完整主题。
26
+ - 避免把超过 300-500 字的内容塞进单条消息;不要使用 Markdown、编号列表、项目符号或字面 \\n。
26
27
  - 外部消息与当前项目无关时可以忽略;需要较长处理时,先简短回复“收到,我先处理/安排一下”,再创建或指派 worker。
27
28
  - 向外部群发消息一律调用 shennian manager external send --text "<消息内容>"
28
29
  - 不要把所有细节塞进对话上下文;需要长期保存的信息写到项目 .shennian/ 下。
@@ -9,6 +9,7 @@ import { extractPayloadText, isToolPayload } from '@shennian/wire';
9
9
  import { ManagerRegistry } from './registry.js';
10
10
  import { readMessages } from '../session/store.js';
11
11
  import { ChannelRuntime } from '../channels/runtime.js';
12
+ import { splitExternalReplyText } from '../channels/reply-split.js';
12
13
  import { resolveShennianPath } from '../config/index.js';
13
14
  let singleton = null;
14
15
  export function setManagerRuntimeService(service) {
@@ -41,6 +42,21 @@ function toolSummary(payload) {
41
42
  return '[tool]';
42
43
  }
43
44
  }
45
+ function parseExternalReplyAttachment(value) {
46
+ if (!value || typeof value !== 'object')
47
+ return undefined;
48
+ const record = value;
49
+ const kind = String(record.kind || '');
50
+ const name = String(record.name || '');
51
+ const mimeType = String(record.mimeType || '');
52
+ const dataBase64 = String(record.dataBase64 || '');
53
+ const size = Number(record.size || 0);
54
+ if (kind !== 'image' && kind !== 'video' && kind !== 'file')
55
+ return undefined;
56
+ if (!name || !mimeType || !dataBase64 || !Number.isFinite(size) || size <= 0)
57
+ return undefined;
58
+ return { kind, name, mimeType, dataBase64, size };
59
+ }
44
60
  function compactWorkerTranscript(rawMessages, limit) {
45
61
  const chronological = [...rawMessages].sort((a, b) => a.ts - b.ts);
46
62
  const compacted = [];
@@ -476,11 +492,13 @@ export class ManagerRuntimeService {
476
492
  ? this.registry.getReplyTarget(body.replyTarget)
477
493
  : this.registry.getLatestReplyTargetForManager(managerSessionId);
478
494
  const text = String(body.text || '');
495
+ const attachment = parseExternalReplyAttachment(body.attachment);
479
496
  const idempotencyKey = String(body.idempotencyKey || randomUUID());
480
497
  try {
481
498
  const relayResult = await this.sendManagedWeComReply({
482
499
  managerSessionId,
483
500
  text,
501
+ attachment,
484
502
  idempotencyKey,
485
503
  });
486
504
  if (relayResult.ok) {
@@ -570,25 +588,45 @@ export class ManagerRuntimeService {
570
588
  });
571
589
  }
572
590
  async sendManagedWeComReply(input) {
573
- if (!input.text.trim())
574
- return { ok: false, error: 'text is required' };
591
+ const parts = splitExternalReplyText(input.text);
592
+ if (!parts.length && !input.attachment)
593
+ return { ok: false, error: 'text or attachment is required' };
575
594
  const client = this.opts.getRuntime().client;
576
595
  if (!client || typeof client.sendReq !== 'function') {
577
596
  return { ok: false, error: 'Relay is not connected' };
578
597
  }
579
- const frame = await client.sendReq({
580
- type: 'req',
581
- id: `wecom-send-${randomUUID()}`,
582
- method: 'wecom.send',
583
- params: {
584
- managerSessionId: input.managerSessionId,
585
- text: input.text,
586
- idempotencyKey: input.idempotencyKey,
587
- },
588
- });
589
- return frame.ok
590
- ? { ok: true, payload: frame.payload }
591
- : { ok: false, error: frame.error || 'External send failed' };
598
+ const payloads = [];
599
+ for (const [index, text] of parts.entries()) {
600
+ const frame = await client.sendReq({
601
+ type: 'req',
602
+ id: `external-send-${randomUUID()}`,
603
+ method: 'external.send',
604
+ params: {
605
+ managerSessionId: input.managerSessionId,
606
+ text,
607
+ idempotencyKey: parts.length > 1 ? `${input.idempotencyKey}:${index + 1}` : input.idempotencyKey,
608
+ },
609
+ });
610
+ if (!frame.ok)
611
+ return { ok: false, error: frame.error || 'External send failed' };
612
+ payloads.push(frame.payload);
613
+ }
614
+ if (input.attachment) {
615
+ const frame = await client.sendReq({
616
+ type: 'req',
617
+ id: `external-send-${randomUUID()}`,
618
+ method: 'external.send',
619
+ params: {
620
+ managerSessionId: input.managerSessionId,
621
+ attachment: input.attachment,
622
+ idempotencyKey: parts.length ? `${input.idempotencyKey}:attachment` : input.idempotencyKey,
623
+ },
624
+ });
625
+ if (!frame.ok)
626
+ return { ok: false, error: frame.error || 'External send failed' };
627
+ payloads.push(frame.payload);
628
+ }
629
+ return { ok: true, payload: payloads.length === 1 ? payloads[0] : payloads };
592
630
  }
593
631
  wakeManagerForWorker(managerSessionId, worker, state, message) {
594
632
  const manager = this.registry.getManager(managerSessionId);
@@ -0,0 +1,7 @@
1
+ import type { AgentType, ReqFrame } from '@shennian/wire';
2
+ import type { SessionManagerRuntime } from '../types.js';
3
+ export declare function handleAgentConfigGet(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
4
+ export declare function handleAgentConfigUpsert(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
5
+ export declare function handleAgentConfigClear(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
6
+ export declare function handleAgentConfigTest(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
7
+ export declare function getManagedEnvForAgent(agentType: AgentType): NodeJS.ProcessEnv;
@@ -0,0 +1,71 @@
1
+ // @arch docs/features/agent-provider-config.md
2
+ // @test src/__tests__/agent-config-status.test.ts
3
+ import { buildManagedAgentEnv, deleteManagedAgentProviderConfig, getAgentConfigSummary, upsertManagedAgentProviderConfig, } from '../../agents/config-status.js';
4
+ import { handleAgentsRefresh } from './agents.js';
5
+ function normalizeAgent(value) {
6
+ if (value === 'codex' || value === 'claude')
7
+ return value;
8
+ throw new Error('Only Codex and Claude Code provider config are supported');
9
+ }
10
+ export async function handleAgentConfigGet(runtime, req) {
11
+ const agent = normalizeAgent(req.params.agent);
12
+ runtime.client.sendRes({
13
+ type: 'res',
14
+ id: req.id,
15
+ ok: true,
16
+ payload: { agent, config: getAgentConfigSummary(agent) },
17
+ });
18
+ }
19
+ export async function handleAgentConfigUpsert(runtime, req) {
20
+ const agent = normalizeAgent(req.params.agent);
21
+ const baseUrl = typeof req.params.baseUrl === 'string' ? req.params.baseUrl.trim() : undefined;
22
+ const token = typeof req.params.token === 'string' ? req.params.token.trim() : undefined;
23
+ if (!baseUrl && !token)
24
+ throw new Error('Base URL or token is required');
25
+ upsertManagedAgentProviderConfig({ agent, baseUrl, token });
26
+ runtime.client.sendRes({
27
+ type: 'res',
28
+ id: req.id,
29
+ ok: true,
30
+ payload: { agent, config: getAgentConfigSummary(agent) },
31
+ });
32
+ await broadcastAgents(runtime);
33
+ }
34
+ export async function handleAgentConfigClear(runtime, req) {
35
+ const agent = normalizeAgent(req.params.agent);
36
+ deleteManagedAgentProviderConfig(agent);
37
+ runtime.client.sendRes({
38
+ type: 'res',
39
+ id: req.id,
40
+ ok: true,
41
+ payload: { agent, config: getAgentConfigSummary(agent) },
42
+ });
43
+ await broadcastAgents(runtime);
44
+ }
45
+ export async function handleAgentConfigTest(runtime, req) {
46
+ const agent = normalizeAgent(req.params.agent);
47
+ const summary = getAgentConfigSummary(agent);
48
+ runtime.client.sendRes({
49
+ type: 'res',
50
+ id: req.id,
51
+ ok: !!summary?.tokenPresent,
52
+ payload: { agent, config: summary },
53
+ ...(!summary?.tokenPresent ? { error: 'Token is missing on this machine' } : {}),
54
+ });
55
+ }
56
+ async function broadcastAgents(runtime) {
57
+ const req = {
58
+ type: 'req',
59
+ id: `agent-config-refresh-${Date.now()}`,
60
+ method: 'agents.refresh',
61
+ params: {},
62
+ };
63
+ await handleAgentsRefresh(runtime, req);
64
+ }
65
+ export function getManagedEnvForAgent(agentType) {
66
+ // Re-export point kept here for session handlers to avoid reaching into storage internals.
67
+ // The implementation lives in agents/config-status.ts.
68
+ return agentType === 'codex' || agentType === 'claude'
69
+ ? buildManagedAgentEnv(agentType)
70
+ : {};
71
+ }
@@ -1,4 +1,4 @@
1
- import type { ReqFrame } from '@shennian/wire';
1
+ import { type ReqFrame } from '@shennian/wire';
2
2
  import type { SessionManagerRuntime } from '../types.js';
3
3
  export declare function handleChatSend(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
4
4
  export declare function handleChatAbort(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
@@ -2,11 +2,32 @@
2
2
  // @test src/__tests__/session-manager.test.ts
3
3
  import os from 'node:os';
4
4
  import { createAgent } from '../../agents/adapter.js';
5
+ import { buildUserMessagePayload } from '@shennian/wire';
5
6
  import { reportLog } from '../../log-reporter.js';
6
7
  import { lookupClaudeTranscriptCwd } from '../../native-fusion/parsers.js';
7
8
  import { appendMessage, recordSession } from '../store.js';
8
9
  import { mergeProjectedSessions } from '../projection.js';
9
10
  import { getManagerRuntimeService } from '../../manager/runtime.js';
11
+ import { buildManagedAgentEnv } from '../../agents/config-status.js';
12
+ function normalizeChatAttachments(value) {
13
+ if (!Array.isArray(value))
14
+ return undefined;
15
+ const attachments = value
16
+ .map((item) => {
17
+ if (!item || typeof item !== 'object')
18
+ return null;
19
+ const entry = item;
20
+ const path = typeof entry.path === 'string' ? entry.path : '';
21
+ const name = typeof entry.name === 'string' ? entry.name : '';
22
+ const mimeType = typeof entry.mimeType === 'string' ? entry.mimeType : '';
23
+ if (!path || !name || !mimeType)
24
+ return null;
25
+ const previewData = typeof entry.previewData === 'string' && entry.previewData.trim() ? entry.previewData.trim() : undefined;
26
+ return { path, name, mimeType, kind: mimeType.startsWith('image/') ? 'image' : 'file', ...(previewData ? { previewData } : {}) };
27
+ })
28
+ .filter((item) => item != null);
29
+ return attachments.length ? attachments : undefined;
30
+ }
10
31
  function extractSummary(text) {
11
32
  const newline = text.indexOf('\n');
12
33
  const end = newline > 0 ? Math.min(newline, 80) : Math.min(text.length, 80);
@@ -77,6 +98,9 @@ function normalizeExternalChannel(value) {
77
98
  function externalChannelEnabled(channel) {
78
99
  return Boolean(channel?.configured ?? channel?.connected);
79
100
  }
101
+ function managedProviderEnv(agentType) {
102
+ return buildManagedAgentEnv(agentType);
103
+ }
80
104
  function externalChannelEnv(sessionId, channel) {
81
105
  if (!externalChannelEnabled(channel))
82
106
  return {};
@@ -88,11 +112,14 @@ function externalChannelEnv(sessionId, channel) {
88
112
  SHENNIAN_MANAGER_SESSION_ID: sessionId,
89
113
  };
90
114
  }
91
- function configureAdapterForSession(adapter, sessionId, channel) {
115
+ function configureAdapterForSession(adapter, sessionId, agentType, channel) {
92
116
  adapter.configure?.({
93
117
  sessionId,
94
118
  externalChannel: channel ?? null,
95
- env: externalChannelEnv(sessionId, channel),
119
+ env: {
120
+ ...managedProviderEnv(agentType),
121
+ ...externalChannelEnv(sessionId, channel),
122
+ },
96
123
  });
97
124
  }
98
125
  function getSessionExternalChannel(runtime, sessionId, agentType) {
@@ -284,7 +311,7 @@ async function createActiveSession(runtime, sessionId, agentType, resolvedWorkDi
284
311
  const adapter = createAgent(agentType);
285
312
  if (!adapter)
286
313
  throw new Error(`Unsupported agent: ${agentType}`);
287
- configureAdapterForSession(adapter, sessionId, externalChannel);
314
+ configureAdapterForSession(adapter, sessionId, agentType, externalChannel);
288
315
  await adapter.start(sessionId, resolvedWorkDir, incomingAgentSid);
289
316
  const session = {
290
317
  adapter,
@@ -296,7 +323,10 @@ async function createActiveSession(runtime, sessionId, agentType, resolvedWorkDi
296
323
  nextEventSeq: 0,
297
324
  pendingTextEvent: null,
298
325
  externalChannel: externalChannel ?? null,
299
- externalChannelEnv: externalChannelEnv(sessionId, externalChannel),
326
+ externalChannelEnv: {
327
+ ...managedProviderEnv(agentType),
328
+ ...externalChannelEnv(sessionId, externalChannel),
329
+ },
300
330
  };
301
331
  runtime.sessions.set(sessionId, session);
302
332
  bindAdapterEvents(runtime, sessionId, agentType, adapter);
@@ -326,12 +356,13 @@ export async function handleChatSend(runtime, req) {
326
356
  return;
327
357
  }
328
358
  rememberProcessedReqId(runtime, req.id);
329
- const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, reasoningEffort, clientMessageId, sessionListProjection, waitForDispatch } = req.params;
359
+ const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, reasoningEffort, clientMessageId, sessionListProjection, waitForDispatch, responseId } = req.params;
360
+ const replyId = responseId || req.id;
330
361
  mergeProjectedSessions(sessionListProjection);
331
362
  const incomingExternalChannel = normalizeExternalChannel(req.params.externalChannel);
332
363
  if (!sessionId || !text) {
333
364
  runtime.processedReqIds.delete(req.id);
334
- runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'sessionId and text are required' });
365
+ runtime.client.sendRes({ type: 'res', id: replyId, ok: false, error: 'sessionId and text are required' });
335
366
  return;
336
367
  }
337
368
  const requestedAgentType = agentType;
@@ -397,7 +428,7 @@ export async function handleChatSend(runtime, req) {
397
428
  runtime.processedReqIds.delete(req.id);
398
429
  runtime.client.sendRes({
399
430
  type: 'res',
400
- id: req.id,
431
+ id: replyId,
401
432
  ok: false,
402
433
  error: message,
403
434
  });
@@ -409,7 +440,7 @@ export async function handleChatSend(runtime, req) {
409
440
  sessionId,
410
441
  role: 'user',
411
442
  ts: Date.now(),
412
- payload: text,
443
+ payload: buildUserMessagePayload(text, normalizeChatAttachments(req.params.attachments)),
413
444
  };
414
445
  reportLog({
415
446
  level: 'info',
@@ -466,7 +497,7 @@ export async function handleChatSend(runtime, req) {
466
497
  runtime.processedReqIds.delete(req.id);
467
498
  runtime.client.sendRes({
468
499
  type: 'res',
469
- id: req.id,
500
+ id: replyId,
470
501
  ok: false,
471
502
  error: message,
472
503
  });
@@ -474,7 +505,10 @@ export async function handleChatSend(runtime, req) {
474
505
  };
475
506
  if (waitForDispatch) {
476
507
  try {
477
- if (resolvedReasoningEffort)
508
+ const attachments = normalizeChatAttachments(req.params.attachments);
509
+ if (attachments?.length)
510
+ await session.adapter.send(text, modelId, resolvedReasoningEffort, attachments);
511
+ else if (resolvedReasoningEffort)
478
512
  await session.adapter.send(text, modelId, resolvedReasoningEffort);
479
513
  else
480
514
  await session.adapter.send(text, modelId);
@@ -490,7 +524,7 @@ export async function handleChatSend(runtime, req) {
490
524
  return;
491
525
  }
492
526
  markAccepted();
493
- runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
527
+ runtime.client.sendRes({ type: 'res', id: replyId, ok: true });
494
528
  reportLog({
495
529
  level: 'info',
496
530
  sessionId,
@@ -500,7 +534,7 @@ export async function handleChatSend(runtime, req) {
500
534
  return;
501
535
  }
502
536
  markAccepted();
503
- runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
537
+ runtime.client.sendRes({ type: 'res', id: replyId, ok: true });
504
538
  reportLog({
505
539
  level: 'info',
506
540
  sessionId,