1
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 2m1s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 53s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 2m41s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 2m17s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m35s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m5s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 2m10s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m54s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 55s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 2m56s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m30s

This commit is contained in:
2026-01-03 19:29:23 +08:00
parent f2720a3d15
commit ae6cb9d422
8 changed files with 133 additions and 23 deletions

View File

@@ -57,6 +57,24 @@ jobs:
go-version: '1.24' go-version: '1.24'
cache: true cache: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- name: Build frontend
run: |
cd web
npm ci
npm run build
mkdir -p ../internal/server/app/dist
cp -r dist/* ../internal/server/app/dist/
cd ..
echo "Frontend build completed"
ls -la internal/server/app/dist/
- name: Build all platforms - name: Build all platforms
run: | run: |
mkdir -p dist mkdir -p dist

5
.gitignore vendored
View File

@@ -26,10 +26,11 @@ Thumbs.db
# 前端 node_modules # 前端 node_modules
web/node_modules/ web/node_modules/
# 构建产物 (源码在 web/dist嵌入用的在 pkg/webserver/dist) # 构建产物 (源码在 web/dist嵌入用的在 internal/server/app/dist)
web/dist/ web/dist/
pkg/webserver/dist/ pkg/webserver/dist/
**/dist/** internal/server/app/dist/*
!internal/server/app/dist/.gitkeep
build/** build/**
# 日志 # 日志

View File

@@ -102,4 +102,11 @@ type StoreInstallRequest struct {
DownloadURL string `json:"download_url" binding:"required,url"` DownloadURL string `json:"download_url" binding:"required,url"`
SignatureURL string `json:"signature_url" binding:"required,url"` SignatureURL string `json:"signature_url" binding:"required,url"`
ClientID string `json:"client_id" binding:"required"` ClientID string `json:"client_id" binding:"required"`
RemotePort int `json:"remote_port"`
}
// JSPluginPushRequest 推送 JS 插件到客户端请求
// @Description 推送 JS 插件到指定客户端
type JSPluginPushRequest struct {
RemotePort int `json:"remote_port"`
} }

View File

@@ -158,10 +158,12 @@ func (h *JSPluginHandler) Delete(c *gin.Context) {
// @Summary 推送插件到客户端 // @Summary 推送插件到客户端
// @Description 将 JS 插件推送到指定客户端 // @Description 将 JS 插件推送到指定客户端
// @Tags JS插件 // @Tags JS插件
// @Accept json
// @Produce json // @Produce json
// @Security Bearer // @Security Bearer
// @Param name path string true "插件名称" // @Param name path string true "插件名称"
// @Param clientID path string true "客户端ID" // @Param clientID path string true "客户端ID"
// @Param request body dto.JSPluginPushRequest false "推送配置"
// @Success 200 {object} Response // @Success 200 {object} Response
// @Failure 400 {object} Response // @Failure 400 {object} Response
// @Failure 404 {object} Response // @Failure 404 {object} Response
@@ -170,6 +172,10 @@ func (h *JSPluginHandler) PushToClient(c *gin.Context) {
pluginName := c.Param("name") pluginName := c.Param("name")
clientID := c.Param("clientID") clientID := c.Param("clientID")
// 解析请求体(可选)
var pushReq dto.JSPluginPushRequest
c.ShouldBindJSON(&pushReq) // 忽略错误,允许空请求体
// 检查客户端是否在线 // 检查客户端是否在线
online, _, _ := h.app.GetServer().GetClientStatus(clientID) online, _, _ := h.app.GetServer().GetClientStatus(clientID)
if !online { if !online {
@@ -195,6 +201,7 @@ func (h *JSPluginHandler) PushToClient(c *gin.Context) {
Source: plugin.Source, Source: plugin.Source,
Signature: plugin.Signature, Signature: plugin.Signature,
RuleName: plugin.Name, RuleName: plugin.Name,
RemotePort: pushReq.RemotePort,
Config: plugin.Config, Config: plugin.Config,
AutoStart: plugin.AutoStart, AutoStart: plugin.AutoStart,
} }
@@ -205,8 +212,9 @@ func (h *JSPluginHandler) PushToClient(c *gin.Context) {
} }
Success(c, gin.H{ Success(c, gin.H{
"status": "ok", "status": "ok",
"plugin": pluginName, "plugin": pluginName,
"client": clientID, "client": clientID,
"remote_port": pushReq.RemotePort,
}) })
} }

View File

@@ -131,6 +131,7 @@ func (h *StoreHandler) Install(c *gin.Context) {
Source: string(source), Source: string(source),
Signature: string(signature), Signature: string(signature),
RuleName: req.PluginName, RuleName: req.PluginName,
RemotePort: req.RemotePort,
AutoStart: true, AutoStart: true,
} }

1
web/components.d.ts vendored
View File

@@ -12,6 +12,7 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
LogViewer: typeof import('./src/components/LogViewer.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
} }

View File

@@ -49,8 +49,8 @@ export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`)
// 扩展商店 // 扩展商店
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[] }>('/store/plugins') export const getStorePlugins = () => get<{ plugins: StorePluginInfo[] }>('/store/plugins')
export const installStorePlugin = (pluginName: string, downloadUrl: string, signatureUrl: string, clientId: string) => export const installStorePlugin = (pluginName: string, downloadUrl: string, signatureUrl: string, clientId: string, remotePort?: number) =>
post('/store/install', { plugin_name: pluginName, download_url: downloadUrl, signature_url: signatureUrl, client_id: clientId }) post('/store/install', { plugin_name: pluginName, download_url: downloadUrl, signature_url: signatureUrl, client_id: clientId, remote_port: remotePort || 0 })
// 客户端插件配置 // 客户端插件配置
export const getClientPluginConfig = (clientId: string, pluginName: string) => export const getClientPluginConfig = (clientId: string, pluginName: string) =>
@@ -64,8 +64,8 @@ export const createJSPlugin = (plugin: JSPlugin) => post('/js-plugins', plugin)
export const getJSPlugin = (name: string) => get<JSPlugin>(`/js-plugin/${name}`) export const getJSPlugin = (name: string) => get<JSPlugin>(`/js-plugin/${name}`)
export const updateJSPlugin = (name: string, plugin: JSPlugin) => put(`/js-plugin/${name}`, plugin) export const updateJSPlugin = (name: string, plugin: JSPlugin) => put(`/js-plugin/${name}`, plugin)
export const deleteJSPlugin = (name: string) => del(`/js-plugin/${name}`) export const deleteJSPlugin = (name: string) => del(`/js-plugin/${name}`)
export const pushJSPluginToClient = (pluginName: string, clientId: string) => export const pushJSPluginToClient = (pluginName: string, clientId: string, remotePort?: number) =>
post(`/js-plugin/${pluginName}/push/${clientId}`) post(`/js-plugin/${pluginName}/push/${clientId}`, { remote_port: remotePort || 0 })
export const updateJSPluginConfig = (name: string, config: Record<string, string>) => export const updateJSPluginConfig = (name: string, config: Record<string, string>) =>
put(`/js-plugin/${name}/config`, { config }) put(`/js-plugin/${name}/config`, { config })
export const setJSPluginEnabled = (name: string, enabled: boolean) => export const setJSPluginEnabled = (name: string, enabled: boolean) =>

View File

@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import { import {
NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi, NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi,
NEmpty, NSpin, NIcon, NSwitch, NTabs, NTabPane, useMessage, NEmpty, NSpin, NIcon, NSwitch, NTabs, NTabPane, useMessage,
NSelect, NModal, NInput NSelect, NModal, NInput, NInputNumber
} from 'naive-ui' } from 'naive-ui'
import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline, CodeSlashOutline, SettingsOutline } from '@vicons/ionicons5' import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline, CodeSlashOutline, SettingsOutline } from '@vicons/ionicons5'
import { import {
@@ -131,12 +131,34 @@ const loadClients = async () => {
} }
} }
const handlePushJSPlugin = async (pluginName: string, clientId: string) => { // JS 插件推送相关
const showPushModal = ref(false)
const selectedJSPlugin = ref<JSPlugin | null>(null)
const pushClientId = ref('')
const pushRemotePort = ref<number | null>(8080)
const pushing = ref(false)
const openPushModal = (plugin: JSPlugin) => {
selectedJSPlugin.value = plugin
pushClientId.value = ''
pushRemotePort.value = 8080
showPushModal.value = true
}
const handlePushJSPlugin = async () => {
if (!selectedJSPlugin.value || !pushClientId.value) {
message.warning('请选择要推送到的客户端')
return
}
pushing.value = true
try { try {
await pushJSPluginToClient(pluginName, clientId) await pushJSPluginToClient(selectedJSPlugin.value.name, pushClientId.value, pushRemotePort.value || 0)
message.success(`已推送 ${pluginName}${clientId}`) message.success(`已推送 ${selectedJSPlugin.value.name}${pushClientId.value},监听端口: ${pushRemotePort.value || '未指定'}`)
} catch (e) { showPushModal.value = false
message.error('推送失败') } catch (e: any) {
message.error(e.response?.data || '推送失败')
} finally {
pushing.value = false
} }
} }
@@ -208,11 +230,13 @@ const toggleJSPlugin = async (plugin: JSPlugin) => {
const showInstallModal = ref(false) const showInstallModal = ref(false)
const selectedStorePlugin = ref<StorePluginInfo | null>(null) const selectedStorePlugin = ref<StorePluginInfo | null>(null)
const selectedClientId = ref('') const selectedClientId = ref('')
const storePluginRemotePort = ref<number | null>(8080)
const installing = ref(false) const installing = ref(false)
const openInstallModal = (plugin: StorePluginInfo) => { const openInstallModal = (plugin: StorePluginInfo) => {
selectedStorePlugin.value = plugin selectedStorePlugin.value = plugin
selectedClientId.value = '' selectedClientId.value = ''
storePluginRemotePort.value = 8080
showInstallModal.value = true showInstallModal.value = true
} }
@@ -235,7 +259,8 @@ const handleInstallStorePlugin = async () => {
selectedStorePlugin.value.name, selectedStorePlugin.value.name,
selectedStorePlugin.value.download_url, selectedStorePlugin.value.download_url,
selectedStorePlugin.value.signature_url, selectedStorePlugin.value.signature_url,
selectedClientId.value selectedClientId.value,
storePluginRemotePort.value || 0
) )
message.success(`已安装 ${selectedStorePlugin.value.name} 到客户端`) message.success(`已安装 ${selectedStorePlugin.value.name} 到客户端`)
showInstallModal.value = false showInstallModal.value = false
@@ -404,14 +429,14 @@ onMounted(() => {
<template #icon><n-icon><SettingsOutline /></n-icon></template> <template #icon><n-icon><SettingsOutline /></n-icon></template>
配置 配置
</n-button> </n-button>
<n-select <n-button
v-if="onlineClients.length > 0" v-if="onlineClients.length > 0"
placeholder="推送到..."
size="small" size="small"
style="width: 140px;" type="primary"
:options="onlineClients.map(c => ({ label: c.nickname || c.id, value: c.id }))" @click="openPushModal(plugin)"
@update:value="(v: string) => handlePushJSPlugin(plugin.name, v)" >
/> 推送到客户端
</n-button>
</n-space> </n-space>
</template> </template>
</n-card> </n-card>
@@ -439,6 +464,16 @@ onMounted(() => {
placeholder="选择要安装到的客户端" placeholder="选择要安装到的客户端"
:options="onlineClients.map(c => ({ label: c.nickname || c.id, value: c.id }))" :options="onlineClients.map(c => ({ label: c.nickname || c.id, value: c.id }))"
/> />
<div>
<p style="margin: 0 0 8px 0; color: #666; font-size: 13px;">远程端口服务端监听端口:</p>
<n-input-number
v-model:value="storePluginRemotePort"
:min="1"
:max="65535"
placeholder="输入端口号"
style="width: 100%;"
/>
</div>
</n-space> </n-space>
<template #footer> <template #footer>
<n-space justify="end"> <n-space justify="end">
@@ -477,5 +512,44 @@ onMounted(() => {
</n-space> </n-space>
</template> </template>
</n-modal> </n-modal>
<!-- JS 插件推送模态框 -->
<n-modal v-model:show="showPushModal" preset="card" title="推送插件到客户端" style="width: 400px;">
<n-space vertical :size="16">
<div v-if="selectedJSPlugin">
<p style="margin: 0 0 8px 0;"><strong>插件:</strong> {{ selectedJSPlugin.name }}</p>
<p style="margin: 0; color: #666;">{{ selectedJSPlugin.description || '无描述' }}</p>
</div>
<n-select
v-model:value="pushClientId"
placeholder="选择要推送到的客户端"
:options="onlineClients.map(c => ({ label: c.nickname || c.id, value: c.id }))"
/>
<div>
<p style="margin: 0 0 8px 0; color: #666; font-size: 13px;">远程端口服务端监听端口:</p>
<n-input-number
v-model:value="pushRemotePort"
:min="1"
:max="65535"
placeholder="输入端口号"
style="width: 100%;"
/>
<p style="margin: 8px 0 0 0; color: #999; font-size: 12px;">用户可以通过 服务端IP:端口 访问此插件提供的服务</p>
</div>
</n-space>
<template #footer>
<n-space justify="end">
<n-button @click="showPushModal = false">取消</n-button>
<n-button
type="primary"
:loading="pushing"
:disabled="!pushClientId"
@click="handlePushJSPlugin"
>
推送
</n-button>
</n-space>
</template>
</n-modal>
</div> </div>
</template> </template>