index.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. /**
  2. * 更新发布工具
  3. *
  4. * Copyright © 2025 imengyu.top imengyu-update-server
  5. */
  6. import { program } from 'commander';
  7. import { password, input, confirm, select } from '@inquirer/prompts';
  8. import { writeFile, readFile } from 'node:fs/promises';
  9. import { postAppUpdate, postWebUpdate } from './postUpdate.mjs';
  10. import Table from 'cli-table3';
  11. import md5 from 'md5';
  12. import axios from 'axios';
  13. import path from 'path';
  14. import { dirname } from 'node:path';
  15. import { fileURLToPath } from 'node:url';
  16. //基础配置
  17. //========================================
  18. const __filename = fileURLToPath(import.meta.url);
  19. const __dirname = dirname(__filename);
  20. const constant = {
  21. ServerUrl: 'http://update-server1.imengyu.top/',
  22. TokenSave: path.resolve(__dirname, './_token.json'),
  23. };
  24. let currentData = {
  25. token: '',
  26. identifier: '',
  27. };
  28. readFile(constant.TokenSave).then((res) => {
  29. currentData = JSON.parse(res);
  30. if (!currentData.identifier)
  31. currentData.identifier = `commandClient${Math.floor(Math.random() * 1000)}`;
  32. start();
  33. }).catch(() => {
  34. start();
  35. })
  36. const axiosInstance = axios.create({
  37. baseURL: constant.ServerUrl,
  38. timeoutErrorMessage: '请求超时,请检查网络连接',
  39. responseType: 'json',
  40. withCredentials: false,
  41. validateStatus: () => true,
  42. });
  43. axiosInstance.interceptors.request.use((value) => {
  44. value.headers['authorization'] = JSON.stringify({
  45. auth: currentData?.token?.authName,
  46. validity: currentData?.token?.authKey,
  47. nonce: "aaaaaaaaaa",
  48. identifier: currentData.identifier,
  49. key: 'abc123',
  50. });
  51. value.url = value.url + (value.url.includes('?') ? '&' : '?' ) + `identifier=${currentData.identifier}`
  52. return value;
  53. });
  54. axiosInstance.interceptors.response.use((value) => {
  55. if (value.data.success)
  56. return value.data;
  57. else
  58. return Promise.reject(value.data);
  59. });
  60. function getErrorMessage(e) {
  61. return e instanceof Error ? e.message : (typeof e === 'object' ? e : ('' + e));
  62. }
  63. //登录相关
  64. //========================================
  65. async function checkLogged() {
  66. try {
  67. await axiosInstance.get('/auth');
  68. console.log('已登录');
  69. } catch (e) {
  70. console.error('获取状态失败:', getErrorMessage(e));
  71. }
  72. }
  73. async function login(user) {
  74. try {
  75. const pass = await password({ message: '输入密码' });
  76. const res = await axiosInstance.post('/auth?rember=true', {
  77. method: 'key',
  78. key: `${user}@${md5(pass)}`,
  79. })
  80. currentData.token = {
  81. authName: res.data.authName,
  82. authKey: res.data.authKey,
  83. };
  84. writeFile(constant.TokenSave, JSON.stringify(currentData));
  85. console.log('登录成功');
  86. } catch (e) {
  87. console.error('登录失败', getErrorMessage(e));
  88. }
  89. }
  90. async function logout() {
  91. currentToken = '';
  92. writeFile(constant.TokenSave, JSON.stringify(currentData));
  93. try {
  94. await axiosInstance.delete('/auth');
  95. console.log('退出登录成功');
  96. } catch(e) {
  97. console.error('退出登录失败', getErrorMessage(e));
  98. }
  99. }
  100. //版本相关
  101. //========================================
  102. async function viewVersion(type) {
  103. if (type === 'all' || !type) {
  104. const data = await axiosInstance.get('/version/list');
  105. const table = new Table({
  106. head: ['ID', '版本'],
  107. colWidths: [10, 20 ]
  108. });
  109. data.data.forEach((d) => {
  110. table.push([ d.id, d.version ]);
  111. })
  112. console.log(table.toString());
  113. } else {
  114. let data = null;
  115. try {
  116. if (Number.isNaN(Number(type)))
  117. data = await axiosInstance.get('/version/get-by-name?name=' + type);
  118. else
  119. data = await axiosInstance.get('/version/' + type);
  120. }
  121. catch (e) {
  122. console.error('Failed to load version info', e);
  123. return;
  124. }
  125. const table = new Table({
  126. head: ['key', 'data'],
  127. colWidths: [30, 60]
  128. });
  129. table.push(
  130. [ 'ID', data.data.id ],
  131. [ '状态', stateConstant[data.data.status] ],
  132. [ '版本号', data.data.version ],
  133. [ '创建时间', new Date(data.data.createAt).toString() ],
  134. [ '设置', data.data.config ],
  135. [ '激活的Web更新ID', data.data.webUpdateId ],
  136. [ '激活的App更新ID', data.data.appUpdateId ],
  137. [ '激活的下一个App更新ID', data.data.appUpdateNextId ],
  138. );
  139. console.log(table.toString());
  140. }
  141. }
  142. async function getVersion(action, type) {
  143. switch(action) {
  144. case 'view':
  145. await viewVersion(type);
  146. break;
  147. case 'new': {
  148. const version = await input({ message: 'Enter version name, like (1.0.0)' });
  149. try {
  150. await axiosInstance.post('/version', {
  151. version: version,
  152. status: 1,
  153. config: "{}",
  154. });
  155. console.log('Add version success');
  156. } catch (e) {
  157. console.error('Failed to add version', e);
  158. }
  159. break;
  160. }
  161. case 'delete': {
  162. const versionId = type ? type : await input({ message: '输入版本ID' });
  163. if (!await confirm({ message: `确定删除版本 ${versionId}?`, default: false }))
  164. return;
  165. if (!await confirm({ message: '确认删除版本?此操作会删除所属版本的所有更新项目、存储等,无法恢复,是否确定删除?', default: false }))
  166. return;
  167. try {
  168. await axiosInstance.delete('/version/' + versionId);
  169. console.log('删除版本成功');
  170. } catch (e) {
  171. console.error('删除版本失败', e);
  172. }
  173. break;
  174. }
  175. case 'set-state': {
  176. const versionId = type ? type : await input({ message: '输入版本ID' });
  177. const state = await select({
  178. message: '设置状态',
  179. choices: [
  180. {
  181. name: 'NotEnable',
  182. value: 0,
  183. },
  184. {
  185. name: 'Normal',
  186. value: 1,
  187. },
  188. {
  189. name: 'Deprecated',
  190. value: 2,
  191. },
  192. ],
  193. });
  194. try {
  195. await axiosInstance.put('/version/' + versionId, {
  196. status: state
  197. });
  198. console.log('设置状态成功');
  199. } catch (e) {
  200. console.error('设置状态失败', e);
  201. }
  202. break;
  203. }
  204. case 'set-config': {
  205. const versionId = type ? type : await input({ message: '输入版本ID' });
  206. const config = await input({ message: '输入配置Json' });
  207. try {
  208. await axiosInstance.put('/version/' + versionId, {
  209. config: config,
  210. });
  211. console.log('设置配置成功');
  212. } catch (e) {
  213. console.error('设置配置失败', e);
  214. }
  215. break;
  216. }
  217. case 'set-active-app-update': {
  218. const versionId = await input({ message: '输入版本ID' });
  219. const updateId = await input({ message: '输入更新ID' });
  220. const isNext = await confirm({ message: 'Set as next active?', default: false });
  221. try {
  222. await axiosInstance.post('/update/active/app', { versionId, updateId, isNext });
  223. console.log('成功');
  224. } catch (e) {
  225. console.error('失败', e);
  226. }
  227. break;
  228. }
  229. case 'set-active-web-update': {
  230. const versionId = await input({ message: '输入版本ID' });
  231. const updateId = await input({ message: '输入更新ID' });
  232. try {
  233. await axiosInstance.post('/update/active/web', { versionId, updateId });
  234. console.log('成功');
  235. } catch (e) {
  236. console.error('失败', e);
  237. }
  238. break;
  239. }
  240. default:
  241. console.error('未知参数', action);
  242. break;
  243. }
  244. }
  245. //选择方法
  246. //========================================
  247. export async function selectVersion(requireString = false, defaultVersionId = null) {
  248. const data = (await axiosInstance.get('/version/list')).data;
  249. if (data.length === 0) {
  250. console.error('没有版本');
  251. return;
  252. }
  253. const resultId = (await select({
  254. choices: data.map(p => ({
  255. value: p.id,
  256. name: p.version,
  257. })),
  258. default: defaultVersionId,
  259. message: '选择一个版本',
  260. }));
  261. if (requireString) {
  262. return data.find(p => p.id === resultId).version
  263. }
  264. return resultId;
  265. }
  266. export async function selectUpdate() {
  267. const versionId = await selectVersion();
  268. const data = (await axiosInstance.get('/version/update?search=' + JSON.stringify({ versionId }))).data;
  269. if (data.length === 0) {
  270. console.error('没有更新');
  271. return;
  272. }
  273. const resultId = (await select({
  274. choices: data.map(p => ({
  275. value: p.id,
  276. name: p.version,
  277. })),
  278. default: defaultVersionId,
  279. message: '选择一个更新',
  280. }));
  281. return resultId;
  282. }
  283. //更新相关
  284. //========================================
  285. const stateConstant = [ 'Deleted', 'Normal', 'Deprecated' ];
  286. const typeConstant = [ 'Unknow', 'Web', 'app' ];
  287. const storageTypeConstant = [ 'Unknow', 'LocalStorage', 'AliOSS' ];
  288. async function postUpdate(type, options) {
  289. switch (type) {
  290. case 'web': {
  291. await postWebUpdate(axiosInstance, options);
  292. break;
  293. }
  294. case 'app': {
  295. await postAppUpdate(axiosInstance, options);
  296. break;
  297. }
  298. default:
  299. console.error('Unknow type', type);
  300. break;
  301. }
  302. }
  303. async function deprecateOrDeleteUpdate(updateId) {
  304. if (!updateId)
  305. updateId = await selectUpdate();
  306. const { currentUpdateInfo, currentVersionInfo } = await viewUpdate(updateId);
  307. const deprecate = (await select({
  308. choices: [
  309. {
  310. name: 'Deprecate',
  311. value: 0,
  312. },
  313. {
  314. name: 'Delete',
  315. value: 1,
  316. },
  317. ],
  318. message: '删除或弃用?',
  319. })) === 0;
  320. if (deprecate && currentUpdateInfo.type !== 1) {
  321. console.log('只有Web更新允许弃用');
  322. return;
  323. }
  324. if (currentUpdateInfo.status === 0) {
  325. console.log(`当前状态 ${stateConstant[currentUpdateInfo.status]} 无法弃用`);
  326. return;
  327. }
  328. if (deprecate) {
  329. if (!await confirm({ message: `确定弃用当前版本 ${currentUpdateInfo.versionCode} ?弃用会删除存储文件以及备份。此操作无法恢复!`, default: false }))
  330. return;
  331. } else {
  332. if (!await confirm({ message: `确定删除当前版本 ${currentUpdateInfo.versionCode} ?此操作无法恢复!`, default: false }))
  333. return;
  334. }
  335. try {
  336. await axiosInstance.post(`/update/${deprecate ? 'deprecate' : 'delete'}`, { updateId });
  337. console.log(`${deprecate ? '弃用' : '删除'} 成功`);
  338. } catch (e) {
  339. console.error(`操作失败`, e);
  340. }
  341. }
  342. async function viewUpdate(updateId) {
  343. let currentUpdateInfo = null
  344. let currentVersionInfo = null
  345. try {
  346. currentUpdateInfo = (await axiosInstance.get('/update/' + updateId)).data;
  347. } catch (e) {
  348. console.error('加载更新信息失败', updateId);
  349. }
  350. try {
  351. currentVersionInfo = (await axiosInstance.get('/version/' + currentUpdateInfo.versionId)).data;
  352. } catch (e) {
  353. console.error('加载版本信息失败', currentUpdateInfo.versionId);
  354. }
  355. const table = new Table({
  356. head: ['key', 'data'],
  357. colWidths: [20, 40]
  358. });
  359. table.push(
  360. [ 'ID', currentUpdateInfo.id ],
  361. [ '所属应用', currentUpdateInfo.name ],
  362. [ '更新信息', currentUpdateInfo.updateInfo ],
  363. [ '版本号', currentUpdateInfo.versionCode ],
  364. [ '创建时间', new Date(currentUpdateInfo.createAt).toString() ],
  365. [ '类型', typeConstant[currentUpdateInfo.type] ],
  366. [ '状态', stateConstant[currentUpdateInfo.status] ],
  367. [ '强制更新', currentUpdateInfo.force ],
  368. [ '公共访问路径', currentUpdateInfo.publicUrl ],
  369. [ '存储类型', storageTypeConstant[currentUpdateInfo.storageType] ],
  370. [ '存储路径', currentUpdateInfo.storagePath ],
  371. );
  372. console.log(table.toString());
  373. if (currentUpdateInfo.activeWebVersionName) {
  374. table.push(
  375. [ '使用中的Web版本', currentUpdateInfo.activeWebVersionName ],
  376. );
  377. }
  378. if (currentUpdateInfo.activeAppVersionName) {
  379. table.push(
  380. [ '使用中的App版本', currentUpdateInfo.activeAppVersionName ],
  381. );
  382. }
  383. return {
  384. currentUpdateInfo,
  385. currentVersionInfo,
  386. }
  387. }
  388. async function getUpdate(action, type, all, options) {
  389. const typeNotANumber = isNaN(new Number(type));
  390. if (action === 'view' && (!type || typeNotANumber)) {
  391. let hasSerch = false;
  392. const search = {};
  393. const sort = {
  394. field: "createAt",
  395. order: "descend"
  396. }
  397. if (typeNotANumber && type !== 'all') {
  398. hasSerch = true;
  399. search.version = type;
  400. }
  401. const data = await axiosInstance.get('/update/list?full=true' + (hasSerch ? ('&search=' + JSON.stringify(search)) : '') + '&sort=' + JSON.stringify(sort));
  402. const table = new Table({
  403. head: ['ID', '版本', '版本号', '类型', '状态'],
  404. colWidths: [10, 10, 20, 10, 15 ]
  405. });
  406. if (type !== 'all' && all !== 'all' && data.data.length > 10) {
  407. data.data = data.data.slice(0, 10);
  408. console.log('filter!', all);
  409. }
  410. data.data.forEach((d) => {
  411. table.push([
  412. d.id,
  413. (d.activeAppVersionName ? `${d.activeAppVersionName} (App)` : (
  414. d.activeWebVersionName? `${d.activeWebVersionName} (Web)` : '无'
  415. )),
  416. d.versionCode, typeConstant[d.type], stateConstant[d.status]
  417. ]);
  418. })
  419. console.log(table.toString());
  420. } else {
  421. switch (action) {
  422. case 'post': {
  423. await postUpdate(type, options);
  424. break;
  425. }
  426. case 'delete': {
  427. await deprecateOrDeleteUpdate(type);
  428. break;
  429. }
  430. case 'view': {
  431. viewUpdate(type);
  432. break;
  433. }
  434. default:
  435. console.error('Unknow action', action);
  436. break;
  437. }
  438. }
  439. }
  440. async function testVersion(version) {
  441. try {
  442. const data = await axiosInstance.get('/update-get-info?version=' + version);
  443. console.log('版本信息', data.data);
  444. } catch(e) {
  445. console.log('获取失败', e);
  446. }
  447. }
  448. //程序入口
  449. //============================================
  450. program
  451. .command('login <user>')
  452. .description('登录')
  453. .action(login);
  454. program
  455. .command('logstate')
  456. .description('检查登录状态')
  457. .action(checkLogged);
  458. program
  459. .command('logout')
  460. .description('退出登录')
  461. .action(logout);
  462. program
  463. .command('test <version>')
  464. .description('测试更新入口')
  465. .action(testVersion);
  466. program
  467. .command('version <action> [type]')
  468. .description('查看版本信息/发布版本/删除版本/设置版本, action 可选 view/new/delete/set-web-update/set-app-update/set-next-app-update')
  469. .action(getVersion);
  470. program
  471. .command('update <action> [type] [all]')
  472. .description('查看更新信息/发布更新, action 可选 view/post, view type 可选 id/all; post type 可选 web/app')
  473. .option('--skip', '跳过构建')
  474. .option('--ndelete', '不删除构建文件')
  475. .action(getUpdate);
  476. function start() {
  477. program.parse(process.argv);
  478. }
  479. process.on('unhandledRejection', (reason, p) => {
  480. console.error('Promise: ', p, 'Reason: ', reason)
  481. })