Files
Hermes-ui/packages/client/src/i18n/locales/en.ts
T
ww 0cc31ee999 feat: add file browser and file download with multi-backend support (#142)
* feat: add file browser and file download with multi-backend support

Adds a built-in File Browser page and a File Download system to Hermes
Web UI, enabling users to browse, edit, preview, upload, and download
files from the workspace directly from the web dashboard.

File Browser (/hermes/files):
- New view FilesView.vue plus components under components/hermes/files/
  (FileTree, FileList, FileBreadcrumb, FileToolbar, FileContextMenu,
  FileEditor, FilePreview, FileRenameModal, FileUploadModal)
- New Pinia store stores/hermes/files.ts for directory tree, selection,
  and editing state
- New API module api/hermes/files.ts
- New server routes routes/hermes/files.ts with CRUD, rename, upload,
  and directory listing
- New service services/hermes/file-provider.ts with a pluggable
  provider architecture (local filesystem + multi-terminal backends)

File Download:
- New server route routes/hermes/download.ts and client API
  api/hermes/download.ts
- Integration in chat messages (MessageItem.vue, MarkdownRenderer.vue)
  to surface downloadable file references

Packaging:
- package.json: add a prepare script so the package can be installed
  directly from a git URL with dist/ built automatically

i18n: add files/download translations to en.ts and zh.ts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: use clipboard fallback for non-secure HTTP contexts

navigator.clipboard is undefined on HTTP intranet deployments (only
available in secure contexts). The previous synchronous calls threw
silently and the success toast still fired, making 'copy' actions
appear broken.

- Add packages/client/src/utils/clipboard.ts with execCommand fallback
  via a hidden textarea
- Use the helper in FileContextMenu (copy file path), CodexLoginModal
  (copy user code), NousLoginModal (copy user code), ChatPanel (copy
  session id)
- Each call now awaits the result and shows success/failure based on
  the actual outcome

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 12:09:39 +08:00

627 lines
21 KiB
TypeScript

export default {
// Login
login: {
title: 'Hermes Web UI',
description: 'Enter your access token to continue. Find it in the server startup logs.',
placeholder: 'Access token',
submit: 'Login',
tokenRequired: 'Please enter your access token',
invalidToken: 'Invalid token',
connectionFailed: 'Cannot connect to server',
passwordLogin: 'Password',
tokenLogin: 'Token',
usernamePlaceholder: 'Username',
passwordPlaceholder: 'Password',
credentialsRequired: 'Please enter username and password',
invalidCredentials: 'Invalid username or password',
passwordMismatch: 'Passwords do not match',
passwordTooShort: 'Password must be at least 6 characters',
setupSuccess: 'Password login configured successfully',
passwordChanged: 'Password changed successfully',
passwordRemoved: 'Password login removed',
setupPassword: 'Set Up Password Login',
changePassword: 'Change Password',
changeUsername: 'Change Username',
removePasswordLogin: 'Remove',
username: 'Username',
currentPassword: 'Current Password',
newPassword: 'New Password',
confirmPassword: 'Confirm Password',
newUsername: 'New Username',
usernameChanged: 'Username changed successfully',
usernameTooShort: 'Username must be at least 2 characters',
setupDescription: 'Set up a username and password for convenient login. The access token will continue to work as a backup.',
removeConfirm: 'Are you sure you want to remove password login? You will need to use the access token to log in.',
passwordLoginNotConfigured: 'Password login is not configured',
passwordLoginConfigured: 'Password login enabled ({username})',
},
// Common
common: {
loading: 'Loading...',
cancel: 'Cancel',
delete: 'Delete',
edit: 'Edit',
save: 'Save',
retry: 'Retry',
saved: 'Saved',
update: 'Update',
create: 'Create',
saveFailed: 'Save failed',
ok: 'OK',
copied: 'Copied',
copy: 'Copy',
noData: 'No data',
fetch: 'Fetch',
add: 'Add',
enable: 'Enable',
disable: 'Disable',
configured: 'Configured',
notConfigured: 'Not configured',
confirm: 'Confirm',
expand: 'Expand',
collapse: 'Collapse',
start: 'Start',
stop: 'Stop',
},
// Sidebar
sidebar: {
chat: 'Chat',
search: 'Search',
jobs: 'Jobs',
models: 'Models',
profiles: 'Profiles',
skills: 'Skills',
memory: 'Memory',
logs: 'Logs',
usage: 'Usage',
channels: 'Channels',
gateways: 'Gateways',
terminal: 'Terminal',
files: 'Files',
groupConversation: 'Conversation',
groupPlatform: 'Platform',
groupAgent: 'Agent',
groupSystem: 'System',
groupMonitoring: 'Monitoring',
groupTools: 'Tools',
settings: 'Settings',
connected: 'Connected',
disconnected: 'Disconnected',
collapse: 'Collapse menu',
expand: 'Expand menu',
updateTip: 'Run "hermes-web-ui update" in terminal to update',
updateVersion: 'Upgrade to v{version}',
updating: 'Updating...',
updateSuccess: 'Update complete, please restart the server',
updateFailed: 'Update failed',
logout: 'Sign Out',
changelog: 'Changelog',
noChangelog: 'No changelog available',
},
// Chat
chat: {
contextRemaining: 'remaining',
emptyState: 'Start a conversation with Hermes Agent',
inputPlaceholder: 'Type a message... (Enter to send, Shift+Enter for new line)',
attachFiles: 'Attach files',
stop: 'Stop',
start: 'Start',
stopGateway: 'Stop Gateway',
send: 'Send',
contextUsed: 'Context used:',
sessions: 'Sessions',
noSessions: 'No sessions',
searchTitle: 'Search Sessions',
searchSubtitle: 'Search by title or message content',
searchHint: 'Cmd/Ctrl+K',
searchPlaceholder: 'Search sessions...',
searchEmpty: 'Recent sessions',
searchRecent: 'Recent session',
searchNoResults: 'No sessions match your search',
searchNoSnippet: 'No snippet available',
searchEnterHint: 'Enter to open · Esc to close',
searchFailed: 'Failed to search sessions',
newChat: 'New Chat',
deleteSession: 'Delete this session?',
sessionDeleted: 'Session deleted',
rename: 'Rename',
pin: 'Pin',
unpin: 'Unpin',
pinned: 'Pinned',
chatMode: 'Chat',
liveMode: 'Live',
liveSessions: 'Live Sessions',
recentBadge: 'Recent',
linkedSessions: '{count} linked',
noVisibleMessages: 'No human-visible messages.',
monitorRoleUser: 'User',
monitorRoleAssistant: 'Assistant',
copySessionId: 'Copy Session ID',
renamed: 'Renamed',
renameFailed: 'Rename failed',
renameSession: 'Rename Session',
enterNewTitle: 'Enter new title',
other: 'Other',
runFailed: 'Run failed',
error: 'Error',
tool: 'Tool',
arguments: 'Arguments',
result: 'Result',
truncated: '... (truncated)',
},
// Jobs
jobs: {
title: 'Scheduled Jobs',
createJob: 'Create Job',
editJob: 'Edit Job',
noJobs: 'No scheduled jobs yet. Create one to get started.',
name: 'Name',
namePlaceholder: 'Job name',
schedule: 'Schedule (Cron Expression)',
schedulePlaceholder: 'e.g. 0 9 * * *',
quickPresets: 'Quick Presets',
selectPreset: 'Select a preset...',
presetEveryMinute: 'Every minute',
presetEvery5Min: 'Every 5 minutes',
presetEveryHour: 'Every hour',
presetEveryDay: 'Every day at 00:00',
presetEveryDay9: 'Every day at 09:00',
presetEveryMonday: 'Every Monday at 09:00',
presetEveryMonth: 'Every month 1st at 09:00',
prompt: 'Prompt',
promptPlaceholder: 'The prompt to execute',
deliverTarget: 'Deliver Target',
origin: 'Origin',
local: 'Local',
repeatCount: 'Repeat Count (optional)',
repeatPlaceholder: 'Leave empty for infinite',
jobCreated: 'Job created',
jobUpdated: 'Job updated',
nameRequired: 'Name is required',
scheduleRequired: 'Schedule is required',
loadFailed: 'Failed to load job',
jobPaused: 'Job paused',
jobResumed: 'Job resumed',
jobTriggered: 'Job triggered',
jobDeleted: 'Job deleted',
status: {
running: 'Running',
paused: 'Paused',
disabled: 'Disabled',
scheduled: 'Scheduled',
},
info: {
schedule: 'Schedule',
lastRun: 'Last Run',
nextRun: 'Next Run',
deliver: 'Deliver',
repeat: 'Repeat',
},
action: {
pause: 'Pause',
pauseJob: 'Pause job',
resume: 'Resume',
resumeJob: 'Resume job',
runNow: 'Run Now',
triggerImmediately: 'Trigger immediately',
},
},
// Skills
skills: {
title: 'Skills',
searchPlaceholder: 'Search skills...',
noMatch: 'No skills match your search',
noSkills: 'No skills found',
backTo: 'Back to',
attachedFiles: 'Attached Files',
loadFailed: 'Failed to load skill',
fileLoadFailed: 'Failed to load file',
toggleFailed: 'Failed to toggle skill',
},
// Memory
memory: {
title: 'Memory',
refresh: 'Refresh',
loadFailed: 'Failed to load memory',
myNotes: 'My Notes',
noNotes: 'No notes yet.',
notesPlaceholder: 'Write your notes...',
userProfile: 'User Profile',
noProfile: 'No profile yet.',
profilePlaceholder: 'Write your profile...',
soul: 'Soul',
noSoul: 'No soul configuration yet.',
soulPlaceholder: 'Write soul configuration...',
},
// Models
models: {
title: 'Models',
searchPlaceholder: 'Search models...',
addProvider: 'Add Provider',
providerType: 'Provider Type',
preset: 'Preset',
custom: 'Custom',
selectProvider: 'Select Provider',
chooseProvider: 'Choose a provider...',
name: 'Name',
autoGeneratedName: 'Auto-generated from Base URL',
baseUrl: 'Base URL',
baseUrlPlaceholder: 'e.g. https://api.example.com/v1',
apiKey: 'API Key',
apiKeyPlaceholder: 'sk-...',
defaultModel: 'Default Model',
selectOrInput: 'Select or type a model name...',
selectModel: 'Select a model...',
providerAdded: 'Provider added',
providerDeleted: 'Provider deleted',
deleteProvider: 'Delete Provider',
deleteConfirm: 'Are you sure you want to delete "{name}"?',
codexLoginTitle: 'OpenAI Codex Login',
codexWaiting: 'Enter this code at the authorization page to complete login:',
codexCopyCode: 'Code copied',
codexOpenLink: 'Open authorization page',
codexApproved: 'Login successful',
codexExpired: 'Authorization expired. Please try again.',
nousLoginTitle: 'Nous Portal Login',
nousWaiting: 'Enter this code at the authorization page to complete login:',
nousCopyCode: 'Code copied',
nousOpenLink: 'Open authorization page',
nousApproved: 'Login successful',
nousDenied: 'Authorization was denied. Please try again.',
nousExpired: 'Authorization expired. Please try again.',
noProviders: 'No providers found. Add a custom provider to get started.',
builtIn: 'Built-in',
customType: 'Custom',
provider: 'Provider',
local: 'Local ({host})',
selectProviderRequired: 'Please select a provider',
baseUrlRequired: 'Base URL is required',
apiKeyRequired: 'API Key is required',
modelRequired: 'Default Model is required',
enterBaseUrl: 'Please enter Base URL first',
unexpectedFormat: 'Unexpected response format',
foundModels: 'Found {count} models',
fetchFailed: 'Failed to fetch models',
},
// Profiles
gateways: {
title: 'Gateways',
running: 'Running',
stopped: 'Stopped',
started: 'Started',
startFailed: 'Failed to start gateway',
stopFailed: 'Failed to stop gateway',
},
profiles: {
title: 'Profiles',
create: 'Create Profile',
import: 'Import',
export: 'Export',
rename: 'Rename',
delete: 'Delete',
switchTo: 'Switch to',
switchConfirm: 'Switching to profile "{name}" will restart the gateway. Continue?',
switchSuccess: 'Switched to profile "{name}"',
switchFailed: 'Failed to switch profile. Gateway may need manual restart.',
createSuccess: 'Profile "{name}" created',
createFailed: 'Failed to create profile',
renameSuccess: 'Profile renamed',
renameFailed: 'Failed to rename profile',
deleteConfirm: 'Are you sure you want to delete profile "{name}"?',
deleteSuccess: 'Profile deleted',
deleteFailed: 'Failed to delete profile',
exportSuccess: 'Profile exported',
exportFailed: 'Failed to export profile',
importSuccess: 'Profile imported',
importFailed: 'Failed to import profile',
importSelectFile: 'Select archive file',
importInvalidFile: 'Please select a valid archive (.tar.gz, .tgz, .gz, .zip)',
name: 'Profile Name',
namePlaceholder: 'English letters, numbers, hyphens only',
newName: 'New Name',
newNamePlaceholder: 'Enter new name',
cloneFromCurrent: 'Clone from current profile',
archivePath: 'Archive Path',
archivePathPlaceholder: 'Server path to archive file',
importName: 'Profile Name (optional)',
importNamePlaceholder: 'Leave empty to use archive name',
active: 'Active',
model: 'Model',
gateway: 'Gateway',
alias: 'Alias',
provider: 'Provider',
path: 'Path',
skills: 'Skills',
hasEnv: 'Has .env',
hasSoulMd: 'Has soul.md',
noProfiles: 'No profiles found. Create one to get started.',
},
// Logs
logs: {
title: 'Logs',
all: 'All',
searchPlaceholder: 'Search...',
refresh: 'Refresh',
noEntries: 'No log entries',
},
// Settings
settings: {
title: 'Settings',
saved: 'Saved',
saveFailed: 'Save failed',
tabs: {
display: 'Display',
account: 'Account',
agent: 'Agent',
memory: 'Memory',
session: 'Session',
privacy: 'Privacy',
apiServer: 'API Server',
models: 'Models',
},
models: {
apiKey: 'API Key',
apiKeyPlaceholder: 'Enter API key',
save: 'Save',
saved: 'Saved',
saveFailed: 'Save failed',
noProviders: 'No providers configured',
},
display: {
streaming: 'Stream Responses',
streamingHint: 'Show AI replies in real-time',
compact: 'Compact Mode',
compactHint: 'Reduce message spacing',
showReasoning: 'Show Reasoning',
showReasoningHint: 'Show model thinking process',
showCost: 'Show Cost',
showCostHint: 'Show token usage in replies',
inlineDiffs: 'Inline Diffs',
inlineDiffsHint: 'Show code changes inline',
bellOnComplete: 'Completion Sound',
bellOnCompleteHint: 'Play sound when AI finishes',
busyInputMode: 'Busy Input Mode',
busyInputModeHint: 'Allow input while AI is processing',
theme: 'Theme',
themeHint: 'Choose light, dark, or follow system preference',
themeLight: 'Light',
themeDark: 'Dark',
themeSystem: 'System',
},
agent: {
maxTurns: 'Max Turns',
maxTurnsHint: 'Maximum interaction rounds per conversation',
gatewayTimeout: 'Gateway Timeout',
gatewayTimeoutHint: 'Request timeout in seconds',
restartDrainTimeout: 'Restart Drain Timeout',
restartDrainTimeoutHint: 'Drain timeout before restart in seconds',
toolEnforcement: 'Tool Enforcement',
toolEnforcementHint: 'Control tool call execution mode',
auto: 'Auto',
always: 'Always',
never: 'Never',
},
memory: {
enabled: 'Enable Memory',
enabledHint: 'Allow AI to remember conversation context',
userProfile: 'User Profile',
userProfileHint: 'Allow AI to remember user preferences',
charLimit: 'Memory Char Limit',
charLimitHint: 'Max characters for MEMORY.md',
userCharLimit: 'User Profile Char Limit',
userCharLimitHint: 'Max characters for USER.md',
},
session: {
mode: 'Reset Mode',
modeHint: 'Trigger condition for session reset',
modeBoth: 'Idle + Scheduled',
modeIdle: 'Idle Only',
modeHourly: 'Scheduled Only',
idleMinutes: 'Idle Timeout',
idleMinutesHint: 'Wait time before auto-reset (minutes)',
atHour: 'Scheduled Reset Time',
humanOnly: 'Show human sessions only',
humanOnlyHint: 'Hide sub-agent/session monitor noise by default',
liveMonitorHumanOnly: 'Live monitor: show human sessions only',
liveMonitorHumanOnlyHint: 'Hide sub-agent/session monitor noise in the Live monitor by default',
atHourHint: 'Reset session at this hour daily',
},
privacy: {
redactPii: 'Redact PII',
redactPiiHint: 'Auto-detect and hide sensitive info (passwords, keys, etc.)',
},
apiServer: {
enable: 'Enable',
enableHint: 'Enable API server',
host: 'Host',
hostHint: 'Listen address',
port: 'Port',
portHint: 'Listen port',
key: 'Key',
keyHint: 'API access key',
cors: 'CORS Origins',
corsHint: 'Allowed cross-origin sources',
},
},
// Platform channel settings
platform: {
requireMention: "Require {'@'}Mention",
requireMentionGroup: "Require {'@'}mention in groups to respond",
requireMentionChannel: "Require {'@'}mention in channels to respond",
requireMentionRoom: "Require {'@'}mention in rooms to respond",
reactions: 'Reactions',
reactionsHint: 'React to messages with emoji',
freeResponseChats: 'Free Response Chats',
freeResponseChatsHint: "Chat IDs that respond without {'@'}mention (comma-separated)",
freeResponseChannels: 'Free Response Channels',
freeResponseChannelsHint: "Channel IDs that respond without {'@'}mention (comma-separated)",
freeResponseRooms: 'Free Response Rooms',
freeResponseRoomsHint: "Room IDs that respond without {'@'}mention (comma-separated)",
mentionPatterns: 'Custom Mention Patterns',
mentionPatternsHint: 'Additional trigger patterns',
autoThread: 'Auto Thread',
autoThreadHint: "Auto-create reply threads after {'@'}mention",
autoThreadHintRoom: 'Auto-create reply threads in rooms',
dmMentionThreads: 'DM Mention Threads',
dmMentionThreadsHint: 'Use thread replies for mentions in DMs',
allowBots: 'Allow Bot Messages',
allowBotsHint: 'Respond to messages from other bots',
allowedChannels: 'Allowed Channels',
allowedChannelsHint: 'Whitelist channel IDs (comma-separated)',
ignoredChannels: 'Ignored Channels',
ignoredChannelsHint: 'Channels where bot never responds (comma-separated)',
noThreadChannels: 'No-Thread Channels',
noThreadChannelsHint: 'Channels where bot responds without threads (comma-separated)',
botToken: 'Bot Token',
botTokenHint: 'Bot token from developer portal',
accessToken: 'Access Token',
accessTokenHint: 'Matrix access token',
homeserver: 'Homeserver URL',
homeserverHint: 'Matrix homeserver URL',
appId: 'App ID',
appIdHint: 'Feishu App ID',
appSecret: 'App Secret',
appSecretHint: 'Feishu App Secret',
clientId: 'Client ID',
clientIdHint: 'DingTalk Client ID',
clientSecret: 'Client Secret',
clientSecretHint: 'DingTalk Client Secret',
botId: 'Bot ID',
botIdHint: 'WeCom Bot ID',
wecomSecretHint: 'WeCom Bot Secret',
waEnabled: 'Enable WhatsApp',
waEnabledHint: 'Enable WhatsApp via QR code pairing',
weixinToken: 'Weixin Token',
weixinTokenHint: 'From weixin CLI QR login (hermes weixin)',
accountId: 'Account ID',
accountIdHint: 'Weixin account ID',
qrLogin: 'QR Login',
qrRelogin: 'Re-login',
qrFetching: 'Fetching QR code...',
qrScanHint: 'Scan with WeChat to login',
qrScanedHint: 'Scaned, please confirm on phone...',
},
// Language
language: {
label: 'Language',
zh: '中文',
en: 'English',
},
// Terminal
terminal: {
sessions: 'Sessions',
newTab: 'New Terminal',
closeSession: 'Close this session?',
sessionExited: 'Exited',
processExited: 'Process exited with code {code}',
},
// Usage
usage: {
title: 'Usage Statistics',
refresh: 'Refresh',
totalTokens: 'Total Tokens',
inputTokens: 'Input',
outputTokens: 'Output',
totalSessions: 'Total Sessions',
avgPerDay: '~{n}/day avg',
estimatedCost: 'Est. Cost',
cacheHitRate: 'Cache Hit Rate',
modelBreakdown: 'Model Breakdown',
dailyTrend: 'Daily Usage (Last 30 Days)',
date: 'Date',
tokens: 'Tokens',
cache: 'Cache',
sessions: 'Sessions',
cost: 'Cost',
noData: 'No usage data',
},
// Files
files: {
title: 'Files',
tree: 'Directory Tree',
list: 'File List',
breadcrumbRoot: 'Home',
newFile: 'New File',
newFolder: 'New Folder',
upload: 'Upload',
refresh: 'Refresh',
open: 'Open',
edit: 'Edit',
preview: 'Preview',
download: 'Download',
copyPath: 'Copy Path',
rename: 'Rename',
delete: 'Delete',
name: 'Name',
size: 'Size',
modified: 'Modified',
actions: 'Actions',
emptyDir: 'Empty directory',
loading: 'Loading...',
confirmDelete: 'Are you sure you want to delete "{name}"?',
confirmDeleteDir: 'Are you sure you want to delete directory "{name}" and all its contents?',
deleteFailed: 'Delete failed',
deleted: 'Deleted',
renameTo: 'Rename to',
newFileName: 'File name',
newFolderName: 'Folder name',
created: 'Created',
createFailed: 'Create failed',
renamed: 'Renamed',
renameFailed: 'Rename failed',
uploadSuccess: 'Uploaded {count} file(s)',
uploadFailed: 'Upload failed',
saveFailed: 'Save failed',
saved: 'Saved',
unsavedChanges: 'You have unsaved changes. Discard?',
pathCopied: 'Path copied',
fileTooLarge: 'File too large (max 10MB)',
permissionDenied: 'Cannot modify protected file',
notFound: 'File or directory not found',
backendError: 'File operation failed',
dragDropHint: 'Drag files here to upload',
closeEditor: 'Close Editor',
saveFile: 'Save',
},
// Download
download: {
downloading: 'Downloading...',
downloadFailed: 'Download failed',
fileNotFound: 'File not found or deleted',
fileTooLarge: 'File too large (exceeds limit)',
backendError: 'File read failed, remote environment may be unavailable',
backendTimeout: 'File read timed out',
unsupportedBackend: 'Current terminal backend does not support file download',
invalidPath: 'Invalid file path',
download: 'Download',
},
// Changelog
changelog: {
new_0_4_3_1: 'Add username/password login alongside token authentication',
new_0_4_3_2: 'Add account settings for managing credentials (setup, change password, change username)',
new_0_4_3_3: 'Add logout button to sidebar',
new_0_4_3_4: 'Add version changelog popup (click version number)',
new_0_4_2_1: 'Add token usage tracking, context display, and dynamic context length',
new_0_4_2_2: 'Add session search modal',
new_0_4_2_3: 'Restore group chat system with Socket.IO and SQLite persistence',
new_0_4_2_4: 'Add pinned sessions and live monitor in Chat page',
new_0_4_2_5: 'Fix builtin provider detection and model matching',
new_0_4_1_1: 'Fix auth bypass, SPA serving, and provider improvements',
},
}