feat(extensions): 恢复 GOSTC 支持并保留 EasyFrp

This commit is contained in:
mofeng-git
2026-10-31 09:57:38 +08:00
parent 499a273df8
commit d564d8baee
19 changed files with 493 additions and 15 deletions

View File

@@ -26,6 +26,8 @@ import type {
ExtensionLogs,
TtydConfig,
TtydConfigUpdate,
GostcConfig,
GostcConfigUpdate,
EasyfrpConfig,
EasyfrpConfigUpdate,
EasytierConfig,
@@ -201,6 +203,12 @@ export const extensionsApi = {
body: JSON.stringify(config),
}),
updateGostc: (config: GostcConfigUpdate) =>
request<GostcConfig>('/extensions/gostc/config', {
method: 'PATCH',
body: JSON.stringify(config),
}),
updateEasyfrp: (config: EasyfrpConfigUpdate) =>
request<EasyfrpConfig>('/extensions/easyfrp/config', {
method: 'PATCH',

View File

@@ -1128,6 +1128,17 @@ export default {
openInNewTab: 'Open in New Tab',
shell: 'Shell',
},
gostc: {
title: 'GOSTC NAT Traversal',
desc: 'NAT traversal via GOSTC',
addr: 'Server Address',
addrPlaceholder: 'Hostname or IP (required)',
addrRequired: 'Enter the GOSTC server address',
key: 'Client Key',
keyPlaceholder: 'Client key (required)',
keyRequired: 'Enter the GOSTC client key',
tls: 'Enable TLS',
},
easyfrp: {
title: 'EasyFrp NAT Traversal',
desc: 'Connect to a tunnel service with the EasyFrp client',

View File

@@ -1127,6 +1127,17 @@ export default {
openInNewTab: '在新标签页打开',
shell: 'Shell',
},
gostc: {
title: 'GOSTC 内网穿透',
desc: '通过 GOSTC 实现内网穿透',
addr: '服务器地址',
addrPlaceholder: '主机名或 IP(必填)',
addrRequired: '请填写 GOSTC 服务器地址',
key: '客户端密钥',
keyPlaceholder: '客户端密钥(必填)',
keyRequired: '请填写 GOSTC 客户端密钥',
tls: '启用 TLS',
},
easyfrp: {
title: 'EasyFrp 内网穿透',
desc: '使用 EasyFrp 客户端连接穿透服务',

View File

@@ -197,6 +197,13 @@ export interface TtydConfig {
shell: string;
}
export interface GostcConfig {
enabled: boolean;
addr: string;
key: string;
tls: boolean;
}
export interface EasyfrpConfig {
enabled: boolean;
control_urls: string[];
@@ -251,6 +258,7 @@ export interface FrpcConfig {
export interface ExtensionsConfig {
ttyd: TtydConfig;
gostc: GostcConfig;
easyfrp: EasyfrpConfig;
easytier: EasytierConfig;
frpc: FrpcConfig;
@@ -436,6 +444,7 @@ export interface ExtensionInfo {
export enum ExtensionId {
Ttyd = "ttyd",
Gostc = "gostc",
Easyfrp = "easyfrp",
Easytier = "easytier",
Frpc = "frpc",
@@ -452,6 +461,12 @@ export interface TtydInfo {
config: TtydConfig;
}
export interface GostcInfo {
available: boolean;
status: ExtensionStatus;
config: GostcConfig;
}
export interface EasyfrpInfo {
available: boolean;
status: ExtensionStatus;
@@ -466,6 +481,7 @@ export interface FrpcInfo {
export interface ExtensionsStatus {
ttyd: TtydInfo;
gostc: GostcInfo;
easyfrp: EasyfrpInfo;
easytier: EasytierInfo;
frpc: FrpcInfo;
@@ -488,6 +504,13 @@ export interface FrpcConfigUpdate {
custom_toml?: string;
}
export interface GostcConfigUpdate {
enabled?: boolean;
addr?: string;
key?: string;
tls?: boolean;
}
export interface EasyfrpConfigUpdate {
enabled?: boolean;
control_urls?: string[];

View File

@@ -446,12 +446,14 @@ const extensions = ref<ExtensionsStatus | null>(null)
const extensionsLoading = ref(false)
const extensionLogs = ref<Record<string, string[]>>({
ttyd: [],
gostc: [],
easyfrp: [],
easytier: [],
frpc: [],
})
const showLogs = ref<Record<string, boolean>>({
ttyd: false,
gostc: false,
easyfrp: false,
easytier: false,
frpc: false,
@@ -461,6 +463,7 @@ const showTerminalDialog = ref(false)
const extConfig = ref({
ttyd: { enabled: false, shell: '/bin/bash' },
gostc: { enabled: false, addr: '', key: '', tls: true },
easyfrp: { enabled: false, control_urls: [] as string[] },
easytier: {
enabled: false,
@@ -489,6 +492,12 @@ const extConfig = ref({
},
})
const gostcValidationMessage = computed(() => {
if (!extConfig.value.gostc.addr.trim()) return t('extensions.gostc.addrRequired')
if (!extConfig.value.gostc.key) return t('extensions.gostc.keyRequired')
return ''
})
const easyfrpUrlsText = computed({
get: () => extConfig.value.easyfrp.control_urls.join('\n'),
set: (value: string) => {
@@ -1771,6 +1780,11 @@ async function loadExtensions(only?: ExtensionConfigId) {
enabled: ttyd.enabled,
shell: ttyd.shell,
}
if (!only || only === 'gostc')
extConfig.value.gostc = {
...extensions.value.gostc.config,
key: extensions.value.gostc.config.key || '',
}
if (!only || only === 'easyfrp')
extConfig.value.easyfrp = { ...extensions.value.easyfrp.config }
const easytier = extensions.value.easytier.config
@@ -1809,11 +1823,12 @@ async function loadExtensions(only?: ExtensionConfigId) {
}
}
type ExtensionConfigId = 'ttyd' | 'easyfrp' | 'easytier' | 'frpc'
type ExtensionConfigId = 'ttyd' | 'gostc' | 'easyfrp' | 'easytier' | 'frpc'
type ValidatedExtensionConfigId = Exclude<ExtensionConfigId, 'ttyd'>
const extensionBusy = ref<Record<ExtensionConfigId, boolean>>({
ttyd: false,
gostc: false,
easyfrp: false,
easytier: false,
frpc: false,
@@ -1855,6 +1870,8 @@ async function refreshExtensionLogs(id: ExtensionConfigId) {
async function persistExtensionConfig(id: ExtensionConfigId) {
if (id === 'ttyd') {
await extensionsApi.updateTtyd(extConfig.value.ttyd)
} else if (id === 'gostc') {
await extensionsApi.updateGostc(extConfig.value.gostc)
} else if (id === 'easyfrp') {
await extensionsApi.updateEasyfrp(extConfig.value.easyfrp)
} else if (id === 'easytier') {
@@ -2084,7 +2101,9 @@ function showValidationError(message: string): boolean {
function validateExtensionConfig(id: ValidatedExtensionConfigId): boolean {
let message = ''
if (id === 'easyfrp') {
if (id === 'gostc') {
message = gostcValidationMessage.value
} else if (id === 'easyfrp') {
message = easyfrpValidationMessage.value
} else if (id === 'easytier') {
message = easytierValidationMessage.value
@@ -4747,6 +4766,162 @@ watch(isWindows, () => {
<!-- Remote Access Section -->
<div v-show="activeSection === 'ext-remote-access'" class="settings-section-services">
<Card>
<CardHeader class="settings-card-heading"
><CardTitle :help="t('extensions.gostc.desc')">{{
t('extensions.gostc.title')
}}</CardTitle>
<div class="settings-card-actions">
<div class="settings-service-actions" v-if="extensions?.gostc?.available">
<Button
variant="outline"
:aria-label="t('extensions.start') + ' ' + t('extensions.gostc.title')"
:disabled="
extensionsLoading ||
!!gostcValidationMessage ||
isExtRunning(extensions?.gostc?.status) ||
extensionBusy.gostc
"
size="sm"
@click="startExtension('gostc')"
>
<Play class="size-4 mr-1" />
{{ t('extensions.start') }}
</Button>
<Button
:aria-label="t('extensions.stop') + ' ' + t('extensions.gostc.title')"
:disabled="
extensionsLoading ||
!isExtRunning(extensions?.gostc?.status) ||
extensionBusy.gostc
"
size="sm"
variant="outline"
@click="stopExtension('gostc')"
>
<Square class="size-4 mr-1" />
{{ t('extensions.stop') }}
</Button>
</div>
<div class="settings-save-action" v-if="extensions?.gostc?.available">
<Button
:disabled="
loading ||
isExtRunning(extensions?.gostc?.status) ||
extensionBusy.gostc
"
@click="saveExtensionConfig('gostc')"
>
<Loader2 v-if="extensionBusy.gostc" class="size-4 mr-2 animate-spin" /><Save v-else class="size-4 mr-2" />{{ t('common.save') }}
</Button>
</div>
</div></CardHeader
>
<CardContent class="space-y-4">
<div
v-if="!extensions?.gostc?.available"
class="text-sm text-muted-foreground bg-muted p-3 rounded-md"
>
{{
t('extensions.binaryNotFound', {
path: isWindows ? 'gostc.exe' : '/usr/bin/gostc',
})
}}
</div>
<template v-else>
<!-- Status and controls -->
<!-- Config -->
<div class="settings-field-list grid gap-4">
<div class="settings-toggle flex items-center justify-between">
<Label>{{ t('extensions.autoStart') }}</Label>
<Switch
v-model="extConfig.gostc.enabled"
:disabled="isExtRunning(extensions?.gostc?.status)"
/>
</div>
<div class="settings-field grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.gostc.addr') }}</Label>
<div class="sm:col-span-3 space-y-1">
<Input
v-model="extConfig.gostc.addr"
type="text"
autocomplete="off"
:placeholder="t('extensions.gostc.addrPlaceholder')"
:disabled="isExtRunning(extensions?.gostc?.status)"
/>
<p
v-if="extConfig.gostc.enabled && !extConfig.gostc.addr.trim()"
class="text-xs text-destructive"
>
{{ t('extensions.gostc.addrRequired') }}
</p>
</div>
</div>
<div class="settings-field grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.gostc.key') }}</Label>
<div class="sm:col-span-3 space-y-1">
<Input
v-model="extConfig.gostc.key"
type="password"
autocomplete="off"
:placeholder="t('extensions.gostc.keyPlaceholder')"
:disabled="isExtRunning(extensions?.gostc?.status)"
/>
<p
v-if="extConfig.gostc.enabled && !extConfig.gostc.key"
class="text-xs text-destructive"
>
{{ t('extensions.gostc.keyRequired') }}
</p>
</div>
</div>
<div class="settings-toggle flex items-center justify-between">
<Label>{{ t('extensions.gostc.tls') }}</Label>
<Switch
v-model="extConfig.gostc.tls"
:disabled="isExtRunning(extensions?.gostc?.status)"
/>
</div>
</div>
<!-- Logs -->
<div class="space-y-2">
<Collapsible
v-model:open="showLogs.gostc"
@update:open="(open) => open && refreshExtensionLogs('gostc')"
>
<CollapsibleTrigger as-child
><Button
type="button"
variant="ghost"
size="sm"
class="gap-2 text-muted-foreground"
><ChevronRight
:class="[
'size-4 transition-transform',
showLogs.gostc ? 'rotate-90' : '',
]"
/>{{ t('extensions.viewLogs') }}</Button
></CollapsibleTrigger
>
<CollapsibleContent class="space-y-2 pt-2">
<pre
class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono"
>{{
(extensionLogs.gostc || []).join('\n') || t('extensions.noLogs')
}}</pre
>
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('gostc')">
<RefreshCw class="size-3 mr-1" />
{{ t('common.refresh') }}
</Button>
</CollapsibleContent>
</Collapsible>
</div>
</template>
</CardContent>
</Card>
<Card>
<CardHeader class="settings-card-heading"
><CardTitle :help="t('extensions.easyfrp.desc')">{{