123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- /**
- * 更新发布工具
- *
- * Copyright © 2025 imengyu.top imengyu-update-server
- */
- import { program } from 'commander';
- import { password, input, confirm, select } from '@inquirer/prompts';
- import { writeFile, readFile } from 'node:fs/promises';
- import { postAppUpdate, postWebUpdate } from './postUpdate.mjs';
- import Table from 'cli-table3';
- import md5 from 'md5';
- import axios from 'axios';
- import path from 'path';
- import { dirname } from 'node:path';
- import { fileURLToPath } from 'node:url';
- //基础配置
- //========================================
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = dirname(__filename);
- const constant = {
- ServerUrl: 'http://update-server1.imengyu.top/',
- TokenSave: path.resolve(__dirname, './_token.json'),
- };
- let currentData = {
- token: '',
- identifier: '',
- };
- readFile(constant.TokenSave).then((res) => {
- currentData = JSON.parse(res);
- if (!currentData.identifier)
- currentData.identifier = `commandClient${Math.floor(Math.random() * 1000)}`;
- start();
- }).catch(() => {
- start();
- })
- const axiosInstance = axios.create({
- baseURL: constant.ServerUrl,
- timeoutErrorMessage: '请求超时,请检查网络连接',
- responseType: 'json',
- withCredentials: false,
- validateStatus: () => true,
- });
- axiosInstance.interceptors.request.use((value) => {
- value.headers['authorization'] = JSON.stringify({
- auth: currentData?.token?.authName,
- validity: currentData?.token?.authKey,
- nonce: "aaaaaaaaaa",
- identifier: currentData.identifier,
- key: 'abc123',
- });
- value.url = value.url + (value.url.includes('?') ? '&' : '?' ) + `identifier=${currentData.identifier}`
- return value;
- });
- axiosInstance.interceptors.response.use((value) => {
- if (value.data.success)
- return value.data;
- else
- return Promise.reject(value.data);
- });
- function getErrorMessage(e) {
- return e instanceof Error ? e.message : (typeof e === 'object' ? e : ('' + e));
- }
- //登录相关
- //========================================
- async function checkLogged() {
- try {
- await axiosInstance.get('/auth');
- console.log('已登录');
- } catch (e) {
- console.error('获取状态失败:', getErrorMessage(e));
- }
- }
- async function login(user) {
- try {
- const pass = await password({ message: '输入密码' });
- const res = await axiosInstance.post('/auth?rember=true', {
- method: 'key',
- key: `${user}@${md5(pass)}`,
- })
- currentData.token = {
- authName: res.data.authName,
- authKey: res.data.authKey,
- };
- writeFile(constant.TokenSave, JSON.stringify(currentData));
- console.log('登录成功');
- } catch (e) {
- console.error('登录失败', getErrorMessage(e));
- }
- }
- async function logout() {
- currentToken = '';
- writeFile(constant.TokenSave, JSON.stringify(currentData));
- try {
- await axiosInstance.delete('/auth');
- console.log('退出登录成功');
- } catch(e) {
- console.error('退出登录失败', getErrorMessage(e));
- }
- }
- //版本相关
- //========================================
- async function viewVersion(type) {
- if (type === 'all' || !type) {
- const data = await axiosInstance.get('/version/list');
- const table = new Table({
- head: ['ID', '版本'],
- colWidths: [10, 20 ]
- });
- data.data.forEach((d) => {
- table.push([ d.id, d.version ]);
- })
- console.log(table.toString());
- } else {
- let data = null;
- try {
- if (Number.isNaN(Number(type)))
- data = await axiosInstance.get('/version/get-by-name?name=' + type);
- else
- data = await axiosInstance.get('/version/' + type);
- }
- catch (e) {
- console.error('Failed to load version info', e);
- return;
- }
- const table = new Table({
- head: ['key', 'data'],
- colWidths: [30, 60]
- });
- table.push(
- [ 'ID', data.data.id ],
- [ '状态', stateConstant[data.data.status] ],
- [ '版本号', data.data.version ],
- [ '创建时间', new Date(data.data.createAt).toString() ],
- [ '设置', data.data.config ],
- [ '激活的Web更新ID', data.data.webUpdateId ],
- [ '激活的App更新ID', data.data.appUpdateId ],
- [ '激活的下一个App更新ID', data.data.appUpdateNextId ],
- );
- console.log(table.toString());
- }
- }
- async function getVersion(action, type) {
- switch(action) {
- case 'view':
- await viewVersion(type);
- break;
- case 'new': {
- const version = await input({ message: 'Enter version name, like (1.0.0)' });
- try {
- await axiosInstance.post('/version', {
- version: version,
- status: 1,
- config: "{}",
- });
- console.log('Add version success');
- } catch (e) {
- console.error('Failed to add version', e);
- }
- break;
- }
- case 'delete': {
- const versionId = type ? type : await input({ message: '输入版本ID' });
- if (!await confirm({ message: `确定删除版本 ${versionId}?`, default: false }))
- return;
- if (!await confirm({ message: '确认删除版本?此操作会删除所属版本的所有更新项目、存储等,无法恢复,是否确定删除?', default: false }))
- return;
- try {
- await axiosInstance.delete('/version/' + versionId);
- console.log('删除版本成功');
- } catch (e) {
- console.error('删除版本失败', e);
- }
- break;
- }
- case 'set-state': {
- const versionId = type ? type : await input({ message: '输入版本ID' });
- const state = await select({
- message: '设置状态',
- choices: [
- {
- name: 'NotEnable',
- value: 0,
- },
- {
- name: 'Normal',
- value: 1,
- },
- {
- name: 'Deprecated',
- value: 2,
- },
- ],
- });
- try {
- await axiosInstance.put('/version/' + versionId, {
- status: state
- });
- console.log('设置状态成功');
- } catch (e) {
- console.error('设置状态失败', e);
- }
- break;
- }
- case 'set-config': {
- const versionId = type ? type : await input({ message: '输入版本ID' });
- const config = await input({ message: '输入配置Json' });
- try {
- await axiosInstance.put('/version/' + versionId, {
- config: config,
- });
- console.log('设置配置成功');
- } catch (e) {
- console.error('设置配置失败', e);
- }
- break;
- }
- case 'set-active-app-update': {
- const versionId = await input({ message: '输入版本ID' });
- const updateId = await input({ message: '输入更新ID' });
- const isNext = await confirm({ message: 'Set as next active?', default: false });
- try {
- await axiosInstance.post('/update/active/app', { versionId, updateId, isNext });
- console.log('成功');
- } catch (e) {
- console.error('失败', e);
- }
- break;
- }
- case 'set-active-web-update': {
- const versionId = await input({ message: '输入版本ID' });
- const updateId = await input({ message: '输入更新ID' });
- try {
- await axiosInstance.post('/update/active/web', { versionId, updateId });
- console.log('成功');
- } catch (e) {
- console.error('失败', e);
- }
- break;
- }
- default:
- console.error('未知参数', action);
- break;
- }
- }
- //选择方法
- //========================================
- export async function selectVersion(requireString = false, defaultVersionId = null) {
- const data = (await axiosInstance.get('/version/list')).data;
- if (data.length === 0) {
- console.error('没有版本');
- return;
- }
- const resultId = (await select({
- choices: data.map(p => ({
- value: p.id,
- name: p.version,
- })),
- default: defaultVersionId,
- message: '选择一个版本',
- }));
- if (requireString) {
- return data.find(p => p.id === resultId).version
- }
- return resultId;
- }
- export async function selectUpdate() {
- const versionId = await selectVersion();
- const data = (await axiosInstance.get('/version/update?search=' + JSON.stringify({ versionId }))).data;
- if (data.length === 0) {
- console.error('没有更新');
- return;
- }
- const resultId = (await select({
- choices: data.map(p => ({
- value: p.id,
- name: p.version,
- })),
- default: defaultVersionId,
- message: '选择一个更新',
- }));
- return resultId;
- }
- //更新相关
- //========================================
- const stateConstant = [ 'Deleted', 'Normal', 'Deprecated' ];
- const typeConstant = [ 'Unknow', 'Web', 'app' ];
- const storageTypeConstant = [ 'Unknow', 'LocalStorage', 'AliOSS' ];
- async function postUpdate(type, options) {
- switch (type) {
- case 'web': {
- await postWebUpdate(axiosInstance, options);
- break;
- }
- case 'app': {
- await postAppUpdate(axiosInstance, options);
- break;
- }
- default:
- console.error('Unknow type', type);
- break;
- }
- }
- async function deprecateOrDeleteUpdate(updateId) {
- if (!updateId)
- updateId = await selectUpdate();
- const { currentUpdateInfo, currentVersionInfo } = await viewUpdate(updateId);
- const deprecate = (await select({
- choices: [
- {
- name: 'Deprecate',
- value: 0,
- },
- {
- name: 'Delete',
- value: 1,
- },
- ],
- message: '删除或弃用?',
- })) === 0;
- if (deprecate && currentUpdateInfo.type !== 1) {
- console.log('只有Web更新允许弃用');
- return;
- }
- if (currentUpdateInfo.status === 0) {
- console.log(`当前状态 ${stateConstant[currentUpdateInfo.status]} 无法弃用`);
- return;
- }
- if (deprecate) {
- if (!await confirm({ message: `确定弃用当前版本 ${currentUpdateInfo.versionCode} ?弃用会删除存储文件以及备份。此操作无法恢复!`, default: false }))
- return;
- } else {
- if (!await confirm({ message: `确定删除当前版本 ${currentUpdateInfo.versionCode} ?此操作无法恢复!`, default: false }))
- return;
- }
- try {
- await axiosInstance.post(`/update/${deprecate ? 'deprecate' : 'delete'}`, { updateId });
- console.log(`${deprecate ? '弃用' : '删除'} 成功`);
- } catch (e) {
- console.error(`操作失败`, e);
- }
- }
- async function viewUpdate(updateId) {
-
- let currentUpdateInfo = null
- let currentVersionInfo = null
- try {
- currentUpdateInfo = (await axiosInstance.get('/update/' + updateId)).data;
- } catch (e) {
- console.error('加载更新信息失败', updateId);
- }
- try {
- currentVersionInfo = (await axiosInstance.get('/version/' + currentUpdateInfo.versionId)).data;
- } catch (e) {
- console.error('加载版本信息失败', currentUpdateInfo.versionId);
- }
- const table = new Table({
- head: ['key', 'data'],
- colWidths: [20, 40]
- });
- table.push(
- [ 'ID', currentUpdateInfo.id ],
- [ '所属应用', currentUpdateInfo.name ],
- [ '更新信息', currentUpdateInfo.updateInfo ],
- [ '版本号', currentUpdateInfo.versionCode ],
- [ '创建时间', new Date(currentUpdateInfo.createAt).toString() ],
- [ '类型', typeConstant[currentUpdateInfo.type] ],
- [ '状态', stateConstant[currentUpdateInfo.status] ],
- [ '强制更新', currentUpdateInfo.force ],
- [ '公共访问路径', currentUpdateInfo.publicUrl ],
- [ '存储类型', storageTypeConstant[currentUpdateInfo.storageType] ],
- [ '存储路径', currentUpdateInfo.storagePath ],
- );
- console.log(table.toString());
- if (currentUpdateInfo.activeWebVersionName) {
- table.push(
- [ '使用中的Web版本', currentUpdateInfo.activeWebVersionName ],
- );
- }
- if (currentUpdateInfo.activeAppVersionName) {
- table.push(
- [ '使用中的App版本', currentUpdateInfo.activeAppVersionName ],
- );
- }
- return {
- currentUpdateInfo,
- currentVersionInfo,
- }
- }
- async function getUpdate(action, type, all, options) {
- const typeNotANumber = isNaN(new Number(type));
- if (action === 'view' && (!type || typeNotANumber)) {
- let hasSerch = false;
- const search = {};
- const sort = {
- field: "createAt",
- order: "descend"
- }
- if (typeNotANumber && type !== 'all') {
- hasSerch = true;
- search.version = type;
- }
- const data = await axiosInstance.get('/update/list?full=true' + (hasSerch ? ('&search=' + JSON.stringify(search)) : '') + '&sort=' + JSON.stringify(sort));
- const table = new Table({
- head: ['ID', '版本', '版本号', '类型', '状态'],
- colWidths: [10, 10, 20, 10, 15 ]
- });
- if (type !== 'all' && all !== 'all' && data.data.length > 10) {
- data.data = data.data.slice(0, 10);
- console.log('filter!', all);
- }
- data.data.forEach((d) => {
- table.push([
- d.id,
- (d.activeAppVersionName ? `${d.activeAppVersionName} (App)` : (
- d.activeWebVersionName? `${d.activeWebVersionName} (Web)` : '无'
- )),
- d.versionCode, typeConstant[d.type], stateConstant[d.status]
- ]);
- })
-
- console.log(table.toString());
- } else {
- switch (action) {
- case 'post': {
- await postUpdate(type, options);
- break;
- }
- case 'delete': {
- await deprecateOrDeleteUpdate(type);
- break;
- }
- case 'view': {
- viewUpdate(type);
- break;
- }
- default:
- console.error('Unknow action', action);
- break;
- }
- }
- }
- async function testVersion(version) {
- try {
- const data = await axiosInstance.get('/update-get-info?version=' + version);
- console.log('版本信息', data.data);
- } catch(e) {
- console.log('获取失败', e);
- }
- }
- //程序入口
- //============================================
- program
- .command('login <user>')
- .description('登录')
- .action(login);
- program
- .command('logstate')
- .description('检查登录状态')
- .action(checkLogged);
- program
- .command('logout')
- .description('退出登录')
- .action(logout);
- program
- .command('test <version>')
- .description('测试更新入口')
- .action(testVersion);
- program
- .command('version <action> [type]')
- .description('查看版本信息/发布版本/删除版本/设置版本, action 可选 view/new/delete/set-web-update/set-app-update/set-next-app-update')
- .action(getVersion);
- program
- .command('update <action> [type] [all]')
- .description('查看更新信息/发布更新, action 可选 view/post, view type 可选 id/all; post type 可选 web/app')
- .option('--skip', '跳过构建')
- .option('--ndelete', '不删除构建文件')
- .action(getUpdate);
- function start() {
- program.parse(process.argv);
- }
- process.on('unhandledRejection', (reason, p) => {
- console.error('Promise: ', p, 'Reason: ', reason)
- })
|