shennian 0.2.74 → 0.2.76

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.
@@ -219,6 +219,7 @@ export function registerManagerCommand(program) {
219
219
  .command('send')
220
220
  .description('Send a message to the Manager-bound external channel')
221
221
  .requiredOption('--text <text>', 'Message text')
222
+ .option('--reply-target <id>', 'Daemon-generated reply target; omitted means latest external message for this Manager')
222
223
  .option('--idempotency-key <key>', 'Idempotency key')
223
224
  .action(sendExternal);
224
225
  external
@@ -226,11 +227,13 @@ export function registerManagerCommand(program) {
226
227
  .description('Send an image file to the Manager-bound external channel')
227
228
  .requiredOption('--path <path>', 'Image file path')
228
229
  .option('--caption <text>', 'Optional text to send before the image')
230
+ .option('--reply-target <id>', 'Daemon-generated reply target; omitted means latest external message for this Manager')
229
231
  .option('--idempotency-key <key>', 'Idempotency key')
230
232
  .action(async (opts) => {
231
233
  await sendExternal({
232
234
  text: opts.caption,
233
235
  attachment: readExternalAttachment(opts.path, 'image'),
236
+ replyTarget: opts.replyTarget,
234
237
  idempotencyKey: opts.idempotencyKey,
235
238
  });
236
239
  });
@@ -239,11 +242,13 @@ export function registerManagerCommand(program) {
239
242
  .description('Send a video file to the Manager-bound external channel')
240
243
  .requiredOption('--path <path>', 'Video file path')
241
244
  .option('--caption <text>', 'Optional text to send before the video')
245
+ .option('--reply-target <id>', 'Daemon-generated reply target; omitted means latest external message for this Manager')
242
246
  .option('--idempotency-key <key>', 'Idempotency key')
243
247
  .action(async (opts) => {
244
248
  await sendExternal({
245
249
  text: opts.caption,
246
250
  attachment: readExternalAttachment(opts.path, 'video'),
251
+ replyTarget: opts.replyTarget,
247
252
  idempotencyKey: opts.idempotencyKey,
248
253
  });
249
254
  });
@@ -252,11 +257,13 @@ export function registerManagerCommand(program) {
252
257
  .description('Send a file to the Manager-bound external channel')
253
258
  .requiredOption('--path <path>', 'File path')
254
259
  .option('--caption <text>', 'Optional text to send before the file')
260
+ .option('--reply-target <id>', 'Daemon-generated reply target; omitted means latest external message for this Manager')
255
261
  .option('--idempotency-key <key>', 'Idempotency key')
256
262
  .action(async (opts) => {
257
263
  await sendExternal({
258
264
  text: opts.caption,
259
265
  attachment: readExternalAttachment(opts.path, 'file'),
266
+ replyTarget: opts.replyTarget,
260
267
  idempotencyKey: opts.idempotencyKey,
261
268
  });
262
269
  });
@@ -328,6 +335,17 @@ export function registerManagerCommand(program) {
328
335
  else
329
336
  printWeChatRpaStatus((result.channel ?? null));
330
337
  });
338
+ wechatRpa
339
+ .command('sync')
340
+ .description('Run one immediate WeChat RPA sync and print the updated runtime status')
341
+ .option('--json', 'Print JSON')
342
+ .action(async (opts) => {
343
+ const result = await ipc('/wechat-rpa/channel/sync', {});
344
+ if (opts.json)
345
+ printJson(result);
346
+ else
347
+ printWeChatRpaStatus((result.channel ?? null));
348
+ });
331
349
  wechatRpa
332
350
  .command('upsert')
333
351
  .description('Create or update a local WeChat RPA channel binding')
@@ -12,6 +12,7 @@ import { ChannelRuntime } from '../channels/runtime.js';
12
12
  import { splitExternalReplyText } from '../channels/reply-split.js';
13
13
  import { resolveShennianPath } from '../config/index.js';
14
14
  import { buildExternalChannelInstructions } from '../agents/external-channel-instructions.js';
15
+ const MAX_MANAGER_IPC_BODY_BYTES = Number(process.env.SHENNIAN_MANAGER_IPC_BODY_MAX_BYTES || 2 * 1024 * 1024);
15
16
  let singleton = null;
16
17
  export function setManagerRuntimeService(service) {
17
18
  singleton = service;
@@ -71,18 +72,19 @@ function parseExternalReplyAttachment(value) {
71
72
  const localPath = String(record.localPath || '');
72
73
  const url = String(record.url || '');
73
74
  const size = Number(record.size || 0);
75
+ if (dataBase64)
76
+ throw new Error('Manager IPC external attachments must use localPath or url; dataBase64 is not accepted');
74
77
  if (kind !== 'image' && kind !== 'video' && kind !== 'file')
75
78
  return undefined;
76
79
  if (!name || !mimeType || !Number.isFinite(size) || size < 0)
77
80
  return undefined;
78
- if (!dataBase64 && !localPath && !url)
81
+ if (!localPath && !url)
79
82
  return undefined;
80
83
  return {
81
84
  kind,
82
85
  name,
83
86
  mimeType,
84
87
  size,
85
- ...(dataBase64 ? { dataBase64 } : {}),
86
88
  ...(localPath ? { localPath } : {}),
87
89
  ...(url ? { url } : {}),
88
90
  };
@@ -147,8 +149,15 @@ function compactWorkerTranscript(rawMessages, limit) {
147
149
  }
148
150
  async function readJson(req) {
149
151
  const chunks = [];
150
- for await (const chunk of req)
151
- chunks.push(Buffer.from(chunk));
152
+ let total = 0;
153
+ for await (const chunk of req) {
154
+ const buffer = Buffer.from(chunk);
155
+ total += buffer.byteLength;
156
+ if (Number.isFinite(MAX_MANAGER_IPC_BODY_BYTES) && MAX_MANAGER_IPC_BODY_BYTES > 0 && total > MAX_MANAGER_IPC_BODY_BYTES) {
157
+ throw new Error(`Manager IPC request body is too large. Max: ${MAX_MANAGER_IPC_BODY_BYTES} bytes.`);
158
+ }
159
+ chunks.push(buffer);
160
+ }
152
161
  const raw = Buffer.concat(chunks).toString('utf-8');
153
162
  return raw ? JSON.parse(raw) : {};
154
163
  }
@@ -668,6 +677,12 @@ export class ManagerRuntimeService {
668
677
  json(res, 200, { ok: true, channel });
669
678
  return;
670
679
  }
680
+ if (url.pathname === '/wechat-rpa/channel/sync') {
681
+ const { channel, messages } = await this.channelRuntime.syncManagerWeChatRpaChannel(managerSessionId);
682
+ this.broadcastManagerChannelStatus(managerSessionId);
683
+ json(res, 200, { ok: true, channel, messages });
684
+ return;
685
+ }
671
686
  json(res, 404, { ok: false, error: `Unknown manager IPC path: ${url.pathname}` });
672
687
  }
673
688
  catch (err) {
@@ -757,7 +772,8 @@ ${message || worker.summary || '(无可见摘要)'}
757
772
  const modelId = config?.modelId || manager?.modelId || '';
758
773
  const attachmentInputs = externalAttachmentsForAgent(event.attachments);
759
774
  const attachmentSummary = externalAttachmentSummary(event.attachments, event.text);
760
- const visibleBody = [event.text, attachmentSummary].filter(Boolean).join('\n');
775
+ const visibleReplyTarget = event.replyTarget ? `回复目标:${event.replyTarget}` : '';
776
+ const visibleBody = [event.text, attachmentSummary, visibleReplyTarget].filter(Boolean).join('\n');
761
777
  const visibleMessage = visibleBody
762
778
  ? `外部消息 / ${event.sender.name || event.sender.id}\n${visibleBody}`
763
779
  : `外部消息 / ${event.sender.name || event.sender.id}`;
@@ -794,6 +810,7 @@ ${message || worker.summary || '(无可见摘要)'}
794
810
  origin: 'external',
795
811
  attachments: input.attachments,
796
812
  externalChannel: input.externalChannel ?? null,
813
+ replyTarget: input.replyTarget,
797
814
  },
798
815
  });
799
816
  }
@@ -879,7 +896,7 @@ function parseWeChatRpaGroups(value) {
879
896
  .filter((item) => item.name);
880
897
  }
881
898
  function parseWeChatRpaSource(value) {
882
- return value === 'macos-flow' || value === 'macos-probe' || value === 'fixture-jsonl' ? value : undefined;
899
+ return value === 'macos-flow' || value === 'macos-probe' || value === 'windows-visual-flow' || value === 'fixture-jsonl' ? value : undefined;
883
900
  }
884
901
  function externalAttachmentsForAgent(attachments) {
885
902
  const result = attachments
@@ -142,7 +142,7 @@ function externalChannelEnabled(channel) {
142
142
  function managedProviderEnv(agentType) {
143
143
  return buildManagedAgentEnv(agentType);
144
144
  }
145
- function externalChannelEnv(sessionId, channel) {
145
+ function externalChannelEnv(sessionId, channel, replyTarget) {
146
146
  if (!externalChannelEnabled(channel))
147
147
  return {};
148
148
  const service = getManagerRuntimeService();
@@ -151,15 +151,16 @@ function externalChannelEnv(sessionId, channel) {
151
151
  ...injected,
152
152
  SHENNIAN_EXTERNAL_SESSION_ID: sessionId,
153
153
  SHENNIAN_MANAGER_SESSION_ID: sessionId,
154
+ ...(replyTarget?.trim() ? { SHENNIAN_EXTERNAL_REPLY_TARGET: replyTarget.trim() } : {}),
154
155
  };
155
156
  }
156
- function configureAdapterForSession(adapter, sessionId, agentType, channel) {
157
+ function configureAdapterForSession(adapter, sessionId, agentType, channel, replyTarget) {
157
158
  adapter.configure?.({
158
159
  sessionId,
159
160
  externalChannel: channel ?? null,
160
161
  env: {
161
162
  ...managedProviderEnv(agentType),
162
- ...externalChannelEnv(sessionId, channel),
163
+ ...externalChannelEnv(sessionId, channel, replyTarget),
163
164
  },
164
165
  });
165
166
  }
@@ -357,12 +358,12 @@ async function disposeSession(session) {
357
358
  session.adapter.removeAllListeners();
358
359
  await session.adapter.stop().catch(() => { });
359
360
  }
360
- async function createActiveSession(runtime, sessionId, agentType, resolvedWorkDir, incomingAgentSid, externalChannel) {
361
+ async function createActiveSession(runtime, sessionId, agentType, resolvedWorkDir, incomingAgentSid, externalChannel, externalReplyTarget) {
361
362
  runtime.evictIdleSessions();
362
363
  const adapter = createAgent(agentType);
363
364
  if (!adapter)
364
365
  throw new Error(`Unsupported agent: ${agentType}`);
365
- configureAdapterForSession(adapter, sessionId, agentType, externalChannel);
366
+ configureAdapterForSession(adapter, sessionId, agentType, externalChannel, externalReplyTarget);
366
367
  await adapter.start(sessionId, resolvedWorkDir, incomingAgentSid);
367
368
  const session = {
368
369
  adapter,
@@ -374,9 +375,10 @@ async function createActiveSession(runtime, sessionId, agentType, resolvedWorkDi
374
375
  nextEventSeq: 0,
375
376
  pendingTextEvent: null,
376
377
  externalChannel: externalChannel ?? null,
378
+ externalReplyTarget: externalReplyTarget ?? null,
377
379
  externalChannelEnv: {
378
380
  ...managedProviderEnv(agentType),
379
- ...externalChannelEnv(sessionId, externalChannel),
381
+ ...externalChannelEnv(sessionId, externalChannel, externalReplyTarget),
380
382
  },
381
383
  };
382
384
  runtime.sessions.set(sessionId, session);
@@ -412,6 +414,9 @@ export async function handleChatSend(runtime, req) {
412
414
  const replyId = responseId || req.id;
413
415
  mergeProjectedSessions(sessionListProjection);
414
416
  const incomingExternalChannel = normalizeExternalChannel(req.params.externalChannel);
417
+ const incomingReplyTarget = typeof req.params.replyTarget === 'string'
418
+ ? req.params.replyTarget.trim()
419
+ : '';
415
420
  if (!sessionId || !text) {
416
421
  runtime.processedReqIds.delete(req.id);
417
422
  runtime.client.sendRes({ type: 'res', id: replyId, ok: false, error: 'sessionId and text are required' });
@@ -467,7 +472,7 @@ export async function handleChatSend(runtime, req) {
467
472
  }
468
473
  if (!session) {
469
474
  try {
470
- session = await createActiveSession(runtime, sessionId, requestedAgentType, resolvedWorkDir, incomingAgentSid, incomingExternalChannel);
475
+ session = await createActiveSession(runtime, sessionId, requestedAgentType, resolvedWorkDir, incomingAgentSid, incomingExternalChannel, incomingReplyTarget);
471
476
  }
472
477
  catch (err) {
473
478
  const message = err instanceof Error && err.message.startsWith('Unsupported agent:')
@@ -63,6 +63,7 @@ function queueMessageFromParams(params) {
63
63
  clientMessageId: params.clientMessageId ?? null,
64
64
  attachments: normalizeAttachments(params.attachments),
65
65
  externalChannel: params.externalChannel ?? null,
66
+ replyTarget: params.replyTarget ?? null,
66
67
  origin: params.origin,
67
68
  createdAt: timestamp,
68
69
  updatedAt: timestamp,
@@ -301,6 +302,7 @@ export class ChatQueueManager {
301
302
  clientMessageId: message.clientMessageId ?? message.id,
302
303
  attachments: message.attachments,
303
304
  externalChannel: message.externalChannel,
305
+ replyTarget: message.replyTarget,
304
306
  waitForDispatch: true,
305
307
  },
306
308
  });
@@ -20,6 +20,7 @@ export type ActiveSession = {
20
20
  } | null;
21
21
  externalChannel?: ExternalChannelSessionStatus | null;
22
22
  externalChannelEnv?: NodeJS.ProcessEnv;
23
+ externalReplyTarget?: string | null;
23
24
  };
24
25
  export type PendingTransfer = {
25
26
  tempPath: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.2.74",
3
+ "version": "0.2.76",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,8 +34,8 @@
34
34
  "node": ">=18"
35
35
  },
36
36
  "scripts": {
37
- "build": "tsc && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
38
- "build:publish": "node -e \"const fs=require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('.tsbuildinfo.publish', { force: true })\" && tsc -p tsconfig.publish.json && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
37
+ "build": "tsc && node scripts/copy-wechat-rpa-assets.mjs && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
38
+ "build:publish": "node -e \"const fs=require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('.tsbuildinfo.publish', { force: true })\" && tsc -p tsconfig.publish.json && node scripts/copy-wechat-rpa-assets.mjs && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
39
39
  "dev": "tsc --watch",
40
40
  "prepublishOnly": "pnpm build:publish"
41
41
  },