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
@@ -0,0 +1,220 @@
1
+ // @arch docs/features/file-management-enhancements.md#文件夹打包下载
2
+ // @test src/__tests__/archive-zip.test.ts
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ const DEFAULT_MAX_FILES = 5000;
6
+ const DEFAULT_MAX_TOTAL_SIZE = 1024 * 1024 * 1024;
7
+ const ZIP_VERSION_NEEDED = 20;
8
+ const ZIP_UTF8_FLAG = 0x0800;
9
+ const ZIP_STORE_METHOD = 0;
10
+ const CHUNK_SIZE = 1024 * 1024;
11
+ const crcTable = new Uint32Array(256);
12
+ for (let index = 0; index < 256; index += 1) {
13
+ let value = index;
14
+ for (let bit = 0; bit < 8; bit += 1) {
15
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
16
+ }
17
+ crcTable[index] = value >>> 0;
18
+ }
19
+ function updateCrc32(crc, buffer) {
20
+ let next = crc;
21
+ for (let index = 0; index < buffer.length; index += 1) {
22
+ next = crcTable[(next ^ buffer[index]) & 0xff] ^ (next >>> 8);
23
+ }
24
+ return next >>> 0;
25
+ }
26
+ function dosDateTime(mtimeMs) {
27
+ const date = new Date(mtimeMs || Date.now());
28
+ const year = Math.max(1980, Math.min(2107, date.getFullYear()));
29
+ return {
30
+ date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
31
+ time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
32
+ };
33
+ }
34
+ function writeUInt32(buffer, value, offset) {
35
+ buffer.writeUInt32LE(value >>> 0, offset);
36
+ }
37
+ function writeLocalHeader(fd, entry) {
38
+ const name = Buffer.from(entry.name, 'utf8');
39
+ const { date, time } = dosDateTime(entry.mtimeMs);
40
+ const header = Buffer.alloc(30);
41
+ writeUInt32(header, 0x04034b50, 0);
42
+ header.writeUInt16LE(ZIP_VERSION_NEEDED, 4);
43
+ header.writeUInt16LE(ZIP_UTF8_FLAG, 6);
44
+ header.writeUInt16LE(ZIP_STORE_METHOD, 8);
45
+ header.writeUInt16LE(time, 10);
46
+ header.writeUInt16LE(date, 12);
47
+ writeUInt32(header, entry.crc32, 14);
48
+ writeUInt32(header, entry.size, 18);
49
+ writeUInt32(header, entry.size, 22);
50
+ header.writeUInt16LE(name.length, 26);
51
+ header.writeUInt16LE(0, 28);
52
+ fs.writeSync(fd, header);
53
+ fs.writeSync(fd, name);
54
+ }
55
+ function writeCentralDirectoryHeader(fd, entry) {
56
+ const name = Buffer.from(entry.name, 'utf8');
57
+ const { date, time } = dosDateTime(entry.mtimeMs);
58
+ const header = Buffer.alloc(46);
59
+ writeUInt32(header, 0x02014b50, 0);
60
+ header.writeUInt16LE(0x031e, 4);
61
+ header.writeUInt16LE(ZIP_VERSION_NEEDED, 6);
62
+ header.writeUInt16LE(ZIP_UTF8_FLAG, 8);
63
+ header.writeUInt16LE(ZIP_STORE_METHOD, 10);
64
+ header.writeUInt16LE(time, 12);
65
+ header.writeUInt16LE(date, 14);
66
+ writeUInt32(header, entry.crc32, 16);
67
+ writeUInt32(header, entry.size, 20);
68
+ writeUInt32(header, entry.size, 24);
69
+ header.writeUInt16LE(name.length, 28);
70
+ header.writeUInt16LE(0, 30);
71
+ header.writeUInt16LE(0, 32);
72
+ header.writeUInt16LE(0, 34);
73
+ header.writeUInt16LE(0, 36);
74
+ writeUInt32(header, entry.isDir ? 0x10 : 0, 38);
75
+ writeUInt32(header, entry.offset, 42);
76
+ fs.writeSync(fd, header);
77
+ fs.writeSync(fd, name);
78
+ }
79
+ function writeEndOfCentralDirectory(fd, entryCount, centralSize, centralOffset) {
80
+ const header = Buffer.alloc(22);
81
+ writeUInt32(header, 0x06054b50, 0);
82
+ header.writeUInt16LE(0, 4);
83
+ header.writeUInt16LE(0, 6);
84
+ header.writeUInt16LE(entryCount, 8);
85
+ header.writeUInt16LE(entryCount, 10);
86
+ writeUInt32(header, centralSize, 12);
87
+ writeUInt32(header, centralOffset, 16);
88
+ header.writeUInt16LE(0, 20);
89
+ fs.writeSync(fd, header);
90
+ }
91
+ function normalizeZipName(relativePath, isDir) {
92
+ const normalized = relativePath.split(path.sep).join('/');
93
+ return isDir && !normalized.endsWith('/') ? `${normalized}/` : normalized;
94
+ }
95
+ function collectEntries(sourceDir, options) {
96
+ const sourceStat = fs.statSync(sourceDir);
97
+ const rootName = normalizeZipName(path.basename(sourceDir) || 'folder', true);
98
+ const entries = [{
99
+ name: rootName,
100
+ sourcePath: null,
101
+ crc32: 0,
102
+ size: 0,
103
+ isDir: true,
104
+ mtimeMs: sourceStat.mtimeMs,
105
+ }];
106
+ let fileCount = 0;
107
+ let totalSize = 0;
108
+ const visit = (dirPath) => {
109
+ const raw = fs.readdirSync(dirPath, { withFileTypes: true });
110
+ for (const dirent of raw) {
111
+ const fullPath = path.join(dirPath, dirent.name);
112
+ const relative = path.relative(sourceDir, fullPath);
113
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
114
+ continue;
115
+ const stat = fs.lstatSync(fullPath);
116
+ if (stat.isSymbolicLink())
117
+ continue;
118
+ if (stat.isDirectory()) {
119
+ entries.push({
120
+ name: path.posix.join(rootName, normalizeZipName(relative, true)),
121
+ sourcePath: null,
122
+ crc32: 0,
123
+ size: 0,
124
+ isDir: true,
125
+ mtimeMs: stat.mtimeMs,
126
+ });
127
+ visit(fullPath);
128
+ continue;
129
+ }
130
+ if (!stat.isFile())
131
+ continue;
132
+ fileCount += 1;
133
+ if (fileCount > options.maxFiles)
134
+ throw new Error(`Too many files: ${fileCount}`);
135
+ totalSize += stat.size;
136
+ if (totalSize > options.maxTotalSize) {
137
+ throw new Error(`Folder too large: ${totalSize} bytes`);
138
+ }
139
+ entries.push({
140
+ name: path.posix.join(rootName, normalizeZipName(relative, false)),
141
+ sourcePath: fullPath,
142
+ crc32: computeFileCrc32(fullPath),
143
+ size: stat.size,
144
+ isDir: false,
145
+ mtimeMs: stat.mtimeMs,
146
+ });
147
+ }
148
+ };
149
+ visit(sourceDir);
150
+ return entries;
151
+ }
152
+ function computeFileCrc32(filePath) {
153
+ const fd = fs.openSync(filePath, 'r');
154
+ const buffer = Buffer.alloc(CHUNK_SIZE);
155
+ let crc = 0xffffffff;
156
+ try {
157
+ while (true) {
158
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
159
+ if (bytesRead <= 0)
160
+ break;
161
+ crc = updateCrc32(crc, buffer.subarray(0, bytesRead));
162
+ }
163
+ }
164
+ finally {
165
+ fs.closeSync(fd);
166
+ }
167
+ return (crc ^ 0xffffffff) >>> 0;
168
+ }
169
+ function writeFileData(fd, filePath) {
170
+ const sourceFd = fs.openSync(filePath, 'r');
171
+ const buffer = Buffer.alloc(CHUNK_SIZE);
172
+ try {
173
+ while (true) {
174
+ const bytesRead = fs.readSync(sourceFd, buffer, 0, buffer.length, null);
175
+ if (bytesRead <= 0)
176
+ break;
177
+ fs.writeSync(fd, buffer.subarray(0, bytesRead));
178
+ }
179
+ }
180
+ finally {
181
+ fs.closeSync(sourceFd);
182
+ }
183
+ }
184
+ export function createZipArchive(sourceDir, outputPath, options = {}) {
185
+ const sourceStat = fs.statSync(sourceDir);
186
+ if (!sourceStat.isDirectory())
187
+ throw new Error('Not a directory');
188
+ const limits = {
189
+ maxFiles: options.maxFiles ?? DEFAULT_MAX_FILES,
190
+ maxTotalSize: options.maxTotalSize ?? DEFAULT_MAX_TOTAL_SIZE,
191
+ };
192
+ const scanned = collectEntries(sourceDir, limits);
193
+ const zipEntries = [];
194
+ let fileCount = 0;
195
+ let totalSize = 0;
196
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
197
+ const fd = fs.openSync(outputPath, 'w');
198
+ try {
199
+ for (const entry of scanned) {
200
+ const offset = fs.fstatSync(fd).size;
201
+ const nextEntry = { ...entry, offset };
202
+ writeLocalHeader(fd, nextEntry);
203
+ if (nextEntry.sourcePath) {
204
+ writeFileData(fd, nextEntry.sourcePath);
205
+ fileCount += 1;
206
+ totalSize += nextEntry.size;
207
+ }
208
+ zipEntries.push(nextEntry);
209
+ }
210
+ const centralOffset = fs.fstatSync(fd).size;
211
+ for (const entry of zipEntries)
212
+ writeCentralDirectoryHeader(fd, entry);
213
+ const centralSize = fs.fstatSync(fd).size - centralOffset;
214
+ writeEndOfCentralDirectory(fd, zipEntries.length, centralSize, centralOffset);
215
+ }
216
+ finally {
217
+ fs.closeSync(fd);
218
+ }
219
+ return { outputPath, fileCount, totalSize };
220
+ }
@@ -1,7 +1,14 @@
1
1
  // @arch docs/features/agent-provider-config.md
2
2
  // @test src/__tests__/agent-config-status.test.ts
3
+ // @test src/__tests__/agent-config-handler.test.ts
4
+ import { mkdir } from 'node:fs/promises';
5
+ import { createAgent } from '../../agents/adapter.js';
3
6
  import { buildManagedAgentEnv, deleteManagedAgentProviderConfig, getAgentConfigSummary, upsertManagedAgentProviderConfig, } from '../../agents/config-status.js';
7
+ import { resolveShennianPath } from '../../config/index.js';
4
8
  import { handleAgentsRefresh } from './agents.js';
9
+ const AGENT_TEST_TIMEOUT_MS = 30_000;
10
+ const AGENT_TEST_PROMPT = '这是一次神念连接测试。请只回复 OK,不要解释。';
11
+ const AGENT_TEST_WORK_DIR = resolveShennianPath('tmp', 'agent-config-test');
5
12
  function normalizeAgent(value) {
6
13
  if (value === 'codex' || value === 'claude' || value === 'pi')
7
14
  return value;
@@ -45,13 +52,85 @@ export async function handleAgentConfigClear(runtime, req) {
45
52
  export async function handleAgentConfigTest(runtime, req) {
46
53
  const agent = normalizeAgent(req.params.agent);
47
54
  const summary = getAgentConfigSummary(agent);
48
- runtime.client.sendRes({
49
- type: 'res',
50
- id: req.id,
51
- ok: !!summary?.tokenPresent,
52
- payload: { agent, config: summary },
53
- ...(!summary?.tokenPresent ? { error: 'Token is missing on this machine' } : {}),
55
+ try {
56
+ const result = await runAgentConfigTest(agent);
57
+ runtime.client.sendRes({
58
+ type: 'res',
59
+ id: req.id,
60
+ ok: true,
61
+ payload: { agent, config: getAgentConfigSummary(agent), test: result },
62
+ });
63
+ }
64
+ catch (err) {
65
+ runtime.client.sendRes({
66
+ type: 'res',
67
+ id: req.id,
68
+ ok: false,
69
+ payload: { agent, config: summary },
70
+ error: err instanceof Error ? err.message : String(err),
71
+ });
72
+ }
73
+ }
74
+ async function runAgentConfigTest(agent) {
75
+ const adapter = createAgent(agent);
76
+ if (!adapter)
77
+ throw new Error(`Unsupported agent: ${agent}`);
78
+ const sessionId = `agent-test-${agent}-${Date.now()}`;
79
+ await mkdir(AGENT_TEST_WORK_DIR, { recursive: true });
80
+ let settled = false;
81
+ let failWait = () => { };
82
+ const waitForReply = new Promise((resolve, reject) => {
83
+ const timeout = setTimeout(() => {
84
+ if (settled)
85
+ return;
86
+ settled = true;
87
+ reject(new Error('Agent test timed out after 30 seconds'));
88
+ }, AGENT_TEST_TIMEOUT_MS);
89
+ function finish(result) {
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ clearTimeout(timeout);
94
+ resolve(result);
95
+ }
96
+ function fail(error) {
97
+ if (settled)
98
+ return;
99
+ settled = true;
100
+ clearTimeout(timeout);
101
+ reject(error);
102
+ }
103
+ failWait = fail;
104
+ adapter.on('agentEvent', (event) => {
105
+ if (event.state === 'delta' && event.text && !event.thinking) {
106
+ finish({ mode: 'agent-run', reply: event.text.slice(0, 120) });
107
+ return;
108
+ }
109
+ if (event.state === 'final') {
110
+ finish({ mode: 'agent-run' });
111
+ return;
112
+ }
113
+ if (event.state === 'error') {
114
+ fail(new Error(event.message || 'Agent test failed'));
115
+ }
116
+ });
117
+ adapter.on('error', fail);
54
118
  });
119
+ try {
120
+ adapter.configure?.({
121
+ sessionId,
122
+ env: getManagedEnvForAgent(agent),
123
+ });
124
+ await adapter.start(sessionId, AGENT_TEST_WORK_DIR, null);
125
+ const sendPromise = adapter.send(AGENT_TEST_PROMPT).catch((err) => {
126
+ failWait(err instanceof Error ? err : new Error(String(err)));
127
+ });
128
+ return await Promise.race([waitForReply, sendPromise.then(() => waitForReply)]);
129
+ }
130
+ finally {
131
+ adapter.removeAllListeners();
132
+ await adapter.stop().catch(() => { });
133
+ }
55
134
  }
56
135
  async function broadcastAgents(runtime) {
57
136
  const req = {
@@ -34,6 +34,31 @@ function extractSummary(text) {
34
34
  const end = newline > 0 ? Math.min(newline, 80) : Math.min(text.length, 80);
35
35
  return text.slice(0, end);
36
36
  }
37
+ function buildRelayAgentPayload(event, sessionId, extra = {}) {
38
+ if (event.state === 'tool-call' || event.state === 'tool-result') {
39
+ return {
40
+ state: event.state,
41
+ runId: event.runId,
42
+ seq: event.seq,
43
+ sessionId,
44
+ ...(event.name ? { name: event.name } : {}),
45
+ ...(event.source ? { source: event.source } : {}),
46
+ ...(event.agentSessionId ? { agentSessionId: event.agentSessionId } : {}),
47
+ ...extra,
48
+ };
49
+ }
50
+ return { ...event, sessionId, ...extra };
51
+ }
52
+ function formatAgentSendFailure(agentType, err) {
53
+ const raw = err instanceof Error ? err.message : String(err);
54
+ if (agentType === 'pi' &&
55
+ (raw.includes('429') || raw.includes('daily_quota_exceeded') || raw.includes('nian_quota_exceeded'))) {
56
+ return raw.includes('too quickly') || raw.includes('per minute')
57
+ ? 'Nian 请求过于频繁,请稍后再试。'
58
+ : 'Nian 今日额度已用完,次日自动恢复。';
59
+ }
60
+ return `Agent send failed: ${raw}`;
61
+ }
37
62
  function getNativeSourceAgentType(agentType, modelId) {
38
63
  if (agentType !== 'manager')
39
64
  return agentType;
@@ -94,6 +119,21 @@ function normalizeExternalChannel(value) {
94
119
  name: typeof raw.name === 'string' ? raw.name : null,
95
120
  canReply: raw.canReply === undefined || raw.canReply === null ? null : Boolean(raw.canReply),
96
121
  systemPrompt: typeof raw.systemPrompt === 'string' ? raw.systemPrompt : null,
122
+ wechatRpaSource: typeof raw.wechatRpaSource === 'string' ? raw.wechatRpaSource : null,
123
+ wechatRpaGroups: Array.isArray(raw.wechatRpaGroups)
124
+ ? raw.wechatRpaGroups
125
+ .map((item) => ({ name: String(item?.name || '').trim() }))
126
+ .filter((item) => item.name)
127
+ : null,
128
+ pollIntervalMs: Number.isFinite(raw.pollIntervalMs) ? Number(raw.pollIntervalMs) : null,
129
+ recentLimit: Number.isFinite(raw.recentLimit) ? Number(raw.recentLimit) : null,
130
+ idleSeconds: Number.isFinite(raw.idleSeconds) ? Number(raw.idleSeconds) : null,
131
+ forceForeground: raw.forceForeground === undefined || raw.forceForeground === null ? null : Boolean(raw.forceForeground),
132
+ noRestore: raw.noRestore === undefined || raw.noRestore === null ? null : Boolean(raw.noRestore),
133
+ downloadAttachments: raw.downloadAttachments === undefined || raw.downloadAttachments === null ? null : Boolean(raw.downloadAttachments),
134
+ downloadAttachmentsDir: typeof raw.downloadAttachmentsDir === 'string' ? raw.downloadAttachmentsDir : null,
135
+ cloudOcrUrl: typeof raw.cloudOcrUrl === 'string' ? raw.cloudOcrUrl : null,
136
+ cloudOcrMode: typeof raw.cloudOcrMode === 'string' ? raw.cloudOcrMode : null,
97
137
  };
98
138
  }
99
139
  function externalChannelEnabled(channel) {
@@ -148,7 +188,7 @@ function bindAdapterEvents(runtime, sessionId, agentType, adapter) {
148
188
  runtime.client.sendAgentEvent({
149
189
  type: 'event',
150
190
  event: 'agent',
151
- payload: { ...event, sessionId, ...extra },
191
+ payload: buildRelayAgentPayload(event, sessionId, extra),
152
192
  seq: event.seq,
153
193
  id: `agent-evt-${event.runId}-${event.seq}`,
154
194
  });
@@ -500,7 +540,7 @@ export async function handleChatSend(runtime, req) {
500
540
  });
501
541
  };
502
542
  const handleSendFailure = async (err, respondToReq) => {
503
- const message = `Agent send failed: ${err instanceof Error ? err.message : String(err)}`;
543
+ const message = formatAgentSendFailure(requestedAgentType, err);
504
544
  console.error(`[chat.send] send failed reqId=${req.id} sessionId=${sessionId} agentType=${agentType} workDir=${resolvedWorkDir} agentSessionId=${session.agentSessionId ?? incomingAgentSid ?? ''}: ${message}`);
505
545
  runtime.sessions.delete(sessionId);
506
546
  try {
@@ -518,6 +558,22 @@ export async function handleChatSend(runtime, req) {
518
558
  seq: 0,
519
559
  },
520
560
  });
561
+ if (!respondToReq) {
562
+ const errorEnvelope = {
563
+ id: `agent-error-${req.id}-${Date.now()}`,
564
+ sessionId,
565
+ role: 'agent',
566
+ ts: Date.now(),
567
+ payload: message,
568
+ };
569
+ appendMessage(sessionId, errorEnvelope);
570
+ sendSessionMessageEvent(runtime, errorEnvelope, {
571
+ agentType: requestedAgentType,
572
+ workDir: displayWorkDir,
573
+ agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
574
+ modelId,
575
+ });
576
+ }
521
577
  if (respondToReq) {
522
578
  runtime.processedReqIds.delete(req.id);
523
579
  runtime.client.sendRes({
@@ -5,6 +5,7 @@ export declare function handleFsRead(runtime: SessionManagerRuntime, req: ReqFra
5
5
  export declare function handleFsWrite(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
6
6
  export declare function handleFsRename(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
7
7
  export declare function handleFsExportMarkdownPdf(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
8
+ export declare function handleFsArchiveZip(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
8
9
  export declare function handleFsTransfer(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
9
10
  export declare function handleFsTransferStart(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
10
11
  export declare function handleFsTransferChunk(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
@@ -4,9 +4,12 @@ import fs from 'node:fs';
4
4
  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
+ import { createZipArchive } from '../archive-zip.js';
7
8
  const FILE_SYSTEM_ROOTS_PATH = '__roots__';
8
9
  const MAX_FOLDER_UPLOAD_FILES = 2000;
9
10
  const MAX_FOLDER_UPLOAD_TOTAL_SIZE = 1024 * 1024 * 1024;
11
+ const MAX_ARCHIVE_FILES = 5000;
12
+ const MAX_ARCHIVE_TOTAL_SIZE = 1024 * 1024 * 1024;
10
13
  function isWindowsAbsolutePath(pathValue) {
11
14
  return /^[A-Za-z]:([\\/]|$)/.test(pathValue) || /^\\\\[^\\]+\\[^\\]+/.test(pathValue);
12
15
  }
@@ -263,7 +266,6 @@ export async function handleFsWrite(runtime, req) {
263
266
  export async function handleFsRename(runtime, req) {
264
267
  const requestedPath = req.params.path;
265
268
  const newName = req.params.newName;
266
- const rootPath = req.params.rootPath || requestedPath;
267
269
  if (!requestedPath || !newName) {
268
270
  runtime.client.sendRes({
269
271
  type: 'res',
@@ -277,6 +279,8 @@ export async function handleFsRename(runtime, req) {
277
279
  runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Invalid newName' });
278
280
  return;
279
281
  }
282
+ const api = pathApiForPath(requestedPath);
283
+ const rootPath = req.params.rootPath || api.dirname(requestedPath);
280
284
  const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
281
285
  if (!resolved.ok) {
282
286
  runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
@@ -373,6 +377,58 @@ export async function handleFsExportMarkdownPdf(runtime, req) {
373
377
  });
374
378
  }
375
379
  }
380
+ function safeArchiveBaseName(name) {
381
+ const trimmed = name.trim().replace(/[<>:"/\\|?*\u0000-\u001f]+/g, '-').replace(/[. ]+$/g, '');
382
+ return trimmed || 'folder';
383
+ }
384
+ export async function handleFsArchiveZip(runtime, req) {
385
+ const requestedPath = req.params.path;
386
+ const rootPath = req.params.rootPath || requestedPath;
387
+ if (!requestedPath) {
388
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'path is required' });
389
+ return;
390
+ }
391
+ const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
392
+ if (!resolved.ok) {
393
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
394
+ return;
395
+ }
396
+ try {
397
+ const stat = fs.statSync(resolved.path);
398
+ if (!stat.isDirectory()) {
399
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Not a directory' });
400
+ return;
401
+ }
402
+ const api = pathApiForPath(resolved.path);
403
+ const baseName = safeArchiveBaseName(api.basename(resolved.path));
404
+ const archiveDir = path.join(os.tmpdir(), 'shennian-archives');
405
+ const outputPath = path.join(archiveDir, `${baseName}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.zip`);
406
+ const result = createZipArchive(resolved.path, outputPath, {
407
+ maxFiles: MAX_ARCHIVE_FILES,
408
+ maxTotalSize: MAX_ARCHIVE_TOTAL_SIZE,
409
+ });
410
+ runtime.client.sendRes({
411
+ type: 'res',
412
+ id: req.id,
413
+ ok: true,
414
+ payload: {
415
+ sourcePath: resolved.path,
416
+ outputPath: result.outputPath,
417
+ entry: makeFsEntry(result.outputPath),
418
+ fileCount: result.fileCount,
419
+ totalSize: result.totalSize,
420
+ },
421
+ });
422
+ }
423
+ catch (err) {
424
+ runtime.client.sendRes({
425
+ type: 'res',
426
+ id: req.id,
427
+ ok: false,
428
+ error: err instanceof Error ? err.message : String(err),
429
+ });
430
+ }
431
+ }
376
432
  export async function handleFsTransfer(runtime, req) {
377
433
  const { name, targetPath, data, direct } = req.params;
378
434
  if (!name || !data) {
@@ -9,7 +9,7 @@ import { handleAgentConfigClear, handleAgentConfigGet, handleAgentConfigTest, ha
9
9
  import { handleChatAbort, handleChatSend } from './handlers/chat.js';
10
10
  import { handleSessionRefresh } from './handlers/session-refresh.js';
11
11
  import { handleSessionTitleSet } from './handlers/title.js';
12
- import { cleanupPendingTransfers, handleFsLs, handleFsRead, handleFsRename, handleFsWrite, handleFsTransfer, handleFsTransferAbort, handleFsTransferChunk, handleFsTransferFinish, handleFsTransferStart, handleFsExportMarkdownPdf, } from './handlers/fs.js';
12
+ import { cleanupPendingTransfers, handleFsLs, handleFsRead, handleFsRename, handleFsWrite, handleFsTransfer, handleFsTransferAbort, handleFsTransferChunk, handleFsTransferFinish, handleFsTransferStart, handleFsExportMarkdownPdf, handleFsArchiveZip, } from './handlers/fs.js';
13
13
  import { handleSkillDoctor, handleSkillInstall, handleSkillList, handleSkillSetup, handleSkillUse, } from './handlers/skills.js';
14
14
  import { handleRegionProbe, handleRegionSwitch, handleUpgradeSetPolicy, } from './handlers/control.js';
15
15
  import { ManagerRuntimeService, setManagerRuntimeService } from '../manager/runtime.js';
@@ -124,6 +124,9 @@ export class SessionManager {
124
124
  case 'fs.export.markdown-pdf':
125
125
  await handleFsExportMarkdownPdf(runtime, req);
126
126
  break;
127
+ case 'fs.archive.zip':
128
+ await handleFsArchiveZip(runtime, req);
129
+ break;
127
130
  case 'fs.rename':
128
131
  await handleFsRename(runtime, req);
129
132
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.2.72",
3
+ "version": "0.2.74",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,14 +33,20 @@
33
33
  "engines": {
34
34
  "node": ">=18"
35
35
  },
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)\"",
39
+ "dev": "tsc --watch",
40
+ "prepublishOnly": "pnpm build:publish"
41
+ },
36
42
  "dependencies": {
37
43
  "@mariozechner/pi-agent-core": "^0.64.0",
38
44
  "@sinclair/typebox": "^0.34.49",
45
+ "@shennian/wire": "workspace:*",
39
46
  "chalk": "^5.4.1",
40
47
  "commander": "^13.1.0",
41
48
  "qrcode-terminal": "^0.12.0",
42
- "ws": "^8.18.1",
43
- "@shennian/wire": "0.1.5"
49
+ "ws": "^8.18.1"
44
50
  },
45
51
  "devDependencies": {
46
52
  "@types/node": "^20",
@@ -48,10 +54,5 @@
48
54
  "@types/ws": "^8.18.1",
49
55
  "tsx": "^4.19.4",
50
56
  "typescript": "^5.9.3"
51
- },
52
- "scripts": {
53
- "build": "tsc && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
54
- "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)\"",
55
- "dev": "tsc --watch"
56
57
  }
57
- }
58
+ }