Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ function countdown(expiresAt: string): string {
const parsed = parseTimestamp(expiresAt)
const remaining = parsed - nowMs.value
if (!Number.isFinite(remaining)) return '—'
if (remaining <= 0) return '已过期'
if (remaining <= 0) return t('agent.approval.state.expired')
const total = Math.floor(remaining / 1_000)
const minutes = Math.floor(total / 60)
const seconds = total % 60
Expand Down
6 changes: 3 additions & 3 deletions packages/client-ui/src/components/agent/AgentChatSession.vue
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ async function removeConversation() {
await runtime.api.agents.deleteConversation(props.conversationId)
await router.push('/agent')
} catch (error) {
toast.show({ text: error instanceof Error ? error.message : '删除失败,请稍后重试。', tone: 'error' })
toast.show({ text: error instanceof Error ? error.message : t('agent.chat.deleteError'), tone: 'error' })
} finally {
deletePending.value = false
deleteConfirm.value = false
Expand All @@ -211,7 +211,7 @@ async function changePolicy(policy: 'manual' | 'auto') {
policyConfirm.value = false
await loadDetail()
} catch (error) {
toast.show({ text: error instanceof Error ? error.message : '无法更新审批方式。', tone: 'error' })
toast.show({ text: error instanceof Error ? error.message : t('agent.chat.policyUpdateError'), tone: 'error' })
} finally {
policyPending.value = false
}
Expand Down Expand Up @@ -284,7 +284,7 @@ async function cancelRun() {
}
conversation.history.value = { ...base, runs }
} else {
toast.show({ text: error instanceof Error ? error.message : '取消失败,请稍后重试。', tone: 'error' })
toast.show({ text: error instanceof Error ? error.message : t('agent.chat.cancelError'), tone: 'error' })
}
} finally {
canceling.value = false
Expand Down
2 changes: 1 addition & 1 deletion packages/client-ui/src/components/agent/AgentComposer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ function isRuntimeUnavailableError(error: unknown): boolean {
}

function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : '操作失败,请稍后重试。'
return error instanceof Error ? error.message : t('common.actionFailed')
}

const normalizedText = computed(() => normalizePlainText(text.value))
Expand Down
16 changes: 8 additions & 8 deletions packages/client-ui/src/components/agent/AgentMessageList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ onMounted(scrollToBottom)
watch(
() => props.history,
(next) => {
statusText.value = deriveAnnouncements(previousHistory, next).join(';')
statusText.value = deriveAnnouncements(previousHistory, next).join(t('agent.chat.announce.separator'))
previousHistory = next
if (stickToBottom) void nextTick(scrollToBottom)
},
Expand All @@ -144,20 +144,20 @@ function deriveAnnouncements(previous: AgentHistoryState | null, next: AgentHist
const out: string[] = []
for (const [id, call] of next.toolCalls) {
const before = previous.toolCalls.get(id)
const label = call.toolName === '' ? '调用' : call.toolName
if (call.status === 'completed' && (before === undefined || before.status === 'running')) out.push(`工具 ${label} 已完成`)
else if (call.status === 'failed' && (before === undefined || before.status === 'running')) out.push(`工具 ${label} 执行失败`)
const label = call.toolName === '' ? t('agent.chat.defaultToolLabel') : call.toolName
if (call.status === 'completed' && (before === undefined || before.status === 'running')) out.push(t('agent.chat.announce.toolCompleted', { label }))
else if (call.status === 'failed' && (before === undefined || before.status === 'running')) out.push(t('agent.chat.announce.toolFailed', { label }))
}
for (const [id, run] of next.runs) {
const before = previous.runs.get(id)
if (run.status === 'finished' && (before === undefined || before.status === 'active')) out.push('运行已完成')
else if (run.status === 'error' && (before === undefined || before.status === 'active')) out.push('运行出错')
if (run.status === 'finished' && (before === undefined || before.status === 'active')) out.push(t('agent.chat.announce.runFinished'))
else if (run.status === 'error' && (before === undefined || before.status === 'active')) out.push(t('agent.chat.announce.runError'))
}
for (const [id, permission] of next.permissions) {
if (permission.state === 'pending' && !previous.permissions.has(id)) out.push('收到新的审批请求')
if (permission.state === 'pending' && !previous.permissions.has(id)) out.push(t('agent.chat.announce.newApproval'))
}
const state = next.backend.state
if (state !== null && state !== previous.backend.state) out.push(`后端状态:${state}`)
if (state !== null && state !== previous.backend.state) out.push(t('agent.chat.announce.backendState', { state }))
return out
}
</script>
Original file line number Diff line number Diff line change
@@ -1,46 +1,48 @@
<template>
<div v-if="authorization.open.value" class="dialog-backdrop">
<section ref="panel" role="dialog" aria-modal="true" aria-label="重新验证身份" class="dialog-panel agent-reauth-dialog" @keydown="onKeydown">
<section ref="panel" role="dialog" aria-modal="true" :aria-label="t('agent.reauth.title')" class="dialog-panel agent-reauth-dialog" @keydown="onKeydown">
<header>
<div>
<p class="eyebrow">Security Check</p>
<h2>重新验证身份</h2>
<h2>{{ t('agent.reauth.title') }}</h2>
</div>
<button type="button" class="icon-button icon-only" aria-label="关闭" data-action="close-reauth-dialog" @click="cancel">
<button type="button" class="icon-button icon-only" :aria-label="t('common.close')" data-action="close-reauth-dialog" @click="cancel">
<X :size="18" aria-hidden="true" />
</button>
</header>
<p class="agent-reauth-dialog__hint">
<ShieldCheck :size="16" aria-hidden="true" />
<span v-if="native">请在系统浏览器中完成身份验证。</span>
<span v-else>为保护终端安全,执行敏感操作前需要重新验证管理员身份。验证通过后本次操作会自动继续。</span>
<span v-if="native">{{ t('agent.reauth.nativeHint') }}</span>
<span v-else>{{ t('agent.reauth.webHint') }}</span>
</p>
<form v-if="!native" class="security-form" @submit.prevent="submit">
<template v-if="!challenge">
<label for="reauth-root-credential">Root 凭据</label>
<label for="reauth-root-credential">{{ t('agent.reauth.rootCredential') }}</label>
<input id="reauth-root-credential" ref="credentialInput" v-model="credential" name="root_credential" type="password" autocomplete="off" required />
</template>
<template v-else>
<label for="reauth-totp">动态验证码</label>
<label for="reauth-totp">{{ t('agent.reauth.totp') }}</label>
<input id="reauth-totp" v-model="totp" name="totp" inputmode="numeric" autocomplete="one-time-code" required />
</template>
<p v-if="error" class="form-error" role="alert">{{ error }}</p>
<div class="dialog-actions">
<button ref="cancelButton" type="button" class="secondary-button" :disabled="busy" @click="cancel">取消</button>
<button type="submit" class="primary-button" :disabled="busy">{{ busy ? '正在验证…' : '验证' }}</button>
<button ref="cancelButton" type="button" class="secondary-button" :disabled="busy" @click="cancel">{{ t('common.cancel') }}</button>
<button type="submit" class="primary-button" :disabled="busy">{{ busy ? t('settings.totp.verifying') : t('agent.reauth.verify') }}</button>
</div>
</form>
<div v-else class="dialog-actions">
<button ref="cancelButton" type="button" class="secondary-button" @click="cancel">取消</button>
<button ref="cancelButton" type="button" class="secondary-button" @click="cancel">{{ t('common.cancel') }}</button>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { ShieldCheck, X } from '@lucide/vue'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { useI18n } from '../../i18n'
import { useClientRuntime } from '../../runtime'
import { useSensitiveAuthorization } from '../../composables/useSensitiveAuthorization'
const { t } = useI18n()
const runtime = useClientRuntime()
const authorization = useSensitiveAuthorization()
const native = computed(() => runtime.sensitiveAuthorization.mode === 'native-oauth')
Expand All @@ -66,7 +68,7 @@ async function submit() {
if (signal.aborted) return
if ('status' in result) { challenge.value = result.challenge_id; await nextTick(); panel.value?.querySelector<HTMLInputElement>('input')?.focus() }
else if (result.authenticated) { challenge.value = null; authorization.finish(true) }
} catch { if (!signal.aborted) error.value = '验证失败,请重试。' }
} catch { if (!signal.aborted) error.value = t('agent.reauth.failed') }
finally { credential.value = ''; totp.value = ''; busy.value = false }
}
function onKeydown(event: KeyboardEvent) {
Expand Down
8 changes: 4 additions & 4 deletions packages/client-ui/src/components/agent/AgentSetupForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ const panesTouched = ref(false)
const introHelp = computed(() => {
const disclosure = props.setup.disclosure
return disclosure === null
? '部署尚未配置模型提供方,请联系管理员完成部署。'
: `Agent 使用部署配置的模型 ${disclosure.provider_id} · ${disclosure.model_id} 读取授权窗格的内容;写入命令时仍会逐一弹审批。`
? t('agent.setup.noProviderHelp')
: t('agent.setup.providerHelp', { provider: disclosure.provider_id, model: disclosure.model_id })
})
watch(() => props.panes.map((pane) => pane.pane_id).join(','), () => {
if (panesTouched.value) return
Expand All @@ -50,8 +50,8 @@ function submit() {
localError.value = ''
const disclosure = props.setup.disclosure
if (props.busy) return
if (!paneIds.value.length || paneIds.value.some((id) => !props.panes.some((pane) => pane.pane_id === id))) { localError.value = '请至少选择一个当前窗格。'; return }
if (disclosure === null || props.setup.topology_revision === null) { localError.value = '提供方配置暂不可用,请管理员完成部署。'; return }
if (!paneIds.value.length || paneIds.value.some((id) => !props.panes.some((pane) => pane.pane_id === id))) { localError.value = t('agent.setup.selectPaneError'); return }
if (disclosure === null || props.setup.topology_revision === null) { localError.value = t('agent.setup.providerUnavailableError'); return }
const common = { paneIds: [...paneIds.value], topologyRevision: props.setup.topology_revision, disclosureFingerprint: disclosure.disclosure_fingerprint, accepted: true as const }
const matching = props.profiles.find((profile) => profile.provider_id === disclosure.provider_id && profile.model_id === disclosure.model_id)
emit('submit', matching
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@
class="terminal-agent-resize-handle"
:class="`terminal-agent-resize-handle--${edge}`"
:data-agent-resize-handle="edge"
:aria-label="`调整 Agent 面板大小(${resizeLabels[edge]})`"
:aria-label="t('agent.panel.resizeAria', { edge: t(`agent.panel.edge.${edge}`) })"
@pointerdown.stop.prevent="(event) => beginResize(edge, event)"
/>
</template>
Expand Down Expand Up @@ -306,7 +306,7 @@ async function savePaneSettings() {
settingsOpen.value = false
await refresh()
} catch (cause) {
settingsError.value = cause instanceof ApiError ? agentReasonMessage(cause.code) : '无法保存窗格授权。'
settingsError.value = cause instanceof ApiError ? agentReasonMessage(cause.code) : t('agent.panel.savePanesError')
} finally {
mutating.value = false
}
Expand Down Expand Up @@ -337,7 +337,7 @@ async function changeWritePolicy(policy: 'manual' | 'auto') {
pendingAutoPolicy.value = false
await refresh()
} catch (cause) {
error.value = cause instanceof Error ? cause.message : '无法更新写入审批方式。'
error.value = cause instanceof Error ? cause.message : t('agent.panel.writePolicyError')
} finally {
mutating.value = false
}
Expand Down Expand Up @@ -400,9 +400,6 @@ const mergedStatusLabel = computed(() => {
return key ? t(key) : backendState.value
})
const resizeEdges: FloatingPanelResizeEdge[] = ['n', 'e', 's', 'w', 'ne', 'se', 'sw', 'nw']
const resizeLabels: Record<FloatingPanelResizeEdge, string> = {
n: '上边缘', e: '右边缘', s: '下边缘', w: '左边缘', ne: '右上角', se: '右下角', sw: '左下角', nw: '左上角',
}

watch(() => setup.value?.state, (state) => {
if (state && state !== 'ready') settingsOpen.value = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async function pollForEnrollment() {
open.value = false
emit('added')
void nextTick(() => { if (returnFocus?.isConnected) returnFocus.focus() })
} catch (error) { if (open.value) message.value = error instanceof ApiError ? error.message : '无法检查电脑是否已添加。' }
} catch (error) { if (open.value) message.value = error instanceof ApiError ? error.message : t('computers.enrollment.pollError') }
finally { pollingEnrollment = false }
}
function startEnrollmentPolling() {
Expand All @@ -122,7 +122,7 @@ function validName(value: string) {
async function create(automatic = false) {
if (busy.value || !open.value) return
if (!validName(displayName.value)) {
message.value = '电脑名称须为 1 至 128 个无控制字符的字符。'
message.value = t('computers.enrollment.nameInvalid')
return
}
busy.value = true
Expand All @@ -141,7 +141,7 @@ async function create(automatic = false) {
startTimer()
startEnrollmentPolling()
if (!automatic) void nextTick(() => copyButton.value?.focus())
} catch (error) { message.value = error instanceof ApiError ? error.message : automatic ? '注册码刷新失败,请手动重试。' : '无法创建注册码。' }
} catch (error) { message.value = error instanceof ApiError ? error.message : automatic ? t('computers.enrollment.refreshError') : t('computers.enrollment.createError') }
finally { busy.value = false }
}
async function copy() { if (!command.value) return; await runtime.clipboard.writeText(command.value); copied.value = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ async function removeSelected() {
selected.value = null
clearSecrets()
clients.value = clients.value.filter((client) => client.client_id !== removed)
toast.show({ text: '已撤销', tone: 'success' })
toast.show({ text: t('settings.clients.revokedToast'), tone: 'success' })
} catch {
error.value = '撤销失败,请检查凭据后重试。'
error.value = t('settings.clients.revokeError')
}
}
async function onApproved() { approvalOpen.value = false; await load() }
Expand Down
10 changes: 5 additions & 5 deletions packages/client-ui/src/components/terminal/MobileKeyBar.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="mobile-keybar-shell">
<div class="mobile-keybar" :aria-label="t('terminal.mobileKeybar.aria')" @pointermove.stop>
<!-- 修饰键组 -->
<!-- Modifier keys -->
<button
v-for="key in modifierKeys"
:key="key.id"
Expand All @@ -26,20 +26,20 @@
Prefix
</button>

<!-- 方向导航组 -->
<!-- Navigation keys -->
<span class="mobile-keybar-divider" aria-hidden="true" />
<button type="button" class="mobile-key-btn mobile-key-btn--nav" :disabled="disabled" :aria-label="t('terminal.mobileKeybar.up')" @click="sendArrow('\u001b[A')">↑</button>
<button type="button" class="mobile-key-btn mobile-key-btn--nav" :disabled="disabled" :aria-label="t('terminal.mobileKeybar.down')" @click="sendArrow('\u001b[B')">↓</button>
<button type="button" class="mobile-key-btn mobile-key-btn--nav" :disabled="disabled" :aria-label="t('terminal.mobileKeybar.left')" @click="sendArrow('\u001b[D')">←</button>
<button type="button" class="mobile-key-btn mobile-key-btn--nav" :disabled="disabled" :aria-label="t('terminal.mobileKeybar.right')" @click="sendArrow('\u001b[C')">→</button>

<!-- 高频控制与中断组 -->
<!-- Control & interrupt keys -->
<span class="mobile-keybar-divider" aria-hidden="true" />
<button type="button" class="mobile-key-btn mobile-key-btn--danger" :disabled="disabled" :aria-label="t('terminal.mobileKeybar.interrupt')" @click="sendInterrupt">^C</button>
<button type="button" class="mobile-key-btn" :disabled="disabled" aria-label="EOF (Ctrl+D)" @click="sendEof">^D</button>
<button type="button" class="mobile-key-btn mobile-key-btn--primary" :disabled="disabled" :aria-label="t('terminal.mobileKeybar.enter')" @click="sendEnter">↵</button>

<!-- 高频 Shell 符号组 -->
<!-- Shell symbol keys -->
<button type="button" class="mobile-key-btn mobile-key-btn--sym" :disabled="disabled" @click="sendChar('/')">/</button>
<button type="button" class="mobile-key-btn mobile-key-btn--sym" :disabled="disabled" @click="sendChar('-')">-</button>
<button type="button" class="mobile-key-btn mobile-key-btn--sym" :disabled="disabled" @click="sendChar('|')">|</button>
Expand Down Expand Up @@ -72,7 +72,7 @@ const modifierKeys: Array<{ id: ModifierKey; label: string }> = [
{ id: 'shift', label: 'Shift' },
]

const usablePrefix = computed(() => !!props.prefix && !/未报告|未绑定|Unreported|Unbound/i.test(props.prefix))
const usablePrefix = computed(() => !!props.prefix && props.prefix !== t('terminal.unreportedPrefix') && props.prefix !== t('terminal.tmux.unbound') && !/\u672a\u62a5\u544a|\u672a\u7ed1\u5b9a|Unreported|Unbound/i.test(props.prefix))
let blurTimer: unknown | null = null

function vibrate(ms = 12) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<span v-if="validationError" class="form-error" role="alert">{{ validationError }}</span>
</form>
<button v-else data-term-name data-action="edit-term-name" class="terminal-name-trigger" type="button" :aria-label="t('terminal.editNameAria', { title })" :title="t('terminal.editNameTitle')" @click="startEditing"><strong>{{ title }}</strong></button>
<small data-computer-name :title="computerName">{{ computerName }}</small>
<small data-computer-name :title="resolvedComputerName">{{ resolvedComputerName }}</small>
</div>
</div>
<span data-connection-status class="terminal-status">{{ statusLabel }}</span>
Expand Down Expand Up @@ -57,12 +57,13 @@ import { useI18n } from '../../i18n'
import type { DisplayMode } from '../../terminal/viewport'
import DisplayMenu from './DisplayMenu.vue'
const { t, toggleLocale } = useI18n()
const props = withDefaults(defineProps<{ title: string; computerName?: string; status?: TerminalConnectionStatus; displayMode: DisplayMode; displayMenuOpen?: boolean; viewportLocked?: boolean }>(), { computerName: 'Computer 未报告', status: 'connecting', displayMenuOpen: false, viewportLocked: false })
const props = withDefaults(defineProps<{ title: string; computerName?: string | undefined; status?: TerminalConnectionStatus; displayMode: DisplayMode; displayMenuOpen?: boolean; viewportLocked?: boolean }>(), { status: 'connecting', displayMenuOpen: false, viewportLocked: false })
const emit = defineEmits<{ 'update:displayMode': [mode: DisplayMode]; 'update:displayMenuOpen': [open: boolean]; 'update:viewportLocked': [locked: boolean]; rename: [name: string] }>()
const editing = ref(false)
const draft = ref('')
const validationError = ref('')
const nameInput = ref<HTMLInputElement | null>(null)
const resolvedComputerName = computed(() => props.computerName ?? t('terminal.unreportedComputer'))
const statusLabel = computed(() => t(`terminal.status.${props.status}`))
function startEditing() { draft.value = props.title; validationError.value = ''; editing.value = true; void nextTick(() => nameInput.value?.focus()) }
function cancel() { editing.value = false; validationError.value = '' }
Expand Down
Loading
Loading