Phase 002: rebrand to hermes-ui and consolidate i18n to zh/en
Removes the ekko / hermes-web-ui branding from the whole tree and drops eight locale files, so the project presents one name and one language pair. Data compatibility is deliberate, not accidental: DB filenames, the JWT audience, and skill manifest ownership all still recognise their pre-rename values so existing deployments under ~/.hermes-web-ui keep working without a re-login or a re-injection. scripts/harness-check.mjs now scans for the legacy strings and fails the build on any new occurrence, with an explicit allow-list (and reasons) for the compatibility paths that must keep them.
This commit is contained in:
@@ -71,8 +71,8 @@ let pendingAgentBridgeHealthRefresh: Promise<AgentBridgeHealthPayload> | null =
|
||||
/**
|
||||
* Whether the periodic npm-registry version check is disabled.
|
||||
*
|
||||
* Useful when hermes-web-ui is bundled inside a packaged distribution
|
||||
* (e.g. a desktop app) where the user can't `npm install -g hermes-web-ui@latest`
|
||||
* Useful when hermes-ui is bundled inside a packaged distribution
|
||||
* (e.g. a desktop app) where the user can't `npm install -g hermes-ui@latest`
|
||||
* to upgrade — the "update available" prompt would be misleading and
|
||||
* the periodic outbound HTTP request to the npm registry is unnecessary.
|
||||
*
|
||||
@@ -106,7 +106,7 @@ function isNewerVersion(candidate: string, current: string): boolean {
|
||||
export async function checkLatestVersion(): Promise<void> {
|
||||
if (isUpdateCheckDisabled()) return
|
||||
try {
|
||||
const packageName = PACKAGE_INFO?.name || 'hermes-web-ui'
|
||||
const packageName = PACKAGE_INFO?.name || 'hermes-ui'
|
||||
const registryName = encodeURIComponent(packageName)
|
||||
const res = await fetch(`https://registry.npmjs.org/${registryName}/latest`, { signal: AbortSignal.timeout(10000) })
|
||||
if (res.ok) {
|
||||
|
||||
@@ -233,7 +233,7 @@ export async function submit(ctx: any) {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'hermes-web-ui/1.0',
|
||||
'User-Agent': 'hermes-ui/1.0',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: 'authorization_code',
|
||||
|
||||
@@ -318,7 +318,7 @@ export async function start(ctx: any) {
|
||||
state,
|
||||
nonce,
|
||||
plan: 'generic',
|
||||
referrer: 'hermes-web-ui',
|
||||
referrer: 'hermes-ui',
|
||||
}).toString()}`
|
||||
sessions.set(sessionId, {
|
||||
id: sessionId,
|
||||
|
||||
@@ -9,8 +9,8 @@ let updateInProgress = false
|
||||
const NODE_ENVIRONMENT_MISSING_CODE = 'node_environment_missing'
|
||||
const DOCKER_ENVIRONMENT_CODE = 'docker_environment'
|
||||
|
||||
const PREVIEW_DIR_NAME = 'hermes-web-ui-pereview'
|
||||
const PREVIEW_HOME_DIR_NAME = 'hermes-web-ui-pereview-home'
|
||||
const PREVIEW_DIR_NAME = 'hermes-ui-preview'
|
||||
const PREVIEW_HOME_DIR_NAME = 'hermes-ui-preview-home'
|
||||
const PREVIEW_BACKEND_PORT = 8650
|
||||
const PREVIEW_FRONTEND_PORT = 8651
|
||||
const PREVIEW_AGENT_BRIDGE_PORT = 18650
|
||||
@@ -133,9 +133,12 @@ function getPreviewRepoGitUrl(): string {
|
||||
|
||||
function getPreviewRepoApiUrl(): string {
|
||||
const baseUrl = getPreviewRepoBaseUrl()
|
||||
const match = baseUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/)
|
||||
if (!match) throw new Error(`Preview zip fallback only supports GitHub repositories: ${baseUrl}`)
|
||||
return `https://api.github.com/repos/${match[1]}/${match[2]}`
|
||||
const match = baseUrl.match(/^https:\/\/([^/]+)\/([^/]+)\/([^/]+)$/)
|
||||
if (!match) throw new Error(`Preview API fallback only supports HTTPS repository URLs: ${baseUrl}`)
|
||||
const [, host, owner, repo] = match
|
||||
if (host === 'github.com') return `https://api.github.com/repos/${owner}/${repo}`
|
||||
// Gitea and Forgejo expose the same tag list shape under /api/v1.
|
||||
return `https://${host}/api/v1/repos/${owner}/${repo}`
|
||||
}
|
||||
|
||||
function getPreviewGithubRepoParts(): { owner: string; repo: string } {
|
||||
@@ -362,7 +365,7 @@ function getPreviewViteHostArg() {
|
||||
}
|
||||
|
||||
function getGlobalPackageBin(root: string) {
|
||||
return join(root, 'hermes-web-ui', 'bin', 'hermes-web-ui.mjs')
|
||||
return join(root, 'hermes-ui', 'bin', 'hermes-ui.mjs')
|
||||
}
|
||||
|
||||
function getCurrentNodeEnv() {
|
||||
@@ -725,8 +728,8 @@ function assertPreviewPackage() {
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
||||
if (pkg?.name !== 'hermes-web-ui') {
|
||||
throw new Error(`Preview directory is not hermes-web-ui: ${getPreviewDir()}`)
|
||||
if (pkg?.name !== 'hermes-ui') {
|
||||
throw new Error(`Preview directory is not hermes-ui: ${getPreviewDir()}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,7 +935,7 @@ async function downloadGithubZip(ref: string, targetDir: string, type: 'tag' | '
|
||||
const url = `https://codeload.github.com/${owner}/${repo}/${archiveKind}/refs/${refKind}/${encodeURIComponent(ref)}`
|
||||
appendPreviewActionLog(`download archive: ${url}`)
|
||||
const res = await fetch(url, {
|
||||
headers: { 'User-Agent': 'hermes-web-ui-preview' },
|
||||
headers: { 'User-Agent': 'hermes-ui-preview' },
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
})
|
||||
if (!res.ok) throw new Error(`Failed to download GitHub archive: HTTP ${res.status}`)
|
||||
@@ -1031,7 +1034,7 @@ function getGlobalRoot() {
|
||||
function getGlobalCliScript() {
|
||||
const cli = getGlobalPackageBin(getGlobalRoot())
|
||||
if (!existsSync(cli)) {
|
||||
throw new Error(`Updated hermes-web-ui CLI not found: ${cli}`)
|
||||
throw new Error(`Updated hermes-ui CLI not found: ${cli}`)
|
||||
}
|
||||
return cli
|
||||
}
|
||||
@@ -1043,7 +1046,7 @@ function runUpdateInstall() {
|
||||
console.warn('[update] failed to clean npm cache, continuing update:', err)
|
||||
}
|
||||
|
||||
return runNpmSync(['install', '-g', 'hermes-web-ui@latest'], { timeout: 10 * 60 * 1000 })
|
||||
return runNpmSync(['install', '-g', 'hermes-ui@latest'], { timeout: 10 * 60 * 1000 })
|
||||
}
|
||||
|
||||
function spawnRestart(port: string) {
|
||||
@@ -1062,7 +1065,7 @@ export async function handleUpdate(ctx: any) {
|
||||
ctx.status = 409
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'hermes-web-ui update is already in progress',
|
||||
message: 'hermes-ui update is already in progress',
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1073,7 +1076,7 @@ export async function handleUpdate(ctx: any) {
|
||||
ctx.body = {
|
||||
success: false,
|
||||
code: DOCKER_ENVIRONMENT_CODE,
|
||||
message: 'hermes-web-ui update is not available inside Docker. '
|
||||
message: 'hermes-ui update is not available inside Docker. '
|
||||
+ 'Please pull a new image and recreate the container:\n\n'
|
||||
+ ' docker compose pull\n'
|
||||
+ ' docker compose up -d --force-recreate',
|
||||
@@ -1089,7 +1092,7 @@ export async function handleUpdate(ctx: any) {
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: output.trim() || 'hermes-web-ui updated successfully',
|
||||
message: output.trim() || 'hermes-ui updated successfully',
|
||||
}
|
||||
|
||||
keepUpdateLockForRestart = true
|
||||
@@ -1153,7 +1156,7 @@ export async function previewTags(ctx: any) {
|
||||
try {
|
||||
appendPreviewActionLog('load tags with GitHub API')
|
||||
const res = await fetch(`${getPreviewRepoApiUrl()}/tags?per_page=100`, {
|
||||
headers: { 'User-Agent': 'hermes-web-ui-preview' },
|
||||
headers: { 'User-Agent': 'hermes-ui-preview' },
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
})
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -12,6 +12,8 @@ const DB_DIR = isTest
|
||||
: isDev
|
||||
? resolve(process.cwd(), 'packages/server/data')
|
||||
: config.appHome
|
||||
// These filenames predate the hermes-web-ui -> hermes-ui rename. They are kept so
|
||||
// existing deployments under ~/.hermes-web-ui keep finding their data.
|
||||
const DB_PATH = resolve(DB_DIR, 'hermes-web-ui.db')
|
||||
const JSON_PATH = resolve(DB_DIR, 'hermes-web-ui.json')
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ function startLanDiscovery(): void {
|
||||
}
|
||||
|
||||
export async function bootstrap() {
|
||||
console.log(`hermes-web-ui v${APP_VERSION} starting...`)
|
||||
console.log(`hermes-ui v${APP_VERSION} starting...`)
|
||||
await mkdir(config.uploadDir, { recursive: true })
|
||||
if (shouldCreateWebUiDataDir()) {
|
||||
await mkdir(config.dataDir, { recursive: true })
|
||||
|
||||
@@ -39,14 +39,14 @@ export const AI_OUTPUT_FORMAT_GUIDELINES = `
|
||||
\`\`\`
|
||||
[屏幕录制](/tmp/screen-recording.mp4)
|
||||
[操作演示](/tmp/demo.webm)
|
||||
[录屏2026-05-08 15.19.46](/Users/ekko/Desktop/录屏2026-05-08%2015.19.46.mov)
|
||||
[录屏2026-05-08 15.19.46](</Users/ekko/Desktop/录屏2026-05-08 15.19.46.mov>)
|
||||
[录屏2026-05-08 15.19.46](/Users/demo/Desktop/录屏2026-05-08%2015.19.46.mov)
|
||||
[录屏2026-05-08 15.19.46](</Users/demo/Desktop/录屏2026-05-08 15.19.46.mov>)
|
||||
[Windows 录屏](<C:/Users/Administrator/Desktop/screen recording.mov>)
|
||||
\`\`\`
|
||||
|
||||
错误示例:
|
||||
\`\`\`
|
||||
[录屏2026-05-08 15.19.46](/Users/ekko/Desktop/录屏2026-05-08 15.19.46.mov)
|
||||
[录屏2026-05-08 15.19.46](/Users/demo/Desktop/录屏2026-05-08 15.19.46.mov)
|
||||

|
||||
\`\`\`
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ interface JwtPayload {
|
||||
username: string
|
||||
role: UserRole
|
||||
type: 'access'
|
||||
aud: 'hermes-web-ui'
|
||||
aud: string
|
||||
iat: number
|
||||
exp: number
|
||||
}
|
||||
@@ -39,7 +39,10 @@ declare module 'koa' {
|
||||
}
|
||||
}
|
||||
|
||||
const JWT_AUDIENCE = 'hermes-web-ui'
|
||||
const JWT_AUDIENCE = 'hermes-ui'
|
||||
// Tokens signed before the hermes-web-ui -> hermes-ui rename still carry the old
|
||||
// audience. Keep accepting them so upgrades do not force a re-login.
|
||||
const LEGACY_JWT_AUDIENCES = new Set(['hermes-web-ui'])
|
||||
const DEFAULT_EXPIRES_SECONDS = 60 * 60 * 24 * 30
|
||||
const MIN_EXPIRES_SECONDS = 1
|
||||
const MAX_EXPIRES_SECONDS = 60 * 60 * 24 * 365
|
||||
@@ -167,7 +170,8 @@ export function verifyUserJwt(token: string, secret: string, now = Date.now()):
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(body, 'base64url').toString('utf-8')) as Partial<JwtPayload>
|
||||
if (payload.type !== 'access' || payload.aud !== JWT_AUDIENCE) return null
|
||||
if (payload.type !== 'access') return null
|
||||
if (payload.aud !== JWT_AUDIENCE && !LEGACY_JWT_AUDIENCES.has(String(payload.aud))) return null
|
||||
if (!payload.sub || !payload.username || !payload.role || !payload.exp) return null
|
||||
if (Math.floor(now / 1000) >= payload.exp) return null
|
||||
return payload as JwtPayload
|
||||
|
||||
@@ -820,9 +820,9 @@ function isDesktopRuntime(): boolean {
|
||||
function candidateBundledMcpScripts(): string[] {
|
||||
return [
|
||||
process.env.HERMES_WEB_UI_MCP_BIN,
|
||||
join(process.cwd(), 'bin/hermes-studio-mcp.mjs'),
|
||||
join(__dirname, '../../bin/hermes-studio-mcp.mjs'),
|
||||
join(__dirname, '../../../../../bin/hermes-studio-mcp.mjs'),
|
||||
join(process.cwd(), 'bin/hermes-ui-mcp.mjs'),
|
||||
join(__dirname, '../../bin/hermes-ui-mcp.mjs'),
|
||||
join(__dirname, '../../../../../bin/hermes-ui-mcp.mjs'),
|
||||
join(process.cwd(), 'bin/hermes-web-ui-mcp.mjs'),
|
||||
join(__dirname, '../../bin/hermes-web-ui-mcp.mjs'),
|
||||
join(__dirname, '../../../../../bin/hermes-web-ui-mcp.mjs'),
|
||||
|
||||
@@ -44,6 +44,9 @@ function shouldIncludeManagedMcpServers(): boolean {
|
||||
function candidateBundledMcpScripts(): string[] {
|
||||
return [
|
||||
process.env.HERMES_WEB_UI_MCP_BIN,
|
||||
join(process.cwd(), 'bin/hermes-ui-mcp.mjs'),
|
||||
join(__dirname, '../../bin/hermes-ui-mcp.mjs'),
|
||||
join(__dirname, '../../../../../bin/hermes-ui-mcp.mjs'),
|
||||
join(process.cwd(), 'bin/hermes-studio-mcp.mjs'),
|
||||
join(__dirname, '../../bin/hermes-studio-mcp.mjs'),
|
||||
join(__dirname, '../../../../../bin/hermes-studio-mcp.mjs'),
|
||||
|
||||
@@ -91,7 +91,7 @@ export async function fetchPetdexManifest(options: { force?: boolean } = {}): Pr
|
||||
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
||||
try {
|
||||
const response = await fetch(PETDEX_MANIFEST_URL, {
|
||||
headers: { 'User-Agent': 'hermes-web-ui-petdex' },
|
||||
headers: { 'User-Agent': 'hermes-ui-petdex' },
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (!response.ok) {
|
||||
@@ -111,7 +111,7 @@ export async function fetchPetdexAsset(urlValue: string): Promise<{ buffer: Buff
|
||||
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: { 'User-Agent': 'hermes-web-ui-petdex' },
|
||||
headers: { 'User-Agent': 'hermes-ui-petdex' },
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (!response.ok) throw new Error(`petdex asset request failed: ${response.status}`)
|
||||
|
||||
@@ -133,7 +133,7 @@ async function fetchBytes(urlValue: string, maxBytes: number): Promise<{ buffer:
|
||||
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: { 'User-Agent': 'hermes-web-ui-pets' },
|
||||
headers: { 'User-Agent': 'hermes-ui-pets' },
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (!response.ok) throw new Error(`Pet asset request failed: ${response.status}`)
|
||||
|
||||
@@ -6,7 +6,13 @@ import { detectHermesRootHome } from './hermes-path'
|
||||
import { logger } from '../logger'
|
||||
|
||||
const MANIFEST_FILENAME = '.webui-managed-skills.json'
|
||||
const MANIFEST_OWNER = 'hermes-web-ui'
|
||||
const MANIFEST_OWNER = 'hermes-ui'
|
||||
// Manifests written before the hermes-web-ui -> hermes-ui rename still carry the old
|
||||
// owner. Recognising them keeps managed skills managed instead of re-injecting them.
|
||||
const LEGACY_MANIFEST_OWNERS = new Set(['hermes-web-ui'])
|
||||
function isManagedOwner(owner: string | undefined): boolean {
|
||||
return owner === MANIFEST_OWNER || LEGACY_MANIFEST_OWNERS.has(String(owner))
|
||||
}
|
||||
const HASH_IGNORED_FILENAMES = new Set([
|
||||
'.DS_Store',
|
||||
'Thumbs.db',
|
||||
@@ -177,7 +183,7 @@ export class HermesSkillInjector {
|
||||
|
||||
const currentHash = await this.hashDir(targetSkillDir)
|
||||
const manifestEntry = manifest[entry.name]
|
||||
const isManaged = manifestEntry?.owner === MANIFEST_OWNER
|
||||
const isManaged = isManagedOwner(manifestEntry?.owner)
|
||||
const isUnchangedManagedCopy = isManaged && manifestEntry?.installed_hash === currentHash
|
||||
const isExistingBundledCopy = !manifestEntry && currentHash === sourceHash
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SttNoSpeechDetectedError, type SttTranscribeInput, type SttTranscribeRe
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://openspeech.bytedance.com/api/v3/auc/bigmodel'
|
||||
const DEFAULT_RESOURCE_ID = 'volc.seedasr.auc'
|
||||
const DEFAULT_USER_ID = 'hermes-web-ui'
|
||||
const DEFAULT_USER_ID = 'hermes-ui'
|
||||
const DONE_STATUS = '20000000'
|
||||
const RUNNING_STATUSES = new Set(['20000001', '20000002'])
|
||||
const STATUS_MESSAGES: Record<string, string> = {
|
||||
|
||||
@@ -75,9 +75,9 @@ function isDesktopRuntime(): boolean {
|
||||
function candidateBundledMcpScripts(): string[] {
|
||||
return [
|
||||
process.env.HERMES_WEB_UI_MCP_BIN,
|
||||
join(process.cwd(), 'bin/hermes-studio-mcp.mjs'),
|
||||
join(__dirname, '../../bin/hermes-studio-mcp.mjs'),
|
||||
join(__dirname, '../../../../../bin/hermes-studio-mcp.mjs'),
|
||||
join(process.cwd(), 'bin/hermes-ui-mcp.mjs'),
|
||||
join(__dirname, '../../bin/hermes-ui-mcp.mjs'),
|
||||
join(__dirname, '../../../../../bin/hermes-ui-mcp.mjs'),
|
||||
join(process.cwd(), 'bin/hermes-web-ui-mcp.mjs'),
|
||||
join(__dirname, '../../bin/hermes-web-ui-mcp.mjs'),
|
||||
join(__dirname, '../../../../../bin/hermes-web-ui-mcp.mjs'),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { assertSafeTtsBaseUrl } from './url-safety'
|
||||
const DEFAULT_BASE_URL = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional'
|
||||
const DEFAULT_RESOURCE_ID = 'seed-tts-2.0'
|
||||
const DEFAULT_SPEAKER = 'zh_female_xiaohe_uranus_bigtts'
|
||||
const DEFAULT_USER_ID = 'hermes-web-ui'
|
||||
const DEFAULT_USER_ID = 'hermes-ui'
|
||||
const MAX_ERROR_DETAIL_LENGTH = 500
|
||||
const DEFAULT_FORMAT = 'mp3'
|
||||
const DEFAULT_SAMPLE_RATE = 24000
|
||||
|
||||
@@ -8,7 +8,7 @@ const MAX_LOG_SIZE = 3 * 1024 * 1024 // 3MB
|
||||
const CHECK_INTERVAL = 60_000 // Check every minute
|
||||
|
||||
const logDir = process.env.VITEST
|
||||
? resolve(tmpdir(), 'hermes-web-ui-test-logs', String(process.pid))
|
||||
? resolve(tmpdir(), 'hermes-ui-test-logs', String(process.pid))
|
||||
: resolve(config.appHome, 'logs')
|
||||
mkdirSync(logDir, { recursive: true })
|
||||
|
||||
|
||||
@@ -400,8 +400,8 @@ export async function downloadWebUiVersion(version: string, source: VersionDownl
|
||||
if (!cleanVersion) throw new Error('Web UI version is required')
|
||||
|
||||
const releaseTag = `v${cleanVersion}`
|
||||
const assetName = `hermes-web-ui-${cleanVersion}.tar.gz`
|
||||
const manifestName = `hermes-web-ui-${cleanVersion}.json`
|
||||
const assetName = `hermes-ui-${cleanVersion}.tar.gz`
|
||||
const manifestName = `hermes-ui-${cleanVersion}.json`
|
||||
const manifestUrl = downloadAssetUrl(manifestName, releaseTag, source)
|
||||
onProgress?.({ stage: 'resolve', message: 'runtimeVersions.jobStage.resolveWebUi' })
|
||||
const manifest = await fetchJson<{ asset?: { sha256?: string; size?: number } }>(manifestUrl)
|
||||
@@ -424,7 +424,7 @@ export async function downloadWebUiVersion(version: string, source: VersionDownl
|
||||
onProgress?.({ stage: 'extract', message: 'runtimeVersions.jobStage.extractWebUi' })
|
||||
await extractTarGzip(archive, tempRoot)
|
||||
const extractedRoot = join(tempRoot, 'webui')
|
||||
for (const required of ['package.json', 'bin/hermes-web-ui.mjs', 'dist/server/index.js']) {
|
||||
for (const required of ['package.json', 'bin/hermes-ui.mjs', 'dist/server/index.js']) {
|
||||
if (!existsSync(join(extractedRoot, required))) throw new Error(`Web UI archive is missing required file: ${required}`)
|
||||
}
|
||||
onProgress?.({ stage: 'install', message: 'runtimeVersions.jobStage.installWebUi' })
|
||||
|
||||
Reference in New Issue
Block a user