index.mjs 15 KB

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