os.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import { getMainWindow } from "ee-core/electron";
  2. import { app, dialog, shell } from "electron";
  3. import { logger } from "ee-core/log";
  4. import path from "path";
  5. import fs from "fs";
  6. import http from "http";
  7. import https from "https";
  8. import FormData from "form-data";
  9. class Os {
  10. async minimizeWindow() {
  11. getMainWindow().minimize();
  12. }
  13. async maximizeWindow() {
  14. getMainWindow().maximize();
  15. }
  16. async unmaximizeWindow() {
  17. getMainWindow().unmaximize();
  18. }
  19. async closeWindow() {
  20. getMainWindow().close();
  21. }
  22. async isMaximized() {
  23. return getMainWindow().isMaximized();
  24. }
  25. async toggleDevTools() {
  26. getMainWindow().webContents.toggleDevTools();
  27. }
  28. async openUserDataFolder() {
  29. const userDataPath = app.getPath('userData');
  30. await shell.openPath(userDataPath);
  31. }
  32. async openInstallFolder() {
  33. const exePath = app.getPath('exe');
  34. const installDir = path.dirname(exePath);
  35. await shell.openPath(installDir);
  36. }
  37. async selectDirectory() {
  38. const win = getMainWindow();
  39. const result = await dialog.showOpenDialog(win, {
  40. title: '选择目录',
  41. properties: ['openDirectory', 'createDirectory'],
  42. });
  43. if (result.canceled || !result.filePaths || result.filePaths.length === 0) {
  44. return '';
  45. }
  46. return result.filePaths[0];
  47. }
  48. /**
  49. * 选择单个本机文件(如基站包 .bin)。
  50. * @param options { title?, filterName?, extensions?: string[] } 扩展名过滤,传 ['bin'] 即只列 .bin
  51. */
  52. async selectFile(options = {}) {
  53. const win = getMainWindow();
  54. const extensions = Array.isArray(options?.extensions) ? options.extensions.filter(Boolean) : [];
  55. const result = await dialog.showOpenDialog(win, {
  56. title: options?.title || '选择文件',
  57. properties: ['openFile'],
  58. filters: extensions.length ? [{ name: options?.filterName || '文件', extensions }] : undefined,
  59. });
  60. if (result.canceled || !result.filePaths || result.filePaths.length === 0) {
  61. return '';
  62. }
  63. return result.filePaths[0];
  64. }
  65. async getAppInfo() {
  66. return {
  67. version: app.getVersion(),
  68. name: app.getName(),
  69. electronVersion: process.versions.electron,
  70. chromeVersion: process.versions.chrome,
  71. nodeVersion: process.versions.node,
  72. platform: process.platform,
  73. arch: process.arch,
  74. userDataPath: app.getPath('userData'),
  75. exePath: app.getPath('exe'),
  76. };
  77. }
  78. async openExternal(url) {
  79. if (!url || typeof url !== 'string') return false;
  80. try {
  81. await shell.openExternal(url);
  82. return true;
  83. } catch (e) {
  84. logger.error('[os] openExternal failed:', e);
  85. return false;
  86. }
  87. }
  88. /**
  89. * 通过 Node.js 主进程流式上传清鉴数据源文件
  90. * - 校验选中目录最后一级必须为 QingJian
  91. * - 在 QingJian 目录下拼接 workspace 作为扫描根
  92. * - 扫描 workspace 下一层文件夹,再读取每个文件夹下的文件(不递归更深层)
  93. * - 用 fs.createReadStream 流式读取,避免大文件占内存
  94. * - 用 form-data 构建 multipart,http/https 模块发送
  95. * @param params.dirPath 目录完整磁盘路径(最后一级必须为 QingJian)
  96. * @param params.uploadUrl 后端接口完整 URL
  97. * @param params.token 鉴权 token(可选)
  98. */
  99. async uploadQingJianFiles(params) {
  100. logger.info('[os] uploadQingJianFiles params:', params);
  101. const { dirPath, uploadUrl, token } = params || {};
  102. if (!dirPath || !uploadUrl) {
  103. logger.error('[os] uploadQingJianFiles params missing:', params);
  104. throw new Error('请选择 QingJian 目录');
  105. }
  106. // 1. 校验选中目录最后一级必须为 QingJian
  107. const dirName = path.basename(dirPath);
  108. if (dirName !== 'QingJian') {
  109. throw new Error('请选择 QingJian 目录');
  110. }
  111. // 2. 在选中目录下拼接 workspace,作为扫描根目录
  112. const workspacePath = path.join(dirPath, 'workspace');
  113. const platPath = path.join(dirPath, 'db',"plat");
  114. // 3. 扫描 workspace 下一层文件夹,再读取每个文件夹下的文件(不递归更深层)
  115. // id = 文件直接所在的父级目录名(即子文件夹名)
  116. const subEntries = await fs.promises.readdir(workspacePath, { withFileTypes: true });
  117. const subDirs = subEntries.filter(e => e.isDirectory());
  118. if (subDirs.length === 0) {
  119. throw new Error('workspace 目录下没有子文件夹');
  120. }
  121. const files = [];
  122. for (const sub of subDirs) {
  123. const subDirPath = path.join(workspacePath, sub.name);
  124. const subFiles = await fs.promises.readdir(subDirPath, { withFileTypes: true });
  125. for (const f of subFiles) {
  126. if (f.isFile()) {
  127. files.push({ id: sub.name, name: f.name, filePath: path.join(subDirPath, f.name) });
  128. }
  129. }
  130. }
  131. files.push({ id: 'plat', name: 'plat', filePath: platPath });
  132. if (files.length === 0) {
  133. throw new Error('workspace 的子文件夹下没有可上传的文件');
  134. }
  135. // 4. 构建 multipart FormData
  136. const form = new FormData();
  137. form.append('path', dirPath);
  138. for (let i = 0; i < files.length; i++) {
  139. const { id, name: fileName, filePath } = files[i];
  140. const stream = fs.createReadStream(filePath);
  141. form.append(`items[${i}].file`, stream, { filename: fileName });
  142. form.append(`items[${i}].name`, fileName);
  143. form.append(`items[${i}].id`, id);
  144. }
  145. // 3. 发送 HTTP 请求(流式 pipe,不占内存)
  146. return await new Promise((resolve, reject) => {
  147. const urlObj = new URL(uploadUrl);
  148. const lib = urlObj.protocol === 'https:' ? https : http;
  149. const headers = form.getHeaders();
  150. if (token) {
  151. headers['Authorization'] = token;
  152. }
  153. const req = lib.request(uploadUrl, {
  154. method: 'POST',
  155. headers,
  156. }, (res) => {
  157. let data = '';
  158. res.setEncoding('utf8');
  159. res.on('data', (chunk) => { data += chunk; });
  160. res.on('end', () => {
  161. const status = res.statusCode || 0;
  162. if (status >= 200 && status < 300) {
  163. try {
  164. resolve(JSON.parse(data));
  165. } catch {
  166. resolve(data);
  167. }
  168. } else {
  169. reject(new Error(`HTTP ${status}: ${data}`));
  170. }
  171. });
  172. });
  173. req.on('error', (e) => {
  174. logger.error('[os] uploadQingJianFiles request error:', e);
  175. reject(e.message);
  176. });
  177. form.pipe(req);
  178. });
  179. }
  180. }
  181. Os.toString = () => 'Os';
  182. export const os = new Os();