shennian 0.2.88 → 0.2.90
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 +22 -0
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.d.ts +6 -0
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.d.ts +35 -0
- package/dist/src/agents/codex-control.js +2 -0
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.d.ts +8 -0
- package/dist/src/agents/codex.js +15 -863
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.d.ts +4 -1
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.d.ts +1 -0
- package/dist/src/channels/runtime.js +5 -533
- package/dist/src/channels/secret-registry.d.ts +1 -0
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
- package/dist/src/channels/wechat-channel/anchor.js +1 -0
- package/dist/src/channels/wechat-channel/client.d.ts +74 -0
- package/dist/src/channels/wechat-channel/client.js +1 -0
- package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
- package/dist/src/channels/wechat-channel/cooldown.js +1 -0
- package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -0
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +37 -0
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -0
- package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
- package/dist/src/channels/wechat-channel/helper-client.js +3 -0
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -0
- package/dist/src/channels/wechat-channel/index.d.ts +17 -0
- package/dist/src/channels/wechat-channel/index.js +1 -0
- package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
- package/dist/src/channels/wechat-channel/ledger.js +1 -0
- package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -0
- package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
- package/dist/src/channels/wechat-channel/message-key.js +1 -0
- package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
- package/dist/src/channels/wechat-channel/observer.js +1 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +69 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -0
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -0
- package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
- package/dist/src/channels/wechat-channel/runner.js +1 -0
- package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
- package/dist/src/channels/wechat-channel/runtime.js +1 -0
- package/dist/src/channels/wechat-channel/scheduler.d.ts +35 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -0
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.d.ts +21 -0
- package/dist/src/channels/wechat-rpa.js +6 -1022
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -389
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.d.ts +10 -0
- package/dist/src/fs/text-decoder.js +1 -0
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1003
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.d.ts +10 -0
- package/dist/src/native-fusion/service.js +2 -198
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -733
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -747
- package/dist/src/session/handlers/session-refresh.js +1 -35
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.d.ts +3 -0
- package/dist/src/session/handlers/tool-detail.js +1 -0
- package/dist/src/session/manager.d.ts +3 -0
- package/dist/src/session/manager.js +1 -261
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.d.ts +4 -0
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
- package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
|
@@ -1,747 +1 @@
|
|
|
1
|
-
// @arch docs/architecture/cli/daemon.md#会话管理
|
|
2
|
-
// @test src/__tests__/session-manager.test.ts
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { convertMarkdownToPdf, defaultPdfOutputPath, MarkdownPdfBrowserMissingError, } from '../../tools/markdown-to-pdf.js';
|
|
7
|
-
import { createZipArchive } from '../archive-zip.js';
|
|
8
|
-
const FILE_SYSTEM_ROOTS_PATH = '__roots__';
|
|
9
|
-
const MAX_FOLDER_UPLOAD_FILES = 2000;
|
|
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;
|
|
13
|
-
function isWindowsAbsolutePath(pathValue) {
|
|
14
|
-
return /^[A-Za-z]:([\\/]|$)/.test(pathValue) || /^\\\\[^\\]+\\[^\\]+/.test(pathValue);
|
|
15
|
-
}
|
|
16
|
-
function pathApiForPath(pathValue) {
|
|
17
|
-
return isWindowsAbsolutePath(pathValue) ? path.win32 : path.posix;
|
|
18
|
-
}
|
|
19
|
-
function makeFsEntry(entryPath) {
|
|
20
|
-
const stat = fs.statSync(entryPath);
|
|
21
|
-
const api = pathApiForPath(entryPath);
|
|
22
|
-
return {
|
|
23
|
-
name: api.basename(entryPath),
|
|
24
|
-
path: entryPath,
|
|
25
|
-
isDir: stat.isDirectory(),
|
|
26
|
-
size: stat.isFile() ? stat.size : undefined,
|
|
27
|
-
modifiedAt: stat.mtimeMs,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function listFileSystemRoots() {
|
|
31
|
-
if (os.platform() === 'win32') {
|
|
32
|
-
const entries = [];
|
|
33
|
-
for (let code = 65; code <= 90; code += 1) {
|
|
34
|
-
const drive = `${String.fromCharCode(code)}:\\`;
|
|
35
|
-
if (fs.existsSync(drive))
|
|
36
|
-
entries.push({ name: drive, path: drive, isDir: true });
|
|
37
|
-
}
|
|
38
|
-
return entries;
|
|
39
|
-
}
|
|
40
|
-
return [{ name: '/', path: '/', isDir: true }];
|
|
41
|
-
}
|
|
42
|
-
function fsErrorMessage(err, fallbackPath) {
|
|
43
|
-
const code = typeof err === 'object' && err && 'code' in err
|
|
44
|
-
? String(err.code)
|
|
45
|
-
: '';
|
|
46
|
-
if (code === 'ENOENT')
|
|
47
|
-
return `Directory not found: ${fallbackPath}`;
|
|
48
|
-
if (code === 'EACCES' || code === 'EPERM')
|
|
49
|
-
return `Permission denied: ${fallbackPath}`;
|
|
50
|
-
return err instanceof Error ? err.message : String(err);
|
|
51
|
-
}
|
|
52
|
-
function isSafeRelativeUploadPath(relativePath) {
|
|
53
|
-
if (!relativePath || relativePath === '.' || relativePath.trim() !== relativePath)
|
|
54
|
-
return false;
|
|
55
|
-
if (path.posix.isAbsolute(relativePath) || path.win32.isAbsolute(relativePath))
|
|
56
|
-
return false;
|
|
57
|
-
const normalized = relativePath.replace(/\\/g, '/');
|
|
58
|
-
if (normalized !== relativePath)
|
|
59
|
-
return false;
|
|
60
|
-
return normalized
|
|
61
|
-
.split('/')
|
|
62
|
-
.every((segment) => segment && segment !== '.' && segment !== '..' && !hasControlChar(segment));
|
|
63
|
-
}
|
|
64
|
-
function isSafeRenameName(name) {
|
|
65
|
-
const trimmed = name.trim();
|
|
66
|
-
if (!trimmed || trimmed !== name)
|
|
67
|
-
return false;
|
|
68
|
-
if (name === '.' || name === '..')
|
|
69
|
-
return false;
|
|
70
|
-
if (hasControlChar(name) || /[\\/:]/.test(name))
|
|
71
|
-
return false;
|
|
72
|
-
if (/[<>|"?*]/.test(name))
|
|
73
|
-
return false;
|
|
74
|
-
if (/[. ]$/.test(name))
|
|
75
|
-
return false;
|
|
76
|
-
const upper = name.split('.')[0]?.toUpperCase();
|
|
77
|
-
if (upper && /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(upper))
|
|
78
|
-
return false;
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
function hasControlChar(value) {
|
|
82
|
-
return Array.from(value).some((char) => {
|
|
83
|
-
const code = char.charCodeAt(0);
|
|
84
|
-
return code >= 0x00 && code <= 0x1f;
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
function decodeManifest(value) {
|
|
88
|
-
if (!Array.isArray(value))
|
|
89
|
-
return [];
|
|
90
|
-
return value.map((item) => ({
|
|
91
|
-
relativePath: String(item.relativePath || ''),
|
|
92
|
-
size: Number(item.size || 0),
|
|
93
|
-
mimeType: item.mimeType,
|
|
94
|
-
modifiedAt: item.modifiedAt,
|
|
95
|
-
}));
|
|
96
|
-
}
|
|
97
|
-
export async function handleFsLs(runtime, req) {
|
|
98
|
-
const requestedPath = req.params.path || os.homedir();
|
|
99
|
-
const rootPath = req.params.rootPath || requestedPath;
|
|
100
|
-
if (requestedPath === FILE_SYSTEM_ROOTS_PATH) {
|
|
101
|
-
runtime.client.sendRes({
|
|
102
|
-
type: 'res',
|
|
103
|
-
id: req.id,
|
|
104
|
-
ok: true,
|
|
105
|
-
payload: { path: FILE_SYSTEM_ROOTS_PATH, entries: listFileSystemRoots() },
|
|
106
|
-
});
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
|
|
110
|
-
if (!resolved.ok) {
|
|
111
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const dirPath = resolved.path;
|
|
115
|
-
try {
|
|
116
|
-
const raw = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
117
|
-
const joinPath = isWindowsAbsolutePath(dirPath) ? path.win32.join : path.join;
|
|
118
|
-
const entries = raw
|
|
119
|
-
.map((entry) => ({
|
|
120
|
-
name: entry.name,
|
|
121
|
-
path: joinPath(dirPath, entry.name),
|
|
122
|
-
isDir: entry.isDirectory(),
|
|
123
|
-
}))
|
|
124
|
-
.sort((left, right) => {
|
|
125
|
-
if (left.isDir !== right.isDir)
|
|
126
|
-
return left.isDir ? -1 : 1;
|
|
127
|
-
return left.name.localeCompare(right.name);
|
|
128
|
-
});
|
|
129
|
-
runtime.client.sendRes({
|
|
130
|
-
type: 'res',
|
|
131
|
-
id: req.id,
|
|
132
|
-
ok: true,
|
|
133
|
-
payload: { path: dirPath, entries },
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
catch (err) {
|
|
137
|
-
runtime.client.sendRes({
|
|
138
|
-
type: 'res',
|
|
139
|
-
id: req.id,
|
|
140
|
-
ok: false,
|
|
141
|
-
error: fsErrorMessage(err, dirPath),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
export async function handleFsRead(runtime, req) {
|
|
146
|
-
const requestedPath = req.params.path;
|
|
147
|
-
const rootPath = req.params.rootPath || requestedPath;
|
|
148
|
-
const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
|
|
149
|
-
if (!resolved.ok) {
|
|
150
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const filePath = resolved.path;
|
|
154
|
-
const encoding = req.params.encoding || 'utf8';
|
|
155
|
-
const offset = req.params.offset;
|
|
156
|
-
const length = req.params.length;
|
|
157
|
-
try {
|
|
158
|
-
const stat = fs.statSync(filePath);
|
|
159
|
-
if (!stat.isFile()) {
|
|
160
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Not a file' });
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (offset != null && length != null) {
|
|
164
|
-
const readLen = Math.min(length, Math.max(0, stat.size - offset));
|
|
165
|
-
if (readLen <= 0) {
|
|
166
|
-
runtime.client.sendRes({
|
|
167
|
-
type: 'res',
|
|
168
|
-
id: req.id,
|
|
169
|
-
ok: true,
|
|
170
|
-
payload: { data: '', offset, length: 0, totalSize: stat.size, path: filePath },
|
|
171
|
-
});
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const fd = fs.openSync(filePath, 'r');
|
|
175
|
-
try {
|
|
176
|
-
const buffer = Buffer.alloc(readLen);
|
|
177
|
-
const bytesRead = fs.readSync(fd, buffer, 0, readLen, offset);
|
|
178
|
-
runtime.client.sendRes({
|
|
179
|
-
type: 'res',
|
|
180
|
-
id: req.id,
|
|
181
|
-
ok: true,
|
|
182
|
-
payload: {
|
|
183
|
-
data: buffer.subarray(0, bytesRead).toString('base64'),
|
|
184
|
-
offset,
|
|
185
|
-
length: bytesRead,
|
|
186
|
-
totalSize: stat.size,
|
|
187
|
-
path: filePath,
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
finally {
|
|
192
|
-
fs.closeSync(fd);
|
|
193
|
-
}
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
const maxSize = req.params.maxSize || (encoding === 'base64' ? 5 * 1024 * 1024 : 512 * 1024);
|
|
197
|
-
if (stat.size > maxSize) {
|
|
198
|
-
runtime.client.sendRes({
|
|
199
|
-
type: 'res',
|
|
200
|
-
id: req.id,
|
|
201
|
-
ok: false,
|
|
202
|
-
error: `File too large: ${stat.size} bytes (max ${maxSize})`,
|
|
203
|
-
payload: { size: stat.size, path: filePath },
|
|
204
|
-
});
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (encoding === 'base64') {
|
|
208
|
-
const buffer = fs.readFileSync(filePath);
|
|
209
|
-
runtime.client.sendRes({
|
|
210
|
-
type: 'res',
|
|
211
|
-
id: req.id,
|
|
212
|
-
ok: true,
|
|
213
|
-
payload: { data: buffer.toString('base64'), path: filePath, size: stat.size },
|
|
214
|
-
});
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
218
|
-
runtime.client.sendRes({
|
|
219
|
-
type: 'res',
|
|
220
|
-
id: req.id,
|
|
221
|
-
ok: true,
|
|
222
|
-
payload: { content, path: filePath, size: stat.size },
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
catch (err) {
|
|
226
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
export async function handleFsWrite(runtime, req) {
|
|
230
|
-
const requestedPath = req.params.path;
|
|
231
|
-
const content = req.params.content;
|
|
232
|
-
const rootPath = req.params.rootPath || requestedPath;
|
|
233
|
-
if (!requestedPath || typeof content !== 'string') {
|
|
234
|
-
runtime.client.sendRes({
|
|
235
|
-
type: 'res',
|
|
236
|
-
id: req.id,
|
|
237
|
-
ok: false,
|
|
238
|
-
error: 'path and content are required',
|
|
239
|
-
});
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
|
|
243
|
-
if (!resolved.ok) {
|
|
244
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
const existing = fs.existsSync(resolved.path) ? fs.statSync(resolved.path) : null;
|
|
249
|
-
if (existing && !existing.isFile()) {
|
|
250
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Not a file' });
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
fs.writeFileSync(resolved.path, content, 'utf-8');
|
|
254
|
-
const stat = fs.statSync(resolved.path);
|
|
255
|
-
runtime.client.sendRes({
|
|
256
|
-
type: 'res',
|
|
257
|
-
id: req.id,
|
|
258
|
-
ok: true,
|
|
259
|
-
payload: { path: resolved.path, size: stat.size, modifiedAt: stat.mtimeMs },
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
catch (err) {
|
|
263
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
export async function handleFsRename(runtime, req) {
|
|
267
|
-
const requestedPath = req.params.path;
|
|
268
|
-
const newName = req.params.newName;
|
|
269
|
-
if (!requestedPath || !newName) {
|
|
270
|
-
runtime.client.sendRes({
|
|
271
|
-
type: 'res',
|
|
272
|
-
id: req.id,
|
|
273
|
-
ok: false,
|
|
274
|
-
error: 'path and newName are required',
|
|
275
|
-
});
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
if (!isSafeRenameName(newName)) {
|
|
279
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Invalid newName' });
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
const api = pathApiForPath(requestedPath);
|
|
283
|
-
const rootPath = req.params.rootPath || api.dirname(requestedPath);
|
|
284
|
-
const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
|
|
285
|
-
if (!resolved.ok) {
|
|
286
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
try {
|
|
290
|
-
const api = pathApiForPath(resolved.path);
|
|
291
|
-
const newPath = api.join(api.dirname(resolved.path), newName);
|
|
292
|
-
const checkedNewPath = runtime.resolveAuthorizedPath(newPath, rootPath);
|
|
293
|
-
if (!checkedNewPath.ok) {
|
|
294
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: checkedNewPath.error });
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
if (fs.existsSync(checkedNewPath.path)) {
|
|
298
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Target already exists' });
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
fs.renameSync(resolved.path, checkedNewPath.path);
|
|
302
|
-
runtime.client.sendRes({
|
|
303
|
-
type: 'res',
|
|
304
|
-
id: req.id,
|
|
305
|
-
ok: true,
|
|
306
|
-
payload: {
|
|
307
|
-
oldPath: resolved.path,
|
|
308
|
-
newPath: checkedNewPath.path,
|
|
309
|
-
entry: makeFsEntry(checkedNewPath.path),
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
catch (err) {
|
|
314
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
export async function handleFsExportMarkdownPdf(runtime, req) {
|
|
318
|
-
const requestedPath = req.params.path;
|
|
319
|
-
const rootPath = req.params.rootPath || path.dirname(requestedPath || '.');
|
|
320
|
-
const title = typeof req.params.title === 'string' ? req.params.title : undefined;
|
|
321
|
-
if (!requestedPath) {
|
|
322
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'path is required' });
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
|
|
326
|
-
if (!resolved.ok) {
|
|
327
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (!/\.mdx?$/i.test(resolved.path)) {
|
|
331
|
-
runtime.client.sendRes({
|
|
332
|
-
type: 'res',
|
|
333
|
-
id: req.id,
|
|
334
|
-
ok: false,
|
|
335
|
-
error: 'Only Markdown files can be exported to PDF',
|
|
336
|
-
});
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
const outputPath = defaultPdfOutputPath(resolved.path);
|
|
340
|
-
const checkedOutput = runtime.resolveAuthorizedPath(outputPath, rootPath);
|
|
341
|
-
if (!checkedOutput.ok) {
|
|
342
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: checkedOutput.error });
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
try {
|
|
346
|
-
const result = await convertMarkdownToPdf(resolved.path, {
|
|
347
|
-
outputPath: checkedOutput.path,
|
|
348
|
-
title,
|
|
349
|
-
});
|
|
350
|
-
runtime.client.sendRes({
|
|
351
|
-
type: 'res',
|
|
352
|
-
id: req.id,
|
|
353
|
-
ok: true,
|
|
354
|
-
payload: {
|
|
355
|
-
sourcePath: resolved.path,
|
|
356
|
-
outputPath: result.outputPath,
|
|
357
|
-
entry: makeFsEntry(result.outputPath),
|
|
358
|
-
},
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
catch (err) {
|
|
362
|
-
if (err instanceof MarkdownPdfBrowserMissingError) {
|
|
363
|
-
runtime.client.sendRes({
|
|
364
|
-
type: 'res',
|
|
365
|
-
id: req.id,
|
|
366
|
-
ok: false,
|
|
367
|
-
error: 'This machine needs the PDF export component before Markdown files can be exported to PDF.',
|
|
368
|
-
payload: err.setup,
|
|
369
|
-
});
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
runtime.client.sendRes({
|
|
373
|
-
type: 'res',
|
|
374
|
-
id: req.id,
|
|
375
|
-
ok: false,
|
|
376
|
-
error: err instanceof Error ? err.message : String(err),
|
|
377
|
-
});
|
|
378
|
-
}
|
|
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
|
-
}
|
|
432
|
-
export async function handleFsTransfer(runtime, req) {
|
|
433
|
-
const { name, targetPath, data, direct } = req.params;
|
|
434
|
-
if (!name || !data) {
|
|
435
|
-
runtime.client.sendRes({
|
|
436
|
-
type: 'res',
|
|
437
|
-
id: req.id,
|
|
438
|
-
ok: false,
|
|
439
|
-
error: 'name and data are required',
|
|
440
|
-
});
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
try {
|
|
444
|
-
const rootPath = req.params.rootPath || targetPath || os.homedir();
|
|
445
|
-
const resolved = runtime.resolveAuthorizedPath(targetPath || rootPath, rootPath);
|
|
446
|
-
if (!resolved.ok) {
|
|
447
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
const baseDir = resolved.path;
|
|
451
|
-
const uploadDir = direct ? baseDir : path.join(baseDir, '.uploads');
|
|
452
|
-
if (!direct)
|
|
453
|
-
fs.mkdirSync(uploadDir, { recursive: true });
|
|
454
|
-
const filePath = path.join(uploadDir, path.basename(name));
|
|
455
|
-
const buffer = Buffer.from(data, 'base64');
|
|
456
|
-
fs.writeFileSync(filePath, buffer);
|
|
457
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { path: filePath } });
|
|
458
|
-
}
|
|
459
|
-
catch (err) {
|
|
460
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
export async function handleFsTransferStart(runtime, req) {
|
|
464
|
-
const { name, targetPath, totalSize, direct, kind, baseName } = req.params;
|
|
465
|
-
const manifest = decodeManifest(req.params.manifest);
|
|
466
|
-
const isFolder = kind === 'folder';
|
|
467
|
-
const transferName = isFolder ? baseName || name : name;
|
|
468
|
-
if (!transferName || (!isFolder && !totalSize)) {
|
|
469
|
-
runtime.client.sendRes({
|
|
470
|
-
type: 'res',
|
|
471
|
-
id: req.id,
|
|
472
|
-
ok: false,
|
|
473
|
-
error: 'name and totalSize are required',
|
|
474
|
-
});
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
try {
|
|
478
|
-
const rootPath = req.params.rootPath || targetPath || os.homedir();
|
|
479
|
-
const resolved = runtime.resolveAuthorizedPath(targetPath || rootPath, rootPath);
|
|
480
|
-
if (!resolved.ok) {
|
|
481
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
const baseDir = resolved.path;
|
|
485
|
-
const destinationDir = direct ? baseDir : path.join(baseDir, '.uploads');
|
|
486
|
-
if (!direct)
|
|
487
|
-
fs.mkdirSync(destinationDir, { recursive: true });
|
|
488
|
-
const transferId = `tf-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
489
|
-
if (isFolder) {
|
|
490
|
-
if (!manifest.length) {
|
|
491
|
-
runtime.client.sendRes({
|
|
492
|
-
type: 'res',
|
|
493
|
-
id: req.id,
|
|
494
|
-
ok: false,
|
|
495
|
-
error: 'manifest is required for folder uploads',
|
|
496
|
-
});
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
if (manifest.length > MAX_FOLDER_UPLOAD_FILES) {
|
|
500
|
-
runtime.client.sendRes({
|
|
501
|
-
type: 'res',
|
|
502
|
-
id: req.id,
|
|
503
|
-
ok: false,
|
|
504
|
-
error: `Too many files: ${manifest.length}`,
|
|
505
|
-
});
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
const folderName = path.basename(transferName);
|
|
509
|
-
if (!isSafeRenameName(folderName)) {
|
|
510
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Invalid folder name' });
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
const targetDir = path.join(destinationDir, folderName);
|
|
514
|
-
const checkedTargetDir = runtime.resolveAuthorizedPath(targetDir, rootPath);
|
|
515
|
-
if (!checkedTargetDir.ok) {
|
|
516
|
-
runtime.client.sendRes({
|
|
517
|
-
type: 'res',
|
|
518
|
-
id: req.id,
|
|
519
|
-
ok: false,
|
|
520
|
-
error: checkedTargetDir.error,
|
|
521
|
-
});
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
const seen = new Set();
|
|
525
|
-
let aggregateSize = 0;
|
|
526
|
-
const files = new Map();
|
|
527
|
-
for (const item of manifest) {
|
|
528
|
-
if (!isSafeRelativeUploadPath(item.relativePath)) {
|
|
529
|
-
runtime.client.sendRes({
|
|
530
|
-
type: 'res',
|
|
531
|
-
id: req.id,
|
|
532
|
-
ok: false,
|
|
533
|
-
error: `Invalid relativePath: ${item.relativePath}`,
|
|
534
|
-
});
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
if (!Number.isFinite(item.size) || item.size < 0) {
|
|
538
|
-
runtime.client.sendRes({
|
|
539
|
-
type: 'res',
|
|
540
|
-
id: req.id,
|
|
541
|
-
ok: false,
|
|
542
|
-
error: `Invalid size: ${item.relativePath}`,
|
|
543
|
-
});
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
if (seen.has(item.relativePath)) {
|
|
547
|
-
runtime.client.sendRes({
|
|
548
|
-
type: 'res',
|
|
549
|
-
id: req.id,
|
|
550
|
-
ok: false,
|
|
551
|
-
error: `Duplicate relativePath: ${item.relativePath}`,
|
|
552
|
-
});
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
seen.add(item.relativePath);
|
|
556
|
-
aggregateSize += item.size;
|
|
557
|
-
if (aggregateSize > MAX_FOLDER_UPLOAD_TOTAL_SIZE) {
|
|
558
|
-
runtime.client.sendRes({
|
|
559
|
-
type: 'res',
|
|
560
|
-
id: req.id,
|
|
561
|
-
ok: false,
|
|
562
|
-
error: `Folder too large: ${aggregateSize} bytes`,
|
|
563
|
-
});
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
const finalPath = path.join(checkedTargetDir.path, ...item.relativePath.split('/'));
|
|
567
|
-
const checkedFinalPath = runtime.resolveAuthorizedPath(finalPath, rootPath);
|
|
568
|
-
if (!checkedFinalPath.ok) {
|
|
569
|
-
runtime.client.sendRes({
|
|
570
|
-
type: 'res',
|
|
571
|
-
id: req.id,
|
|
572
|
-
ok: false,
|
|
573
|
-
error: checkedFinalPath.error,
|
|
574
|
-
});
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
const tempPath = path.join(os.tmpdir(), `.shennian-upload-${transferId}-${files.size}`);
|
|
578
|
-
fs.writeFileSync(tempPath, Buffer.alloc(0));
|
|
579
|
-
files.set(item.relativePath, {
|
|
580
|
-
relativePath: item.relativePath,
|
|
581
|
-
tempPath,
|
|
582
|
-
targetPath: checkedFinalPath.path,
|
|
583
|
-
size: item.size,
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
runtime.pendingTransfers.set(transferId, {
|
|
587
|
-
tempPath: '',
|
|
588
|
-
targetPath: checkedTargetDir.path,
|
|
589
|
-
totalSize: aggregateSize,
|
|
590
|
-
kind: 'folder',
|
|
591
|
-
rootPath,
|
|
592
|
-
targetDir: checkedTargetDir.path,
|
|
593
|
-
files,
|
|
594
|
-
});
|
|
595
|
-
runtime.client.sendRes({
|
|
596
|
-
type: 'res',
|
|
597
|
-
id: req.id,
|
|
598
|
-
ok: true,
|
|
599
|
-
payload: { transferId, path: checkedTargetDir.path },
|
|
600
|
-
});
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
const tempPath = path.join(os.tmpdir(), `.shennian-upload-${transferId}`);
|
|
604
|
-
const finalPath = path.join(destinationDir, path.basename(transferName));
|
|
605
|
-
fs.writeFileSync(tempPath, Buffer.alloc(0));
|
|
606
|
-
runtime.pendingTransfers.set(transferId, {
|
|
607
|
-
tempPath,
|
|
608
|
-
targetPath: finalPath,
|
|
609
|
-
totalSize,
|
|
610
|
-
kind: 'file',
|
|
611
|
-
});
|
|
612
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { transferId } });
|
|
613
|
-
}
|
|
614
|
-
catch (err) {
|
|
615
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
export async function handleFsTransferChunk(runtime, req) {
|
|
619
|
-
const { transferId, offset, data, relativePath } = req.params;
|
|
620
|
-
const transfer = runtime.pendingTransfers.get(transferId);
|
|
621
|
-
if (!transfer) {
|
|
622
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Invalid transferId' });
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
try {
|
|
626
|
-
const target = transfer.kind === 'folder'
|
|
627
|
-
? transfer.files?.get(String(relativePath || ''))
|
|
628
|
-
: { tempPath: transfer.tempPath, size: transfer.totalSize };
|
|
629
|
-
if (!target) {
|
|
630
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Invalid relativePath' });
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
const buffer = Buffer.from(data, 'base64');
|
|
634
|
-
if (offset < 0 || offset + buffer.length > target.size) {
|
|
635
|
-
runtime.client.sendRes({
|
|
636
|
-
type: 'res',
|
|
637
|
-
id: req.id,
|
|
638
|
-
ok: false,
|
|
639
|
-
error: 'Chunk exceeds declared size',
|
|
640
|
-
});
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
const fd = fs.openSync(target.tempPath, 'r+');
|
|
644
|
-
try {
|
|
645
|
-
fs.writeSync(fd, buffer, 0, buffer.length, offset);
|
|
646
|
-
}
|
|
647
|
-
finally {
|
|
648
|
-
fs.closeSync(fd);
|
|
649
|
-
}
|
|
650
|
-
runtime.client.sendRes({
|
|
651
|
-
type: 'res',
|
|
652
|
-
id: req.id,
|
|
653
|
-
ok: true,
|
|
654
|
-
payload: { written: buffer.length },
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
catch (err) {
|
|
658
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
export async function handleFsTransferFinish(runtime, req) {
|
|
662
|
-
const { transferId } = req.params;
|
|
663
|
-
const transfer = runtime.pendingTransfers.get(transferId);
|
|
664
|
-
if (!transfer) {
|
|
665
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Invalid transferId' });
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
try {
|
|
669
|
-
if (transfer.kind === 'folder') {
|
|
670
|
-
let count = 0;
|
|
671
|
-
for (const file of transfer.files?.values() ?? []) {
|
|
672
|
-
fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
|
|
673
|
-
fs.renameSync(file.tempPath, file.targetPath);
|
|
674
|
-
count += 1;
|
|
675
|
-
}
|
|
676
|
-
runtime.pendingTransfers.delete(transferId);
|
|
677
|
-
runtime.client.sendRes({
|
|
678
|
-
type: 'res',
|
|
679
|
-
id: req.id,
|
|
680
|
-
ok: true,
|
|
681
|
-
payload: { path: transfer.targetDir ?? transfer.targetPath, count },
|
|
682
|
-
});
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
fs.renameSync(transfer.tempPath, transfer.targetPath);
|
|
686
|
-
runtime.pendingTransfers.delete(transferId);
|
|
687
|
-
runtime.client.sendRes({
|
|
688
|
-
type: 'res',
|
|
689
|
-
id: req.id,
|
|
690
|
-
ok: true,
|
|
691
|
-
payload: { path: transfer.targetPath },
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
catch {
|
|
695
|
-
try {
|
|
696
|
-
fs.copyFileSync(transfer.tempPath, transfer.targetPath);
|
|
697
|
-
fs.unlinkSync(transfer.tempPath);
|
|
698
|
-
runtime.pendingTransfers.delete(transferId);
|
|
699
|
-
runtime.client.sendRes({
|
|
700
|
-
type: 'res',
|
|
701
|
-
id: req.id,
|
|
702
|
-
ok: true,
|
|
703
|
-
payload: { path: transfer.targetPath },
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
catch (err) {
|
|
707
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
export async function handleFsTransferAbort(runtime, req) {
|
|
712
|
-
const { transferId } = req.params;
|
|
713
|
-
const transfer = runtime.pendingTransfers.get(transferId);
|
|
714
|
-
if (transfer) {
|
|
715
|
-
const tempPaths = transfer.kind === 'folder'
|
|
716
|
-
? Array.from(transfer.files?.values() ?? []).map((file) => file.tempPath)
|
|
717
|
-
: [transfer.tempPath];
|
|
718
|
-
for (const tempPath of tempPaths) {
|
|
719
|
-
try {
|
|
720
|
-
if (tempPath)
|
|
721
|
-
fs.unlinkSync(tempPath);
|
|
722
|
-
}
|
|
723
|
-
catch {
|
|
724
|
-
// ignore cleanup failures
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
runtime.pendingTransfers.delete(transferId);
|
|
728
|
-
}
|
|
729
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
|
|
730
|
-
}
|
|
731
|
-
export function cleanupPendingTransfers(runtime) {
|
|
732
|
-
for (const [, transfer] of runtime.pendingTransfers) {
|
|
733
|
-
const tempPaths = transfer.kind === 'folder'
|
|
734
|
-
? Array.from(transfer.files?.values() ?? []).map((file) => file.tempPath)
|
|
735
|
-
: [transfer.tempPath];
|
|
736
|
-
for (const tempPath of tempPaths) {
|
|
737
|
-
try {
|
|
738
|
-
if (tempPath)
|
|
739
|
-
fs.unlinkSync(tempPath);
|
|
740
|
-
}
|
|
741
|
-
catch {
|
|
742
|
-
// ignore cleanup failures
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
runtime.pendingTransfers.clear();
|
|
747
|
-
}
|
|
1
|
+
import d from"node:fs";import g from"node:os";import p from"node:path";import{convertMarkdownToPdf as C,defaultPdfOutputPath as j,MarkdownPdfBrowserMissingError as O}from"../../tools/markdown-to-pdf.js";import{createZipArchive as L}from"../archive-zip.js";import{BinaryTextPreviewError as B,decodeTextBufferAuto as U,isAutoTextEncoding as X}from"../../fs/text-decoder.js";const D="__roots__",Z=2e3,H=1024*1024*1024,W=5e3,V=1024*1024*1024;function E(e){return/^[A-Za-z]:([\\/]|$)/.test(e)||/^\\\\[^\\]+\\[^\\]+/.test(e)}function v(e){return E(e)?p.win32:p.posix}function T(e){const t=d.statSync(e);return{name:v(e).basename(e),path:e,isDir:t.isDirectory(),size:t.isFile()?t.size:void 0,modifiedAt:t.mtimeMs}}function Y(){if(g.platform()==="win32"){const e=[];for(let t=65;t<=90;t+=1){const r=`${String.fromCharCode(t)}:\\`;d.existsSync(r)&&e.push({name:r,path:r,isDir:!0})}return e}return[{name:"/",path:"/",isDir:!0}]}function G(e,t){const r=typeof e=="object"&&e&&"code"in e?String(e.code):"";return r==="ENOENT"?`Directory not found: ${t}`:r==="EACCES"||r==="EPERM"?`Permission denied: ${t}`:e instanceof Error?e.message:String(e)}function J(e){if(!e||e==="."||e.trim()!==e||p.posix.isAbsolute(e)||p.win32.isAbsolute(e))return!1;const t=e.replace(/\\/g,"/");return t!==e?!1:t.split("/").every(r=>r&&r!=="."&&r!==".."&&!M(r))}function $(e){const t=e.trim();if(!t||t!==e||e==="."||e===".."||M(e)||/[\\/:]/.test(e)||/[<>|"?*]/.test(e)||/[. ]$/.test(e))return!1;const r=e.split(".")[0]?.toUpperCase();return!(r&&/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(r))}function M(e){return Array.from(e).some(t=>{const r=t.charCodeAt(0);return r>=0&&r<=31})}function K(e){return Array.isArray(e)?e.map(t=>({relativePath:String(t.relativePath||""),size:Number(t.size||0),mimeType:t.mimeType,modifiedAt:t.modifiedAt})):[]}async function ne(e,t){const r=t.params.path||g.homedir(),a=t.params.rootPath||r;if(r===D){e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:D,entries:Y()}});return}const n=e.resolveAuthorizedPath(r,a);if(!n.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:n.error});return}const s=n.path;try{const o=d.readdirSync(s,{withFileTypes:!0}),c=E(s)?p.win32.join:p.join,i=o.map(l=>({name:l.name,path:c(s,l.name),isDir:l.isDirectory()})).sort((l,f)=>l.isDir!==f.isDir?l.isDir?-1:1:l.name.localeCompare(f.name));e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:s,entries:i}})}catch(o){e.client.sendRes({type:"res",id:t.id,ok:!1,error:G(o,s)})}}async function oe(e,t){const r=t.params.path,a=t.params.rootPath||r,n=e.resolveAuthorizedPath(r,a);if(!n.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:n.error});return}const s=n.path,o=t.params.encoding,c=typeof o=="string"?o:"utf8",i=t.params.offset,l=t.params.length;try{const f=d.statSync(s);if(!f.isFile()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a file"});return}if(i!=null&&l!=null){const u=Math.min(l,Math.max(0,f.size-i));if(u<=0){e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:"",offset:i,length:0,totalSize:f.size,path:s}});return}const P=d.openSync(s,"r");try{const k=Buffer.alloc(u),S=d.readSync(P,k,0,u,i);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:k.subarray(0,S).toString("base64"),offset:i,length:S,totalSize:f.size,path:s}})}finally{d.closeSync(P)}return}const y=t.params.maxSize||(c==="base64"?5*1024*1024:512*1024);if(f.size>y){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`File too large: ${f.size} bytes (max ${y})`,payload:{size:f.size,path:s}});return}if(c==="base64"){const u=d.readFileSync(s);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:u.toString("base64"),path:s,size:f.size}});return}if(X(c)){const u=d.readFileSync(s),P=U(u);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{content:P.content,path:s,size:f.size,encoding:P.encoding,encodingFallback:P.fallback}});return}const m=d.readFileSync(s,"utf-8");e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{content:m,path:s,size:f.size}})}catch(f){e.client.sendRes({type:"res",id:t.id,ok:!1,error:f instanceof B?f.message:String(f)})}}async function ie(e,t){const r=t.params.path,a=t.params.content,n=t.params.rootPath||r;if(!r||typeof a!="string"){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path and content are required"});return}const s=e.resolveAuthorizedPath(r,n);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}try{const o=d.existsSync(s.path)?d.statSync(s.path):null;if(o&&!o.isFile()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a file"});return}d.writeFileSync(s.path,a,"utf-8");const c=d.statSync(s.path);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:s.path,size:c.size,modifiedAt:c.mtimeMs}})}catch(o){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(o)})}}async function de(e,t){const r=t.params.path,a=t.params.newName;if(!r||!a){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path and newName are required"});return}if(!$(a)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid newName"});return}const n=v(r),s=t.params.rootPath||n.dirname(r),o=e.resolveAuthorizedPath(r,s);if(!o.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:o.error});return}try{const c=v(o.path),i=c.join(c.dirname(o.path),a),l=e.resolveAuthorizedPath(i,s);if(!l.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:l.error});return}if(d.existsSync(l.path)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Target already exists"});return}d.renameSync(o.path,l.path),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{oldPath:o.path,newPath:l.path,entry:T(l.path)}})}catch(c){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(c)})}}async function ce(e,t){const r=t.params.path,a=t.params.rootPath||p.dirname(r||"."),n=typeof t.params.title=="string"?t.params.title:void 0;if(!r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path is required"});return}const s=e.resolveAuthorizedPath(r,a);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}if(!/\.mdx?$/i.test(s.path)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Only Markdown files can be exported to PDF"});return}const o=j(s.path),c=e.resolveAuthorizedPath(o,a);if(!c.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:c.error});return}try{const i=await C(s.path,{outputPath:c.path,title:n});e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{sourcePath:s.path,outputPath:i.outputPath,entry:T(i.outputPath)}})}catch(i){if(i instanceof O){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"This machine needs the PDF export component before Markdown files can be exported to PDF.",payload:i.setup});return}e.client.sendRes({type:"res",id:t.id,ok:!1,error:i instanceof Error?i.message:String(i)})}}function Q(e){let t="";for(const a of e.trim())a.charCodeAt(0)<=31||'<>:"/\\|?*'.includes(a)?t.endsWith("-")||(t+="-"):t+=a;return t.replace(/[. ]+$/g,"")||"folder"}async function le(e,t){const r=t.params.path,a=t.params.rootPath||r;if(!r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path is required"});return}const n=e.resolveAuthorizedPath(r,a);if(!n.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:n.error});return}try{if(!d.statSync(n.path).isDirectory()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a directory"});return}const o=v(n.path),c=Q(o.basename(n.path)),i=p.join(g.tmpdir(),"shennian-archives"),l=p.join(i,`${c}-${Date.now()}-${Math.random().toString(36).slice(2,8)}.zip`),f=L(n.path,l,{maxFiles:W,maxTotalSize:V});e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{sourcePath:n.path,outputPath:f.outputPath,entry:T(f.outputPath),fileCount:f.fileCount,totalSize:f.totalSize}})}catch(s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s instanceof Error?s.message:String(s)})}}async function fe(e,t){const{name:r,targetPath:a,data:n,direct:s}=t.params;if(!r||!n){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"name and data are required"});return}try{const o=t.params.rootPath||a||g.homedir(),c=e.resolveAuthorizedPath(a||o,o);if(!c.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:c.error});return}const i=c.path,l=s?i:p.join(i,".uploads");s||d.mkdirSync(l,{recursive:!0});const f=p.join(l,p.basename(r)),y=Buffer.from(n,"base64");d.writeFileSync(f,y),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:f}})}catch(o){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(o)})}}async function pe(e,t){const{name:r,targetPath:a,totalSize:n,direct:s,kind:o,baseName:c}=t.params,i=K(t.params.manifest),l=o==="folder",f=l&&c||r;if(!f||!l&&!n){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"name and totalSize are required"});return}try{const y=t.params.rootPath||a||g.homedir(),m=e.resolveAuthorizedPath(a||y,y);if(!m.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:m.error});return}const u=m.path,P=s?u:p.join(u,".uploads");s||d.mkdirSync(P,{recursive:!0});const k=`tf-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;if(l){if(!i.length){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"manifest is required for folder uploads"});return}if(i.length>Z){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`Too many files: ${i.length}`});return}const x=p.basename(f);if(!$(x)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid folder name"});return}const I=p.join(P,x),R=e.resolveAuthorizedPath(I,y);if(!R.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:R.error});return}const b=new Set;let z=0;const A=new Map;for(const h of i){if(!J(h.relativePath)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`Invalid relativePath: ${h.relativePath}`});return}if(!Number.isFinite(h.size)||h.size<0){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`Invalid size: ${h.relativePath}`});return}if(b.has(h.relativePath)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`Duplicate relativePath: ${h.relativePath}`});return}if(b.add(h.relativePath),z+=h.size,z>H){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`Folder too large: ${z} bytes`});return}const _=p.join(R.path,...h.relativePath.split("/")),F=e.resolveAuthorizedPath(_,y);if(!F.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:F.error});return}const w=p.join(g.tmpdir(),`.shennian-upload-${k}-${A.size}`);d.writeFileSync(w,Buffer.alloc(0)),A.set(h.relativePath,{relativePath:h.relativePath,tempPath:w,targetPath:F.path,size:h.size})}e.pendingTransfers.set(k,{tempPath:"",targetPath:R.path,totalSize:z,kind:"folder",rootPath:y,targetDir:R.path,files:A}),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{transferId:k,path:R.path}});return}const S=p.join(g.tmpdir(),`.shennian-upload-${k}`),N=p.join(P,p.basename(f));d.writeFileSync(S,Buffer.alloc(0)),e.pendingTransfers.set(k,{tempPath:S,targetPath:N,totalSize:n,kind:"file"}),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{transferId:k}})}catch(y){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(y)})}}async function he(e,t){const{transferId:r,offset:a,data:n,relativePath:s}=t.params,o=e.pendingTransfers.get(r);if(!o){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid transferId"});return}try{const c=o.kind==="folder"?o.files?.get(String(s||"")):{tempPath:o.tempPath,size:o.totalSize};if(!c){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid relativePath"});return}const i=Buffer.from(n,"base64");if(a<0||a+i.length>c.size){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Chunk exceeds declared size"});return}const l=d.openSync(c.tempPath,"r+");try{d.writeSync(l,i,0,i.length,a)}finally{d.closeSync(l)}e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{written:i.length}})}catch(c){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(c)})}}async function ye(e,t){const{transferId:r}=t.params,a=e.pendingTransfers.get(r);if(!a){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid transferId"});return}try{if(a.kind==="folder"){let n=0;for(const s of a.files?.values()??[])d.mkdirSync(p.dirname(s.targetPath),{recursive:!0}),d.renameSync(s.tempPath,s.targetPath),n+=1;e.pendingTransfers.delete(r),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:a.targetDir??a.targetPath,count:n}});return}d.renameSync(a.tempPath,a.targetPath),e.pendingTransfers.delete(r),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:a.targetPath}})}catch{try{d.copyFileSync(a.tempPath,a.targetPath),d.unlinkSync(a.tempPath),e.pendingTransfers.delete(r),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:a.targetPath}})}catch(n){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(n)})}}}async function ue(e,t){const{transferId:r}=t.params,a=e.pendingTransfers.get(r);if(a){const n=a.kind==="folder"?Array.from(a.files?.values()??[]).map(s=>s.tempPath):[a.tempPath];for(const s of n)try{s&&d.unlinkSync(s)}catch{}e.pendingTransfers.delete(r)}e.client.sendRes({type:"res",id:t.id,ok:!0})}function Pe(e){for(const[,t]of e.pendingTransfers){const r=t.kind==="folder"?Array.from(t.files?.values()??[]).map(a=>a.tempPath):[t.tempPath];for(const a of r)try{a&&d.unlinkSync(a)}catch{}}e.pendingTransfers.clear()}export{Pe as cleanupPendingTransfers,le as handleFsArchiveZip,ce as handleFsExportMarkdownPdf,ne as handleFsLs,oe as handleFsRead,de as handleFsRename,fe as handleFsTransfer,ue as handleFsTransferAbort,he as handleFsTransferChunk,ye as handleFsTransferFinish,pe as handleFsTransferStart,ie as handleFsWrite};
|