[codex] add version preview workflow (#1086)

* add version preview workflow

* fix sidebar group test

* fix legacy usage schema migration
This commit is contained in:
ekko
2026-05-28 12:30:49 +08:00
committed by GitHub
parent 7997bfa2b7
commit 1734bac9b4
30 changed files with 1528 additions and 464 deletions
+1
View File
@@ -3,6 +3,7 @@ import router from '@/router'
const DEFAULT_BASE_URL = ''
function getBaseUrl(): string {
if (import.meta.env.VITE_HERMES_PREVIEW === '1') return DEFAULT_BASE_URL
return localStorage.getItem('hermes_server_url') || DEFAULT_BASE_URL
}
+3 -2
View File
@@ -267,8 +267,9 @@ export function buildKanbanEventsWebSocketUrl(opts?: KanbanBoardOptions): string
return `${websocketProtocol(base)}//${new URL(base).host}${path}`
}
const host = import.meta.env.DEV
? formatHostForPort(location.hostname, 8648)
const directDevPort = import.meta.env.VITE_HERMES_DIRECT_WS_PORT
const host = import.meta.env.DEV && directDevPort
? formatHostForPort(location.hostname, Number(directDevPort))
: location.host
return `${websocketProtocol()}//${host}${path}`
}
+58
View File
@@ -9,6 +9,34 @@ export interface HealthResponse {
node_version?: string
}
export interface PreviewTag {
name: string
sha: string
}
export interface PreviewStatus {
preview_dir: string
exists: boolean
has_package: boolean
installed: boolean
running: boolean
pid: number | null
current_tag: string
frontend_url: string
agent_bridge_endpoint: string
log_path: string
webui_home: string
action_log_path: string
dev_log_path: string
action_log: string
dev_log: string
}
export interface PreviewActionResponse extends PreviewStatus {
success: boolean
message?: string
}
// Config-based model types
export interface ModelInfo {
id: string
@@ -84,6 +112,36 @@ export async function triggerUpdate(): Promise<{ success: boolean; message: stri
return request<{ success: boolean; message: string }>('/api/hermes/update', { method: 'POST' })
}
export async function fetchPreviewStatus(): Promise<PreviewStatus> {
return request<PreviewStatus>('/api/hermes/update/preview')
}
export async function fetchPreviewTags(): Promise<{ tags: PreviewTag[] }> {
return request<{ tags: PreviewTag[] }>('/api/hermes/update/preview/tags')
}
export async function preparePreview(tag: string): Promise<PreviewActionResponse> {
return request<PreviewActionResponse>('/api/hermes/update/preview/prepare', {
method: 'POST',
body: JSON.stringify({ tag }),
})
}
export async function installPreview(): Promise<PreviewActionResponse> {
return request<PreviewActionResponse>('/api/hermes/update/preview/install', { method: 'POST' })
}
export async function startPreview(tag?: string): Promise<PreviewActionResponse> {
return request<PreviewActionResponse>('/api/hermes/update/preview/start', {
method: 'POST',
body: JSON.stringify({ tag }),
})
}
export async function stopPreview(): Promise<PreviewActionResponse> {
return request<PreviewActionResponse>('/api/hermes/update/preview/stop', { method: 'POST' })
}
export async function fetchConfigModels(): Promise<ConfigModelsResponse> {
return request<ConfigModelsResponse>('/api/hermes/config/models')
}
@@ -148,8 +148,9 @@ function buildWsUrl(): string {
return `${wsProtocol}//${new URL(base).host}/api/hermes/terminal${token ? `?token=${encodeURIComponent(token)}` : ""}`;
}
const host = import.meta.env.DEV
? formatHostForPort(location.hostname, 8648)
const directDevPort = import.meta.env.VITE_HERMES_DIRECT_WS_PORT;
const host = import.meta.env.DEV && directDevPort
? formatHostForPort(location.hostname, Number(directDevPort))
: location.host;
return `${wsProtocol}//${host}/api/hermes/terminal${token ? `?token=${encodeURIComponent(token)}` : ""}`;
}
@@ -0,0 +1,313 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { NAlert, NButton, NDescriptions, NDescriptionsItem, NSelect, NSpace, NTag, useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import {
fetchPreviewStatus,
fetchPreviewTags,
installPreview,
preparePreview,
startPreview,
stopPreview,
type PreviewStatus,
type PreviewTag,
} from '@/api/hermes/system'
const { t } = useI18n()
const message = useMessage()
const loading = ref(false)
const tagsLoading = ref(false)
const actionLoading = ref('')
const tags = ref<PreviewTag[]>([])
const selectedTag = ref('')
const status = ref<PreviewStatus | null>(null)
const tagOptions = computed(() => tags.value.map(tag => ({
label: tag.name,
value: tag.name,
})))
const actionLog = computed(() => status.value?.action_log || '')
const devLog = computed(() => status.value?.dev_log || '')
function applyErrorStatus(err: any) {
const messageText = String(err?.message || '')
const jsonStart = messageText.indexOf('{')
if (jsonStart < 0) return
try {
const parsed = JSON.parse(messageText.slice(jsonStart))
if (parsed && typeof parsed === 'object' && 'preview_dir' in parsed) {
status.value = parsed as PreviewStatus
}
} catch {}
}
async function loadStatus() {
status.value = await fetchPreviewStatus()
if (!selectedTag.value && status.value.current_tag) {
selectedTag.value = status.value.current_tag
}
}
async function loadTags() {
tagsLoading.value = true
try {
const res = await fetchPreviewTags()
tags.value = res.tags
if (!selectedTag.value && tags.value[0]) {
selectedTag.value = tags.value[0].name
}
} finally {
tagsLoading.value = false
}
}
async function handleRefresh() {
loading.value = true
try {
await Promise.all([loadStatus(), loadTags()])
} finally {
loading.value = false
}
}
async function runAction(action: string, fn: () => Promise<PreviewStatus & { success?: boolean; message?: string }>, successKey: string) {
actionLoading.value = action
try {
const res = await fn()
status.value = res
if (res.success === false) {
message.warning(res.message || t('githubPreview.actionFailed'))
return
}
message.success(t(successKey))
} catch (err: any) {
applyErrorStatus(err)
message.error(err?.message || t('githubPreview.actionFailed'))
} finally {
actionLoading.value = ''
}
}
function requireTag(): string | null {
if (!selectedTag.value) {
message.warning(t('githubPreview.selectTag'))
return null
}
return selectedTag.value
}
async function handlePrepare() {
const tag = requireTag()
if (!tag) return
await runAction('prepare', () => preparePreview(tag), 'githubPreview.prepareSuccess')
}
async function handleInstall() {
await runAction('install', async () => {
const res = await installPreview()
if (res.success !== false && !res.installed) {
return {
...res,
success: false,
message: res.message || t('githubPreview.actionFailed'),
}
}
return res
}, 'githubPreview.installSuccess')
}
async function handleStart() {
await runAction('start', () => startPreview(selectedTag.value || undefined), 'githubPreview.startSuccess')
}
async function handleStop() {
await runAction('stop', stopPreview, 'githubPreview.stopSuccess')
}
onMounted(async () => {
await handleRefresh()
})
</script>
<template>
<div class="github-preview-settings">
<div class="settings-section">
<div class="control-row">
<NSelect
v-model:value="selectedTag"
class="tag-select"
filterable
:loading="tagsLoading"
:options="tagOptions"
:placeholder="t('githubPreview.selectTag')"
/>
<NSpace>
<NButton type="primary" :loading="actionLoading === 'prepare'" :disabled="!selectedTag" @click="handlePrepare">
{{ t('githubPreview.prepare') }}
</NButton>
<NButton :loading="actionLoading === 'install'" :disabled="!status?.has_package" @click="handleInstall">
{{ t('githubPreview.install') }}
</NButton>
<NButton type="success" :loading="actionLoading === 'start'" :disabled="!status?.installed" @click="handleStart">
{{ t('githubPreview.start') }}
</NButton>
<NButton :loading="actionLoading === 'stop'" :disabled="!status?.running" @click="handleStop">
{{ t('githubPreview.stop') }}
</NButton>
<NButton :loading="loading || tagsLoading" @click="handleRefresh">
{{ t('githubPreview.refresh') }}
</NButton>
</NSpace>
</div>
<p class="section-description">{{ t('githubPreview.description') }}</p>
<NAlert type="info" :bordered="false" class="preview-note">
{{ t('githubPreview.note') }}
</NAlert>
<NDescriptions v-if="status" :column="1" bordered size="small" class="status-table">
<NDescriptionsItem :label="t('githubPreview.path')">
<code>{{ status.preview_dir }}</code>
</NDescriptionsItem>
<NDescriptionsItem :label="t('githubPreview.webuiHome')">
<code>{{ status.webui_home }}</code>
</NDescriptionsItem>
<NDescriptionsItem :label="t('githubPreview.currentTag')">
{{ status.current_tag || '-' }}
</NDescriptionsItem>
<NDescriptionsItem :label="t('githubPreview.repoReady')">
<NTag size="small" :type="status.has_package ? 'success' : 'default'">
{{ status.has_package ? t('githubPreview.yes') : t('githubPreview.no') }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="t('githubPreview.dependencies')">
<NTag size="small" :type="status.installed ? 'success' : 'warning'">
{{ status.installed ? t('githubPreview.yes') : t('githubPreview.no') }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="t('githubPreview.running')">
<NTag size="small" :type="status.running ? 'success' : 'default'">
{{ status.running ? `PID ${status.pid}` : t('githubPreview.notRunning') }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="t('githubPreview.open')">
<a :href="status.frontend_url" target="_blank" rel="noopener noreferrer">{{ status.frontend_url }}</a>
</NDescriptionsItem>
<NDescriptionsItem :label="t('githubPreview.log')">
<code>{{ status.action_log_path }}</code>
</NDescriptionsItem>
<NDescriptionsItem :label="t('githubPreview.devLog')">
<code>{{ status.dev_log_path }}</code>
</NDescriptionsItem>
</NDescriptions>
<div class="log-output">
<div class="log-output-header">{{ t('githubPreview.logOutput') }}</div>
<div class="log-box">
<div class="log-title">{{ t('githubPreview.actionLog') }}</div>
<pre>{{ actionLog || '-' }}</pre>
</div>
<div class="log-box">
<div class="log-title">{{ t('githubPreview.devLog') }}</div>
<pre>{{ devLog || '-' }}</pre>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@use "@/styles/variables" as *;
.github-preview-settings {
width: 100%;
}
.settings-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.section-description {
margin: 0;
color: $text-secondary;
font-size: 13px;
line-height: 1.5;
}
.control-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.tag-select {
width: 260px;
}
.preview-note {
width: 100%;
}
.status-table {
width: 100%;
}
.log-output {
width: 100%;
border: 1px solid $border-color;
border-radius: $radius-md;
background: $bg-card;
overflow: hidden;
}
.log-output-header {
padding: 12px 14px;
border-bottom: 1px solid $border-color;
font-size: 14px;
font-weight: 600;
color: $text-primary;
}
.log-box {
border-bottom: 1px solid $border-color;
&:last-child {
border-bottom: none;
}
}
.log-title {
padding: 8px 14px;
font-size: 12px;
color: $text-secondary;
background: $bg-secondary;
}
pre {
min-height: 180px;
max-height: 320px;
margin: 0;
padding: 12px 14px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-size: 12px;
line-height: 1.5;
}
code {
font-size: 12px;
word-break: break-all;
}
@media (max-width: 1100px) {
.control-row {
align-items: stretch;
}
}
</style>
@@ -27,6 +27,7 @@ const selectedKey = computed(() => {
return route.name as string;
});
const isSuperAdmin = computed(() => isStoredSuperAdmin());
const isVersionPreview = import.meta.env.VITE_HERMES_PREVIEW === '1';
function isNavActive(...names: string[]) {
return names.includes(selectedKey.value);
@@ -35,7 +36,7 @@ const logoPath = '/logo.png';
const { record: collapsedGroups, persist: persistCollapsedGroups } = usePersistentRecord('hermes.sidebar.collapsedGroups');
type SidebarGroupKey = "Conversation" | "Agent" | "Monitoring" | "System";
type SidebarGroupKey = "Conversation" | "Agent" | "Monitoring" | "Tools" | "System";
function groupLabel(key: SidebarGroupKey) {
return t(`sidebar.group${key}${appStore.sidebarCollapsed ? "Short" : ""}`);
@@ -253,6 +254,29 @@ function openChangelog() {
</div>
</div>
<!-- Tools -->
<div class="nav-group">
<div class="nav-group-label" @click="toggleGroup('tools')">
<span>{{ groupLabel("Tools") }}</span>
<svg class="nav-group-arrow" :class="{ collapsed: isGroupCollapsed('tools') }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<div v-show="!isGroupCollapsed('tools')" class="nav-group-items">
<RouteLinkItem v-if="isSuperAdmin && !isVersionPreview" class="nav-item" :to="{ name: 'hermes.versionPreview' }" :active="selectedKey === 'hermes.versionPreview'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<polyline points="7.5 4.21 12 6.81 16.5 4.21" />
<polyline points="7.5 19.79 7.5 14.6 3 12" />
<polyline points="21 12 16.5 14.6 16.5 19.79" />
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
<line x1="12" y1="22.08" x2="12" y2="12" />
</svg>
<span>{{ t("sidebar.versionPreview") }}</span>
</RouteLinkItem>
</div>
</div>
<!-- System -->
<div class="nav-group">
<div class="nav-group-label" @click="toggleGroup('system')">
+32
View File
@@ -148,6 +148,8 @@ export default {
noChangelog: 'Kein Anderungsprotokoll verfugbar',
kanban: 'Kanban',
groupTools: 'Werkzeuge',
groupToolsShort: "Tools",
versionPreview: "Versionsvorschau",
groupPlatform: 'Plattform',
gateways: 'Gateways',
expand: 'Menü ausklappen',
@@ -930,6 +932,36 @@ jobTriggered: 'Job ausgelost',
saved: 'Gespeichert',
},
},
githubPreview: {
title: "Versionsvorschau",
description: "Klont den ausgewählten GitHub-Tag in den Web-UI-Vorschaubereich, installiert Abhängigkeiten und startet ihn mit den Entwicklungsports.",
refresh: "Aktualisieren",
selectTag: "Tag auswählen",
prepare: "Code vorbereiten",
install: "Abhängigkeiten installieren",
start: "Vorschau starten",
stop: "Stoppen",
note: "Der Vorschaucode wird im Web-UI-Datenverzeichnis gespeichert. Produktion bleibt auf Port 8648; die Vorschau nutzt Frontend 8651 und Backend 8650.",
path: "Vorschaupfad",
webuiHome: "Vorschau-Datenverzeichnis",
currentTag: "Aktueller Tag",
repoReady: "Repository bereit",
dependencies: "Abhängigkeiten installiert",
running: "Status",
notRunning: "Nicht gestartet",
open: "Vorschau öffnen",
log: "Pfad zum Aktionslog",
logOutput: "Logausgabe",
actionLog: "Aktionslog",
devLog: "Dev-Server-Log",
yes: "Ja",
no: "Nein",
actionFailed: "Aktion fehlgeschlagen",
prepareSuccess: "Vorschaucode ist bereit",
installSuccess: "Abhängigkeiten installiert",
startSuccess: "Vorschau gestartet",
stopSuccess: "Vorschau gestoppt",
},
// Platform channel settings
platform: {
+32
View File
@@ -137,6 +137,8 @@ export default {
groupMonitoring: 'Monitoring',
groupMonitoringShort: 'Mon',
groupTools: 'Tools',
groupToolsShort: "Tools",
versionPreview: "Version Preview",
settings: 'Settings',
connected: 'Connected',
disconnected: 'Disconnected',
@@ -1032,6 +1034,36 @@ export default {
mimoStylePromptPlaceholder: 'e.g., Bright and bouncy tone, fast pace',
},
},
githubPreview: {
title: "Version Preview",
description: "Clone a selected GitHub tag into the Web UI preview workspace, install dependencies, and run it with the development ports.",
refresh: "Refresh",
selectTag: "Select a tag",
prepare: "Prepare Code",
install: "Install Dependencies",
start: "Start Preview",
stop: "Stop",
note: "Preview code is stored under the Web UI data home. Production remains on port 8648; preview development runs on frontend 8651 and backend 8650.",
path: "Preview Path",
webuiHome: "Preview Data Home",
currentTag: "Current Tag",
repoReady: "Repository Ready",
dependencies: "Dependencies Installed",
running: "Running",
notRunning: "Not running",
open: "Open Preview",
log: "Action Log Path",
logOutput: "Log Output",
actionLog: "Action Log",
devLog: "Dev Server Log",
yes: "Yes",
no: "No",
actionFailed: "Action failed",
prepareSuccess: "Preview code is ready",
installSuccess: "Dependencies installed",
startSuccess: "Preview started",
stopSuccess: "Preview stopped",
},
// Platform channel settings
platform: {
+32
View File
@@ -148,6 +148,8 @@ export default {
noChangelog: 'No hay registro de cambios',
kanban: 'Kanban',
groupTools: 'Herramientas',
groupToolsShort: "Herr.",
versionPreview: "Vista previa de versión",
groupPlatform: 'Plataforma',
gateways: 'Puertas de enlace',
expand: 'Expandir menú',
@@ -930,6 +932,36 @@ jobTriggered: 'Job ejecutado',
saved: 'Guardado',
},
},
githubPreview: {
title: "Vista previa de versión",
description: "Clona el tag de GitHub seleccionado en el espacio de vista previa de Web UI, instala dependencias y lo ejecuta con los puertos de desarrollo.",
refresh: "Actualizar",
selectTag: "Selecciona un tag",
prepare: "Preparar código",
install: "Instalar dependencias",
start: "Iniciar vista previa",
stop: "Detener",
note: "El código de vista previa se guarda bajo el directorio de datos de Web UI. Producción sigue en el puerto 8648; la vista previa usa frontend 8651 y backend 8650.",
path: "Ruta de vista previa",
webuiHome: "Datos de vista previa",
currentTag: "Tag actual",
repoReady: "Repositorio listo",
dependencies: "Dependencias instaladas",
running: "Estado",
notRunning: "No ejecutándose",
open: "Abrir vista previa",
log: "Ruta del log de acciones",
logOutput: "Salida de logs",
actionLog: "Log de acciones",
devLog: "Log del servidor dev",
yes: "Sí",
no: "No",
actionFailed: "Acción fallida",
prepareSuccess: "Código de vista previa listo",
installSuccess: "Dependencias instaladas",
startSuccess: "Vista previa iniciada",
stopSuccess: "Vista previa detenida",
},
// Platform channel settings
platform: {
+32
View File
@@ -148,6 +148,8 @@ export default {
noChangelog: 'Aucun journal disponible',
kanban: 'Kanban',
groupTools: 'Outils',
groupToolsShort: "Outils",
versionPreview: "Aperçu de version",
groupPlatform: 'Plateforme',
gateways: 'Passerelles',
expand: 'Déplier le menu',
@@ -930,6 +932,36 @@ jobTriggered: 'Job declenche',
saved: 'Enregistré',
},
},
githubPreview: {
title: "Aperçu de version",
description: "Clone le tag GitHub sélectionné dans lespace de prévisualisation Web UI, installe les dépendances, puis lance lapplication sur les ports de développement.",
refresh: "Actualiser",
selectTag: "Sélectionner un tag",
prepare: "Préparer le code",
install: "Installer les dépendances",
start: "Démarrer laperçu",
stop: "Arrêter",
note: "Le code de prévisualisation est stocké dans le dossier de données Web UI. La production reste sur le port 8648 ; la prévisualisation utilise le frontend 8651 et le backend 8650.",
path: "Chemin de prévisualisation",
webuiHome: "Données de prévisualisation",
currentTag: "Tag actuel",
repoReady: "Dépôt prêt",
dependencies: "Dépendances installées",
running: "État",
notRunning: "Arrêté",
open: "Ouvrir laperçu",
log: "Chemin du journal daction",
logOutput: "Sortie des journaux",
actionLog: "Journal daction",
devLog: "Journal du serveur dev",
yes: "Oui",
no: "Non",
actionFailed: "Échec de laction",
prepareSuccess: "Code de prévisualisation prêt",
installSuccess: "Dépendances installées",
startSuccess: "Prévisualisation démarrée",
stopSuccess: "Prévisualisation arrêtée",
},
// Platform channel settings
platform: {
+32 -1
View File
@@ -148,6 +148,8 @@ export default {
noChangelog: '更新履歴はありません',
kanban: 'カンバン',
groupTools: 'ツール',
groupToolsShort: "ツール",
versionPreview: "バージョンプレビュー",
groupPlatform: 'プラットフォーム',
gateways: 'ゲートウェイ',
expand: 'メニューを展開',
@@ -930,8 +932,37 @@ export default {
saved: '保存しました',
},
},
githubPreview: {
title: "バージョンプレビュー",
description: "選択した GitHub tag を Web UI のプレビュー作業ディレクトリへクローンし、依存関係をインストールして開発ポートで起動します。",
refresh: "更新",
selectTag: "tag を選択",
prepare: "コードを準備",
install: "依存関係をインストール",
start: "プレビューを開始",
stop: "停止",
note: "プレビューコードは Web UI データホーム配下に保存されます。本番は 8648 のまま、プレビュー開発環境はフロントエンド 8651、バックエンド 8650 で実行されます。",
path: "プレビューパス",
webuiHome: "プレビューデータホーム",
currentTag: "現在の Tag",
repoReady: "リポジトリ準備済み",
dependencies: "依存関係インストール済み",
running: "実行状態",
notRunning: "未実行",
open: "プレビューを開く",
log: "操作ログパス",
logOutput: "ログ出力",
actionLog: "操作ログ",
devLog: "開発サーバーログ",
yes: "はい",
no: "いいえ",
actionFailed: "操作に失敗しました",
prepareSuccess: "プレビューコードの準備が完了しました",
installSuccess: "依存関係をインストールしました",
startSuccess: "プレビューを起動しました",
stopSuccess: "プレビューを停止しました",
},
// プラットフォームチャンネル設定
platform: {
requireMention: "メンションが必要",
requireMentionGroup: "グループで応答するには {'@'}メンションが必要",
+32 -1
View File
@@ -148,6 +148,8 @@ export default {
noChangelog: '변경 이력이 없습니다',
kanban: '칸반',
groupTools: '도구',
groupToolsShort: "도구",
versionPreview: "버전 미리보기",
groupPlatform: '플랫폼',
gateways: '게이트웨이',
expand: '메뉴 펼치기',
@@ -930,8 +932,37 @@ export default {
saved: '저장됨',
},
},
githubPreview: {
title: "버전 미리보기",
description: "선택한 GitHub tag 를 Web UI 미리보기 작업 디렉터리에 클론하고, 의존성을 설치한 뒤 개발 포트로 실행합니다.",
refresh: "새로고침",
selectTag: "tag 선택",
prepare: "코드 준비",
install: "의존성 설치",
start: "미리보기 시작",
stop: "중지",
note: "미리보기 코드는 Web UI 데이터 홈 아래에 저장됩니다. 프로덕션은 8648을 유지하고, 미리보기 개발 환경은 프론트엔드 8651, 백엔드 8650에서 실행됩니다.",
path: "미리보기 경로",
webuiHome: "미리보기 데이터 홈",
currentTag: "현재 Tag",
repoReady: "저장소 준비됨",
dependencies: "의존성 설치됨",
running: "실행 상태",
notRunning: "실행 중 아님",
open: "미리보기 열기",
log: "작업 로그 경로",
logOutput: "로그 출력",
actionLog: "작업 로그",
devLog: "개발 서버 로그",
yes: "예",
no: "아니요",
actionFailed: "작업 실패",
prepareSuccess: "미리보기 코드가 준비되었습니다",
installSuccess: "의존성이 설치되었습니다",
startSuccess: "미리보기가 시작되었습니다",
stopSuccess: "미리보기가 중지되었습니다",
},
// 플랫폼 채널 설정
platform: {
requireMention: "{'@'}멘션 필요",
requireMentionGroup: "그룹에서 {'@'}멘션 시에만 응답",
+32
View File
@@ -148,6 +148,8 @@ export default {
noChangelog: 'Nenhum registro disponivel',
kanban: 'Kanban',
groupTools: 'Ferramentas',
groupToolsShort: "Ferr.",
versionPreview: "Prévia de versão",
groupPlatform: 'Plataforma',
gateways: 'Gateways',
expand: 'Expandir menu',
@@ -930,6 +932,36 @@ jobTriggered: 'Job acionado',
saved: 'Salvo',
},
},
githubPreview: {
title: "Prévia de versão",
description: "Clona a tag do GitHub selecionada para o workspace de prévia do Web UI, instala dependências e executa com as portas de desenvolvimento.",
refresh: "Atualizar",
selectTag: "Selecione uma tag",
prepare: "Preparar código",
install: "Instalar dependências",
start: "Iniciar prévia",
stop: "Parar",
note: "O código de prévia é armazenado no diretório de dados do Web UI. Produção permanece na porta 8648; a prévia usa frontend 8651 e backend 8650.",
path: "Caminho da prévia",
webuiHome: "Dados da prévia",
currentTag: "Tag atual",
repoReady: "Repositório pronto",
dependencies: "Dependências instaladas",
running: "Estado",
notRunning: "Não em execução",
open: "Abrir prévia",
log: "Caminho do log de ações",
logOutput: "Saída de logs",
actionLog: "Log de ações",
devLog: "Log do servidor dev",
yes: "Sim",
no: "Não",
actionFailed: "Ação falhou",
prepareSuccess: "Código de prévia pronto",
installSuccess: "Dependências instaladas",
startSuccess: "Prévia iniciada",
stopSuccess: "Prévia parada",
},
// Platform channel settings
platform: {
+32
View File
@@ -137,6 +137,8 @@ export default {
groupMonitoring: '監控',
groupMonitoringShort: '監控',
groupTools: '工具',
groupToolsShort: "工具",
versionPreview: "版本預覽",
settings: '設定',
connected: '已連線',
disconnected: '未連線',
@@ -1024,6 +1026,36 @@ export default {
mimoStylePromptPlaceholder: '例如:用輕快上揚的語調,語速稍快',
},
},
githubPreview: {
title: "版本預覽",
description: "將選取的 GitHub tag 複製到 Web UI 預覽工作目錄,安裝依賴並以開發連接埠執行。",
refresh: "重新整理",
selectTag: "選擇 tag",
prepare: "準備程式碼",
install: "安裝依賴",
start: "開啟預覽",
stop: "停止",
note: "預覽程式碼存放在 Web UI 資料目錄下。正式環境仍使用 8648,預覽開發環境使用前端 8651、後端 8650。",
path: "預覽路徑",
webuiHome: "預覽資料目錄",
currentTag: "目前 Tag",
repoReady: "倉庫就緒",
dependencies: "依賴已安裝",
running: "執行狀態",
notRunning: "未執行",
open: "開啟預覽",
log: "操作日誌路徑",
logOutput: "日誌輸出",
actionLog: "操作日誌",
devLog: "開發服務日誌",
yes: "是",
no: "否",
actionFailed: "操作失敗",
prepareSuccess: "預覽程式碼已準備好",
installSuccess: "依賴安裝完成",
startSuccess: "預覽已啟動",
stopSuccess: "預覽已停止",
},
// 平台頻道設定
platform: {
+32
View File
@@ -137,6 +137,8 @@ export default {
groupMonitoring: '监控',
groupMonitoringShort: '监控',
groupTools: '工具',
groupToolsShort: "工具",
versionPreview: "版本预览",
settings: '设置',
connected: '已连接',
disconnected: '未连接',
@@ -1024,6 +1026,36 @@ export default {
mimoStylePromptPlaceholder: '例如:用轻快上扬的语调,语速稍快',
},
},
githubPreview: {
title: "版本预览",
description: "将选中的 GitHub tag 克隆到 Web UI 预览工作目录,安装依赖并以开发端口运行。",
refresh: "刷新",
selectTag: "选择 tag",
prepare: "准备代码",
install: "安装依赖",
start: "开启预览",
stop: "停止",
note: "预览代码存放在 Web UI 数据目录下。正式环境仍使用 8648,预览开发环境使用前端 8651、后端 8650。",
path: "预览路径",
webuiHome: "预览数据目录",
currentTag: "当前 Tag",
repoReady: "仓库就绪",
dependencies: "依赖已安装",
running: "运行状态",
notRunning: "未运行",
open: "打开预览",
log: "操作日志路径",
logOutput: "日志输出",
actionLog: "操作日志",
devLog: "开发服务日志",
yes: "是",
no: "否",
actionFailed: "操作失败",
prepareSuccess: "预览代码已准备好",
installSuccess: "依赖安装完成",
startSuccess: "预览已启动",
stopSuccess: "预览已停止",
},
// 平台频道设置
platform: {
+6
View File
@@ -117,6 +117,12 @@ const router = createRouter({
name: 'hermes.files',
component: () => import('@/views/hermes/FilesView.vue'),
},
{
path: '/hermes/version-preview',
name: 'hermes.versionPreview',
component: () => import('@/views/hermes/VersionPreviewView.vue'),
meta: { requiresSuperAdmin: true },
},
],
})
@@ -290,9 +290,9 @@ function buildWsUrl(): string {
return `${wsProtocol}//${new URL(base).host}/api/hermes/terminal${token ? `?token=${encodeURIComponent(token)}` : ""}`;
}
// Dev mode: connect directly to backend port; Production: same host
const host = import.meta.env.DEV
? formatHostForPort(location.hostname, 8648)
const directDevPort = import.meta.env.VITE_HERMES_DIRECT_WS_PORT;
const host = import.meta.env.DEV && directDevPort
? formatHostForPort(location.hostname, Number(directDevPort))
: location.host;
return `${wsProtocol}//${host}/api/hermes/terminal${token ? `?token=${encodeURIComponent(token)}` : ""}`;
}
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import GithubPreviewSettings from '@/components/hermes/settings/GithubPreviewSettings.vue'
const { t } = useI18n()
</script>
<template>
<div class="version-preview-view">
<header class="page-header">
<h2 class="header-title">{{ t('githubPreview.title') }}</h2>
</header>
<div class="page-content">
<GithubPreviewSettings />
</div>
</div>
</template>
<style scoped lang="scss">
@use "@/styles/variables" as *;
.version-preview-view {
height: calc(100 * var(--vh));
display: flex;
flex-direction: column;
}
.page-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
</style>