ai-helper-agent 1.9.5 → 1.10.0

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +156 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-helper-agent",
3
- "version": "1.9.5",
3
+ "version": "1.10.0",
4
4
  "description": "AI Helper 代理服务 - 为 Chrome 扩展提供本地文件读写和系统命令执行能力",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
package/src/server.js CHANGED
@@ -3,10 +3,11 @@ import http from 'http';
3
3
  import { WebSocketServer } from 'ws';
4
4
  import { readFileSync } from 'fs';
5
5
  import { readFile, writeFile, readdir, stat, unlink, rmdir, chmod, mkdir } from 'fs/promises';
6
- import { join, dirname } from 'path';
6
+ import { join, dirname, basename } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
- import { homedir } from 'os';
9
- import { spawn } from 'child_process';
8
+ import { homedir, tmpdir } from 'os';
9
+ import { spawn, execSync } from 'child_process';
10
+ import { randomBytes } from 'crypto';
10
11
  import os from 'os';
11
12
  import { loadConfig } from './config.js';
12
13
  import { verifyToken, getCurrentPairCode, startPairCodeRotation, stopPairCodeRotation, handlePairRequest } from './auth.js';
@@ -107,6 +108,35 @@ async function exists(path) {
107
108
  catch { return false; }
108
109
  }
109
110
 
111
+ /**
112
+ * 根据文件扩展名返回 MIME 类型
113
+ */
114
+ function getMimeType(filePath) {
115
+ const ext = (filePath.split('.').pop() || '').toLowerCase();
116
+ const mimeMap = {
117
+ txt: 'text/plain', md: 'text/markdown', json: 'application/json',
118
+ js: 'application/javascript', mjs: 'application/javascript', ts: 'application/typescript',
119
+ jsx: 'text/jsx', tsx: 'text/tsx',
120
+ html: 'text/html', htm: 'text/html', css: 'text/css', scss: 'text/x-scss', less: 'text/x-less',
121
+ xml: 'application/xml', yaml: 'text/yaml', yml: 'text/yaml',
122
+ py: 'text/x-python', java: 'text/x-java', c: 'text/x-c', cpp: 'text/x-c++', h: 'text/x-c',
123
+ go: 'text/x-go', rs: 'text/x-rust', rb: 'text/x-ruby', php: 'text/x-php',
124
+ sql: 'text/x-sql', sh: 'text/x-sh', bash: 'text/x-sh', zsh: 'text/x-sh',
125
+ cfg: 'text/plain', ini: 'text/plain', toml: 'text/plain', conf: 'text/plain',
126
+ log: 'text/plain', csv: 'text/csv', tsv: 'text/tab-separated-values',
127
+ env: 'text/plain', vue: 'text/x-vue', svelte: 'text/x-svelte', astro: 'text/x-astro', rtf: 'application/rtf',
128
+ svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
129
+ gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', ico: 'image/x-icon',
130
+ pdf: 'application/pdf',
131
+ doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
132
+ xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
133
+ ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
134
+ zip: 'application/zip', gz: 'application/gzip', tar: 'application/x-tar',
135
+ mp3: 'audio/mpeg', wav: 'audio/wav', mp4: 'video/mp4', webm: 'video/webm',
136
+ };
137
+ return mimeMap[ext] || 'application/octet-stream';
138
+ }
139
+
110
140
  const PLATFORM_INFO = {
111
141
  platform: os.platform(),
112
142
  platformName: (() => {
@@ -539,6 +569,129 @@ export function startServer() {
539
569
  return jsonResponse(res, 200, { success: true, path: check.resolved });
540
570
  }
541
571
 
572
+ // 下载文件/目录(返回 base64 内容,目录自动打包为 zip)
573
+ if (pathname === '/api/fs/download') {
574
+ const targetPath = body.path;
575
+ if (!targetPath || typeof targetPath !== 'string') {
576
+ return jsonResponse(res, 400, { success: false, error: '缺少 path 参数' });
577
+ }
578
+ const check = await checkPath(targetPath);
579
+ if (!check.allowed) {
580
+ logSecurity('fs_download_blocked', { path: targetPath, reason: check.reason });
581
+ return jsonResponse(res, 403, { success: false, error: check.reason });
582
+ }
583
+ if (!await exists(check.resolved)) {
584
+ return jsonResponse(res, 404, { success: false, error: '文件/目录不存在' });
585
+ }
586
+
587
+ try {
588
+ const fstat = await stat(check.resolved);
589
+ if (fstat.isDirectory()) {
590
+ // 目录:打包为 zip
591
+ const tmpFile = join(tmpdir(), `ws-dl-${randomBytes(6).toString('hex')}.zip`);
592
+ try {
593
+ const dirName = basename(check.resolved);
594
+ const parentDir = dirname(check.resolved);
595
+ execSync(`cd "${parentDir}" && zip -rq "${tmpFile}" "${dirName}"`, {
596
+ timeout: 120000,
597
+ maxBuffer: 10 * 1024 * 1024,
598
+ stdio: ['ignore', 'pipe', 'pipe']
599
+ });
600
+ const zipBuffer = await readFile(tmpFile);
601
+ logFs('download', { path: check.resolved, type: 'directory', zipSize: zipBuffer.length });
602
+ return jsonResponse(res, 200, {
603
+ success: true,
604
+ type: 'directory',
605
+ name: `${dirName}.zip`,
606
+ size: zipBuffer.length,
607
+ content: zipBuffer.toString('base64'),
608
+ mimeType: 'application/zip'
609
+ });
610
+ } finally {
611
+ try { await unlink(tmpFile); } catch {}
612
+ }
613
+ } else {
614
+ // 单个文件
615
+ if (fstat.size > maxSize) {
616
+ return jsonResponse(res, 400, { success: false, error: `文件过大 (${fstat.size} > ${maxSize})` });
617
+ }
618
+ const buf = await readFile(check.resolved);
619
+ const mimeType = getMimeType(check.resolved);
620
+ logFs('download', { path: check.resolved, type: 'file', size: buf.length });
621
+ return jsonResponse(res, 200, {
622
+ success: true,
623
+ type: 'file',
624
+ name: basename(check.resolved),
625
+ size: buf.length,
626
+ content: buf.toString('base64'),
627
+ mimeType
628
+ });
629
+ }
630
+ } catch (err) {
631
+ logError('fs', 'download_error', { path: targetPath, error: err.message });
632
+ return jsonResponse(res, 500, { success: false, error: `下载失败: ${err.message}` });
633
+ }
634
+ }
635
+
636
+ // 批量下载多个文件/目录(在后端打包为 zip)
637
+ if (pathname === '/api/fs/download-multi') {
638
+ const paths = body.paths;
639
+ if (!Array.isArray(paths) || paths.length === 0) {
640
+ return jsonResponse(res, 400, { success: false, error: '缺少 paths 参数或为空数组' });
641
+ }
642
+
643
+ const resolvedPaths = [];
644
+ for (const p of paths) {
645
+ const check = await checkPath(p);
646
+ if (!check.allowed) {
647
+ logSecurity('fs_download_blocked', { path: p, reason: check.reason });
648
+ return jsonResponse(res, 403, { success: false, error: `路径 "${p}" 不允许访问` });
649
+ }
650
+ if (!await exists(check.resolved)) {
651
+ return jsonResponse(res, 404, { success: false, error: `路径 "${p}" 不存在` });
652
+ }
653
+ resolvedPaths.push(check.resolved);
654
+ }
655
+
656
+ try {
657
+ const tmpFile = join(tmpdir(), `ws-dl-${randomBytes(6).toString('hex')}.zip`);
658
+ try {
659
+ const zipArgs = resolvedPaths.map(p => `"${basename(p)}"`).join(' ');
660
+ const commonParent = resolvedPaths.reduce((a, b) => {
661
+ const partsA = a.split('/').filter(Boolean);
662
+ const partsB = b.split('/').filter(Boolean);
663
+ const common = [];
664
+ for (let i = 0; i < Math.min(partsA.length, partsB.length); i++) {
665
+ if (partsA[i] === partsB[i]) common.push(partsA[i]);
666
+ else break;
667
+ }
668
+ return '/' + common.join('/');
669
+ });
670
+
671
+ execSync(`cd "${commonParent}" && zip -rq "${tmpFile}" ${zipArgs}`, {
672
+ timeout: 120000,
673
+ maxBuffer: 10 * 1024 * 1024,
674
+ stdio: ['ignore', 'pipe', 'pipe']
675
+ });
676
+ const zipBuffer = await readFile(tmpFile);
677
+ logFs('download_multi', { paths: resolvedPaths, zipSize: zipBuffer.length });
678
+ return jsonResponse(res, 200, {
679
+ success: true,
680
+ type: 'archive',
681
+ name: `workspace_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.zip`,
682
+ size: zipBuffer.length,
683
+ content: zipBuffer.toString('base64'),
684
+ mimeType: 'application/zip'
685
+ });
686
+ } finally {
687
+ try { await unlink(tmpFile); } catch {}
688
+ }
689
+ } catch (err) {
690
+ logError('fs', 'download_multi_error', { paths, error: err.message });
691
+ return jsonResponse(res, 500, { success: false, error: `打包失败: ${err.message}` });
692
+ }
693
+ }
694
+
542
695
  // 在本地浏览器中打开文件
543
696
  if (pathname === '/api/browser/open') {
544
697
  const targetPath = body.path;