shennian 0.2.72 → 0.2.74

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.
Files changed (36) hide show
  1. package/dist/src/agents/command-spec.js +19 -12
  2. package/dist/src/agents/external-channel-instructions.d.ts +3 -1
  3. package/dist/src/agents/external-channel-instructions.js +73 -15
  4. package/dist/src/channels/base.d.ts +62 -9
  5. package/dist/src/channels/runtime.d.ts +43 -10
  6. package/dist/src/channels/runtime.js +300 -14
  7. package/dist/src/channels/secret-registry.d.ts +17 -1
  8. package/dist/src/channels/websocket.d.ts +3 -0
  9. package/dist/src/channels/websocket.js +39 -2
  10. package/dist/src/channels/wechat-rpa/macos-flow.d.ts +77 -0
  11. package/dist/src/channels/wechat-rpa/macos-flow.js +254 -0
  12. package/dist/src/channels/wechat-rpa/macos.d.ts +11 -0
  13. package/dist/src/channels/wechat-rpa/macos.js +63 -0
  14. package/dist/src/channels/wechat-rpa/normalizer.d.ts +42 -0
  15. package/dist/src/channels/wechat-rpa/normalizer.js +99 -0
  16. package/dist/src/channels/wechat-rpa.d.ts +51 -0
  17. package/dist/src/channels/wechat-rpa.js +587 -0
  18. package/dist/src/channels/wecom.d.ts +3 -0
  19. package/dist/src/channels/wecom.js +43 -1
  20. package/dist/src/commands/external-attachments.d.ts +1 -1
  21. package/dist/src/commands/external-attachments.js +2 -3
  22. package/dist/src/commands/external.js +19 -1
  23. package/dist/src/commands/manager.js +109 -0
  24. package/dist/src/manager/prompt.d.ts +1 -1
  25. package/dist/src/manager/prompt.js +1 -11
  26. package/dist/src/manager/runtime.d.ts +2 -10
  27. package/dist/src/manager/runtime.js +197 -33
  28. package/dist/src/native-fusion/service.js +7 -0
  29. package/dist/src/session/archive-zip.d.ts +10 -0
  30. package/dist/src/session/archive-zip.js +220 -0
  31. package/dist/src/session/handlers/agent-config.js +85 -6
  32. package/dist/src/session/handlers/chat.js +58 -2
  33. package/dist/src/session/handlers/fs.d.ts +1 -0
  34. package/dist/src/session/handlers/fs.js +57 -1
  35. package/dist/src/session/manager.js +4 -1
  36. package/package.json +10 -9
@@ -5,8 +5,13 @@ import chalk from 'chalk';
5
5
  import { resolveShennianPath } from '../config/index.js';
6
6
  import { readExternalAttachment } from './external-attachments.js';
7
7
  function loadManagerIpcFromRuntimeFile() {
8
+ const filePath = resolveShennianPath('runtime', 'manager-ipc.json');
8
9
  try {
9
- const parsed = JSON.parse(fs.readFileSync(resolveShennianPath('runtime', 'manager-ipc.json'), 'utf-8'));
10
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
11
+ const pid = Number(parsed.pid);
12
+ if (Number.isInteger(pid) && pid > 0 && !processExists(pid)) {
13
+ return { error: `Manager IPC runtime file is stale for PID ${pid}. Restart the desktop daemon and retry.` };
14
+ }
10
15
  return {
11
16
  url: typeof parsed.url === 'string' ? parsed.url : undefined,
12
17
  token: typeof parsed.token === 'string' ? parsed.token : undefined,
@@ -20,6 +25,10 @@ function requireExternalContext(explicitSessionId) {
20
25
  const runtimeIpc = !process.env.SHENNIAN_MANAGER_IPC_URL || !process.env.SHENNIAN_MANAGER_IPC_TOKEN
21
26
  ? loadManagerIpcFromRuntimeFile()
22
27
  : {};
28
+ if (runtimeIpc.error) {
29
+ console.error(chalk.red(`✗ ${runtimeIpc.error}`));
30
+ process.exit(1);
31
+ }
23
32
  const url = process.env.SHENNIAN_MANAGER_IPC_URL || runtimeIpc.url;
24
33
  const token = process.env.SHENNIAN_MANAGER_IPC_TOKEN || runtimeIpc.token;
25
34
  const sessionId = explicitSessionId?.trim() || process.env.SHENNIAN_EXTERNAL_SESSION_ID || process.env.SHENNIAN_MANAGER_SESSION_ID;
@@ -29,6 +38,15 @@ function requireExternalContext(explicitSessionId) {
29
38
  }
30
39
  return { url, token, sessionId };
31
40
  }
41
+ function processExists(pid) {
42
+ try {
43
+ process.kill(pid, 0);
44
+ return true;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
32
50
  async function sendExternal(input) {
33
51
  const ctx = requireExternalContext(input.sessionId);
34
52
  const response = await fetch(`${ctx.url}/external/reply`, {
@@ -39,6 +39,48 @@ function readMessage(opts) {
39
39
  return fs.readFileSync(opts.messageFile, 'utf-8');
40
40
  return opts.message ?? '';
41
41
  }
42
+ function collect(value, previous = []) {
43
+ previous.push(value);
44
+ return previous;
45
+ }
46
+ function parseBool(value) {
47
+ if (value === undefined)
48
+ return undefined;
49
+ return value === 'true';
50
+ }
51
+ function parseNumber(value) {
52
+ const number = Number(value);
53
+ return Number.isFinite(number) ? number : undefined;
54
+ }
55
+ function printWeChatRpaStatus(channel) {
56
+ if (!channel) {
57
+ console.log('not configured');
58
+ return;
59
+ }
60
+ const groups = Array.isArray(channel.wechatRpaGroups)
61
+ ? channel.wechatRpaGroups
62
+ .map((group) => typeof group === 'object' && group ? String(group.name || '').trim() : '')
63
+ .filter(Boolean)
64
+ .join(', ')
65
+ : '';
66
+ const fields = [
67
+ `enabled=${Boolean(channel.enabled)}`,
68
+ `connected=${Boolean(channel.tokenConfigured ?? channel.enabled)}`,
69
+ channel.wechatRpaSource ? `source=${String(channel.wechatRpaSource)}` : '',
70
+ groups ? `groups=${groups}` : 'groups=',
71
+ channel.pollIntervalMs ? `poll=${String(channel.pollIntervalMs)}ms` : '',
72
+ channel.wechatRpaRuntimeState ? `state=${String(channel.wechatRpaRuntimeState)}` : '',
73
+ Number.isFinite(channel.wechatRpaPendingReplyCount) ? `pendingReplies=${String(channel.wechatRpaPendingReplyCount)}` : '',
74
+ channel.wechatRpaLastRunAt ? `lastRun=${String(channel.wechatRpaLastRunAt)}` : '',
75
+ channel.wechatRpaLastMessageAt ? `lastMessage=${String(channel.wechatRpaLastMessageAt)}` : '',
76
+ channel.wechatRpaLastInterruptedAt ? `lastInterrupted=${String(channel.wechatRpaLastInterruptedAt)}` : '',
77
+ channel.wechatRpaLastCloudOcrAt ? `lastCloudOcr=${String(channel.wechatRpaLastCloudOcrAt)}` : '',
78
+ channel.wechatRpaLastCloudOcrPurpose ? `lastCloudOcrPurpose=${String(channel.wechatRpaLastCloudOcrPurpose)}` : '',
79
+ channel.wechatRpaLastCloudOcrRequestId ? `lastCloudOcrRequestId=${String(channel.wechatRpaLastCloudOcrRequestId)}` : '',
80
+ channel.wechatRpaLastError ? `lastError=${String(channel.wechatRpaLastError)}` : 'lastError=none',
81
+ ].filter(Boolean);
82
+ console.log(fields.join('\n'));
83
+ }
42
84
  export function registerManagerCommand(program) {
43
85
  const manager = program.command('manager').description('Manager Agent local tools');
44
86
  const sessions = manager.command('sessions').description('Manage same-project worker sessions');
@@ -263,4 +305,71 @@ export function registerManagerCommand(program) {
263
305
  else
264
306
  printJson(result.channel ?? null);
265
307
  });
308
+ const wechatRpa = manager.command('wechat-rpa').description('Local WeChat RPA channel config');
309
+ wechatRpa
310
+ .command('get')
311
+ .description('Get local WeChat RPA channel config')
312
+ .option('--json', 'Print JSON')
313
+ .action(async (opts) => {
314
+ const result = await ipc('/wechat-rpa/channel/get', {});
315
+ if (opts.json)
316
+ printJson(result);
317
+ else
318
+ printJson(result.channel ?? null);
319
+ });
320
+ wechatRpa
321
+ .command('status')
322
+ .description('Print local WeChat RPA channel runtime status')
323
+ .option('--json', 'Print JSON')
324
+ .action(async (opts) => {
325
+ const result = await ipc('/wechat-rpa/channel/get', {});
326
+ if (opts.json)
327
+ printJson(result);
328
+ else
329
+ printWeChatRpaStatus((result.channel ?? null));
330
+ });
331
+ wechatRpa
332
+ .command('upsert')
333
+ .description('Create or update a local WeChat RPA channel binding')
334
+ .option('--id <id>', 'Channel id')
335
+ .option('--name <name>', 'Channel display name')
336
+ .requiredOption('--enabled <true|false>', 'Whether the channel should be enabled')
337
+ .option('--group <name>', 'WeChat group name; repeat for multiple groups', collect, [])
338
+ .option('--can-reply <true|false>', 'Whether reply should be allowed')
339
+ .option('--poll-interval-ms <n>', 'Polling interval in milliseconds')
340
+ .option('--recent-limit <n>', 'Recent message OCR debug limit')
341
+ .option('--idle-seconds <n>', 'Minimum user idle seconds before foreground automation')
342
+ .option('--force-foreground <true|false>', 'Force WeChat foreground while syncing')
343
+ .option('--restore-previous <true|false>', 'Restore previous foreground app after syncing')
344
+ .option('--download-attachments <true|false>', 'Click and localize inbound attachment candidates')
345
+ .option('--download-attachments-dir <path>', 'Directory for localized inbound WeChat attachments')
346
+ .option('--flow-script-path <path>', 'Override macOS flow script path')
347
+ .option('--cloud-ocr-url <url>', 'Shennian server OCR fallback URL')
348
+ .option('--cloud-ocr-token <token>', 'Shennian user/channel token for OCR fallback')
349
+ .option('--cloud-ocr-mode <off|fallback|always>', 'Cloud OCR mode')
350
+ .option('--json', 'Print JSON')
351
+ .action(async (opts) => {
352
+ const result = await ipc('/wechat-rpa/channel/upsert', {
353
+ id: opts.id,
354
+ name: opts.name,
355
+ enabled: opts.enabled === 'true',
356
+ groups: opts.group.map((name) => ({ name })),
357
+ canReply: parseBool(opts.canReply),
358
+ pollIntervalMs: parseNumber(opts.pollIntervalMs),
359
+ recentLimit: parseNumber(opts.recentLimit),
360
+ idleSeconds: parseNumber(opts.idleSeconds),
361
+ forceForeground: parseBool(opts.forceForeground),
362
+ noRestore: opts.restorePrevious === undefined ? undefined : opts.restorePrevious !== 'true',
363
+ downloadAttachments: parseBool(opts.downloadAttachments),
364
+ downloadAttachmentsDir: opts.downloadAttachmentsDir,
365
+ flowScriptPath: opts.flowScriptPath,
366
+ cloudOcrUrl: opts.cloudOcrUrl,
367
+ cloudOcrToken: opts.cloudOcrToken,
368
+ cloudOcrMode: opts.cloudOcrMode,
369
+ });
370
+ if (opts.json)
371
+ printJson(result);
372
+ else
373
+ printJson(result.channel ?? null);
374
+ });
266
375
  }
@@ -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- \u6BCF\u6B21\u6536\u5230\u65B0\u4EFB\u52A1\u3001\u8865\u5145\u8981\u6C42\u3001\u7EA0\u504F\u6216\u5916\u90E8\u6D88\u606F\u65F6\uFF0C\u5148\u7528 sessions list \u67E5\u770B\u5F53\u524D\u540C\u9879\u76EE worker\uFF1B\u5982\u679C\u53EF\u80FD\u76F8\u5173\uFF0C\u518D\u7528 sessions read \u8BFB\u53D6\u5FC5\u8981\u6458\u8981\u540E\u5224\u65AD\u662F\u5426\u590D\u7528\u3002\n- \u5982\u679C\u5DF2\u6709 worker \u6B63\u5728\u5904\u7406\u540C\u4E00\u76EE\u6807\u3001\u540C\u4E00\u529F\u80FD\u533A\u3001\u540C\u4E00\u6587\u4EF6\u8303\u56F4\u6216\u540C\u4E00\u95EE\u9898\u94FE\u8DEF\uFF0C\u4F18\u5148\u7528 sessions send \u628A\u65B0\u8981\u6C42\u53D1\u7ED9\u8FD9\u4E2A worker\uFF1B\u5373\u4F7F worker \u6B63\u5FD9\u4E5F\u53EF\u4EE5\u53D1\u9001\uFF0C\u9ED8\u8BA4\u4F1A\u8FDB\u5165\u672C\u673A\u961F\u5217\uFF0C\u4E0D\u8981\u56E0\u4E3A\u5B83\u5FD9\u5C31\u65B0\u5EFA worker\u3002\n- \u53EA\u6709\u6CA1\u6709\u76F8\u5173 worker\u3001\u73B0\u6709 worker \u5DF2\u660E\u663E\u4E0D\u9002\u5408\u7EE7\u7EED\u63A8\u8FDB\u3001\u6216\u4EFB\u52A1\u9700\u8981\u5E76\u884C\u62C6\u5206\u7ED9\u4E0D\u540C\u4E13\u957F\u65F6\uFF0C\u624D\u521B\u5EFA\u65B0\u7684 worker\u3002\n- \u521B\u5EFA\u6216\u6307\u6D3E 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\u6587\u5B57\u4E00\u5F8B\u8C03\u7528 shennian manager external send --text \"<\u6D88\u606F\u5185\u5BB9>\"\n- \u5411\u5916\u90E8\u7FA4\u53D1\u56FE\u7247\u8C03\u7528 shennian manager external send-image --path \"<\u56FE\u7247\u7EDD\u5BF9\u8DEF\u5F84>\" --caption \"<\u53EF\u9009\u8BF4\u660E>\"\n- \u5411\u5916\u90E8\u7FA4\u53D1\u89C6\u9891\u8C03\u7528 shennian manager external send-video --path \"<\u89C6\u9891\u7EDD\u5BF9\u8DEF\u5F84>\" --caption \"<\u53EF\u9009\u8BF4\u660E>\"\n- \u5411\u5916\u90E8\u7FA4\u53D1\u6587\u4EF6\u8C03\u7528 shennian manager external send-file --path \"<\u6587\u4EF6\u7EDD\u5BF9\u8DEF\u5F84>\" --caption \"<\u53EF\u9009\u8BF4\u660E>\"\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|claude|gemini|cursor|opencode|pi|custom:name> --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>\uFF08\u7EC8\u6B62\u6B63\u5728\u8FD0\u884C\u7684 worker\uFF1B\u4E5F\u53EF\u7528 terminate/kill \u522B\u540D\uFF09\n- shennian manager sessions read --session-id <id> --limit 200 --json\n- shennian manager memory path\n- shennian manager external send --text <text>\n- shennian manager external send-image --path <path> --caption <text>\n- shennian manager external send-video --path <path> --caption <text>\n- shennian manager external send-file --path <path> --caption <text>\n\n\u53EF\u521B\u5EFA\u7684 worker Agent \u5305\u62EC Codex\u3001Claude Code\u3001Gemini\u3001Cursor\u3001opencode\u3001Nian\uFF0C\u4EE5\u53CA\u672C\u673A custom agent\uFF08custom:<name>\uFF09\u3002\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- \u6BCF\u6B21\u6536\u5230\u65B0\u4EFB\u52A1\u3001\u8865\u5145\u8981\u6C42\u3001\u7EA0\u504F\u6216\u5916\u90E8\u6D88\u606F\u65F6\uFF0C\u5148\u7528 sessions list \u67E5\u770B\u5F53\u524D\u540C\u9879\u76EE worker\uFF1B\u5982\u679C\u53EF\u80FD\u76F8\u5173\uFF0C\u518D\u7528 sessions read \u8BFB\u53D6\u5FC5\u8981\u6458\u8981\u540E\u5224\u65AD\u662F\u5426\u590D\u7528\u3002\n- \u5982\u679C\u5DF2\u6709 worker \u6B63\u5728\u5904\u7406\u540C\u4E00\u76EE\u6807\u3001\u540C\u4E00\u529F\u80FD\u533A\u3001\u540C\u4E00\u6587\u4EF6\u8303\u56F4\u6216\u540C\u4E00\u95EE\u9898\u94FE\u8DEF\uFF0C\u4F18\u5148\u7528 sessions send \u628A\u65B0\u8981\u6C42\u53D1\u7ED9\u8FD9\u4E2A worker\uFF1B\u5373\u4F7F worker \u6B63\u5FD9\u4E5F\u53EF\u4EE5\u53D1\u9001\uFF0C\u9ED8\u8BA4\u4F1A\u8FDB\u5165\u672C\u673A\u961F\u5217\uFF0C\u4E0D\u8981\u56E0\u4E3A\u5B83\u5FD9\u5C31\u65B0\u5EFA worker\u3002\n- \u53EA\u6709\u6CA1\u6709\u76F8\u5173 worker\u3001\u73B0\u6709 worker \u5DF2\u660E\u663E\u4E0D\u9002\u5408\u7EE7\u7EED\u63A8\u8FDB\u3001\u6216\u4EFB\u52A1\u9700\u8981\u5E76\u884C\u62C6\u5206\u7ED9\u4E0D\u540C\u4E13\u957F\u65F6\uFF0C\u624D\u521B\u5EFA\u65B0\u7684 worker\u3002\n- \u521B\u5EFA\u6216\u6307\u6D3E 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- \u5916\u90E8\u6D88\u606F\u662F\u5426\u9700\u8981\u56DE\u590D\u3001\u8FFD\u95EE\u3001\u5FFD\u7565\u3001\u8F6C\u4EA4\u5185\u90E8\u8D1F\u8D23\u4EBA\uFF0C\u6216\u521B\u5EFA/\u6307\u6D3E worker\uFF0C\u7531\u4F60\u6839\u636E\u9879\u76EE\u4E0A\u4E0B\u6587\u663E\u5F0F\u5224\u65AD\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- \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|claude|gemini|cursor|opencode|pi|custom:name> --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>\uFF08\u7EC8\u6B62\u6B63\u5728\u8FD0\u884C\u7684 worker\uFF1B\u4E5F\u53EF\u7528 terminate/kill \u522B\u540D\uFF09\n- shennian manager sessions read --session-id <id> --limit 200 --json\n- shennian manager memory path\n\n\u53EF\u521B\u5EFA\u7684 worker Agent \u5305\u62EC Codex\u3001Claude Code\u3001Gemini\u3001Cursor\u3001opencode\u3001Nian\uFF0C\u4EE5\u53CA\u672C\u673A custom agent\uFF08custom:<name>\uFF09\u3002\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;
@@ -24,14 +24,8 @@ export const MANAGER_SYSTEM_PROMPT = `你是项目经理,是当前项目的管
24
24
  - 不要无限循环;没有明确下一步时询问用户或结束当前 turn 等待系统事件。
25
25
  - 不要自己设置定时唤醒;神念会在用户消息、worker 终态或 worker 长运行健康摘要到来时唤醒你。
26
26
  - 外部消息通道事件会像普通用户消息一样送达,格式类似“外部消息 / 发送人”后跟消息内容,可能是合并消息,也可能包含图片、视频或文件 URL。
27
+ - 外部消息是否需要回复、追问、忽略、转交内部负责人,或创建/指派 worker,由你根据项目上下文显式判断。
27
28
  - 对外你是当前项目的项目经理,不要自称神念、Manager Agent 或 worker,也不要解释内部调度机制;只在需要时用“我这边/我们这边”沟通。
28
- - 对外回复必须像真人聊天:短回复一条发完;内容较多时按自然段拆成 2-4 条连续消息,每条只讲一个完整主题。
29
- - 避免把超过 300-500 字的内容塞进单条消息;不要使用 Markdown、编号列表、项目符号或字面 \\n。
30
- - 外部消息与当前项目无关时可以忽略;需要较长处理时,先简短回复“收到,我先处理/安排一下”,再创建或指派 worker。
31
- - 向外部群发文字一律调用 shennian manager external send --text "<消息内容>"
32
- - 向外部群发图片调用 shennian manager external send-image --path "<图片绝对路径>" --caption "<可选说明>"
33
- - 向外部群发视频调用 shennian manager external send-video --path "<视频绝对路径>" --caption "<可选说明>"
34
- - 向外部群发文件调用 shennian manager external send-file --path "<文件绝对路径>" --caption "<可选说明>"
35
29
  - 不要把所有细节塞进对话上下文;需要长期保存的信息写到项目 .shennian/ 下。
36
30
 
37
31
  需要管理 worker 或外部通道时,使用本地命令:
@@ -45,10 +39,6 @@ export const MANAGER_SYSTEM_PROMPT = `你是项目经理,是当前项目的管
45
39
  - shennian manager sessions stop --session-id <id>(终止正在运行的 worker;也可用 terminate/kill 别名)
46
40
  - shennian manager sessions read --session-id <id> --limit 200 --json
47
41
  - shennian manager memory path
48
- - shennian manager external send --text <text>
49
- - shennian manager external send-image --path <path> --caption <text>
50
- - shennian manager external send-video --path <path> --caption <text>
51
- - shennian manager external send-file --path <path> --caption <text>
52
42
 
53
43
  可创建的 worker Agent 包括 Codex、Claude Code、Gemini、Cursor、opencode、Nian,以及本机 custom agent(custom:<name>)。默认用 sessions send 排队发送 worker 消息:worker 正忙时消息会在本机 daemon 队列里等待,worker 空闲时自动执行。队列里的未执行消息可以 list/edit/delete;已经开始执行的消息不能编辑或删除,只能 stop 后重新发送。只有明确需要打断顺序时才使用 --direct。
54
44
 
@@ -1,5 +1,5 @@
1
1
  import type { AgentEvent, AgentAdapter } from '../agents/adapter.js';
2
- import { type AgentType, type ReqFrame } from '@shennian/wire';
2
+ import { type AgentType, type ExternalChannelSessionStatus, type ReqFrame } from '@shennian/wire';
3
3
  import { ManagerRegistry } from './registry.js';
4
4
  import type { SessionManagerRuntime } from '../session/types.js';
5
5
  import { ChannelRuntime } from '../channels/runtime.js';
@@ -59,15 +59,7 @@ export declare class ManagerRuntimeService {
59
59
  private interruptAndResumeManager;
60
60
  bindManagerAdapterEvents(sessionId: string, adapter: AgentAdapter): void;
61
61
  updateManagerStatus(sessionId: string, status: 'idle' | 'running' | 'interrupting'): void;
62
- getExternalChannelStatus(managerSessionId: string): {
63
- configured: boolean;
64
- connected: boolean;
65
- type?: string;
66
- channelId?: string;
67
- name?: string;
68
- canReply?: boolean;
69
- systemPrompt?: string;
70
- } | null;
62
+ getExternalChannelStatus(managerSessionId: string): ExternalChannelSessionStatus | null;
71
63
  getManagerExternalChannelSystemPrompt(managerSessionId: string): string;
72
64
  }
73
65
  export {};
@@ -11,6 +11,7 @@ import { readMessages } from '../session/store.js';
11
11
  import { ChannelRuntime } from '../channels/runtime.js';
12
12
  import { splitExternalReplyText } from '../channels/reply-split.js';
13
13
  import { resolveShennianPath } from '../config/index.js';
14
+ import { buildExternalChannelInstructions } from '../agents/external-channel-instructions.js';
14
15
  let singleton = null;
15
16
  export function setManagerRuntimeService(service) {
16
17
  singleton = service;
@@ -67,12 +68,24 @@ function parseExternalReplyAttachment(value) {
67
68
  const name = String(record.name || '');
68
69
  const mimeType = String(record.mimeType || '');
69
70
  const dataBase64 = String(record.dataBase64 || '');
71
+ const localPath = String(record.localPath || '');
72
+ const url = String(record.url || '');
70
73
  const size = Number(record.size || 0);
71
74
  if (kind !== 'image' && kind !== 'video' && kind !== 'file')
72
75
  return undefined;
73
- if (!name || !mimeType || !dataBase64 || !Number.isFinite(size) || size <= 0)
76
+ if (!name || !mimeType || !Number.isFinite(size) || size < 0)
74
77
  return undefined;
75
- return { kind, name, mimeType, dataBase64, size };
78
+ if (!dataBase64 && !localPath && !url)
79
+ return undefined;
80
+ return {
81
+ kind,
82
+ name,
83
+ mimeType,
84
+ size,
85
+ ...(dataBase64 ? { dataBase64 } : {}),
86
+ ...(localPath ? { localPath } : {}),
87
+ ...(url ? { url } : {}),
88
+ };
76
89
  }
77
90
  function compactWorkerTranscript(rawMessages, limit) {
78
91
  const chronological = [...rawMessages].sort((a, b) => a.ts - b.ts);
@@ -221,6 +234,7 @@ export class ManagerRuntimeService {
221
234
  fs.writeFileSync(filePath, JSON.stringify({
222
235
  url: this.ipcUrl,
223
236
  token: this.ipcToken,
237
+ pid: process.pid,
224
238
  updatedAt: new Date().toISOString(),
225
239
  }, null, 2), { mode: 0o600 });
226
240
  fs.chmodSync(filePath, 0o600);
@@ -536,45 +550,52 @@ export class ManagerRuntimeService {
536
550
  const text = String(body.text || '');
537
551
  const attachment = parseExternalReplyAttachment(body.attachment);
538
552
  const idempotencyKey = String(body.idempotencyKey || randomUUID());
553
+ const explicitChannelId = String(body.channelId || '');
554
+ const explicitConversationId = String(body.conversationId || '');
555
+ const defaultTarget = !replyTarget && (!explicitChannelId || !explicitConversationId)
556
+ ? await this.channelRuntime.getDefaultReplyTarget(managerSessionId).catch(() => null)
557
+ : null;
558
+ const channelId = replyTarget?.channelId || explicitChannelId || defaultTarget?.channelId || '';
559
+ const conversationId = replyTarget?.conversationId || explicitConversationId || defaultTarget?.conversationId || '';
560
+ if (channelId && this.channelRuntime.getChannelById(channelId)) {
561
+ if (!conversationId)
562
+ throw new Error('No external channel target is available for this Manager');
563
+ const result = await this.channelRuntime.reply({
564
+ managerSessionId,
565
+ channelId,
566
+ conversationId,
567
+ messageId: replyTarget?.messageId ?? undefined,
568
+ text,
569
+ attachment,
570
+ idempotencyKey,
571
+ });
572
+ json(res, result.ok ? 200 : 400, result);
573
+ return;
574
+ }
575
+ let relayResult;
539
576
  try {
540
- const relayResult = await this.sendManagedWeComReply({
577
+ relayResult = await this.sendManagedWeComReply({
541
578
  managerSessionId,
542
579
  text,
543
580
  attachment,
544
581
  idempotencyKey,
545
582
  });
546
- if (relayResult.ok) {
547
- json(res, 200, { ok: true, payload: relayResult.payload });
548
- return;
549
- }
550
- if (!shouldFallbackToLocalChannel(relayResult.error || '')) {
551
- json(res, 400, { ok: false, error: relayResult.error || 'External send failed' });
552
- return;
553
- }
554
583
  }
555
584
  catch (error) {
556
- if (!shouldFallbackToLocalChannel(error instanceof Error ? error.message : String(error))) {
585
+ const message = error instanceof Error ? error.message : String(error);
586
+ if (!shouldFallbackToLocalChannel(message))
557
587
  throw error;
558
- }
588
+ relayResult = { ok: false, error: message };
559
589
  }
560
- const explicitChannelId = String(body.channelId || '');
561
- const explicitConversationId = String(body.conversationId || '');
562
- const defaultTarget = !replyTarget && (!explicitChannelId || !explicitConversationId)
563
- ? await this.channelRuntime.getDefaultReplyTarget(managerSessionId)
564
- : null;
565
- const channelId = replyTarget?.channelId || explicitChannelId || defaultTarget?.channelId || '';
566
- const conversationId = replyTarget?.conversationId || explicitConversationId || defaultTarget?.conversationId || '';
567
- if (!channelId || !conversationId)
568
- throw new Error('No external channel target is available for this Manager');
569
- const result = await this.channelRuntime.reply({
570
- managerSessionId,
571
- channelId,
572
- conversationId,
573
- messageId: replyTarget?.messageId ?? undefined,
574
- text,
575
- idempotencyKey,
576
- });
577
- json(res, result.ok ? 200 : 400, result);
590
+ if (relayResult.ok) {
591
+ json(res, 200, { ok: true, payload: relayResult.payload });
592
+ return;
593
+ }
594
+ if (!shouldFallbackToLocalChannel(relayResult.error || '') || !channelId || !conversationId) {
595
+ json(res, 400, { ok: false, error: relayResult.error || 'External send failed' });
596
+ return;
597
+ }
598
+ json(res, 400, { ok: false, error: `No local external channel is configured for ${channelId}` });
578
599
  return;
579
600
  }
580
601
  if (url.pathname === '/channel/get') {
@@ -607,6 +628,46 @@ export class ManagerRuntimeService {
607
628
  json(res, 200, { ok: true, channel });
608
629
  return;
609
630
  }
631
+ if (url.pathname === '/wechat-rpa/channel/get') {
632
+ json(res, 200, {
633
+ ok: true,
634
+ channel: this.channelRuntime.getManagerChannel(managerSessionId, 'wechat-rpa', { includeSecret: true }),
635
+ });
636
+ return;
637
+ }
638
+ if (url.pathname === '/wechat-rpa/channel/upsert') {
639
+ if (!manager)
640
+ throw new Error('Manager runtime is not registered');
641
+ const channel = await this.channelRuntime.upsertManagerWeChatRpaChannel({
642
+ id: String(body.id || `wechat-rpa:${managerSessionId}`),
643
+ managerSessionId,
644
+ workDir: manager.workDir,
645
+ name: typeof body.name === 'string' ? body.name : undefined,
646
+ enabled: Boolean(body.enabled),
647
+ groups: parseWeChatRpaGroups(body.groups),
648
+ canReply: body.canReply === undefined ? undefined : Boolean(body.canReply),
649
+ systemPrompt: typeof body.systemPrompt === 'string' ? body.systemPrompt : undefined,
650
+ source: parseWeChatRpaSource(body.source),
651
+ pollIntervalMs: optionalNumber(body.pollIntervalMs),
652
+ recentLimit: optionalNumber(body.recentLimit),
653
+ idleSeconds: optionalNumber(body.idleSeconds),
654
+ forceForeground: body.forceForeground === undefined ? undefined : Boolean(body.forceForeground),
655
+ noRestore: body.noRestore === undefined ? undefined : Boolean(body.noRestore),
656
+ downloadAttachments: body.downloadAttachments === undefined ? undefined : Boolean(body.downloadAttachments),
657
+ downloadAttachmentsDir: typeof body.downloadAttachmentsDir === 'string' ? body.downloadAttachmentsDir : undefined,
658
+ flowScriptPath: typeof body.flowScriptPath === 'string' ? body.flowScriptPath : undefined,
659
+ cloudOcrUrl: typeof body.cloudOcrUrl === 'string' ? body.cloudOcrUrl : undefined,
660
+ cloudOcrToken: typeof body.cloudOcrToken === 'string' ? body.cloudOcrToken : undefined,
661
+ cloudOcrMode: parseCloudOcrMode(body.cloudOcrMode),
662
+ });
663
+ this.registry.upsertManager({
664
+ ...manager,
665
+ status: manager.status,
666
+ });
667
+ this.broadcastManagerChannelStatus(managerSessionId);
668
+ json(res, 200, { ok: true, channel });
669
+ return;
670
+ }
610
671
  json(res, 404, { ok: false, error: `Unknown manager IPC path: ${url.pathname}` });
611
672
  }
612
673
  catch (err) {
@@ -687,12 +748,19 @@ ${message || worker.summary || '(无可见摘要)'}
687
748
  handleExternalMessage(managerSessionId, event) {
688
749
  const config = this.channelRuntime.getChannelById(event.channelId)
689
750
  ?? this.channelRuntime.getManagerChannel(managerSessionId, event.channelType);
751
+ const externalChannel = this.channelRuntime.getChannelStatusById(event.channelId)
752
+ ?? this.channelRuntime.getManagerChannelStatus(managerSessionId);
690
753
  const manager = this.registry.getManager(managerSessionId);
691
754
  const agentType = (config?.agentType || (manager ? 'manager' : 'codex'));
692
755
  const workDir = config?.workDir || manager?.workDir || process.cwd();
693
756
  const agentSessionId = config?.agentSessionId ?? manager?.agentSessionId ?? null;
694
757
  const modelId = config?.modelId || manager?.modelId || '';
695
- const visibleMessage = `外部消息 / ${event.sender.name || event.sender.id}\n${event.text}`;
758
+ const attachmentInputs = externalAttachmentsForAgent(event.attachments);
759
+ const attachmentSummary = externalAttachmentSummary(event.attachments, event.text);
760
+ const visibleBody = [event.text, attachmentSummary].filter(Boolean).join('\n');
761
+ const visibleMessage = visibleBody
762
+ ? `外部消息 / ${event.sender.name || event.sender.id}\n${visibleBody}`
763
+ : `外部消息 / ${event.sender.name || event.sender.id}`;
696
764
  this.registry.createReplyTarget({
697
765
  managerSessionId,
698
766
  channelId: event.channelId,
@@ -706,6 +774,8 @@ ${message || worker.summary || '(无可见摘要)'}
706
774
  agentSessionId,
707
775
  modelId,
708
776
  text: visibleMessage,
777
+ attachments: attachmentInputs,
778
+ externalChannel,
709
779
  replyTarget: event.replyTarget,
710
780
  });
711
781
  }
@@ -722,6 +792,8 @@ ${message || worker.summary || '(无可见摘要)'}
722
792
  agentSessionId: input.agentSessionId,
723
793
  modelId: input.modelId,
724
794
  origin: 'external',
795
+ attachments: input.attachments,
796
+ externalChannel: input.externalChannel ?? null,
725
797
  },
726
798
  });
727
799
  }
@@ -793,8 +865,100 @@ ${message || worker.summary || '(无可见摘要)'}
793
865
  }
794
866
  getManagerExternalChannelSystemPrompt(managerSessionId) {
795
867
  return this.channelRuntime
796
- .listManagerChannelSystemPrompts(managerSessionId)
868
+ .listManagerExternalChannels(managerSessionId)
869
+ .map((channel) => buildExternalChannelInstructions(channel, undefined, managerSessionId, 'manager'))
797
870
  .join('\n\n')
798
871
  .trim();
799
872
  }
800
873
  }
874
+ function parseWeChatRpaGroups(value) {
875
+ if (!Array.isArray(value))
876
+ return [];
877
+ return value
878
+ .map((item) => ({ name: String(item?.name || '').trim() }))
879
+ .filter((item) => item.name);
880
+ }
881
+ function parseWeChatRpaSource(value) {
882
+ return value === 'macos-flow' || value === 'macos-probe' || value === 'fixture-jsonl' ? value : undefined;
883
+ }
884
+ function externalAttachmentsForAgent(attachments) {
885
+ const result = attachments
886
+ .map((attachment) => externalAttachmentForAgent(attachment))
887
+ .filter((item) => item != null);
888
+ return result.length ? result : undefined;
889
+ }
890
+ function externalAttachmentSummary(attachments, existingText) {
891
+ const text = existingText || '';
892
+ const lines = attachments
893
+ .filter((attachment) => !externalAttachmentForAgent(attachment))
894
+ .map((attachment) => {
895
+ const label = attachment.name || attachment.type || 'attachment';
896
+ const status = externalAttachmentUnavailableStatus(attachment);
897
+ return `附件:${attachment.type || 'file'} ${label} (${status})`;
898
+ })
899
+ .filter((line) => !text.includes(line));
900
+ return lines.join('\n');
901
+ }
902
+ function externalAttachmentForAgent(attachment) {
903
+ if (attachment.localPath && canUseOriginalLocalAttachment(attachment) && isReadableLocalAttachment(attachment.localPath)) {
904
+ return {
905
+ path: attachment.localPath,
906
+ name: attachment.name || path.basename(attachment.localPath) || 'attachment',
907
+ mimeType: attachment.mimeType || mimeTypeFromExternalAttachment(attachment),
908
+ };
909
+ }
910
+ if (attachment.url && canUseOriginalUrlAttachment(attachment)) {
911
+ return {
912
+ path: attachment.url,
913
+ name: attachment.name || attachment.url.split('/').filter(Boolean).at(-1) || 'attachment',
914
+ mimeType: attachment.mimeType || mimeTypeFromExternalAttachment(attachment),
915
+ };
916
+ }
917
+ if (attachment.thumbnailPath && isReadableLocalAttachment(attachment.thumbnailPath)) {
918
+ return {
919
+ path: attachment.thumbnailPath,
920
+ name: attachment.name ? `${attachment.name}-preview.png` : path.basename(attachment.thumbnailPath) || 'preview.png',
921
+ mimeType: 'image/png',
922
+ };
923
+ }
924
+ return null;
925
+ }
926
+ function externalAttachmentUnavailableStatus(attachment) {
927
+ if (attachment.providerError)
928
+ return attachment.providerError;
929
+ if (attachment.localPath && (!attachment.availability || attachment.availability === 'edge-local'))
930
+ return 'edge-local-unavailable';
931
+ return attachment.availability || 'metadata-only';
932
+ }
933
+ function canUseOriginalLocalAttachment(attachment) {
934
+ return !attachment.providerError
935
+ && (!attachment.availability || attachment.availability === 'edge-local');
936
+ }
937
+ function canUseOriginalUrlAttachment(attachment) {
938
+ return !attachment.providerError
939
+ && (!attachment.availability || attachment.availability === 'server-url');
940
+ }
941
+ function isReadableLocalAttachment(filePath) {
942
+ try {
943
+ return fs.statSync(filePath).isFile();
944
+ }
945
+ catch {
946
+ return false;
947
+ }
948
+ }
949
+ function mimeTypeFromExternalAttachment(attachment) {
950
+ if (attachment.type === 'image')
951
+ return 'image/*';
952
+ if (attachment.type === 'video')
953
+ return 'video/*';
954
+ if (attachment.type === 'audio')
955
+ return 'audio/*';
956
+ return 'application/octet-stream';
957
+ }
958
+ function parseCloudOcrMode(value) {
959
+ return value === 'off' || value === 'fallback' || value === 'always' ? value : undefined;
960
+ }
961
+ function optionalNumber(value) {
962
+ const number = Number(value);
963
+ return Number.isFinite(number) ? number : undefined;
964
+ }
@@ -106,6 +106,13 @@ export class NativeSessionFusionService {
106
106
  for (const filePath of files) {
107
107
  const stat = fs.statSync(filePath);
108
108
  const current = state.files[filePath];
109
+ if (!current) {
110
+ nextFilesState[filePath] = {
111
+ offset: stat.size,
112
+ mtimeMs: stat.mtimeMs,
113
+ };
114
+ continue;
115
+ }
109
116
  const isCodex = isCodexRolloutPath(filePath);
110
117
  const isOpenCode = filePath.endsWith('.opencode-session.json');
111
118
  const isCodexRepairBackfillFile = backfillCodexAppContextWrapper && isCodex && current != null;
@@ -0,0 +1,10 @@
1
+ export type ZipArchiveOptions = {
2
+ maxFiles?: number;
3
+ maxTotalSize?: number;
4
+ };
5
+ export type ZipArchiveResult = {
6
+ outputPath: string;
7
+ fileCount: number;
8
+ totalSize: number;
9
+ };
10
+ export declare function createZipArchive(sourceDir: string, outputPath: string, options?: ZipArchiveOptions): ZipArchiveResult;