feat: 移除无用代码

This commit is contained in:
手瓜一十雪 2024-11-16 12:45:27 +08:00
parent e98bfaac11
commit 83f28795f2
5 changed files with 444 additions and 179 deletions

View File

@ -14,8 +14,8 @@
<span class="item-label">版本信息:</span> <span class="item-label">版本信息:</span>
<span class="item-content"> <span class="item-content">
<t-tag class="tag-item" theme="success"> WebUi: 1.0.0 </t-tag> <t-tag class="tag-item" theme="success"> WebUi: 1.0.0 </t-tag>
<t-tag class="tag-item" theme="success"> NapCat: 1.0.0 </t-tag> <t-tag class="tag-item" theme="success"> NapCat: 4.?.? </t-tag>
<t-tag class="tag-item" theme="success"> Tdesign: 1.0.0 </t-tag> <t-tag class="tag-item" theme="success"> Tdesign: 1.10.3 </t-tag>
</span> </span>
</t-list-item> </t-list-item>
</t-list> </t-list>

View File

@ -1,47 +1,52 @@
<template> <template>
<t-space class="full-space"> <t-space class="full-space">
<template v-if="clientPanelData.length > 0">
<t-tabs v-model="activeTab" :addable="true" theme="card" @add="showAddTabDialog" @remove="removeTab" class="full-tabs"> <t-tabs v-model="activeTab" :addable="true" theme="card" @add="showAddTabDialog" @remove="removeTab" class="full-tabs">
<t-tab-panel <t-tab-panel
v-for="(config, idx) in clientPanelData" v-for="(config, idx) in clientPanelData"
:key="idx" :key="idx"
:label="config.name" :label="config.name"
:removable="true" :removable="true"
:value="idx" :value="idx"
class="full-tab-panel" class="full-tab-panel"
> >
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" /> <component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" />
<div class="button-container"> <div class="button-container">
<t-button @click="saveConfig" style="width: 100px; height: 40px;">保存</t-button> <t-button @click="saveConfig" style="width: 100px; height: 40px;">保存</t-button>
</div> </div>
</t-tab-panel> </t-tab-panel>
</t-tabs> </t-tabs>
<t-dialog </template>
v-model:visible="isDialogVisible" <template v-else>
header="添加新选项卡" <EmptyStateComponent :showAddTabDialog="showAddTabDialog" />
@close="isDialogVisible = false" </template>
@confirm="addTab" <t-dialog
> v-model:visible="isDialogVisible"
<t-form ref="form" :model="newTab"> header="添加网络配置"
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name"> @close="isDialogVisible = false"
<t-input v-model="newTab.name" /> @confirm="addTab"
</t-form-item> >
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type"> <t-form ref="form" :model="newTab">
<t-select v-model="newTab.type"> <t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name">
<t-option value="httpServers">HTTP 服务器</t-option> <t-input v-model="newTab.name" />
<t-option value="httpClients">HTTP 客户端</t-option> </t-form-item>
<t-option value="websocketServers">WebSocket 服务器</t-option> <t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
<t-option value="websocketClients">WebSocket 客户端</t-option> <t-select v-model="newTab.type">
</t-select> <t-option value="httpServers">HTTP 服务器</t-option>
</t-form-item> <t-option value="httpClients">HTTP 客户端</t-option>
</t-form> <t-option value="websocketServers">WebSocket 服务器</t-option>
</t-dialog> <t-option value="websocketClients">WebSocket 客户端</t-option>
</t-select>
</t-form-item>
</t-form>
</t-dialog>
</t-space> </t-space>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted, reactive, Reactive } from 'vue'; import { ref, resolveDynamicComponent, nextTick, Ref, onMounted, reactive, Reactive } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next'; import { MessagePlugin } from 'tdesign-vue-next';
import { import {
httpServerDefaultConfigs, httpServerDefaultConfigs,
httpClientDefaultConfigs, httpClientDefaultConfigs,
websocketServerDefaultConfigs, websocketServerDefaultConfigs,
@ -53,139 +58,140 @@ import {
NetworkConfig, NetworkConfig,
OneBotConfig, OneBotConfig,
mergeOneBotConfigs, mergeOneBotConfigs,
} from '../../../src/onebot/config/config'; } from '../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell'; import { QQLoginManager } from '@/backend/shell';
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue'; import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue'; import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue'; import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue'; import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
import EmptyStateComponent from '@/pages/network/EmptyStateComponent.vue';
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs, httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs, httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs, websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs, websocketClients: websocketClientDefaultConfigs,
}; };
const componentMap: Record< const componentMap: Record<
ConfigKey, ConfigKey,
| typeof HttpServerComponent | typeof HttpServerComponent
| typeof HttpClientComponent | typeof HttpClientComponent
| typeof WebsocketServerComponent | typeof WebsocketServerComponent
| typeof WebsocketClientComponent | typeof WebsocketClientComponent
> = { > = {
httpServers: HttpServerComponent, httpServers: HttpServerComponent,
httpClients: HttpClientComponent, httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent, websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent, websocketClients: WebsocketClientComponent,
}; };
interface ClientPanel { interface ClientPanel {
name: string; name: string;
key: ConfigKey; key: ConfigKey;
data: Ref<ConfigUnion>; data: Ref<ConfigUnion>;
} }
type ComponentKey = keyof typeof componentMap; type ComponentKey = keyof typeof componentMap;
// TODO: store these state in global store (aka pinia) // TODO: store these state in global store (aka pinia)
const activeTab = ref<number>(0); const activeTab = ref<number>(0);
const isDialogVisible = ref(false); const isDialogVisible = ref(false);
const newTab = ref<{ name: string; type: ComponentKey }>({ name: '', type: 'httpServers' }); const newTab = ref<{ name: string; type: ComponentKey }>({ name: '', type: 'httpServers' });
const clientPanelData: Reactive<Array<ClientPanel>> = reactive([]); const clientPanelData: Reactive<Array<ClientPanel>> = reactive([]);
const getComponent = (type: ComponentKey) => { const getComponent = (type: ComponentKey) => {
return componentMap[type]; return componentMap[type];
}; };
const getOB11Config = async (): Promise<OneBotConfig | undefined> => { const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth'); const storedCredential = localStorage.getItem('auth');
if (!storedCredential) { if (!storedCredential) {
console.error('No stored credential found'); console.error('No stored credential found');
return; return;
} }
const loginManager = new QQLoginManager(storedCredential); const loginManager = new QQLoginManager(storedCredential);
return await loginManager.GetOB11Config(); return await loginManager.GetOB11Config();
}; };
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => { const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth'); const storedCredential = localStorage.getItem('auth');
if (!storedCredential) { if (!storedCredential) {
console.error('No stored credential found'); console.error('No stored credential found');
return false; return false;
} }
const loginManager = new QQLoginManager(storedCredential); const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config); return await loginManager.SetOB11Config(config);
}; };
const addToPanel = <T extends ConfigUnion>(configs: T[], key: ConfigKey) => { const addToPanel = <T extends ConfigUnion>(configs: T[], key: ConfigKey) => {
configs.forEach((config) => clientPanelData.push({ name: config.name, data: config, key: key })); configs.forEach((config) => clientPanelData.push({ name: config.name, data: config, key: key }));
}; };
const addConfigDataToPanel = (data: NetworkConfig) => { const addConfigDataToPanel = (data: NetworkConfig) => {
Object.entries(data).forEach(([key, configs]) => { Object.entries(data).forEach(([key, configs]) => {
if (key in defaultConfigs) { if (key in defaultConfigs) {
addToPanel(configs as ConfigUnion[], key as ConfigKey); addToPanel(configs as ConfigUnion[], key as ConfigKey);
} }
}); });
}; };
const parsePanelData = (): NetworkConfig => { const parsePanelData = (): NetworkConfig => {
return { return {
websocketClients: clientPanelData websocketClients: clientPanelData
.filter((panel) => panel.key === 'websocketClients') .filter((panel) => panel.key === 'websocketClients')
.map((panel) => panel.data as WebsocketClientConfig), .map((panel) => panel.data as WebsocketClientConfig),
websocketServers: clientPanelData websocketServers: clientPanelData
.filter((panel) => panel.key === 'websocketServers') .filter((panel) => panel.key === 'websocketServers')
.map((panel) => panel.data as WebsocketServerConfig), .map((panel) => panel.data as WebsocketServerConfig),
httpClients: clientPanelData httpClients: clientPanelData
.filter((panel) => panel.key === 'httpClients') .filter((panel) => panel.key === 'httpClients')
.map((panel) => panel.data as HttpClientConfig), .map((panel) => panel.data as HttpClientConfig),
httpServers: clientPanelData httpServers: clientPanelData
.filter((panel) => panel.key === 'httpServers') .filter((panel) => panel.key === 'httpServers')
.map((panel) => panel.data as HttpServerConfig), .map((panel) => panel.data as HttpServerConfig),
}; };
}; };
const loadConfig = async () => { const loadConfig = async () => {
try { try {
const userConfig = await getOB11Config(); const userConfig = await getOB11Config();
if (!userConfig) return; if (!userConfig) return;
const mergedConfig = mergeOneBotConfigs(userConfig); const mergedConfig = mergeOneBotConfigs(userConfig);
addConfigDataToPanel(mergedConfig.network); addConfigDataToPanel(mergedConfig.network);
} catch (error) { } catch (error) {
console.error('Error loading config:', error); console.error('Error loading config:', error);
} }
}; };
// It's better to "saveConfig" instead of using deep watch // It's better to "saveConfig" instead of using deep watch
const saveConfig = async () => { const saveConfig = async () => {
const config = parsePanelData(); const config = parsePanelData();
const userConfig = await getOB11Config(); const userConfig = await getOB11Config();
if (!userConfig) return; if (!userConfig) return;
userConfig.network = config; userConfig.network = config;
const success = await setOB11Config(userConfig); const success = await setOB11Config(userConfig);
if (success) { if (success) {
MessagePlugin.success('配置保存成功'); MessagePlugin.success('配置保存成功');
} else { } else {
MessagePlugin.error('配置保存失败'); MessagePlugin.error('配置保存失败');
} }
}; };
const showAddTabDialog = () => { const showAddTabDialog = () => {
newTab.value = { name: '', type: 'httpServers' }; newTab.value = { name: '', type: 'httpServers' };
isDialogVisible.value = true; isDialogVisible.value = true;
}; };
const addTab = async () => { const addTab = async () => {
const { name, type } = newTab.value; const { name, type } = newTab.value;
if (clientPanelData.some(panel => panel.name === name)) { if (clientPanelData.some(panel => panel.name === name)) {
MessagePlugin.error('选项卡名称已存在'); MessagePlugin.error('选项卡名称已存在');
return; return;
} }
const defaultConfig = structuredClone(defaultConfigs[type]); const defaultConfig = structuredClone(defaultConfigs[type]);
defaultConfig.name = name; defaultConfig.name = name;
@ -194,74 +200,45 @@ const addTab = async () => {
await nextTick(); await nextTick();
activeTab.value = clientPanelData.length - 1; activeTab.value = clientPanelData.length - 1;
MessagePlugin.success('选项卡添加成功'); MessagePlugin.success('选项卡添加成功');
}; };
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => { const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.splice(payload.index, 1); clientPanelData.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1); activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig(); await saveConfig();
}; };
onMounted(() => { onMounted(() => {
loadConfig(); loadConfig();
}); });
</script> </script>
<style scoped> <style scoped>
.full-space { .full-space {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
justify-content: flex-start; justify-content: flex-start;
} }
.full-tabs { .full-tabs {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.full-tab-panel { .full-tab-panel {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.button-container { .button-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 20px; margin-top: 20px;
} }
</style> </style>
<style scoped>
.full-space {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.full-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.full-tab-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.button-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,22 @@
<template>
<div class="empty-state">
<p>当前没有网络配置</p>
<t-button @click="showAddTabDialog">添加网络配置</t-button>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
defineProps<{ showAddTabDialog: () => void }>();
</script>
<style scoped>
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
}
</style>

View File

@ -0,0 +1,267 @@
<template>
<t-space class="full-space">
<t-tabs v-model="activeTab" :addable="true" theme="card" @add="showAddTabDialog" @remove="removeTab" class="full-tabs">
<t-tab-panel
v-for="(config, idx) in clientPanelData"
:key="idx"
:label="config.name"
:removable="true"
:value="idx"
class="full-tab-panel"
>
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" />
<div class="button-container">
<t-button @click="saveConfig" style="width: 100px; height: 40px;">保存</t-button>
</div>
</t-tab-panel>
</t-tabs>
<t-dialog
v-model:visible="isDialogVisible"
header="添加新选项卡"
@close="isDialogVisible = false"
@confirm="addTab"
>
<t-form ref="form" :model="newTab">
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name">
<t-input v-model="newTab.name" />
</t-form-item>
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
<t-select v-model="newTab.type">
<t-option value="httpServers">HTTP 服务器</t-option>
<t-option value="httpClients">HTTP 客户端</t-option>
<t-option value="websocketServers">WebSocket 服务器</t-option>
<t-option value="websocketClients">WebSocket 客户端</t-option>
</t-select>
</t-form-item>
</t-form>
</t-dialog>
</t-space>
</template>
<script setup lang="ts">
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted, reactive, Reactive } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import {
httpServerDefaultConfigs,
httpClientDefaultConfigs,
websocketServerDefaultConfigs,
websocketClientDefaultConfigs,
HttpClientConfig,
HttpServerConfig,
WebsocketClientConfig,
WebsocketServerConfig,
NetworkConfig,
OneBotConfig,
mergeOneBotConfigs,
} from '../../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell';
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
};
const componentMap: Record<
ConfigKey,
| typeof HttpServerComponent
| typeof HttpClientComponent
| typeof WebsocketServerComponent
| typeof WebsocketClientComponent
> = {
httpServers: HttpServerComponent,
httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent,
};
interface ClientPanel {
name: string;
key: ConfigKey;
data: Ref<ConfigUnion>;
}
type ComponentKey = keyof typeof componentMap;
// TODO: store these state in global store (aka pinia)
const activeTab = ref<number>(0);
const isDialogVisible = ref(false);
const newTab = ref<{ name: string; type: ComponentKey }>({ name: '', type: 'httpServers' });
const clientPanelData: Reactive<Array<ClientPanel>> = reactive([]);
const getComponent = (type: ComponentKey) => {
return componentMap[type];
};
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.GetOB11Config();
};
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return false;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config);
};
const addToPanel = <T extends ConfigUnion>(configs: T[], key: ConfigKey) => {
configs.forEach((config) => clientPanelData.push({ name: config.name, data: config, key: key }));
};
const addConfigDataToPanel = (data: NetworkConfig) => {
Object.entries(data).forEach(([key, configs]) => {
if (key in defaultConfigs) {
addToPanel(configs as ConfigUnion[], key as ConfigKey);
}
});
};
const parsePanelData = (): NetworkConfig => {
return {
websocketClients: clientPanelData
.filter((panel) => panel.key === 'websocketClients')
.map((panel) => panel.data as WebsocketClientConfig),
websocketServers: clientPanelData
.filter((panel) => panel.key === 'websocketServers')
.map((panel) => panel.data as WebsocketServerConfig),
httpClients: clientPanelData
.filter((panel) => panel.key === 'httpClients')
.map((panel) => panel.data as HttpClientConfig),
httpServers: clientPanelData
.filter((panel) => panel.key === 'httpServers')
.map((panel) => panel.data as HttpServerConfig),
};
};
const loadConfig = async () => {
try {
const userConfig = await getOB11Config();
if (!userConfig) return;
const mergedConfig = mergeOneBotConfigs(userConfig);
addConfigDataToPanel(mergedConfig.network);
} catch (error) {
console.error('Error loading config:', error);
}
};
// It's better to "saveConfig" instead of using deep watch
const saveConfig = async () => {
const config = parsePanelData();
const userConfig = await getOB11Config();
if (!userConfig) return;
userConfig.network = config;
const success = await setOB11Config(userConfig);
if (success) {
MessagePlugin.success('配置保存成功');
} else {
MessagePlugin.error('配置保存失败');
}
};
const showAddTabDialog = () => {
newTab.value = { name: '', type: 'httpServers' };
isDialogVisible.value = true;
};
const addTab = async () => {
const { name, type } = newTab.value;
if (clientPanelData.some(panel => panel.name === name)) {
MessagePlugin.error('选项卡名称已存在');
return;
}
const defaultConfig = structuredClone(defaultConfigs[type]);
defaultConfig.name = name;
clientPanelData.push({ name, data: defaultConfig, key: type });
isDialogVisible.value = false;
await nextTick();
activeTab.value = clientPanelData.length - 1;
MessagePlugin.success('选项卡添加成功');
};
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig();
};
onMounted(() => {
loadConfig();
});
</script>
<style scoped>
.full-space {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.full-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.full-tab-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.button-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>
<style scoped>
.full-space {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.full-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.full-tab-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.button-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>

View File

@ -45,7 +45,6 @@ try {
sed -i "s/\\"version\\": \\"${currentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" package.json sed -i "s/\\"version\\": \\"${currentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" package.json
sed -i "s/\\"version\\": \\"${manifestCurrentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" manifest.json sed -i "s/\\"version\\": \\"${manifestCurrentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" manifest.json
sed -i "s/napCatVersion = '.*'/napCatVersion = '${targetVersion}'/g" ./src/common/version.ts sed -i "s/napCatVersion = '.*'/napCatVersion = '${targetVersion}'/g" ./src/common/version.ts
sed -i "s/SettingButton(\\"V.*\\", \\"napcat-update-button\\", \\"secondary\\")/SettingButton(\\"V${targetVersion}\\", \\"napcat-update-button\\", \\"secondary\\")/g" ./static/assets/renderer.js
git add . git add .
git commit -m "release: v${targetVersion}" git commit -m "release: v${targetVersion}"
git push -u origin main`; git push -u origin main`;