codexmate 0.0.28 → 0.0.30
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.
- package/cli/builtin-proxy.js +107 -2
- package/cli/config-bootstrap.js +30 -12
- package/cli/config-health.js +117 -1
- package/cli/local-bridge.js +324 -0
- package/cli/openai-bridge.js +195 -31
- package/cli.js +245 -28
- package/lib/cli-webhook.js +126 -0
- package/package.json +1 -1
- package/web-ui/app.js +28 -8
- package/web-ui/index.html +1 -0
- package/web-ui/logic.codex.mjs +13 -0
- package/web-ui/modules/app.computed.dashboard.mjs +25 -2
- package/web-ui/modules/app.computed.session.mjs +22 -17
- package/web-ui/modules/app.methods.claude-config.mjs +12 -2
- package/web-ui/modules/app.methods.codex-config.mjs +25 -0
- package/web-ui/modules/app.methods.index.mjs +2 -0
- package/web-ui/modules/app.methods.navigation.mjs +39 -8
- package/web-ui/modules/app.methods.providers.mjs +125 -8
- package/web-ui/modules/app.methods.session-actions.mjs +1 -1
- package/web-ui/modules/app.methods.session-browser.mjs +1 -1
- package/web-ui/modules/app.methods.session-trash.mjs +3 -4
- package/web-ui/modules/app.methods.startup-claude.mjs +1 -0
- package/web-ui/modules/app.methods.webhook.mjs +79 -0
- package/web-ui/modules/i18n.dict.mjs +1109 -72
- package/web-ui/modules/i18n.mjs +9 -3
- package/web-ui/modules/skills.methods.mjs +1 -0
- package/web-ui/partials/index/layout-header.html +25 -0
- package/web-ui/partials/index/modals-basic.html +0 -3
- package/web-ui/partials/index/panel-config-claude.html +8 -2
- package/web-ui/partials/index/panel-config-codex.html +28 -3
- package/web-ui/partials/index/panel-dashboard.html +33 -0
- package/web-ui/partials/index/panel-market.html +3 -3
- package/web-ui/partials/index/panel-plugins.html +2 -2
- package/web-ui/partials/index/panel-sessions.html +1 -9
- package/web-ui/partials/index/panel-settings.html +71 -134
- package/web-ui/partials/index/panel-trash.html +88 -0
- package/web-ui/session-helpers.mjs +20 -2
- package/web-ui/styles/dashboard.css +132 -0
- package/web-ui/styles/docs-panel.css +63 -39
- package/web-ui/styles/layout-shell.css +54 -34
- package/web-ui/styles/plugins-panel.css +121 -80
- package/web-ui/styles/sessions-list.css +41 -43
- package/web-ui/styles/sessions-preview.css +34 -38
- package/web-ui/styles/sessions-toolbar-trash.css +31 -27
- package/web-ui/styles/settings-panel.css +197 -33
- package/web-ui/styles/skills-list.css +12 -10
- package/web-ui/styles/skills-market.css +67 -44
- package/web-ui/styles/trash-panel.css +90 -0
- package/web-ui/styles/webhook.css +81 -0
- package/web-ui/styles.css +2 -0
package/web-ui/modules/i18n.mjs
CHANGED
|
@@ -4,7 +4,9 @@ const I18N_STORAGE_KEY = 'codexmateLang';
|
|
|
4
4
|
|
|
5
5
|
function normalizeLang(value) {
|
|
6
6
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
7
|
-
|
|
7
|
+
if (normalized === 'en') return 'en';
|
|
8
|
+
if (normalized === 'ja') return 'ja';
|
|
9
|
+
return 'zh';
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
function interpolate(template, params) {
|
|
@@ -26,7 +28,9 @@ export function createI18nMethods() {
|
|
|
26
28
|
this.lang = next;
|
|
27
29
|
try {
|
|
28
30
|
if (typeof document !== 'undefined' && document.documentElement) {
|
|
29
|
-
|
|
31
|
+
if (next === 'en') document.documentElement.lang = 'en';
|
|
32
|
+
else if (next === 'ja') document.documentElement.lang = 'ja';
|
|
33
|
+
else document.documentElement.lang = 'zh-CN';
|
|
30
34
|
}
|
|
31
35
|
} catch (_) {}
|
|
32
36
|
},
|
|
@@ -40,7 +44,9 @@ export function createI18nMethods() {
|
|
|
40
44
|
} catch (_) {}
|
|
41
45
|
try {
|
|
42
46
|
if (typeof document !== 'undefined' && document.documentElement) {
|
|
43
|
-
|
|
47
|
+
if (next === 'en') document.documentElement.lang = 'en';
|
|
48
|
+
else if (next === 'ja') document.documentElement.lang = 'ja';
|
|
49
|
+
else document.documentElement.lang = 'zh-CN';
|
|
44
50
|
}
|
|
45
51
|
} catch (_) {}
|
|
46
52
|
},
|
|
@@ -54,6 +54,7 @@ export function createSkillsMethods({ api }) {
|
|
|
54
54
|
if (nextTarget !== this.skillsTargetApp) {
|
|
55
55
|
this.skillsTargetApp = nextTarget;
|
|
56
56
|
this.resetSkillsTargetState();
|
|
57
|
+
if (typeof this.saveNavState === 'function') this.saveNavState();
|
|
57
58
|
}
|
|
58
59
|
if (!refresh) {
|
|
59
60
|
return true;
|
|
@@ -107,6 +107,12 @@
|
|
|
107
107
|
:aria-pressed="(lang || 'zh') === 'en'"
|
|
108
108
|
:class="{ active: (lang || 'zh') === 'en' }"
|
|
109
109
|
@click="setLang('en')">EN</button>
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
class="lang-choice-btn"
|
|
113
|
+
:aria-pressed="(lang || 'zh') === 'ja'"
|
|
114
|
+
:class="{ active: (lang || 'zh') === 'ja' }"
|
|
115
|
+
@click="setLang('ja')">日本語</button>
|
|
110
116
|
</div>
|
|
111
117
|
</div>
|
|
112
118
|
|
|
@@ -297,6 +303,19 @@
|
|
|
297
303
|
<span>{{ t('side.system.settings.meta') }}</span>
|
|
298
304
|
</div>
|
|
299
305
|
</button>
|
|
306
|
+
<button
|
|
307
|
+
id="side-tab-trash"
|
|
308
|
+
data-main-tab="trash"
|
|
309
|
+
:aria-current="mainTab === 'trash' ? 'page' : null"
|
|
310
|
+
:class="['side-item', { active: isMainTabNavActive('trash') }]"
|
|
311
|
+
@pointerdown="onMainTabPointerDown('trash', $event)"
|
|
312
|
+
@click="onMainTabClick('trash', $event)">
|
|
313
|
+
<div class="side-item-title">回收站</div>
|
|
314
|
+
<div class="side-item-meta">
|
|
315
|
+
<span>已删除会话</span>
|
|
316
|
+
<span v-if="sessionTrashCount > 0" class="side-item-badge">{{ sessionTrashCount }}</span>
|
|
317
|
+
</div>
|
|
318
|
+
</button>
|
|
300
319
|
</div>
|
|
301
320
|
</div>
|
|
302
321
|
|
|
@@ -314,6 +333,12 @@
|
|
|
314
333
|
:aria-pressed="(lang || 'zh') === 'en'"
|
|
315
334
|
:class="{ active: (lang || 'zh') === 'en' }"
|
|
316
335
|
@click="setLang('en')">EN</button>
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
class="lang-choice-btn"
|
|
339
|
+
:aria-pressed="(lang || 'zh') === 'ja'"
|
|
340
|
+
:class="{ active: (lang || 'zh') === 'ja' }"
|
|
341
|
+
@click="setLang('ja')">日本語</button>
|
|
317
342
|
</div>
|
|
318
343
|
</div>
|
|
319
344
|
</aside>
|
|
@@ -45,8 +45,7 @@
|
|
|
45
45
|
<div class="selector-header">
|
|
46
46
|
<span class="selector-title">{{ t('claude.presetProviders') }}</span>
|
|
47
47
|
</div>
|
|
48
|
-
<div class="btn-group" style="flex-wrap: wrap; gap: 8px;">
|
|
49
|
-
<button type="button" class="btn-mini" @click="closeClaudeConfigModal(); showClaudeConfigModal = true">{{ t('claude.customConfig') }}</button>
|
|
48
|
+
<div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
|
|
50
49
|
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Claude Official'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.anthropic.com'; newClaudeConfig.model = 'claude-sonnet-4'; showClaudeConfigModal = true">Claude Official</button>
|
|
51
50
|
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DeepSeek'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.deepseek.com/anthropic'; newClaudeConfig.model = 'DeepSeek-V3.2'; showClaudeConfigModal = true">DeepSeek</button>
|
|
52
51
|
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Zhipu GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://open.bigmodel.cn/api/anthropic'; newClaudeConfig.model = 'glm-5'; showClaudeConfigModal = true">Zhipu GLM</button>
|
|
@@ -136,6 +135,7 @@
|
|
|
136
135
|
</div>
|
|
137
136
|
</div>
|
|
138
137
|
|
|
138
|
+
|
|
139
139
|
<div class="card-list">
|
|
140
140
|
<div v-for="(config, name) in claudeConfigs" :key="name"
|
|
141
141
|
:class="['card', { active: currentClaudeConfig === name }]"
|
|
@@ -167,6 +167,12 @@
|
|
|
167
167
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
168
168
|
</svg>
|
|
169
169
|
</button>
|
|
170
|
+
<button class="card-action-btn" @click="openCloneClaudeConfigModal(name, config)" :aria-label="t('claude.action.cloneAria', { name })" :title="t('claude.action.clone')">
|
|
171
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
172
|
+
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
|
173
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
174
|
+
</svg>
|
|
175
|
+
</button>
|
|
170
176
|
<button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" disabled :title="t('claude.action.shareDisabled')" :aria-label="t('config.shareCommand.aria')">
|
|
171
177
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
172
178
|
<path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
<div class="selector-header">
|
|
44
44
|
<span class="selector-title">{{ t('config.providerTemplate.title') }}</span>
|
|
45
45
|
</div>
|
|
46
|
-
<div class="btn-group" style="flex-wrap: wrap; gap: 8px;">
|
|
46
|
+
<div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
|
|
47
47
|
<button
|
|
48
48
|
v-for="tpl in codexProviderTemplates"
|
|
49
49
|
:key="tpl.name"
|
|
@@ -236,10 +236,10 @@
|
|
|
236
236
|
<span>{{ provider.name }}</span>
|
|
237
237
|
<span v-if="provider.readOnly" class="provider-readonly-badge">{{ t('config.badge.system') }}</span>
|
|
238
238
|
</div>
|
|
239
|
-
<div class="card-subtitle card-subtitle-model">
|
|
239
|
+
<div v-if="provider.name !== 'local'" class="card-subtitle card-subtitle-model">
|
|
240
240
|
{{ activeProviderModel(provider.name) || t('config.model.unset') }}
|
|
241
241
|
</div>
|
|
242
|
-
<div class="card-subtitle card-subtitle-url">
|
|
242
|
+
<div v-if="provider.name !== 'local'" class="card-subtitle card-subtitle-url">
|
|
243
243
|
{{ displayProviderUrl(provider) || t('config.url.unset') }}
|
|
244
244
|
</div>
|
|
245
245
|
</div>
|
|
@@ -291,6 +291,17 @@
|
|
|
291
291
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
292
292
|
</svg>
|
|
293
293
|
</button>
|
|
294
|
+
<button
|
|
295
|
+
v-if="!provider.readOnly"
|
|
296
|
+
class="card-action-btn"
|
|
297
|
+
@click="openCloneProviderModal(provider)"
|
|
298
|
+
:aria-label="t('config.provider.clone.aria', { name: provider.name })"
|
|
299
|
+
:title="t('config.provider.clone')">
|
|
300
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
301
|
+
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
|
302
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
303
|
+
</svg>
|
|
304
|
+
</button>
|
|
294
305
|
<button
|
|
295
306
|
v-if="!provider.readOnly"
|
|
296
307
|
class="card-action-btn delete"
|
|
@@ -308,5 +319,19 @@
|
|
|
308
319
|
</div>
|
|
309
320
|
</div>
|
|
310
321
|
</div>
|
|
322
|
+
|
|
323
|
+
<div v-if="displayCurrentProvider === 'local'" class="local-bridge-panel" style="margin-top:12px;padding:12px;background:var(--card-bg);border-radius:8px;border:1px solid var(--border-color)">
|
|
324
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:8px;color:var(--text-secondary)">轮询池 — 勾选参与负载均衡的提供商</div>
|
|
325
|
+
<div v-if="localBridgeCandidateProviders().length === 0" style="font-size:12px;color:var(--text-muted)">暂无可用上游 provider,请先添加直连 provider</div>
|
|
326
|
+
<label v-for="cp in localBridgeCandidateProviders()" :key="cp.name"
|
|
327
|
+
style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer;font-size:13px">
|
|
328
|
+
<input type="checkbox"
|
|
329
|
+
:checked="!isLocalBridgeExcluded(cp.name)"
|
|
330
|
+
@change="toggleLocalBridgeExcluded(cp.name)"
|
|
331
|
+
style="accent-color:var(--accent-color)" />
|
|
332
|
+
<span>{{ cp.name }}</span>
|
|
333
|
+
</label>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
311
336
|
</template>
|
|
312
337
|
</div>
|
|
@@ -119,6 +119,39 @@
|
|
|
119
119
|
<strong>{{ inspectorModelLoadStatus }}</strong>
|
|
120
120
|
</div>
|
|
121
121
|
</div>
|
|
122
|
+
<div v-if="providersHealthLoading || providersHealthResult" class="doctor-providers-health">
|
|
123
|
+
<div class="doctor-providers-health-title">
|
|
124
|
+
{{ t('dashboard.providersHealth.title') }}
|
|
125
|
+
<span v-if="providersHealthResult" class="doctor-providers-health-summary">
|
|
126
|
+
{{ providersHealthResult.summary.green }}/{{ providersHealthResult.summary.total }}
|
|
127
|
+
</span>
|
|
128
|
+
<span v-else class="doctor-providers-health-summary">{{ t('dashboard.providersHealth.checking') }}</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div v-if="providersHealthResult" class="doctor-providers-grid">
|
|
131
|
+
<div v-for="entry in providersHealthResult.providers"
|
|
132
|
+
:key="entry.provider"
|
|
133
|
+
class="doctor-provider-chip"
|
|
134
|
+
:class="[entry.status]"
|
|
135
|
+
:title="entry.provider + (entry.remote ? (entry.remote.ok ? ' OK' : ' ' + entry.remote.message) : '')">
|
|
136
|
+
<span class="doctor-provider-name">
|
|
137
|
+
{{ entry.provider }}
|
|
138
|
+
<span v-if="entry.isCurrent" class="doctor-provider-current">{{ t('dashboard.providersHealth.current') }}</span>
|
|
139
|
+
</span>
|
|
140
|
+
<span class="doctor-provider-status-dot"></span>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
<div v-if="providersHealthResult && providersHealthResult.providers.some(e => e.issues && e.issues.length)" class="doctor-provider-issues">
|
|
144
|
+
<div v-for="entry in providersHealthResult.providers.filter(e => e.issues && e.issues.length)"
|
|
145
|
+
:key="entry.provider + '-issues'"
|
|
146
|
+
class="doctor-provider-issue-card">
|
|
147
|
+
<div class="doctor-provider-issue-name">{{ entry.provider }}</div>
|
|
148
|
+
<div v-for="issue in entry.issues" :key="issue.code" class="doctor-provider-issue-row">
|
|
149
|
+
<span class="doctor-provider-issue-text">{{ issue.message }}</span>
|
|
150
|
+
<span v-if="issue.suggestion" class="doctor-provider-issue-fix">{{ issue.suggestion }}</span>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
122
155
|
<div v-if="healthCheckResult" class="doctor-health-result" :class="healthCheckResult.ok ? 'ok' : 'error'">
|
|
123
156
|
<div class="doctor-health-title">
|
|
124
157
|
{{ healthCheckResult.ok ? t('dashboard.health.ok') : t('dashboard.health.fail') }}
|
|
@@ -132,15 +132,15 @@
|
|
|
132
132
|
</div>
|
|
133
133
|
<div class="market-action-grid">
|
|
134
134
|
<button type="button" class="market-action-card" @click="openSkillsManager" :disabled="loading || !!initError || skillsMarketBusy">
|
|
135
|
-
<span class="market-action-title">{{ t('market.action.manage.title') }}</span>
|
|
135
|
+
<span class="market-action-title"><svg style="width:14px;height:14px;vertical-align:-2px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="14" height="14" rx="3"/><path d="M7 7h6M7 10h4"/></svg>{{ t('market.action.manage.title') }}</span>
|
|
136
136
|
<span class="market-action-copy">{{ t('market.action.manage.copy', { target: skillsTargetLabel }) }}</span>
|
|
137
137
|
</button>
|
|
138
138
|
<button type="button" class="market-action-card" @click="scanImportableSkills({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
|
|
139
|
-
<span class="market-action-title">{{ t('market.action.crossImport.title') }}</span>
|
|
139
|
+
<span class="market-action-title"><svg style="width:14px;height:14px;vertical-align:-2px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l3-3 3 3"/><path d="M7 3v6"/><path d="M13 7l3 3 3-3"/><path d="M16 16V7"/></svg>{{ t('market.action.crossImport.title') }}</span>
|
|
140
140
|
<span class="market-action-copy">{{ t('market.action.crossImport.copy', { target: skillsTargetLabel }) }}</span>
|
|
141
141
|
</button>
|
|
142
142
|
<button type="button" class="market-action-card" @click="triggerSkillsZipImport" :disabled="loading || !!initError || skillsMarketBusy">
|
|
143
|
-
<span class="market-action-title">{{ t('market.action.zipImport.title') }}</span>
|
|
143
|
+
<span class="market-action-title"><svg style="width:14px;height:14px;vertical-align:-2px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14l3-3 3 3"/><path d="M7 4v7"/><rect x="12" y="4" width="5" height="12" rx="1"/></svg>{{ t('market.action.zipImport.title') }}</span>
|
|
144
144
|
<span class="market-action-copy">{{ t('market.action.zipImport.copy') }}</span>
|
|
145
145
|
</button>
|
|
146
146
|
</div>
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
role="tab"
|
|
55
55
|
:aria-selected="promptTemplatesMode === 'compose'"
|
|
56
56
|
:class="['mode-pill', { active: promptTemplatesMode === 'compose' }]"
|
|
57
|
-
@click="promptTemplatesMode = 'compose'">{{ t('plugins.promptTemplates.mode.compose') }}</button>
|
|
57
|
+
@click="promptTemplatesMode = 'compose'; if(typeof saveNavState==='function')saveNavState()"><svg style="width:12px;height:12px;vertical-align:-1px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4h12v12H4z"/><path d="M8 8h4M8 11h2"/></svg>{{ t('plugins.promptTemplates.mode.compose') }}</button>
|
|
58
58
|
<button
|
|
59
59
|
type="button"
|
|
60
60
|
role="tab"
|
|
61
61
|
:aria-selected="promptTemplatesMode !== 'compose'"
|
|
62
62
|
:class="['mode-pill', { active: promptTemplatesMode !== 'compose' }]"
|
|
63
|
-
@click="promptTemplatesMode = 'manage'">{{ t('plugins.promptTemplates.mode.manage') }}</button>
|
|
63
|
+
@click="promptTemplatesMode = 'manage'; if(typeof saveNavState==='function')saveNavState()"><svg style="width:12px;height:12px;vertical-align:-1px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 5h14M3 10h10M3 15h12"/></svg>{{ t('plugins.promptTemplates.mode.manage') }}</button>
|
|
64
64
|
</div>
|
|
65
65
|
|
|
66
66
|
<div v-if="promptTemplatesMode === 'compose'" class="prompt-compose">
|
|
@@ -262,15 +262,7 @@
|
|
|
262
262
|
{{ t('sessions.preview.clipped', { count: activeSessionMessages.length }) }}
|
|
263
263
|
</div>
|
|
264
264
|
<div
|
|
265
|
-
v-
|
|
266
|
-
class="session-item-sub session-item-wrap"
|
|
267
|
-
style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
|
|
268
|
-
<span>{{ t('sessions.preview.shownCount', { shown: activeSessionVisibleMessages.length, total: activeSessionMessages.length }) }}</span>
|
|
269
|
-
<span>{{ t('sessions.preview.loadMore', { remain: sessionPreviewRemainingCount }) }}</span>
|
|
270
|
-
</div>
|
|
271
|
-
<div v-if="sessionPreviewLoadingMore" class="session-item-sub session-item-wrap">{{ t('sessions.preview.loadingMore') }}</div>
|
|
272
|
-
<div
|
|
273
|
-
v-for="(msg, idx) in activeSessionVisibleMessages"
|
|
265
|
+
v-for="(msg, idx) in activeSessionMessages"
|
|
274
266
|
:key="getRecordRenderKey(msg, idx)"
|
|
275
267
|
v-memo="[msg.text, msg.timestamp, msg.roleLabel, msg.normalizedRole]"
|
|
276
268
|
:data-message-key="getRecordRenderKey(msg, idx)"
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- 设置面板 -->
|
|
2
2
|
<div
|
|
3
3
|
v-show="mainTab === 'settings'"
|
|
4
4
|
class="mode-content"
|
|
5
5
|
id="panel-settings"
|
|
6
6
|
role="tabpanel"
|
|
7
7
|
:aria-labelledby="'tab-settings'">
|
|
8
|
-
<div class="
|
|
8
|
+
<div class="settings-subtabs-bar" role="tablist" :aria-label="t('settings.tabs.aria')">
|
|
9
9
|
<button
|
|
10
10
|
id="settings-tab-general"
|
|
11
11
|
role="tab"
|
|
12
12
|
aria-controls="settings-panel-general"
|
|
13
13
|
:aria-selected="settingsTab === 'general'"
|
|
14
14
|
:tabindex="settingsTab === 'general' ? 0 : -1"
|
|
15
|
-
:class="['
|
|
15
|
+
:class="['settings-subtab', { active: settingsTab === 'general' }]"
|
|
16
16
|
@click="onSettingsTabClick('general')">
|
|
17
17
|
{{ t('settings.tab.general') }}
|
|
18
18
|
</button>
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
aria-controls="settings-panel-data"
|
|
23
23
|
:aria-selected="settingsTab === 'data'"
|
|
24
24
|
:tabindex="settingsTab === 'data' ? 0 : -1"
|
|
25
|
-
:class="['
|
|
25
|
+
:class="['settings-subtab', { active: settingsTab === 'data' }]"
|
|
26
26
|
@click="onSettingsTabClick('data')">
|
|
27
27
|
{{ t('settings.tab.data') }}
|
|
28
28
|
</button>
|
|
@@ -76,6 +76,29 @@
|
|
|
76
76
|
</div>
|
|
77
77
|
</div>
|
|
78
78
|
</section>
|
|
79
|
+
|
|
80
|
+
<section class="settings-card settings-card--wide" :aria-label="t('settings.webhook.title')">
|
|
81
|
+
<div class="settings-card-header settings-card-header-row">
|
|
82
|
+
<div>
|
|
83
|
+
<div class="settings-card-title">Webhook</div>
|
|
84
|
+
<div class="settings-card-meta">配置变更外发通知</div>
|
|
85
|
+
</div>
|
|
86
|
+
<span :class="webhookConfig.enabled ? 'status-on' : 'status-off'">{{ webhookConfig.enabled ? '● 已启用' : '○ 已禁用' }}</span>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="settings-card-body">
|
|
89
|
+
<div v-if="webhookConfig.url" class="webhook-readonly-row">
|
|
90
|
+
<span class="webhook-readonly-label">URL</span>
|
|
91
|
+
<code class="webhook-readonly-value">{{ webhookConfig.url }}</code>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="webhook-readonly-row">
|
|
94
|
+
<span class="webhook-readonly-label">事件</span>
|
|
95
|
+
<div class="webhook-readonly-tags">
|
|
96
|
+
<span v-for="ev in webhookConfig.events" :key="ev" class="webhook-event-tag">{{ ev }}</span>
|
|
97
|
+
<span v-if="!webhookConfig.events.length" class="settings-card-hint">无</span>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
79
102
|
</div>
|
|
80
103
|
</div>
|
|
81
104
|
</div>
|
|
@@ -87,152 +110,66 @@
|
|
|
87
110
|
aria-labelledby="settings-tab-data">
|
|
88
111
|
<div class="settings-layout">
|
|
89
112
|
<div class="settings-grid">
|
|
90
|
-
<section class="settings-card" :aria-label="t('settings.
|
|
91
|
-
<div class="settings-card-header">
|
|
92
|
-
<div
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<div class="settings-card-body">
|
|
96
|
-
<div class="settings-actions">
|
|
97
|
-
<button class="btn-tool" @click="downloadClaudeDirectory" :disabled="claudeDownloadLoading">
|
|
98
|
-
{{ claudeDownloadLoading ? t('settings.backup.progress', { percent: claudeDownloadProgress }) : t('settings.backup.oneClickClaude') }}
|
|
99
|
-
</button>
|
|
100
|
-
<button class="btn-tool" @click="triggerClaudeImport" :disabled="claudeImportLoading">
|
|
101
|
-
{{ claudeImportLoading ? t('settings.importing') : t('settings.backup.importClaude') }}
|
|
102
|
-
</button>
|
|
103
|
-
</div>
|
|
104
|
-
<input
|
|
105
|
-
ref="claudeImportInput"
|
|
106
|
-
class="sr-only"
|
|
107
|
-
type="file"
|
|
108
|
-
accept=".zip"
|
|
109
|
-
@change="handleClaudeImportChange">
|
|
110
|
-
</div>
|
|
111
|
-
</section>
|
|
112
|
-
|
|
113
|
-
<section class="settings-card" :aria-label="t('settings.codex.title')">
|
|
114
|
-
<div class="settings-card-header">
|
|
115
|
-
<div class="settings-card-title">{{ t('settings.codex.title') }}</div>
|
|
116
|
-
<div class="settings-card-meta">{{ t('settings.codex.meta') }}</div>
|
|
117
|
-
</div>
|
|
118
|
-
<div class="settings-card-body">
|
|
119
|
-
<div class="settings-actions">
|
|
120
|
-
<button class="btn-tool" @click="downloadCodexDirectory" :disabled="codexDownloadLoading">
|
|
121
|
-
{{ codexDownloadLoading ? t('settings.backup.progress', { percent: codexDownloadProgress }) : t('settings.backup.oneClickCodex') }}
|
|
122
|
-
</button>
|
|
123
|
-
<button class="btn-tool" @click="triggerCodexImport" :disabled="codexImportLoading">
|
|
124
|
-
{{ codexImportLoading ? t('settings.importing') : t('settings.backup.importCodex') }}
|
|
125
|
-
</button>
|
|
126
|
-
</div>
|
|
127
|
-
<input
|
|
128
|
-
ref="codexImportInput"
|
|
129
|
-
class="sr-only"
|
|
130
|
-
type="file"
|
|
131
|
-
accept=".zip"
|
|
132
|
-
@change="handleCodexImportChange">
|
|
133
|
-
</div>
|
|
134
|
-
</section>
|
|
135
|
-
|
|
136
|
-
<section class="settings-card settings-card--wide" :aria-label="t('settings.deleteBehavior.title')">
|
|
137
|
-
<div class="settings-card-header">
|
|
138
|
-
<div class="settings-card-title">{{ t('settings.deleteBehavior.title') }}</div>
|
|
139
|
-
<div class="settings-card-meta">{{ t('settings.deleteBehavior.meta') }}</div>
|
|
140
|
-
</div>
|
|
141
|
-
<div class="settings-card-body">
|
|
142
|
-
<label class="health-remote-toggle settings-toggle">
|
|
143
|
-
<input type="checkbox" :checked="sessionTrashEnabled" @change="setSessionTrashEnabled($event.target.checked)">
|
|
144
|
-
<span>{{ t('settings.deleteBehavior.toggle') }}</span>
|
|
145
|
-
</label>
|
|
146
|
-
<div class="settings-card-hint">
|
|
147
|
-
{{ t('settings.deleteBehavior.hint') }}
|
|
113
|
+
<section class="settings-card settings-card--wide" :aria-label="t('settings.backup.title')">
|
|
114
|
+
<div class="settings-card-header settings-card-header-row">
|
|
115
|
+
<div>
|
|
116
|
+
<div class="settings-card-title">{{ t('settings.backup.title') }}</div>
|
|
117
|
+
<div class="settings-card-meta">{{ t('settings.backup.meta') }}</div>
|
|
148
118
|
</div>
|
|
149
119
|
</div>
|
|
150
|
-
</section>
|
|
151
|
-
|
|
152
|
-
<section class="settings-card settings-card--wide" :aria-label="t('settings.trash.retention')">
|
|
153
|
-
<div class="settings-card-header">
|
|
154
|
-
<div class="settings-card-title">{{ t('settings.trash.retention') }}</div>
|
|
155
|
-
<div class="settings-card-meta">{{ t('settings.trash.retentionMeta') }}</div>
|
|
156
|
-
</div>
|
|
157
120
|
<div class="settings-card-body">
|
|
158
|
-
<
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
121
|
+
<div class="settings-backup-grid">
|
|
122
|
+
<div class="settings-backup-slot">
|
|
123
|
+
<div class="settings-backup-slot-label">Claude</div>
|
|
124
|
+
<div class="settings-actions">
|
|
125
|
+
<button class="btn-tool" @click="downloadClaudeDirectory" :disabled="claudeDownloadLoading">
|
|
126
|
+
{{ claudeDownloadLoading ? t('settings.backup.progress', { percent: claudeDownloadProgress }) : t('settings.backup.oneClickClaude') }}
|
|
127
|
+
</button>
|
|
128
|
+
<button class="btn-tool" @click="triggerClaudeImport" :disabled="claudeImportLoading">
|
|
129
|
+
{{ claudeImportLoading ? t('settings.importing') : t('settings.backup.importClaude') }}
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
<input ref="claudeImportInput" class="sr-only" type="file" accept=".zip" @change="handleClaudeImportChange">
|
|
133
|
+
</div>
|
|
134
|
+
<div class="settings-backup-slot">
|
|
135
|
+
<div class="settings-backup-slot-label">Codex</div>
|
|
136
|
+
<div class="settings-actions">
|
|
137
|
+
<button class="btn-tool" @click="downloadCodexDirectory" :disabled="codexDownloadLoading">
|
|
138
|
+
{{ codexDownloadLoading ? t('settings.backup.progress', { percent: codexDownloadProgress }) : t('settings.backup.oneClickCodex') }}
|
|
139
|
+
</button>
|
|
140
|
+
<button class="btn-tool" @click="triggerCodexImport" :disabled="codexImportLoading">
|
|
141
|
+
{{ codexImportLoading ? t('settings.importing') : t('settings.backup.importCodex') }}
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
<input ref="codexImportInput" class="sr-only" type="file" accept=".zip" @change="handleCodexImportChange">
|
|
145
|
+
</div>
|
|
164
146
|
</div>
|
|
165
147
|
</div>
|
|
166
148
|
</section>
|
|
167
149
|
|
|
168
|
-
<section class="settings-card settings-card--wide" :aria-label="t('settings.
|
|
150
|
+
<section class="settings-card settings-card--wide" :aria-label="t('settings.trashConfig.title')">
|
|
169
151
|
<div class="settings-card-header settings-card-header-row">
|
|
170
152
|
<div>
|
|
171
|
-
<div class="settings-card-title">{{ t('settings.
|
|
172
|
-
<div class="settings-card-meta">{{ t('settings.
|
|
173
|
-
</div>
|
|
174
|
-
<div class="settings-card-actions">
|
|
175
|
-
<button class="btn-tool btn-tool-compact" @click="loadSessionTrash({ forceRefresh: true })" :disabled="sessionTrashLoading || sessionTrashClearing">
|
|
176
|
-
{{ sessionTrashLoading ? t('settings.trash.refreshing') : t('settings.trash.refresh') }}
|
|
177
|
-
</button>
|
|
178
|
-
<button class="btn-tool btn-tool-compact" @click="clearSessionTrash" :disabled="sessionTrashClearing || sessionTrashLoading || !(Number(sessionTrashCount) > 0)">
|
|
179
|
-
{{ sessionTrashClearing ? t('settings.trash.clearing') : t('settings.trash.clear') }}
|
|
180
|
-
</button>
|
|
153
|
+
<div class="settings-card-title">{{ t('settings.trashConfig.title') }}</div>
|
|
154
|
+
<div class="settings-card-meta">{{ t('settings.trashConfig.meta') }}</div>
|
|
181
155
|
</div>
|
|
182
156
|
</div>
|
|
183
|
-
|
|
184
157
|
<div class="settings-card-body">
|
|
185
|
-
<div
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<div v-else class="trash-list">
|
|
195
|
-
<div v-for="item in visibleSessionTrashItems" :key="item.trashId" class="trash-item session-item session-card">
|
|
196
|
-
<div class="trash-item-header session-item-header">
|
|
197
|
-
<div class="trash-item-main">
|
|
198
|
-
<div class="trash-item-mainline">
|
|
199
|
-
<div class="trash-item-title">{{ item.title || item.sessionId }}</div>
|
|
200
|
-
<span class="session-count-badge">{{ item.messageCount == null ? 0 : item.messageCount }}</span>
|
|
201
|
-
</div>
|
|
202
|
-
<div class="trash-item-meta session-item-meta">
|
|
203
|
-
<span class="session-source">{{ item.sourceLabel }}</span>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
<div class="trash-item-side">
|
|
207
|
-
<div class="trash-item-actions session-item-actions">
|
|
208
|
-
<button class="btn-mini" @click="restoreSessionTrash(item)" :disabled="sessionTrashLoading || sessionTrashClearing || isSessionTrashActionBusy(item)">
|
|
209
|
-
{{ sessionTrashRestoring[getSessionTrashActionKey(item)] ? t('settings.trash.restoring') : t('settings.trash.restore') }}
|
|
210
|
-
</button>
|
|
211
|
-
<button class="btn-mini delete" @click="purgeSessionTrash(item)" :disabled="sessionTrashLoading || sessionTrashClearing || isSessionTrashActionBusy(item)">
|
|
212
|
-
{{ sessionTrashPurging[getSessionTrashActionKey(item)] ? t('settings.trash.purging') : t('settings.trash.purge') }}
|
|
213
|
-
</button>
|
|
214
|
-
</div>
|
|
215
|
-
<div class="trash-item-time session-item-time">{{ item.deletedAt || item.updatedAt || t('sessions.unknownTime') }}</div>
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
<div v-if="item.cwd" class="trash-item-path session-item-sub session-item-wrap">
|
|
219
|
-
<span class="trash-item-label">{{ t('settings.trash.workspace') }}</span>
|
|
220
|
-
<span>{{ item.cwd }}</span>
|
|
221
|
-
</div>
|
|
222
|
-
<div class="trash-item-path session-item-sub session-item-wrap">
|
|
223
|
-
<span class="trash-item-label">{{ t('settings.trash.originalFile') }}</span>
|
|
224
|
-
<span>{{ item.originalFilePath }}</span>
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
<div v-if="sessionTrashHasMoreItems" class="trash-list-footer">
|
|
228
|
-
<button class="btn-tool btn-tool-compact" @click="loadMoreSessionTrashItems" :disabled="sessionTrashLoading || sessionTrashClearing">
|
|
229
|
-
{{ t('settings.trash.loadMore', { count: sessionTrashHiddenCount }) }}
|
|
230
|
-
</button>
|
|
231
|
-
</div>
|
|
158
|
+
<div class="settings-trash-config-grid">
|
|
159
|
+
<label class="settings-toggle">
|
|
160
|
+
<input type="checkbox" :checked="sessionTrashEnabled" @change="setSessionTrashEnabled($event.target.checked)">
|
|
161
|
+
<span>{{ t('settings.deleteBehavior.toggle') }}</span>
|
|
162
|
+
</label>
|
|
163
|
+
<label class="settings-retention-row">
|
|
164
|
+
<span>{{ t('settings.trash.retentionLabel') }}</span>
|
|
165
|
+
<input type="number" min="1" max="365" :value="sessionTrashRetentionDays" @change="setSessionTrashRetentionDays(Number($event.target.value))" class="settings-retention-input" />
|
|
166
|
+
</label>
|
|
232
167
|
</div>
|
|
168
|
+
<div class="settings-card-hint">{{ t('settings.trash.retentionHint') }}</div>
|
|
233
169
|
</div>
|
|
234
170
|
</section>
|
|
235
171
|
|
|
172
|
+
|
|
236
173
|
<section class="settings-card settings-card--wide settings-card--danger" :aria-label="t('settings.reset.title')">
|
|
237
174
|
<div class="settings-card-header">
|
|
238
175
|
<div class="settings-card-title">{{ t('settings.reset.title') }}</div>
|