shennian 0.2.87 → 0.2.89
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.
- package/dist/assets/wechat-channel/macos/manifest.json +13 -0
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/src/agents/adapter.d.ts +6 -0
- package/dist/src/agents/codex-control.d.ts +35 -0
- package/dist/src/agents/codex-control.js +188 -0
- package/dist/src/agents/codex-utils.d.ts +5 -0
- package/dist/src/agents/codex-utils.js +5 -0
- package/dist/src/agents/codex.d.ts +8 -0
- package/dist/src/agents/codex.js +55 -2
- package/dist/src/agents/model-registry/discovery.js +2 -1
- package/dist/src/channels/base.d.ts +4 -13
- package/dist/src/channels/runtime.d.ts +1 -3
- package/dist/src/channels/runtime.js +32 -5
- package/dist/src/channels/secret-registry.d.ts +1 -4
- package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
- package/dist/src/channels/wechat-channel/anchor.js +65 -0
- package/dist/src/channels/wechat-channel/client.d.ts +74 -0
- package/dist/src/channels/wechat-channel/client.js +96 -0
- package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
- package/dist/src/channels/wechat-channel/cooldown.js +38 -0
- package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
- package/dist/src/channels/wechat-channel/fingerprint.js +71 -0
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +28 -0
- package/dist/src/channels/wechat-channel/helper-assets.js +68 -0
- package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
- package/dist/src/channels/wechat-channel/helper-client.js +149 -0
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
- package/dist/src/channels/wechat-channel/helper-protocol.js +115 -0
- package/dist/src/channels/wechat-channel/index.d.ts +16 -0
- package/dist/src/channels/wechat-channel/index.js +19 -0
- package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
- package/dist/src/channels/wechat-channel/ledger.js +54 -0
- package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
- package/dist/src/channels/wechat-channel/media-resolver.js +181 -0
- package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
- package/dist/src/channels/wechat-channel/message-key.js +105 -0
- package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
- package/dist/src/channels/wechat-channel/observer.js +118 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +66 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +112 -0
- package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
- package/dist/src/channels/wechat-channel/preflight.js +48 -0
- package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
- package/dist/src/channels/wechat-channel/runner.js +84 -0
- package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
- package/dist/src/channels/wechat-channel/runtime.js +66 -0
- package/dist/src/channels/wechat-channel/scheduler.d.ts +30 -0
- package/dist/src/channels/wechat-channel/scheduler.js +152 -0
- package/dist/src/channels/wechat-rpa/macos-flow.d.ts +0 -28
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -134
- package/dist/src/channels/wechat-rpa.d.ts +21 -0
- package/dist/src/channels/wechat-rpa.js +39 -61
- package/dist/src/commands/manager.d.ts +1 -1
- package/dist/src/commands/manager.js +5 -10
- package/dist/src/fs/text-decoder.d.ts +10 -0
- package/dist/src/fs/text-decoder.js +110 -0
- package/dist/src/manager/runtime.js +4 -6
- package/dist/src/native-fusion/service.d.ts +10 -0
- package/dist/src/native-fusion/service.js +27 -0
- package/dist/src/session/handlers/chat.js +18 -2
- package/dist/src/session/handlers/fs.js +39 -3
- package/dist/src/session/handlers/session-refresh.js +12 -0
- package/dist/src/session/handlers/tool-detail.d.ts +3 -0
- package/dist/src/session/handlers/tool-detail.js +218 -0
- package/dist/src/session/manager.d.ts +3 -0
- package/dist/src/session/manager.js +58 -0
- package/dist/src/session/types.d.ts +4 -0
- package/package.json +2 -2
- package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
- package/dist/scripts/wechat-rpa-win-visual.mjs +0 -1735
- package/dist/scripts/wechat-rpa-win.mjs +0 -352
- package/dist/src/channels/wechat-rpa/windows-visual-flow.d.ts +0 -40
- package/dist/src/channels/wechat-rpa/windows-visual-flow.js +0 -189
|
@@ -6,9 +6,9 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
8
|
import { ChannelSecretRegistry } from './secret-registry.js';
|
|
9
|
+
import { resolveShennianPath } from '../config/index.js';
|
|
9
10
|
import { probeMacWeChat, observedMessageFromProbe } from './wechat-rpa/macos.js';
|
|
10
11
|
import { runMacWeChatRpaFlow } from './wechat-rpa/macos-flow.js';
|
|
11
|
-
import { runWindowsWeChatRpaVisualFlow } from './wechat-rpa/windows-visual-flow.js';
|
|
12
12
|
import { normalizeWeChatRpaMessage, WeChatRpaDeduper, weChatRpaConversationId, } from './wechat-rpa/normalizer.js';
|
|
13
13
|
const DEFAULT_POLL_INTERVAL_MS = 5_000;
|
|
14
14
|
const DEFAULT_RECENT_LIMIT = 5;
|
|
@@ -159,7 +159,6 @@ export class WeChatRpaChannelAdapter {
|
|
|
159
159
|
noteManualReview(conn, pendingKey, reason);
|
|
160
160
|
return { status: 'manual-review', reason };
|
|
161
161
|
}
|
|
162
|
-
recordCloudOcrRuntime(conn, flow);
|
|
163
162
|
addTaskSummary(conn, {
|
|
164
163
|
status: 'sent',
|
|
165
164
|
runId: conn.lastRunId ?? null,
|
|
@@ -225,11 +224,6 @@ export class WeChatRpaChannelAdapter {
|
|
|
225
224
|
wechatRpaLastTracePath: conn.lastTracePath ?? null,
|
|
226
225
|
wechatRpaLastTraceSummary: conn.lastTraceSummary ?? null,
|
|
227
226
|
wechatRpaRecentTaskSummaries: conn.recentTaskSummaries,
|
|
228
|
-
wechatRpaLastCloudOcrAt: conn.lastCloudOcrAt ?? null,
|
|
229
|
-
wechatRpaLastCloudOcrPurpose: conn.lastCloudOcrPurpose ?? null,
|
|
230
|
-
wechatRpaLastCloudOcrRequestId: conn.lastCloudOcrRequestId ?? null,
|
|
231
|
-
wechatRpaLastCloudOcrImageHash: conn.lastCloudOcrImageHash ?? null,
|
|
232
|
-
wechatRpaLastCloudOcrUsage: conn.lastCloudOcrUsage ?? null,
|
|
233
227
|
};
|
|
234
228
|
}
|
|
235
229
|
ensureConnection(config) {
|
|
@@ -284,7 +278,6 @@ export class WeChatRpaChannelAdapter {
|
|
|
284
278
|
conn.runtimeState = 'syncing';
|
|
285
279
|
const observed = await this.readObservedMessages(conn.config, secret, (flow) => {
|
|
286
280
|
recordTraceRuntime(conn, flow);
|
|
287
|
-
recordCloudOcrRuntime(conn, flow);
|
|
288
281
|
if (!flow.interrupted)
|
|
289
282
|
return;
|
|
290
283
|
interrupted = true;
|
|
@@ -345,7 +338,6 @@ export class WeChatRpaChannelAdapter {
|
|
|
345
338
|
persistPendingReplyState(conn);
|
|
346
339
|
const flow = await runSendFlow(conn.config, secret, pending.conversationName, pending.skipText ? '' : pending.text, pending.attachmentPath, pending.key);
|
|
347
340
|
recordTraceRuntime(conn, flow);
|
|
348
|
-
recordCloudOcrRuntime(conn, flow);
|
|
349
341
|
const partial = applyPartialSendProgress(flow, {
|
|
350
342
|
text: pending.skipText ? '' : pending.text,
|
|
351
343
|
attachmentPath: pending.attachmentPath,
|
|
@@ -446,16 +438,14 @@ function isWeChatRpaTextMentioned(text, aliases) {
|
|
|
446
438
|
export function windowsVisualFlowHealth(secret, platform = process.platform) {
|
|
447
439
|
if (platform !== 'win32')
|
|
448
440
|
return { ok: false, message: 'WeChat RPA Windows visual flow requires Windows' };
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
return { ok: true, message: 'WeChat RPA Windows visual flow configured' };
|
|
441
|
+
void secret;
|
|
442
|
+
return { ok: false, message: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it' };
|
|
453
443
|
}
|
|
454
444
|
async function readLabMessages(config, secret, onFlow) {
|
|
455
445
|
const api = await importWeChatRpaLabApi(config.workDir);
|
|
456
446
|
const result = [];
|
|
457
447
|
for (const name of configuredGroups(secret)) {
|
|
458
|
-
const flow = await runLabReadFlow(api, secret, name);
|
|
448
|
+
const flow = await runLabReadFlow(api, config, secret, name);
|
|
459
449
|
onFlow?.(flow);
|
|
460
450
|
if (!flow.ok || flow.interrupted)
|
|
461
451
|
continue;
|
|
@@ -467,7 +457,7 @@ async function readLabMessages(config, secret, onFlow) {
|
|
|
467
457
|
}
|
|
468
458
|
return result;
|
|
469
459
|
}
|
|
470
|
-
async function runLabReadFlow(api, secret, groupName) {
|
|
460
|
+
async function runLabReadFlow(api, config, secret, groupName) {
|
|
471
461
|
const kind = selectWeChatRpaLabReadKind(secret.recentLimit);
|
|
472
462
|
const task = {
|
|
473
463
|
kind,
|
|
@@ -475,7 +465,7 @@ async function runLabReadFlow(api, secret, groupName) {
|
|
|
475
465
|
targetGroup: groupName,
|
|
476
466
|
policy: secret.forceForeground ? 'work' : 'polite',
|
|
477
467
|
limit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 1, 50),
|
|
478
|
-
|
|
468
|
+
attachmentsDir: resolveWeChatRpaInboundAttachmentDir(config, secret),
|
|
479
469
|
};
|
|
480
470
|
const result = await api.runWechatRpaReadLatest(task);
|
|
481
471
|
const structuredMessages = Array.isArray(result.data?.structuredMessages) ? result.data.structuredMessages : [];
|
|
@@ -497,7 +487,6 @@ async function runLabReadFlow(api, secret, groupName) {
|
|
|
497
487
|
recentMessages: flowMessages,
|
|
498
488
|
newMessages: flowMessages,
|
|
499
489
|
screenshotPath: result.tracePath,
|
|
500
|
-
cloudOcrUsage: result.data?.hybrid?.usage,
|
|
501
490
|
error: labResultError(result),
|
|
502
491
|
};
|
|
503
492
|
}
|
|
@@ -629,8 +618,7 @@ async function readMacFlowMessages(config, secret, onFlow) {
|
|
|
629
618
|
noRestore: secret.noRestore !== false,
|
|
630
619
|
idleSeconds: clampNumber(secret.idleSeconds, 15, 0, 3600),
|
|
631
620
|
recentLimit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 0, 50),
|
|
632
|
-
downloadAttachmentsDir:
|
|
633
|
-
cloudOcrMode: 'off',
|
|
621
|
+
downloadAttachmentsDir: resolveWeChatRpaInboundAttachmentDir(config, secret),
|
|
634
622
|
});
|
|
635
623
|
onFlow?.(flow);
|
|
636
624
|
if (flow.interrupted)
|
|
@@ -644,26 +632,18 @@ async function readMacFlowMessages(config, secret, onFlow) {
|
|
|
644
632
|
return result;
|
|
645
633
|
}
|
|
646
634
|
async function readWindowsVisualFlowMessages(config, secret, onFlow) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
continue;
|
|
660
|
-
for (const message of flow.newMessages ?? []) {
|
|
661
|
-
const observed = flowMessageToObservedMessage(name, message, secret);
|
|
662
|
-
if (observed)
|
|
663
|
-
result.push(observed);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
return result;
|
|
635
|
+
void config;
|
|
636
|
+
const groupName = configuredGroups(secret)[0] || '<unbound>';
|
|
637
|
+
onFlow?.({
|
|
638
|
+
ok: false,
|
|
639
|
+
groupName,
|
|
640
|
+
interrupted: true,
|
|
641
|
+
reason: 'windows-visual-flow-archived',
|
|
642
|
+
newMessages: [],
|
|
643
|
+
recentMessages: [],
|
|
644
|
+
error: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it',
|
|
645
|
+
});
|
|
646
|
+
return [];
|
|
667
647
|
}
|
|
668
648
|
function flowMessageToObservedMessage(conversationName, message, secret) {
|
|
669
649
|
const text = String(message.text || '').trim();
|
|
@@ -684,16 +664,19 @@ function flowMessageToObservedMessage(conversationName, message, secret) {
|
|
|
684
664
|
}
|
|
685
665
|
async function runSendFlow(config, secret, conversationName, text, attachmentPath, dedupeKey) {
|
|
686
666
|
if (secret.source === 'windows-visual-flow') {
|
|
687
|
-
|
|
667
|
+
void config;
|
|
668
|
+
void text;
|
|
669
|
+
void attachmentPath;
|
|
670
|
+
void dedupeKey;
|
|
671
|
+
return {
|
|
672
|
+
ok: false,
|
|
688
673
|
groupName: conversationName,
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
cloudOcrMode: 'off',
|
|
696
|
-
});
|
|
674
|
+
interrupted: true,
|
|
675
|
+
reason: 'windows-visual-flow-archived',
|
|
676
|
+
newMessages: [],
|
|
677
|
+
recentMessages: [],
|
|
678
|
+
error: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it',
|
|
679
|
+
};
|
|
697
680
|
}
|
|
698
681
|
if (secret.source === 'wechat-rpa-lab') {
|
|
699
682
|
return runLabSendFlow(config, secret, conversationName, text, attachmentPath, dedupeKey);
|
|
@@ -708,12 +691,11 @@ async function runSendFlow(config, secret, conversationName, text, attachmentPat
|
|
|
708
691
|
noRestore: secret.noRestore !== false,
|
|
709
692
|
idleSeconds: clampNumber(secret.idleSeconds, 15, 0, 3600),
|
|
710
693
|
recentLimit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 0, 50),
|
|
711
|
-
downloadAttachmentsDir:
|
|
712
|
-
cloudOcrMode: 'off',
|
|
694
|
+
downloadAttachmentsDir: resolveWeChatRpaInboundAttachmentDir(config, secret),
|
|
713
695
|
});
|
|
714
696
|
}
|
|
715
697
|
function isFlowSource(source) {
|
|
716
|
-
return source === 'macos-flow' || source === '
|
|
698
|
+
return source === 'macos-flow' || source === 'wechat-rpa-lab';
|
|
717
699
|
}
|
|
718
700
|
function applyPartialSendProgress(flow, input) {
|
|
719
701
|
if (input.text && input.attachmentPath && flow.sentReplyObserved && !flow.sentAttachmentObserved) {
|
|
@@ -883,7 +865,6 @@ function noteInterruption(conn, flow, reason) {
|
|
|
883
865
|
conn.interruptionCooldownUntil = Date.now() + INTERRUPTION_COOLDOWN_MS;
|
|
884
866
|
conn.runtimeState = 'cooldown';
|
|
885
867
|
}
|
|
886
|
-
recordCloudOcrRuntime(conn, flow);
|
|
887
868
|
addTaskSummary(conn, {
|
|
888
869
|
status: 'interrupted',
|
|
889
870
|
runId: flow.rpaRunId || conn.lastRunId || null,
|
|
@@ -1016,10 +997,14 @@ function safeFileName(name) {
|
|
|
1016
997
|
.replace(/^[ ._]+|[ ._]+$/g, '')
|
|
1017
998
|
|| 'attachment';
|
|
1018
999
|
}
|
|
1019
|
-
function
|
|
1000
|
+
export function resolveWeChatRpaInboundAttachmentDir(config, secret) {
|
|
1020
1001
|
if (secret.downloadAttachments === false)
|
|
1021
1002
|
return undefined;
|
|
1022
|
-
|
|
1003
|
+
const explicitDir = secret.downloadAttachmentsDir?.trim();
|
|
1004
|
+
if (explicitDir)
|
|
1005
|
+
return path.resolve(explicitDir);
|
|
1006
|
+
const channelKey = crypto.createHash('sha256').update(config.id).digest('hex').slice(0, 16);
|
|
1007
|
+
return resolveShennianPath('wechat-rpa', 'attachments', 'inbound', channelKey);
|
|
1023
1008
|
}
|
|
1024
1009
|
export function annotateWeChatRpaInboundAttachments(attachments) {
|
|
1025
1010
|
if (!Array.isArray(attachments))
|
|
@@ -1041,10 +1026,3 @@ function clampNumber(value, fallback, min, max) {
|
|
|
1041
1026
|
return fallback;
|
|
1042
1027
|
return Math.min(max, Math.max(min, number));
|
|
1043
1028
|
}
|
|
1044
|
-
function recordCloudOcrRuntime(conn, flow) {
|
|
1045
|
-
void conn;
|
|
1046
|
-
void flow;
|
|
1047
|
-
// WeChat RPA production routing is local OCR only. Legacy cloud OCR payload
|
|
1048
|
-
// fields may still appear in old fixtures, but the channel must not publish
|
|
1049
|
-
// them as runtime status or invite a server OCR dependency back in.
|
|
1050
|
-
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
2
|
export declare function registerManagerCommand(program: Command): void;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// @test src/__tests__/manager-runtime.test.ts
|
|
3
3
|
// @test src/__tests__/manager-command.test.ts
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import { Option } from 'commander';
|
|
6
5
|
import chalk from 'chalk';
|
|
7
6
|
import { readExternalAttachment } from './external-attachments.js';
|
|
8
7
|
function requireManagerContext() {
|
|
@@ -75,9 +74,6 @@ function printWeChatRpaStatus(channel) {
|
|
|
75
74
|
channel.wechatRpaLastRunAt ? `lastRun=${String(channel.wechatRpaLastRunAt)}` : '',
|
|
76
75
|
channel.wechatRpaLastMessageAt ? `lastMessage=${String(channel.wechatRpaLastMessageAt)}` : '',
|
|
77
76
|
channel.wechatRpaLastInterruptedAt ? `lastInterrupted=${String(channel.wechatRpaLastInterruptedAt)}` : '',
|
|
78
|
-
channel.wechatRpaLastCloudOcrAt ? `lastLocalOcr=${String(channel.wechatRpaLastCloudOcrAt)}` : '',
|
|
79
|
-
channel.wechatRpaLastCloudOcrPurpose ? `lastLocalOcrPurpose=${String(channel.wechatRpaLastCloudOcrPurpose)}` : '',
|
|
80
|
-
channel.wechatRpaLastCloudOcrRequestId ? `lastLocalOcrRequestId=${String(channel.wechatRpaLastCloudOcrRequestId)}` : '',
|
|
81
77
|
channel.wechatRpaLastError ? `lastError=${String(channel.wechatRpaLastError)}` : 'lastError=none',
|
|
82
78
|
].filter(Boolean);
|
|
83
79
|
console.log(fields.join('\n'));
|
|
@@ -353,20 +349,18 @@ export function registerManagerCommand(program) {
|
|
|
353
349
|
.option('--id <id>', 'Channel id')
|
|
354
350
|
.option('--name <name>', 'Channel display name')
|
|
355
351
|
.requiredOption('--enabled <true|false>', 'Whether the channel should be enabled')
|
|
356
|
-
.option('--group <name>', 'Bound WeChat
|
|
352
|
+
.option('--group <name>', 'Bound WeChat conversation name; pass once per Shennian conversation', collect, [])
|
|
357
353
|
.option('--can-reply <true|false>', 'Whether reply should be allowed')
|
|
358
|
-
.option('--source <macos-flow|
|
|
354
|
+
.option('--source <macos-flow|wechat-rpa-lab|macos-probe|fixture-jsonl>', 'WeChat RPA implementation source')
|
|
359
355
|
.option('--poll-interval-ms <n>', 'Polling interval in milliseconds')
|
|
360
|
-
.option('--recent-limit <n>', 'Recent message
|
|
356
|
+
.option('--recent-limit <n>', 'Recent message limit')
|
|
361
357
|
.option('--idle-seconds <n>', 'Minimum user idle seconds before foreground automation')
|
|
362
358
|
.option('--force-foreground <true|false>', 'Force WeChat foreground while syncing')
|
|
363
359
|
.option('--restore-previous <true|false>', 'Restore previous foreground app after syncing')
|
|
364
360
|
.option('--download-attachments <true|false>', 'Click and localize inbound attachment candidates')
|
|
365
361
|
.option('--download-attachments-dir <path>', 'Directory for localized inbound WeChat attachments')
|
|
362
|
+
.option('--privacy-consent <true|false>', 'Confirm WeChat channel data and privacy consent')
|
|
366
363
|
.option('--flow-script-path <path>', 'Override macOS flow script path')
|
|
367
|
-
.addOption(new Option('--cloud-ocr-url <url>', 'Deprecated; ignored because WeChat RPA uses local-only OCR').hideHelp())
|
|
368
|
-
.addOption(new Option('--cloud-ocr-token <token>', 'Deprecated; ignored because WeChat RPA uses local-only OCR').hideHelp())
|
|
369
|
-
.addOption(new Option('--cloud-ocr-mode <off|fallback|always>', 'Deprecated; ignored because WeChat RPA uses local-only OCR').hideHelp())
|
|
370
364
|
.option('--json', 'Print JSON')
|
|
371
365
|
.action(async (opts) => {
|
|
372
366
|
if (opts.enabled === 'true' && opts.group.length > 1) {
|
|
@@ -386,6 +380,7 @@ export function registerManagerCommand(program) {
|
|
|
386
380
|
noRestore: opts.restorePrevious === undefined ? undefined : opts.restorePrevious !== 'true',
|
|
387
381
|
downloadAttachments: parseBool(opts.downloadAttachments),
|
|
388
382
|
downloadAttachmentsDir: opts.downloadAttachmentsDir,
|
|
383
|
+
privacyConsentAccepted: parseBool(opts.privacyConsent),
|
|
389
384
|
flowScriptPath: opts.flowScriptPath,
|
|
390
385
|
});
|
|
391
386
|
if (opts.json)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class BinaryTextPreviewError extends Error {
|
|
2
|
+
constructor();
|
|
3
|
+
}
|
|
4
|
+
export type AutoTextDecodeResult = {
|
|
5
|
+
content: string;
|
|
6
|
+
encoding: 'utf8' | 'utf16le' | 'utf16be' | 'gb18030' | 'windows-1252';
|
|
7
|
+
fallback: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function isAutoTextEncoding(value: string): boolean;
|
|
10
|
+
export declare function decodeTextBufferAuto(buffer: Buffer): AutoTextDecodeResult;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/daemon.md#文件系统权限边界
|
|
2
|
+
// @test src/__tests__/session-manager.test.ts
|
|
3
|
+
import { TextDecoder } from 'node:util';
|
|
4
|
+
const AUTO_TEXT_ENCODINGS = new Set(['auto', 'utf8-auto', 'utf-8-auto', 'text-auto']);
|
|
5
|
+
const TEXT_SAMPLE_BYTES = 8192;
|
|
6
|
+
const MAX_SUSPICIOUS_CONTROL_RATIO = 0.01;
|
|
7
|
+
export class BinaryTextPreviewError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super('File appears to be binary; use encoding=base64');
|
|
10
|
+
this.name = 'BinaryTextPreviewError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function isAutoTextEncoding(value) {
|
|
14
|
+
return AUTO_TEXT_ENCODINGS.has(value.trim().toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
function startsWithBytes(buffer, bytes) {
|
|
17
|
+
if (buffer.length < bytes.length)
|
|
18
|
+
return false;
|
|
19
|
+
return bytes.every((byte, index) => buffer[index] === byte);
|
|
20
|
+
}
|
|
21
|
+
function isAllowedTextControlByte(byte) {
|
|
22
|
+
return byte === 0x09 || byte === 0x0a || byte === 0x0c || byte === 0x0d || byte === 0x1b;
|
|
23
|
+
}
|
|
24
|
+
function hasBinaryMagic(buffer) {
|
|
25
|
+
return (startsWithBytes(buffer, [0x25, 0x50, 0x44, 0x46]) || // PDF
|
|
26
|
+
startsWithBytes(buffer, [0x50, 0x4b, 0x03, 0x04]) || // ZIP / Office Open XML
|
|
27
|
+
startsWithBytes(buffer, [0x50, 0x4b, 0x05, 0x06]) ||
|
|
28
|
+
startsWithBytes(buffer, [0x50, 0x4b, 0x07, 0x08]) ||
|
|
29
|
+
startsWithBytes(buffer, [0x89, 0x50, 0x4e, 0x47]) || // PNG
|
|
30
|
+
startsWithBytes(buffer, [0xff, 0xd8, 0xff]) || // JPEG
|
|
31
|
+
startsWithBytes(buffer, [0x47, 0x49, 0x46, 0x38]) || // GIF
|
|
32
|
+
startsWithBytes(buffer, [0x52, 0x49, 0x46, 0x46]) || // RIFF / WebP / WAV / AVI
|
|
33
|
+
startsWithBytes(buffer, [0x1f, 0x8b]) || // gzip
|
|
34
|
+
startsWithBytes(buffer, [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c]) || // 7z
|
|
35
|
+
startsWithBytes(buffer, [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07]) || // RAR
|
|
36
|
+
startsWithBytes(buffer, [0x7f, 0x45, 0x4c, 0x46]) || // ELF
|
|
37
|
+
startsWithBytes(buffer, [0xcf, 0xfa, 0xed, 0xfe]) || // Mach-O
|
|
38
|
+
startsWithBytes(buffer, [0xca, 0xfe, 0xba, 0xbe]) ||
|
|
39
|
+
startsWithBytes(buffer, [0x4d, 0x5a]) || // Windows PE
|
|
40
|
+
startsWithBytes(buffer, [0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66]));
|
|
41
|
+
}
|
|
42
|
+
function looksLikeBinaryBuffer(buffer) {
|
|
43
|
+
if (hasBinaryMagic(buffer))
|
|
44
|
+
return true;
|
|
45
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, TEXT_SAMPLE_BYTES));
|
|
46
|
+
if (sample.length === 0)
|
|
47
|
+
return false;
|
|
48
|
+
let suspiciousControls = 0;
|
|
49
|
+
for (const byte of sample) {
|
|
50
|
+
if (byte === 0x00)
|
|
51
|
+
return true;
|
|
52
|
+
if (byte < 0x20 && !isAllowedTextControlByte(byte))
|
|
53
|
+
suspiciousControls += 1;
|
|
54
|
+
}
|
|
55
|
+
return suspiciousControls / sample.length > MAX_SUSPICIOUS_CONTROL_RATIO;
|
|
56
|
+
}
|
|
57
|
+
function isAllowedTextControlCode(codePoint) {
|
|
58
|
+
return codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0c || codePoint === 0x0d || codePoint === 0x1b;
|
|
59
|
+
}
|
|
60
|
+
function hasDecodedControlNoise(content) {
|
|
61
|
+
if (!content)
|
|
62
|
+
return false;
|
|
63
|
+
let suspiciousControls = 0;
|
|
64
|
+
let total = 0;
|
|
65
|
+
for (const char of content.slice(0, TEXT_SAMPLE_BYTES)) {
|
|
66
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
67
|
+
total += 1;
|
|
68
|
+
if (((codePoint >= 0x00 && codePoint < 0x20) || (codePoint >= 0x7f && codePoint <= 0x9f)) &&
|
|
69
|
+
!isAllowedTextControlCode(codePoint)) {
|
|
70
|
+
suspiciousControls += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return total > 0 && suspiciousControls / total > MAX_SUSPICIOUS_CONTROL_RATIO;
|
|
74
|
+
}
|
|
75
|
+
function decodeStrict(buffer, decoderEncoding, encoding, fallback) {
|
|
76
|
+
const content = new TextDecoder(decoderEncoding, { fatal: true }).decode(buffer);
|
|
77
|
+
if (hasDecodedControlNoise(content))
|
|
78
|
+
throw new BinaryTextPreviewError();
|
|
79
|
+
return { content, encoding, fallback };
|
|
80
|
+
}
|
|
81
|
+
export function decodeTextBufferAuto(buffer) {
|
|
82
|
+
if (buffer.length === 0)
|
|
83
|
+
return { content: '', encoding: 'utf8', fallback: false };
|
|
84
|
+
if (startsWithBytes(buffer, [0xef, 0xbb, 0xbf])) {
|
|
85
|
+
return decodeStrict(buffer, 'utf-8', 'utf8', false);
|
|
86
|
+
}
|
|
87
|
+
if (startsWithBytes(buffer, [0xff, 0xfe])) {
|
|
88
|
+
return decodeStrict(buffer, 'utf-16le', 'utf16le', true);
|
|
89
|
+
}
|
|
90
|
+
if (startsWithBytes(buffer, [0xfe, 0xff])) {
|
|
91
|
+
return decodeStrict(buffer, 'utf-16be', 'utf16be', true);
|
|
92
|
+
}
|
|
93
|
+
if (looksLikeBinaryBuffer(buffer))
|
|
94
|
+
throw new BinaryTextPreviewError();
|
|
95
|
+
try {
|
|
96
|
+
return decodeStrict(buffer, 'utf-8', 'utf8', false);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error instanceof BinaryTextPreviewError)
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return decodeStrict(buffer, 'gb18030', 'gb18030', true);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof BinaryTextPreviewError)
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return decodeStrict(buffer, 'windows-1252', 'windows-1252', true);
|
|
110
|
+
}
|
|
@@ -664,10 +664,8 @@ export class ManagerRuntimeService {
|
|
|
664
664
|
noRestore: body.noRestore === undefined ? undefined : Boolean(body.noRestore),
|
|
665
665
|
downloadAttachments: body.downloadAttachments === undefined ? undefined : Boolean(body.downloadAttachments),
|
|
666
666
|
downloadAttachmentsDir: typeof body.downloadAttachmentsDir === 'string' ? body.downloadAttachmentsDir : undefined,
|
|
667
|
+
privacyConsentAccepted: body.privacyConsentAccepted === undefined ? undefined : Boolean(body.privacyConsentAccepted),
|
|
667
668
|
flowScriptPath: typeof body.flowScriptPath === 'string' ? body.flowScriptPath : undefined,
|
|
668
|
-
cloudOcrUrl: typeof body.cloudOcrUrl === 'string' ? body.cloudOcrUrl : undefined,
|
|
669
|
-
cloudOcrToken: typeof body.cloudOcrToken === 'string' ? body.cloudOcrToken : undefined,
|
|
670
|
-
cloudOcrMode: parseCloudOcrMode(body.cloudOcrMode),
|
|
671
669
|
});
|
|
672
670
|
this.registry.upsertManager({
|
|
673
671
|
...manager,
|
|
@@ -973,6 +971,9 @@ function externalChannelForChatEnqueue(channel) {
|
|
|
973
971
|
noRestore: channel.noRestore,
|
|
974
972
|
downloadAttachments: channel.downloadAttachments,
|
|
975
973
|
selfNickname: channel.selfNickname,
|
|
974
|
+
wechatRpaPrivacyConsentAccepted: channel.wechatRpaPrivacyConsentAccepted,
|
|
975
|
+
wechatRpaServerDecisionAvailable: channel.wechatRpaServerDecisionAvailable,
|
|
976
|
+
wechatRpaPreflightChecks: channel.wechatRpaPreflightChecks,
|
|
976
977
|
wechatRpaRuntimeState: channel.wechatRpaRuntimeState,
|
|
977
978
|
wechatRpaLastMessageAt: channel.wechatRpaLastMessageAt,
|
|
978
979
|
wechatRpaPendingReplyCount: channel.wechatRpaPendingReplyCount,
|
|
@@ -1004,9 +1005,6 @@ function mimeTypeFromExternalAttachment(attachment) {
|
|
|
1004
1005
|
return 'audio/*';
|
|
1005
1006
|
return 'application/octet-stream';
|
|
1006
1007
|
}
|
|
1007
|
-
function parseCloudOcrMode(value) {
|
|
1008
|
-
return value === 'off' || value === 'fallback' || value === 'always' ? value : undefined;
|
|
1009
|
-
}
|
|
1010
1008
|
function optionalNumber(value) {
|
|
1011
1009
|
const number = Number(value);
|
|
1012
1010
|
return Number.isFinite(number) ? number : undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CliRelayClient } from '../relay/client.js';
|
|
2
|
+
import type { SessionActivitySnapshot } from '@shennian/wire';
|
|
2
3
|
export declare class NativeSessionFusionService {
|
|
3
4
|
private client;
|
|
4
5
|
private timer;
|
|
@@ -20,6 +21,15 @@ export declare class NativeSessionFusionService {
|
|
|
20
21
|
noteManagedSourceSession(sessionId: string, agentType: string, sourceSessionKey: string | null): void;
|
|
21
22
|
private pruneState;
|
|
22
23
|
scanNow(): Promise<void>;
|
|
24
|
+
getCodexThreadActivity(params: {
|
|
25
|
+
sessionId: string;
|
|
26
|
+
threadId: string;
|
|
27
|
+
workDir?: string;
|
|
28
|
+
}): Promise<SessionActivitySnapshot | null | undefined>;
|
|
29
|
+
interruptCodexThread(params: {
|
|
30
|
+
threadId: string;
|
|
31
|
+
workDir?: string;
|
|
32
|
+
}): Promise<boolean>;
|
|
23
33
|
private runScan;
|
|
24
34
|
private tryClaimManagedEcho;
|
|
25
35
|
private isSuppressed;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { listClaudeTranscriptFiles, listCodexRolloutFiles, listOpenCodeSessionFiles, parseClaudeTranscriptChunk, parseCodexRolloutChunk, parseOpenCodeSessionFile, } from './parsers.js';
|
|
6
|
+
import { probeCodexThreadActivity, interruptCodexThread } from '../agents/codex-control.js';
|
|
6
7
|
import { loadNativeScannerState, saveNativeScannerState } from './state.js';
|
|
7
8
|
const SCAN_INTERVAL_MS = 60_000;
|
|
8
9
|
const CLAIM_TTL_MS = SCAN_INTERVAL_MS * 2;
|
|
@@ -89,6 +90,32 @@ export class NativeSessionFusionService {
|
|
|
89
90
|
});
|
|
90
91
|
return this.scanPromise;
|
|
91
92
|
}
|
|
93
|
+
async getCodexThreadActivity(params) {
|
|
94
|
+
const activity = await probeCodexThreadActivity({
|
|
95
|
+
threadId: params.threadId,
|
|
96
|
+
workDir: params.workDir,
|
|
97
|
+
});
|
|
98
|
+
if (!activity)
|
|
99
|
+
return undefined;
|
|
100
|
+
if (!activity.active || !activity.runPhase)
|
|
101
|
+
return null;
|
|
102
|
+
const nowIso = new Date().toISOString();
|
|
103
|
+
return {
|
|
104
|
+
sessionId: params.sessionId,
|
|
105
|
+
runId: activity.turnId ?? `codex:${params.threadId}`,
|
|
106
|
+
runPhase: activity.runPhase,
|
|
107
|
+
startedAt: nowIso,
|
|
108
|
+
updatedAt: nowIso,
|
|
109
|
+
canStop: activity.canStop,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async interruptCodexThread(params) {
|
|
113
|
+
const activity = await interruptCodexThread({
|
|
114
|
+
threadId: params.threadId,
|
|
115
|
+
workDir: params.workDir,
|
|
116
|
+
});
|
|
117
|
+
return Boolean(activity);
|
|
118
|
+
}
|
|
92
119
|
async runScan() {
|
|
93
120
|
this.pruneState();
|
|
94
121
|
const state = loadNativeScannerState();
|
|
@@ -36,11 +36,13 @@ function extractSummary(text) {
|
|
|
36
36
|
}
|
|
37
37
|
function buildRelayAgentPayload(event, sessionId, extra = {}) {
|
|
38
38
|
if (event.state === 'tool-call' || event.state === 'tool-result') {
|
|
39
|
+
const detailRef = { runId: event.runId, sourceSeq: event.seq };
|
|
39
40
|
return {
|
|
40
41
|
state: event.state,
|
|
41
42
|
runId: event.runId,
|
|
42
43
|
seq: event.seq,
|
|
43
44
|
sessionId,
|
|
45
|
+
detailRef,
|
|
44
46
|
...(event.name ? { name: event.name } : {}),
|
|
45
47
|
...(event.source ? { source: event.source } : {}),
|
|
46
48
|
...(event.agentSessionId ? { agentSessionId: event.agentSessionId } : {}),
|
|
@@ -55,6 +57,8 @@ function runPhaseFromAgentEvent(event) {
|
|
|
55
57
|
return event.runPhase ?? null;
|
|
56
58
|
if (event.state === 'tool-call' || event.state === 'tool-result')
|
|
57
59
|
return 'tool_running';
|
|
60
|
+
if (event.state === 'approval-pending')
|
|
61
|
+
return 'waiting_approval';
|
|
58
62
|
if (event.state === 'delta')
|
|
59
63
|
return event.thinking ? 'thinking' : 'streaming_text';
|
|
60
64
|
if (event.state === 'init' || event.state === 'start')
|
|
@@ -144,8 +148,6 @@ function normalizeExternalChannel(value) {
|
|
|
144
148
|
noRestore: raw.noRestore === undefined || raw.noRestore === null ? null : Boolean(raw.noRestore),
|
|
145
149
|
downloadAttachments: raw.downloadAttachments === undefined || raw.downloadAttachments === null ? null : Boolean(raw.downloadAttachments),
|
|
146
150
|
downloadAttachmentsDir: typeof raw.downloadAttachmentsDir === 'string' ? raw.downloadAttachmentsDir : null,
|
|
147
|
-
cloudOcrUrl: typeof raw.cloudOcrUrl === 'string' ? raw.cloudOcrUrl : null,
|
|
148
|
-
cloudOcrMode: typeof raw.cloudOcrMode === 'string' ? raw.cloudOcrMode : null,
|
|
149
151
|
};
|
|
150
152
|
}
|
|
151
153
|
function externalChannelEnabled(channel) {
|
|
@@ -295,6 +297,8 @@ function bindAdapterEvents(runtime, sessionId, agentType, adapter) {
|
|
|
295
297
|
v: 1,
|
|
296
298
|
type: event.state === 'tool-call' ? 'tool_use' : 'tool_result',
|
|
297
299
|
name: event.name,
|
|
300
|
+
status: event.state === 'tool-call' ? 'running' : 'completed',
|
|
301
|
+
detailRef: { runId: event.runId, sourceSeq: event.seq },
|
|
298
302
|
args: event.args,
|
|
299
303
|
result: event.result,
|
|
300
304
|
}),
|
|
@@ -730,6 +734,18 @@ export async function handleChatAbort(runtime, req) {
|
|
|
730
734
|
}
|
|
731
735
|
catch { /* best-effort: still emit synthetic abort below */ }
|
|
732
736
|
emitSyntheticAbort(runtime, sessionId);
|
|
737
|
+
runtime.activityPublisher?.publish(sessionId, null);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
const params = req.params;
|
|
741
|
+
if (params.agentType === 'codex' && params.agentSessionId && runtime.nativeFusion) {
|
|
742
|
+
const ok = await runtime.nativeFusion.interruptCodexThread({
|
|
743
|
+
threadId: params.agentSessionId,
|
|
744
|
+
workDir: params.workDir,
|
|
745
|
+
});
|
|
746
|
+
if (ok)
|
|
747
|
+
runtime.activityPublisher?.publish(sessionId, null);
|
|
748
|
+
}
|
|
733
749
|
}
|
|
734
750
|
runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
|
|
735
751
|
}
|
|
@@ -5,6 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { convertMarkdownToPdf, defaultPdfOutputPath, MarkdownPdfBrowserMissingError, } from '../../tools/markdown-to-pdf.js';
|
|
7
7
|
import { createZipArchive } from '../archive-zip.js';
|
|
8
|
+
import { BinaryTextPreviewError, decodeTextBufferAuto, isAutoTextEncoding, } from '../../fs/text-decoder.js';
|
|
8
9
|
const FILE_SYSTEM_ROOTS_PATH = '__roots__';
|
|
9
10
|
const MAX_FOLDER_UPLOAD_FILES = 2000;
|
|
10
11
|
const MAX_FOLDER_UPLOAD_TOTAL_SIZE = 1024 * 1024 * 1024;
|
|
@@ -151,7 +152,8 @@ export async function handleFsRead(runtime, req) {
|
|
|
151
152
|
return;
|
|
152
153
|
}
|
|
153
154
|
const filePath = resolved.path;
|
|
154
|
-
const
|
|
155
|
+
const requestedEncoding = req.params.encoding;
|
|
156
|
+
const encoding = typeof requestedEncoding === 'string' ? requestedEncoding : 'utf8';
|
|
155
157
|
const offset = req.params.offset;
|
|
156
158
|
const length = req.params.length;
|
|
157
159
|
try {
|
|
@@ -214,6 +216,23 @@ export async function handleFsRead(runtime, req) {
|
|
|
214
216
|
});
|
|
215
217
|
return;
|
|
216
218
|
}
|
|
219
|
+
if (isAutoTextEncoding(encoding)) {
|
|
220
|
+
const buffer = fs.readFileSync(filePath);
|
|
221
|
+
const decoded = decodeTextBufferAuto(buffer);
|
|
222
|
+
runtime.client.sendRes({
|
|
223
|
+
type: 'res',
|
|
224
|
+
id: req.id,
|
|
225
|
+
ok: true,
|
|
226
|
+
payload: {
|
|
227
|
+
content: decoded.content,
|
|
228
|
+
path: filePath,
|
|
229
|
+
size: stat.size,
|
|
230
|
+
encoding: decoded.encoding,
|
|
231
|
+
encodingFallback: decoded.fallback,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
217
236
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
218
237
|
runtime.client.sendRes({
|
|
219
238
|
type: 'res',
|
|
@@ -223,7 +242,12 @@ export async function handleFsRead(runtime, req) {
|
|
|
223
242
|
});
|
|
224
243
|
}
|
|
225
244
|
catch (err) {
|
|
226
|
-
runtime.client.sendRes({
|
|
245
|
+
runtime.client.sendRes({
|
|
246
|
+
type: 'res',
|
|
247
|
+
id: req.id,
|
|
248
|
+
ok: false,
|
|
249
|
+
error: err instanceof BinaryTextPreviewError ? err.message : String(err),
|
|
250
|
+
});
|
|
227
251
|
}
|
|
228
252
|
}
|
|
229
253
|
export async function handleFsWrite(runtime, req) {
|
|
@@ -378,7 +402,19 @@ export async function handleFsExportMarkdownPdf(runtime, req) {
|
|
|
378
402
|
}
|
|
379
403
|
}
|
|
380
404
|
function safeArchiveBaseName(name) {
|
|
381
|
-
|
|
405
|
+
let safeName = '';
|
|
406
|
+
for (const char of name.trim()) {
|
|
407
|
+
const code = char.charCodeAt(0);
|
|
408
|
+
const unsafe = code <= 0x1f || '<>:"/\\|?*'.includes(char);
|
|
409
|
+
if (unsafe) {
|
|
410
|
+
if (!safeName.endsWith('-'))
|
|
411
|
+
safeName += '-';
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
safeName += char;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const trimmed = safeName.replace(/[. ]+$/g, '');
|
|
382
418
|
return trimmed || 'folder';
|
|
383
419
|
}
|
|
384
420
|
export async function handleFsArchiveZip(runtime, req) {
|
|
@@ -21,6 +21,18 @@ export async function handleSessionRefresh(runtime, req) {
|
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
await runtime.nativeFusion.scanNow();
|
|
24
|
+
if (params.agentType === 'codex' &&
|
|
25
|
+
params.agentSessionId &&
|
|
26
|
+
typeof runtime.nativeFusion.getCodexThreadActivity === 'function') {
|
|
27
|
+
const activity = await runtime.nativeFusion.getCodexThreadActivity({
|
|
28
|
+
sessionId: params.sessionId,
|
|
29
|
+
threadId: params.agentSessionId,
|
|
30
|
+
workDir: params.workDir,
|
|
31
|
+
});
|
|
32
|
+
if (activity !== undefined) {
|
|
33
|
+
runtime.activityPublisher?.publish(params.sessionId, activity);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
24
36
|
runtime.client.sendRes({
|
|
25
37
|
type: 'res',
|
|
26
38
|
id: req.id,
|