// DSH 动态 Cordis 插件 —— 文件拖拽上传(Host 半) // 本文件是 cordis_define 的 code.host 函数体:直接整文件复制使用。 // 职责:接收 Client 上传的 base64 文件内容,写入系统临时目录下的 dsh-file-drop/ 目录(便于系统自动清理)。 return { apply(ctx) { harness.handle('drop-upload', async (args) => { const fs = ctx.get('fs') const shell = ctx.get('shell') const sessions = ctx.get('sessions') const sandboxPolicy = ctx.get('sandboxPolicy') const isObj = (v) => v !== null && typeof v === 'object' const session = sessions !== undefined && isObj(args) && typeof args.sessionId === 'string' ? sessions.get(args.sessionId) : undefined let policy = undefined if (sandboxPolicy !== undefined) { policy = sandboxPolicy.resolve(session !== undefined ? { session } : {}) } let root = policy !== undefined && typeof policy.workspaceRoot === 'string' ? policy.workspaceRoot : (session !== undefined && typeof session.header.cwd === 'string' ? session.header.cwd : undefined) if (typeof root !== 'string' || root === '') return { ok: false, error: 'Cannot resolve workspace root' } const execPolicy = policy !== undefined && policy.mode !== undefined ? { mode: policy.mode, workspaceRoot: root } : undefined // 平台探测(沙箱里没有 process.platform):Windows 的 workspace/cwd 以盘符或 UNC 开头。 // DSH 在 Windows 上挂载 pwsh shell 栈、在 POSIX 上挂载 bash 栈,命令方言与路径分隔符都要跟随。 const isWindows = /^[A-Za-z]:[\\/]/.test(root) || /^[\\/]{2}/.test(root) const sep = isWindows ? '\\' : '/' const files = isObj(args) && Array.isArray(args.files) ? args.files : [] if (files.length === 0) return { ok: false, error: 'No files to save' } if (files.length > 6) return { ok: false, error: 'Too many files (max 6)' } if (fs === undefined) return { ok: false, error: 'Filesystem service unavailable' } if (shell === undefined) return { ok: false, error: 'Shell service unavailable' } let dropDir = '' try { // 一条命令完成:取系统临时目录 → 创建 dsh-file-drop 子目录 → 输出绝对路径。 // POSIX(Linux/macOS):$TMPDIR 缺省 /tmp;Windows:%TEMP%(与 Node os.tmpdir() 同源)。 // workspace-write 模式下系统临时目录与 /tmp 都在沙箱可写根集合内,与 fs 围栏的 writableRoots 一致。 const makeCommand = isWindows ? '$dir = Join-Path ([System.IO.Path]::GetTempPath()) "dsh-file-drop"; New-Item -ItemType Directory -Force -Path $dir | Out-Null; Write-Output $dir' : 'base="${TMPDIR:-/tmp}"; base="${base%/}"; dir="$base/dsh-file-drop"; mkdir -p "$dir" && printf %s "$dir"' const spec = shell.resolve({ command: makeCommand, workdir: root, sandboxPolicy: execPolicy, }) const made = await shell.run(spec) if (made.exitCode !== 0) { return { ok: false, error: 'Failed to create dsh-file-drop directory (exit ' + String(made.exitCode) + ')' } } dropDir = isObj(made.stdout) && typeof made.stdout.text === 'string' ? made.stdout.text.trim() : '' if (dropDir === '') return { ok: false, error: 'Failed to resolve system temp directory' } } catch (err) { return { ok: false, error: 'Failed to create dsh-file-drop directory: ' + messageOf(err) } } const saved = [] for (const item of files) { const base = sanitizeName(isObj(item) && typeof item.name === 'string' ? item.name : '') if (base === '') { saved.push({ name: '', ok: false, error: 'Invalid file name' }) continue } if (isObj(item) && item.binary === true) { saved.push({ name: base, ok: false, error: 'Binary files are not supported' }) continue } const b64 = isObj(item) && typeof item.contentBase64 === 'string' ? item.contentBase64 : '' if (b64 === '') { saved.push({ name: base, ok: false, error: 'Empty content' }) continue } let content = '' try { content = atob(b64) } catch (err) { saved.push({ name: base, ok: false, error: 'Invalid content encoding' }) continue } let name = base let n = 2 while (n < 100) { try { const probe = await fs.resolve(dropDir + sep + name) const st = await fs.stat(probe) if (st === undefined) break } catch (err) { break } name = suffixName(base, n) n += 1 } try { const target = await fs.resolve(dropDir + sep + name) await fs.writeText(target, content, undefined, undefined, execPolicy) saved.push({ name, path: dropDir + sep + name, size: content.length, ok: true }) } catch (err) { saved.push({ name, ok: false, error: 'Write failed: ' + messageOf(err) }) } } const okCount = saved.filter((f) => f.ok === true).length return { ok: okCount > 0, okCount, files: saved } }) function sanitizeName(name) { let out = String(name).replace(/\\/g, '/') const idx = out.lastIndexOf('/') if (idx !== -1) out = out.slice(idx + 1) out = out.replace(/[\u0000-\u001f\u007f]/g, '') // Windows 文件名非法字符与保留设备名(CON/PRN/AUX/NUL/COM1-9/LPT1-9),替换保证三大平台都可落盘。 out = out.replace(/[<>:"|?*]/g, '_') if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(out)) out = '_' + out out = out.trim() if (out === '.' || out === '..' || out === '') return '' if (out.length > 80) { const dot = out.lastIndexOf('.') if (dot > 0) out = out.slice(0, Math.max(1, 80 - (out.length - dot))) + out.slice(dot) else out = out.slice(0, 80) } return out } function suffixName(name, n) { const dot = name.lastIndexOf('.') if (dot > 0) return name.slice(0, dot) + '-' + n + name.slice(dot) return name + '-' + n } function messageOf(err) { if (err !== null && typeof err === 'object' && typeof err.message === 'string') return err.message return String(err) } }, }