codexmate 0.0.38 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/cli/builtin-proxy.js +626 -207
  2. package/cli/openai-bridge.js +541 -210
  3. package/cli.js +19 -1
  4. package/package.json +1 -1
  5. package/web-ui/app.js +12 -3
  6. package/web-ui/modules/app.computed.main-tabs.mjs +37 -30
  7. package/web-ui/modules/app.methods.claude-config.mjs +111 -9
  8. package/web-ui/modules/app.methods.openclaw-editing.mjs +48 -0
  9. package/web-ui/modules/app.methods.openclaw-persist.mjs +13 -7
  10. package/web-ui/modules/app.methods.providers.mjs +36 -10
  11. package/web-ui/modules/app.methods.runtime.mjs +76 -1
  12. package/web-ui/modules/app.methods.startup-claude.mjs +1 -0
  13. package/web-ui/modules/config-mode.computed.mjs +3 -3
  14. package/web-ui/modules/i18n.dict.mjs +13 -0
  15. package/web-ui/modules/i18n.mjs +65 -16
  16. package/web-ui/partials/index/layout-header.html +16 -46
  17. package/web-ui/partials/index/modal-openclaw-config.html +135 -71
  18. package/web-ui/partials/index/modal-webhook.html +8 -8
  19. package/web-ui/partials/index/modals-basic.html +56 -16
  20. package/web-ui/partials/index/panel-config-claude.html +20 -20
  21. package/web-ui/partials/index/panel-config-codex.html +5 -5
  22. package/web-ui/partials/index/panel-config-openclaw.html +70 -64
  23. package/web-ui/partials/index/panel-dashboard.html +62 -77
  24. package/web-ui/partials/index/panel-settings.html +28 -7
  25. package/web-ui/partials/index/panel-trash.html +14 -14
  26. package/web-ui/res/web-ui-render.precompiled.js +846 -539
  27. package/web-ui/styles/controls-forms.css +6 -0
  28. package/web-ui/styles/dashboard.css +46 -14
  29. package/web-ui/styles/layout-shell.css +45 -0
  30. package/web-ui/styles/navigation-panels.css +3 -3
  31. package/web-ui/styles/openclaw-structured.css +383 -33
  32. package/web-ui/styles/responsive.css +68 -0
  33. package/web-ui/styles/sessions-usage.css +105 -9
  34. package/web-ui/styles/settings-panel.css +4 -0
@@ -10,74 +10,80 @@
10
10
  <button type="button" :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">{{ t('tab.config.claude') }}</button>
11
11
  <button type="button" :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">{{ t('tab.config.openclaw') }}</button>
12
12
  </div>
13
- <div class="config-template-hint">
14
- {{ t('openclaw.applyHint') }}
15
- </div>
16
-
17
- <div class="selector-section">
18
- <div class="selector-header">
19
- <span class="selector-title">AGENTS.md</span>
20
- </div>
21
- <div class="config-template-hint">
22
- {{ t('openclaw.agents.hint') }}
23
- </div>
24
- <button class="btn-tool" @click="openOpenclawAgentsEditor" :disabled="loading || !!initError || agentsLoading">
25
- {{ agentsLoading ? t('config.modelLoading') : t('openclaw.agents.open') }}
26
- </button>
27
- </div>
28
-
29
- <div class="selector-section">
30
- <div class="selector-header">
31
- <label class="selector-title" for="openclaw-workspace-file">{{ t('openclaw.workspaceFile') }}</label>
32
- </div>
33
- <input
34
- id="openclaw-workspace-file"
35
- class="form-input"
36
- v-model="openclawWorkspaceFileName"
37
- :placeholder="t('openclaw.workspace.placeholder')">
38
- <div class="config-template-hint">
39
- {{ t('openclaw.workspace.hint') }}
40
- </div>
41
- <button class="btn-tool" @click="openOpenclawWorkspaceEditor" :disabled="loading || !!initError || agentsLoading">
42
- {{ agentsLoading ? t('config.modelLoading') : t('openclaw.workspace.open') }}
43
- </button>
44
- </div>
13
+ <div class="openclaw-layout">
14
+ <section class="settings-card settings-card--wide openclaw-workspace-card">
15
+ <div class="settings-card-body">
16
+ <div class="openclaw-tools-grid">
17
+ <button class="openclaw-tool-btn" @click="openOpenclawAgentsEditor" :disabled="loading || !!initError || agentsLoading">
18
+ <div class="tool-icon">📄</div>
19
+ <div class="tool-content">
20
+ <div class="tool-title">AGENTS.md</div>
21
+ <div class="tool-meta">{{ agentsLoading ? t('config.modelLoading') : t('openclaw.agents.hint') }}</div>
22
+ </div>
23
+ <svg class="tool-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
24
+ <path d="M9 18l6-6-6-6"/>
25
+ </svg>
26
+ </button>
45
27
 
46
- <div class="card-list">
47
- <div v-for="(config, name) in openclawConfigs" :key="name"
48
- :class="['card', { active: currentOpenclawConfig === name }]"
49
- @click="applyOpenclawConfig(name)"
50
- @keydown.enter.self.prevent="applyOpenclawConfig(name)"
51
- @keydown.space.self.prevent="applyOpenclawConfig(name)"
52
- tabindex="0"
53
- role="button"
54
- :aria-current="currentOpenclawConfig === name ? 'true' : null">
55
- <div class="card-leading">
56
- <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
57
- <div class="card-content">
58
- <div class="card-title">{{ name }}</div>
59
- <div class="card-subtitle">{{ openclawSubtitle(config) }}</div>
28
+ <div class="openclaw-workspace-card">
29
+ <label class="workspace-label" for="openclaw-workspace-file">{{ t('openclaw.workspaceFile') }}</label>
30
+ <div class="workspace-input-group">
31
+ <input
32
+ id="openclaw-workspace-file"
33
+ class="form-input"
34
+ v-model="openclawWorkspaceFileName"
35
+ :placeholder="t('openclaw.workspace.placeholder')">
36
+ <button class="btn-tool" @click="openOpenclawWorkspaceEditor" :disabled="loading || !!initError || agentsLoading">
37
+ {{ agentsLoading ? t('config.modelLoading') : t('openclaw.workspace.open') }}
38
+ </button>
39
+ </div>
40
+ <div class="workspace-meta">{{ t('openclaw.workspace.hint') }}</div>
41
+ </div>
60
42
  </div>
61
43
  </div>
62
- <div class="card-trailing">
63
- <span :class="['pill', openclawHasContent(config) ? 'configured' : 'empty']">
64
- {{ openclawHasContent(config) ? t('openclaw.configured') : t('openclaw.notConfigured') }}
65
- </span>
66
- <div class="card-actions" @click.stop>
67
- <button class="card-action-btn" @click="openOpenclawEditModal(name)" :aria-label="t('openclaw.action.editAria', { name })" :title="t('openclaw.action.edit')">
68
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
69
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
70
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
71
- </svg>
72
- </button>
73
- <button v-if="name !== '默认配置'" class="card-action-btn delete" @click="deleteOpenclawConfig(name)" :aria-label="t('openclaw.action.deleteAria', { name })" :title="t('openclaw.action.delete')">
74
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
75
- <path d="M3 6h18"/>
76
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
77
- </svg>
78
- </button>
44
+ </section>
45
+
46
+ <section class="settings-card settings-card--wide openclaw-configs-card" aria-labelledby="openclaw-configs-title">
47
+ <div class="settings-card-body">
48
+ <div class="card-list openclaw-card-list">
49
+ <div v-for="(config, name) in openclawConfigs" :key="name"
50
+ :class="['card', { active: currentOpenclawConfig === name }]"
51
+ @click="applyOpenclawConfig(name)"
52
+ @keydown.enter.self.prevent="applyOpenclawConfig(name)"
53
+ @keydown.space.self.prevent="applyOpenclawConfig(name)"
54
+ tabindex="0"
55
+ role="button"
56
+ :aria-label="t('openclaw.action.applyAria', { name })"
57
+ :aria-current="currentOpenclawConfig === name ? 'true' : null">
58
+ <div class="card-leading">
59
+ <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
60
+ <div class="card-content">
61
+ <div class="card-title">{{ name }}</div>
62
+ <div class="card-subtitle">{{ openclawSubtitle(config) }}</div>
63
+ </div>
64
+ </div>
65
+ <div class="card-trailing">
66
+ <span :class="['pill', openclawHasContent(config) ? 'configured' : 'empty']">
67
+ {{ openclawHasContent(config) ? t('openclaw.configured') : t('openclaw.notConfigured') }}
68
+ </span>
69
+ <div class="card-actions" @click.stop>
70
+ <button class="card-action-btn" @click="openOpenclawEditModal(name)" :aria-label="t('openclaw.action.editAria', { name })" :title="t('openclaw.action.edit')">
71
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
72
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
73
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
74
+ </svg>
75
+ </button>
76
+ <button v-if="!isDefaultOpenclawConfig(name, config)" class="card-action-btn delete" @click="deleteOpenclawConfig(name)" :aria-label="t('openclaw.action.deleteAria', { name })" :title="t('openclaw.action.delete')">
77
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
78
+ <path d="M3 6h18"/>
79
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
80
+ </svg>
81
+ </button>
82
+ </div>
83
+ </div>
84
+ </div>
79
85
  </div>
80
86
  </div>
81
- </div>
87
+ </section>
82
88
  </div>
83
89
  </div>
@@ -16,6 +16,68 @@
16
16
  </button>
17
17
  </div>
18
18
  </div>
19
+ <div class="doctor-status-row">
20
+ <div class="doctor-status-chip" :class="inspectorHealthTone">
21
+ <span>{{ t('dashboard.status.health') }}</span>
22
+ <strong>{{ inspectorHealthStatus }}</strong>
23
+ </div>
24
+ <div class="doctor-status-chip">
25
+ <span>{{ t('dashboard.status.busy') }}</span>
26
+ <strong>{{ inspectorBusyStatus }}</strong>
27
+ </div>
28
+ <div class="doctor-status-chip">
29
+ <span>{{ t('dashboard.status.models') }}</span>
30
+ <strong>{{ inspectorModelLoadStatus }}</strong>
31
+ </div>
32
+ </div>
33
+ <div v-if="healthCheckResult" class="doctor-health-result" :class="healthCheckResult.ok ? 'ok' : 'error'">
34
+ <div class="doctor-health-title">
35
+ {{ healthCheckResult.ok ? t('dashboard.health.ok') : t('dashboard.health.fail') }}
36
+ <span v-if="healthCheckResult.issues && healthCheckResult.issues.length">({{ t('dashboard.health.issues', { count: healthCheckResult.issues.length }) }})</span>
37
+ </div>
38
+ </div>
39
+ <div v-if="healthCheckResult && healthCheckResult.report" class="doctor-action-list">
40
+ <template v-if="healthCheckResult.report.issues && healthCheckResult.report.issues.length">
41
+ <div v-for="issue in healthCheckResult.report.issues" :key="issue.id" class="doctor-action-card">
42
+ <div class="doctor-action-head">
43
+ <div class="doctor-action-title">{{ issue.problem || (issue.problemKey ? t(issue.problemKey, issue.problemParams) : '') }}</div>
44
+ <div :class="['doctor-action-severity', issue.severity]">{{ issue.severityLabel || issue.severity }}</div>
45
+ </div>
46
+ <div class="doctor-action-impact">{{ issue.impact || (issue.impactKey ? t(issue.impactKey, issue.impactParams) : '') }}</div>
47
+ <div
48
+ v-if="issue.actions && issue.actions.some(action => action && action.type === 'navigate' && action.target)"
49
+ class="doctor-action-buttons">
50
+ <template v-for="(action, index) in issue.actions" :key="issue.id + '-action-' + index">
51
+ <button
52
+ v-if="action.type === 'navigate' && action.target"
53
+ type="button"
54
+ class="btn-tool btn-tool-compact"
55
+ @click="action.target ? switchMainTab(action.target) : null">
56
+ {{ action.label || (action.labelKey ? t(action.labelKey, action.labelParams) : t('dashboard.doctor.open')) }}
57
+ </button>
58
+ </template>
59
+ </div>
60
+ </div>
61
+ </template>
62
+ <div class="doctor-action-footer">
63
+ <button
64
+ type="button"
65
+ class="btn-tool btn-tool-compact"
66
+ @click="healthCheckResult && healthCheckResult.report
67
+ ? downloadTextFile('codexmate-doctor.json', JSON.stringify(healthCheckResult.report, null, 2), 'application/json;charset=utf-8')
68
+ : null">
69
+ {{ t('dashboard.doctor.export.json') }}
70
+ </button>
71
+ <button
72
+ type="button"
73
+ class="btn-tool btn-tool-compact"
74
+ @click="healthCheckResult && healthCheckResult.report
75
+ ? downloadTextFile('codexmate-doctor.md', String(healthCheckResult.markdown || ''), 'text/markdown;charset=utf-8')
76
+ : null">
77
+ {{ t('dashboard.doctor.export.md') }}
78
+ </button>
79
+ </div>
80
+ </div>
19
81
  <div class="doctor-grid">
20
82
  <button type="button" class="doctor-card" @click="switchMainTab('config')" :disabled="loading || !!initError">
21
83
  <div class="doctor-card-title">{{ t('dashboard.card.config') }}</div>
@@ -105,82 +167,5 @@
105
167
  </div>
106
168
  </button>
107
169
  </div>
108
- <div class="doctor-status-row">
109
- <div class="doctor-status-chip" :class="inspectorHealthTone">
110
- <span>{{ t('dashboard.status.health') }}</span>
111
- <strong>{{ inspectorHealthStatus }}</strong>
112
- </div>
113
- <div class="doctor-status-chip">
114
- <span>{{ t('dashboard.status.busy') }}</span>
115
- <strong>{{ inspectorBusyStatus }}</strong>
116
- </div>
117
- <div class="doctor-status-chip">
118
- <span>{{ t('dashboard.status.models') }}</span>
119
- <strong>{{ inspectorModelLoadStatus }}</strong>
120
- </div>
121
- </div>
122
- <div v-if="healthCheckResult" class="doctor-health-result" :class="healthCheckResult.ok ? 'ok' : 'error'">
123
- <div class="doctor-health-title">
124
- {{ healthCheckResult.ok ? t('dashboard.health.ok') : t('dashboard.health.fail') }}
125
- <span v-if="healthCheckResult.issues && healthCheckResult.issues.length">({{ t('dashboard.health.issues', { count: healthCheckResult.issues.length }) }})</span>
126
- </div>
127
- </div>
128
- <div v-if="healthCheckResult && healthCheckResult.report && healthCheckResult.report.issues && healthCheckResult.report.issues.length" class="doctor-action-list">
129
- <div v-for="issue in healthCheckResult.report.issues" :key="issue.id" class="doctor-action-card">
130
- <div class="doctor-action-head">
131
- <div class="doctor-action-title">{{ issue.problem || (issue.problemKey ? t(issue.problemKey, issue.problemParams) : '') }}</div>
132
- <div :class="['doctor-action-severity', issue.severity]">{{ issue.severityLabel || issue.severity }}</div>
133
- </div>
134
- <div class="doctor-action-impact">{{ issue.impact || (issue.impactKey ? t(issue.impactKey, issue.impactParams) : '') }}</div>
135
- <div v-if="issue.actions && issue.actions.length" class="doctor-action-buttons">
136
- <template v-for="(action, index) in issue.actions" :key="issue.id + '-action-' + index">
137
- <button
138
- v-if="action.type === 'navigate'"
139
- type="button"
140
- class="btn-tool btn-tool-compact"
141
- @click="switchMainTab(action.target)">
142
- {{ action.label || (action.labelKey ? t(action.labelKey, action.labelParams) : t('dashboard.doctor.open')) }}
143
- </button>
144
- <button
145
- v-else-if="action.type === 'run-check'"
146
- type="button"
147
- class="btn-tool btn-tool-compact"
148
- @click="runHealthCheck({ doctor: true, forceRefresh: true })"
149
- :disabled="healthCheckLoading">
150
- {{ t('dashboard.doctor.runChecks') }}
151
- </button>
152
- <button
153
- v-else-if="action.type === 'export'"
154
- type="button"
155
- class="btn-tool btn-tool-compact"
156
- @click="healthCheckResult && healthCheckResult.report
157
- ? (action.format === 'md'
158
- ? downloadTextFile('codexmate-doctor.md', String(healthCheckResult.markdown || ''), 'text/markdown;charset=utf-8')
159
- : downloadTextFile('codexmate-doctor.json', JSON.stringify(healthCheckResult.report, null, 2), 'application/json;charset=utf-8'))
160
- : null">
161
- {{ action.format === 'md' ? t('dashboard.doctor.export.md') : t('dashboard.doctor.export.json') }}
162
- </button>
163
- </template>
164
- </div>
165
- </div>
166
- <div class="doctor-action-footer">
167
- <button
168
- type="button"
169
- class="btn-tool btn-tool-compact"
170
- @click="healthCheckResult && healthCheckResult.report
171
- ? downloadTextFile('codexmate-doctor.json', JSON.stringify(healthCheckResult.report, null, 2), 'application/json;charset=utf-8')
172
- : null">
173
- {{ t('dashboard.doctor.export.json') }}
174
- </button>
175
- <button
176
- type="button"
177
- class="btn-tool btn-tool-compact"
178
- @click="healthCheckResult && healthCheckResult.report
179
- ? downloadTextFile('codexmate-doctor.md', String(healthCheckResult.markdown || ''), 'text/markdown;charset=utf-8')
180
- : null">
181
- {{ t('dashboard.doctor.export.md') }}
182
- </button>
183
- </div>
184
- </div>
185
170
  </div>
186
171
  </div>
@@ -40,6 +40,27 @@
40
40
  role="tabpanel"
41
41
  aria-labelledby="settings-tab-general">
42
42
  <div class="settings-grid">
43
+ <section id="settings-language" class="settings-card" :aria-label="t('settings.language.title')">
44
+ <div class="settings-card-main">
45
+ <div class="settings-card-content">
46
+ <div class="settings-card-title">{{ t('settings.language.title') }}</div>
47
+ <p class="settings-card-desc">{{ t('settings.language.meta') }}</p>
48
+ <label class="selector-label" for="settings-language-select">{{ t('settings.language.label') }}</label>
49
+ <select
50
+ id="settings-language-select"
51
+ class="model-select settings-language-select"
52
+ :value="lang"
53
+ @change="setLang($event.target.value)">
54
+ <option
55
+ v-for="option in languageOptions()"
56
+ :key="option.code"
57
+ :value="option.code">{{ option.nativeName }} · {{ option.englishName }}</option>
58
+ </select>
59
+ <p class="settings-card-hint">{{ t('settings.language.hint') }}</p>
60
+ </div>
61
+ </div>
62
+ </section>
63
+
43
64
  <section class="settings-card" :aria-label="t('settings.sharePrefix.title')">
44
65
  <div class="settings-card-main">
45
66
  <div class="settings-card-content">
@@ -76,21 +97,21 @@
76
97
  </div>
77
98
  </section>
78
99
 
79
- <section class="settings-card" :aria-label="'Webhook'">
100
+ <section class="settings-card" :aria-label="t('settings.webhook.title')">
80
101
  <div class="settings-card-main">
81
102
  <div class="settings-card-content">
82
- <div class="settings-card-title">Webhook</div>
83
- <p class="settings-card-desc">配置变更时外发通知</p>
103
+ <div class="settings-card-title">{{ t('settings.webhook.title') }}</div>
104
+ <p class="settings-card-desc">{{ t('settings.webhook.meta') }}</p>
84
105
  <div class="webhook-status">
85
106
  <span class="webhook-status-dot" :class="{ active: webhookConfig.enabled }"></span>
86
- <span class="webhook-status-label">{{ webhookConfig.enabled ? '已启用' : '已禁用' }}</span>
107
+ <span class="webhook-status-label">{{ webhookConfig.enabled ? t('settings.webhook.enabled') : t('settings.webhook.disabled') }}</span>
87
108
  <code v-if="webhookConfig.url" class="webhook-url">{{ webhookConfig.url }}</code>
88
109
  </div>
89
110
  </div>
90
111
  </div>
91
112
  <button class="settings-card-action" @click="openWebhookModal" :class="{ 'settings-card-action--active': webhookConfig.enabled }">
92
- <span v-if="webhookConfig.enabled">{{ webhookConfig.url ? '编辑' : '配置' }}</span>
93
- <span v-else>启用</span>
113
+ <span v-if="webhookConfig.enabled">{{ webhookConfig.url ? t('settings.webhook.edit') : t('settings.webhook.configure') }}</span>
114
+ <span v-else>{{ t('settings.webhook.enable') }}</span>
94
115
  </button>
95
116
  </section>
96
117
  </div>
@@ -136,7 +157,7 @@
136
157
  <div class="settings-retention">
137
158
  <label for="settings-trash-retention-days">{{ t('settings.trash.retentionLabel') }}</label>
138
159
  <input id="settings-trash-retention-days" type="number" min="1" max="365" :value="sessionTrashRetentionDays" @change="setSessionTrashRetentionDays(Number($event.target.value))" class="settings-retention-input" />
139
- <span>天</span>
160
+ <span>{{ t('settings.trash.retentionUnit') }}</span>
140
161
  </div>
141
162
  <p class="settings-card-hint">{{ t('settings.trash.retentionHint') }}</p>
142
163
  </div>
@@ -1,4 +1,4 @@
1
- <!-- 回收站面板 -->
1
+ <!-- Trash panel -->
2
2
  <div
3
3
  v-show="mainTab === 'trash'"
4
4
  class="mode-content"
@@ -6,7 +6,7 @@
6
6
  role="tabpanel"
7
7
  aria-labelledby="tab-trash">
8
8
  <div v-if="!loading" class="trash-panel-shell">
9
- <!-- 空态 -->
9
+ <!-- Empty state -->
10
10
  <div v-if="getSessionTrashViewState() === 'empty'" class="trash-empty-state">
11
11
  <svg class="trash-empty-svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="1.2">
12
12
  <path d="M20 22l4 32h16l4-32"/>
@@ -15,38 +15,38 @@
15
15
  <path d="M28 30v16M36 30v16" stroke-width="1.6" stroke-linecap="round"/>
16
16
  </svg>
17
17
  <div class="trash-empty-title">{{ t('settings.trash.empty') }}</div>
18
- <div class="trash-empty-hint">删除的会话保留 {{ sessionTrashRetentionDays }} 天后自动清理</div>
18
+ <div class="trash-empty-hint">{{ t('settings.trash.emptyHint', { days: sessionTrashRetentionDays }) }}</div>
19
19
  </div>
20
20
 
21
- <!-- 加载态 -->
21
+ <!-- Loading state -->
22
22
  <div v-else-if="getSessionTrashViewState() === 'loading'" class="trash-empty-state">
23
23
  <div class="trash-spinner"></div>
24
24
  <div class="trash-empty-title">{{ t('settings.trash.loading') }}</div>
25
25
  </div>
26
26
 
27
- <!-- 错误态 -->
27
+ <!-- Error state -->
28
28
  <div v-else-if="getSessionTrashViewState() === 'retry'" class="trash-empty-state">
29
29
  <svg class="trash-empty-svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="1.2">
30
30
  <circle cx="32" cy="32" r="22"/>
31
31
  <path d="M32 20v16M32 44v2" stroke-width="2" stroke-linecap="round"/>
32
32
  </svg>
33
33
  <div class="trash-empty-title">{{ t('settings.trash.retry') }}</div>
34
- <button class="btn-tool" @click="loadSessionTrash({ forceRefresh: true })">重试</button>
34
+ <button class="btn-tool" @click="loadSessionTrash({ forceRefresh: true })">{{ t('common.retry') }}</button>
35
35
  </div>
36
36
 
37
- <!-- 列表态 -->
37
+ <!-- List state -->
38
38
  <template v-else>
39
39
  <div class="trash-toolbar">
40
40
  <div class="trash-toolbar-left">
41
- <span class="trash-toolbar-count">{{ sessionTrashCount }} 个已删除会话</span>
42
- <span class="trash-toolbar-retention">{{ sessionTrashRetentionDays }} 天后自动清理</span>
41
+ <span class="trash-toolbar-count">{{ t('settings.trash.count', { count: sessionTrashCount }) }}</span>
42
+ <span class="trash-toolbar-retention">{{ t('settings.trash.retentionShort', { days: sessionTrashRetentionDays }) }}</span>
43
43
  </div>
44
44
  <div class="trash-toolbar-right">
45
- <button class="btn-mini" @click="loadSessionTrash({ forceRefresh: true })" :disabled="sessionTrashLoading" :title="t('sessions.refresh')">
45
+ <button class="btn-mini" @click="loadSessionTrash({ forceRefresh: true })" :disabled="sessionTrashLoading" :aria-label="t('sessions.refresh')" :title="t('sessions.refresh')">
46
46
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="btn-icon-sm"><path d="M21 2v6h-6M3 12a9 9 0 0115-6.7L21 8M3 22v-6h6M21 12a9 9 0 01-15 6.7L3 16"/></svg>
47
47
  </button>
48
48
  <button class="btn-mini delete" @click="clearSessionTrash" :disabled="sessionTrashClearing || sessionTrashLoading || !(Number(sessionTrashCount) > 0)">
49
- {{ sessionTrashClearing ? '清空中…' : '清空' }}
49
+ {{ sessionTrashClearing ? t('settings.trash.clearing') : t('settings.trash.clearShort') }}
50
50
  </button>
51
51
  </div>
52
52
  </div>
@@ -63,10 +63,10 @@
63
63
  </div>
64
64
  </div>
65
65
  <div class="trash-item-actions">
66
- <button class="trash-action-btn restore" @click="restoreSessionTrash(item)" :disabled="sessionTrashLoading || sessionTrashClearing || isSessionTrashActionBusy(item)" :title="sessionTrashRestoring[getSessionTrashActionKey(item)] ? '恢复中…' : '恢复'">
66
+ <button class="trash-action-btn restore" @click="restoreSessionTrash(item)" :disabled="sessionTrashLoading || sessionTrashClearing || isSessionTrashActionBusy(item)" :aria-label="sessionTrashRestoring[getSessionTrashActionKey(item)] ? t('settings.trash.restoring') : t('settings.trash.restore')" :title="sessionTrashRestoring[getSessionTrashActionKey(item)] ? t('settings.trash.restoring') : t('settings.trash.restore')">
67
67
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 119 9"/><path d="M3 4v6h6"/></svg>
68
68
  </button>
69
- <button class="trash-action-btn delete" @click="purgeSessionTrash(item)" :disabled="sessionTrashLoading || sessionTrashClearing || isSessionTrashActionBusy(item)" :title="sessionTrashPurging[getSessionTrashActionKey(item)] ? '删除中…' : '彻底删除'">
69
+ <button class="trash-action-btn delete" @click="purgeSessionTrash(item)" :disabled="sessionTrashLoading || sessionTrashClearing || isSessionTrashActionBusy(item)" :aria-label="sessionTrashPurging[getSessionTrashActionKey(item)] ? t('settings.trash.purging') : t('settings.trash.purge')" :title="sessionTrashPurging[getSessionTrashActionKey(item)] ? t('settings.trash.purging') : t('settings.trash.purge')">
70
70
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6M10 11v6M14 11v6"/></svg>
71
71
  </button>
72
72
  </div>
@@ -74,7 +74,7 @@
74
74
  </div>
75
75
  <div v-if="sessionTrashHasMoreItems" class="trash-list-footer">
76
76
  <button class="btn-tool btn-tool-compact" @click="loadMoreSessionTrashItems" :disabled="sessionTrashLoading || sessionTrashClearing">
77
- 加载更多({{ sessionTrashHiddenCount }} 条)
77
+ {{ t('settings.trash.loadMoreItems', { count: sessionTrashHiddenCount }) }}
78
78
  </button>
79
79
  </div>
80
80
  </div>