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.
- package/dist/src/agents/command-spec.js +19 -12
- package/dist/src/agents/external-channel-instructions.d.ts +3 -1
- package/dist/src/agents/external-channel-instructions.js +73 -15
- package/dist/src/channels/base.d.ts +62 -9
- package/dist/src/channels/runtime.d.ts +43 -10
- package/dist/src/channels/runtime.js +300 -14
- package/dist/src/channels/secret-registry.d.ts +17 -1
- package/dist/src/channels/websocket.d.ts +3 -0
- package/dist/src/channels/websocket.js +39 -2
- package/dist/src/channels/wechat-rpa/macos-flow.d.ts +77 -0
- package/dist/src/channels/wechat-rpa/macos-flow.js +254 -0
- package/dist/src/channels/wechat-rpa/macos.d.ts +11 -0
- package/dist/src/channels/wechat-rpa/macos.js +63 -0
- package/dist/src/channels/wechat-rpa/normalizer.d.ts +42 -0
- package/dist/src/channels/wechat-rpa/normalizer.js +99 -0
- package/dist/src/channels/wechat-rpa.d.ts +51 -0
- package/dist/src/channels/wechat-rpa.js +587 -0
- package/dist/src/channels/wecom.d.ts +3 -0
- package/dist/src/channels/wecom.js +43 -1
- package/dist/src/commands/external-attachments.d.ts +1 -1
- package/dist/src/commands/external-attachments.js +2 -3
- package/dist/src/commands/external.js +19 -1
- package/dist/src/commands/manager.js +109 -0
- package/dist/src/manager/prompt.d.ts +1 -1
- package/dist/src/manager/prompt.js +1 -11
- package/dist/src/manager/runtime.d.ts +2 -10
- package/dist/src/manager/runtime.js +197 -33
- package/dist/src/native-fusion/service.js +7 -0
- package/dist/src/session/archive-zip.d.ts +10 -0
- package/dist/src/session/archive-zip.js +220 -0
- package/dist/src/session/handlers/agent-config.js +85 -6
- package/dist/src/session/handlers/chat.js +58 -2
- package/dist/src/session/handlers/fs.d.ts +1 -0
- package/dist/src/session/handlers/fs.js +57 -1
- package/dist/src/session/manager.js +4 -1
- 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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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:
|
|
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 =
|
|
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.
|
|
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
|
+
}
|