codexmate 0.1.27 → 0.1.28
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/config-bootstrap.js +2 -9
- package/cli.js +39 -26
- package/package.json +1 -1
- package/web-ui/app.js +4 -0
- package/web-ui/index.html +11 -0
- package/web-ui/modules/app.methods.index.mjs +2 -0
- package/web-ui/modules/app.methods.theme.mjs +38 -0
- package/web-ui/modules/app.methods.web-ui-preferences.mjs +9 -0
- package/web-ui/modules/i18n/locales/en.mjs +6 -1
- package/web-ui/modules/i18n/locales/ja.mjs +6 -1
- package/web-ui/modules/i18n/locales/vi.mjs +6 -1
- package/web-ui/modules/i18n/locales/zh-tw.mjs +6 -1
- package/web-ui/modules/i18n/locales/zh.mjs +6 -1
- package/web-ui/partials/index/panel-sessions.html +2 -2
- package/web-ui/partials/index/panel-settings.html +24 -0
- package/web-ui/res/web-ui-render.precompiled.js +26 -2
- package/web-ui/styles/base-theme.css +1 -0
- package/web-ui/styles/controls-forms.css +1 -1
- package/web-ui/styles/sessions-toolbar-trash.css +1 -1
- package/web-ui/styles/sessions-usage.css +1 -1
- package/web-ui/styles/settings-panel.css +2 -2
- package/web-ui/styles/theme-dark-tokens.css +83 -0
- package/web-ui/styles/theme-dark.css +1355 -0
- package/web-ui/styles.css +1 -0
package/cli/config-bootstrap.js
CHANGED
|
@@ -17,10 +17,8 @@ function createConfigBootstrapController(deps = {}) {
|
|
|
17
17
|
writeModels,
|
|
18
18
|
readCurrentModels,
|
|
19
19
|
writeCurrentModels,
|
|
20
|
-
updateAuthJson,
|
|
21
20
|
CONFIG_DIR,
|
|
22
21
|
CONFIG_FILE,
|
|
23
|
-
AUTH_FILE,
|
|
24
22
|
MODELS_FILE,
|
|
25
23
|
RECENT_CONFIGS_FILE,
|
|
26
24
|
INIT_MARK_FILE,
|
|
@@ -49,10 +47,8 @@ function createConfigBootstrapController(deps = {}) {
|
|
|
49
47
|
if (typeof writeModels !== 'function') throw new Error('createConfigBootstrapController 缺少 writeModels');
|
|
50
48
|
if (typeof readCurrentModels !== 'function') throw new Error('createConfigBootstrapController 缺少 readCurrentModels');
|
|
51
49
|
if (typeof writeCurrentModels !== 'function') throw new Error('createConfigBootstrapController 缺少 writeCurrentModels');
|
|
52
|
-
if (typeof updateAuthJson !== 'function') throw new Error('createConfigBootstrapController 缺少 updateAuthJson');
|
|
53
50
|
if (!CONFIG_DIR) throw new Error('createConfigBootstrapController 缺少 CONFIG_DIR');
|
|
54
51
|
if (!CONFIG_FILE) throw new Error('createConfigBootstrapController 缺少 CONFIG_FILE');
|
|
55
|
-
if (!AUTH_FILE) throw new Error('createConfigBootstrapController 缺少 AUTH_FILE');
|
|
56
52
|
if (!MODELS_FILE) throw new Error('createConfigBootstrapController 缺少 MODELS_FILE');
|
|
57
53
|
if (!RECENT_CONFIGS_FILE) throw new Error('createConfigBootstrapController 缺少 RECENT_CONFIGS_FILE');
|
|
58
54
|
if (!INIT_MARK_FILE) throw new Error('createConfigBootstrapController 缺少 INIT_MARK_FILE');
|
|
@@ -131,6 +127,7 @@ base_url = "http://127.0.0.1:3737/bridge/local/v1"
|
|
|
131
127
|
wire_api = "responses"
|
|
132
128
|
requires_openai_auth = true
|
|
133
129
|
preferred_auth_method = "codexmate"
|
|
130
|
+
experimental_bearer_token = "codexmate"
|
|
134
131
|
codexmate_bridge = "local"
|
|
135
132
|
request_max_retries = 4
|
|
136
133
|
stream_max_retries = 10
|
|
@@ -181,7 +178,7 @@ stream_idle_timeout_ms = 300000
|
|
|
181
178
|
// Check if [model_providers.local] section already exists
|
|
182
179
|
if (/\[model_providers\.local\]/.test(content)) return;
|
|
183
180
|
|
|
184
|
-
const localSection = `\n[model_providers.local]\nname = "local"\nbase_url = "http://127.0.0.1:3737/bridge/local/v1"\nwire_api = "responses"\nrequires_openai_auth = true\npreferred_auth_method = "codexmate"\ncodexmate_bridge = "local"\nrequest_max_retries = 4\nstream_max_retries = 10\nstream_idle_timeout_ms = 300000\n`;
|
|
181
|
+
const localSection = `\n[model_providers.local]\nname = "local"\nbase_url = "http://127.0.0.1:3737/bridge/local/v1"\nwire_api = "responses"\nrequires_openai_auth = true\npreferred_auth_method = "codexmate"\nexperimental_bearer_token = "codexmate"\ncodexmate_bridge = "local"\nrequest_max_retries = 4\nstream_max_retries = 10\nstream_idle_timeout_ms = 300000\n`;
|
|
185
182
|
fs.appendFileSync(CONFIG_FILE, localSection, 'utf-8');
|
|
186
183
|
}
|
|
187
184
|
|
|
@@ -263,10 +260,6 @@ stream_idle_timeout_ms = 300000
|
|
|
263
260
|
currentModels[defaultProvider] = defaultModel;
|
|
264
261
|
writeCurrentModels(currentModels);
|
|
265
262
|
}
|
|
266
|
-
|
|
267
|
-
if (!fs.existsSync(AUTH_FILE)) {
|
|
268
|
-
updateAuthJson('');
|
|
269
|
-
}
|
|
270
263
|
}
|
|
271
264
|
|
|
272
265
|
function writeInitMark(payload) {
|
package/cli.js
CHANGED
|
@@ -757,6 +757,7 @@ base_url = "http://127.0.0.1:3737/bridge/local/v1"
|
|
|
757
757
|
wire_api = "responses"
|
|
758
758
|
requires_openai_auth = true
|
|
759
759
|
preferred_auth_method = "codexmate"
|
|
760
|
+
experimental_bearer_token = "codexmate"
|
|
760
761
|
codexmate_bridge = "local"
|
|
761
762
|
request_max_retries = 4
|
|
762
763
|
stream_max_retries = 10
|
|
@@ -893,17 +894,27 @@ function writeCurrentModels(data) {
|
|
|
893
894
|
fs.writeFileSync(CURRENT_MODELS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
894
895
|
}
|
|
895
896
|
|
|
896
|
-
function
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
897
|
+
function ensureActiveProviderBearerToken(content, providerName, providerSegments = null) {
|
|
898
|
+
// 单文件认证:auth.json 不再写入,激活 provider 块缺 experimental_bearer_token 时
|
|
899
|
+
// 从 preferred_auth_method(内部键仓)惰性补写,保证 codex 仅凭 config.toml 取到密钥。
|
|
900
|
+
const ranges = findProviderSectionRanges(content, providerName, providerSegments);
|
|
901
|
+
if (!ranges.length) return content;
|
|
902
|
+
let updated = content;
|
|
903
|
+
const sorted = [...ranges].sort((a, b) => b.start - a.start);
|
|
904
|
+
for (const range of sorted) {
|
|
905
|
+
const block = updated.slice(range.start, range.end);
|
|
906
|
+
if (/(^|\n)\s*experimental_bearer_token\s*=/.test(block)) continue;
|
|
907
|
+
const prefMatch = block.match(/^(\s*)preferred_auth_method\s*=\s*"((?:\\.|[^"\\\n])*)"\s*(?:#.*)?$/m);
|
|
908
|
+
if (!prefMatch || !prefMatch[2].trim()) continue;
|
|
909
|
+
const insertLine = `${prefMatch[1]}experimental_bearer_token = "${prefMatch[2]}"`;
|
|
910
|
+
const lineEndIndex = block.indexOf('\n', prefMatch.index + prefMatch[0].length);
|
|
911
|
+
const insertAt = lineEndIndex === -1
|
|
912
|
+
? range.start + block.length
|
|
913
|
+
: range.start + lineEndIndex + 1;
|
|
914
|
+
const prefix = lineEndIndex === -1 ? '\n' : '';
|
|
915
|
+
updated = updated.slice(0, insertAt) + prefix + insertLine + '\n' + updated.slice(insertAt);
|
|
904
916
|
}
|
|
905
|
-
|
|
906
|
-
fs.writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
917
|
+
return updated;
|
|
907
918
|
}
|
|
908
919
|
|
|
909
920
|
function isPlainObject(value) {
|
|
@@ -1029,6 +1040,10 @@ function normalizeSidebarCollapsedPreference(value) {
|
|
|
1029
1040
|
return normalizeBooleanPreference(value, false);
|
|
1030
1041
|
}
|
|
1031
1042
|
|
|
1043
|
+
function normalizeThemePreference(value) {
|
|
1044
|
+
return value === 'dark' ? 'dark' : 'light';
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1032
1047
|
function normalizeSessionFilterSourcePreference(value) {
|
|
1033
1048
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1034
1049
|
if (normalized === 'all' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi') return normalized;
|
|
@@ -1100,6 +1115,7 @@ function normalizeWebUiPreferences(value = {}) {
|
|
|
1100
1115
|
sysPromptMode: normalizeSysPromptModePreference(source.sysPromptMode),
|
|
1101
1116
|
projectClaudeMdPath: typeof source.projectClaudeMdPath === 'string' ? source.projectClaudeMdPath : '',
|
|
1102
1117
|
sidebarCollapsed: normalizeSidebarCollapsedPreference(source.sidebarCollapsed),
|
|
1118
|
+
theme: normalizeThemePreference(source.theme),
|
|
1103
1119
|
starPrompted: normalizeBooleanPreference(source.starPrompted, false),
|
|
1104
1120
|
sessionLoadNativeDialog: normalizeBooleanPreference(source.sessionLoadNativeDialog, false),
|
|
1105
1121
|
language: typeof source.language === 'string' ? source.language : '',
|
|
@@ -1549,6 +1565,7 @@ const PROVIDER_CONFIG_KEYS = new Set([
|
|
|
1549
1565
|
'wire_api',
|
|
1550
1566
|
'requires_openai_auth',
|
|
1551
1567
|
'preferred_auth_method',
|
|
1568
|
+
'experimental_bearer_token',
|
|
1552
1569
|
'request_max_retries',
|
|
1553
1570
|
'stream_max_retries',
|
|
1554
1571
|
'stream_idle_timeout_ms'
|
|
@@ -2753,7 +2770,6 @@ const {
|
|
|
2753
2770
|
writeModels,
|
|
2754
2771
|
readCurrentModels,
|
|
2755
2772
|
writeCurrentModels,
|
|
2756
|
-
updateAuthJson,
|
|
2757
2773
|
CONFIG_DIR,
|
|
2758
2774
|
CONFIG_FILE,
|
|
2759
2775
|
AUTH_FILE,
|
|
@@ -3071,8 +3087,8 @@ function applyConfigTemplate(params = {}) {
|
|
|
3071
3087
|
return { error: `模板中找不到当前 provider: ${activeProvider}` };
|
|
3072
3088
|
}
|
|
3073
3089
|
|
|
3074
|
-
|
|
3075
|
-
|
|
3090
|
+
const ensuredTemplate = ensureActiveProviderBearerToken(template.trim(), activeProvider);
|
|
3091
|
+
writeConfig(ensuredTemplate + '\n');
|
|
3076
3092
|
|
|
3077
3093
|
const models = readModels();
|
|
3078
3094
|
if (!models.includes(parsed.model)) {
|
|
@@ -3310,6 +3326,7 @@ function addProviderToConfig(params = {}) {
|
|
|
3310
3326
|
`wire_api = "responses"`,
|
|
3311
3327
|
`requires_openai_auth = true`,
|
|
3312
3328
|
`preferred_auth_method = "${safeKey}"`,
|
|
3329
|
+
`experimental_bearer_token = "${safeKey}"`,
|
|
3313
3330
|
`models = [{ id = "${escapeTomlBasicString(model)}", name = "${escapeTomlBasicString(model)}" }]`,
|
|
3314
3331
|
...extraLines,
|
|
3315
3332
|
`request_max_retries = 4`,
|
|
@@ -10921,17 +10938,16 @@ function cmdSwitch(providerName, silent = false) {
|
|
|
10921
10938
|
throw new Error('提供商不存在');
|
|
10922
10939
|
}
|
|
10923
10940
|
|
|
10924
|
-
//
|
|
10941
|
+
// 切换提供商(单文件认证:同步确保目标块含 experimental_bearer_token)
|
|
10925
10942
|
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
10926
|
-
const
|
|
10943
|
+
const switchedContent = content.replace(
|
|
10927
10944
|
/^(model_provider\s*=\s*)(["']).*?(["'])/m,
|
|
10928
10945
|
`$1$2${providerName}$3`
|
|
10929
10946
|
);
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
updateAuthJson(apiKey);
|
|
10947
|
+
const providerSegments = providers[providerName] && Array.isArray(providers[providerName].__codexmate_legacy_segments)
|
|
10948
|
+
? providers[providerName].__codexmate_legacy_segments
|
|
10949
|
+
: null;
|
|
10950
|
+
writeConfig(ensureActiveProviderBearerToken(switchedContent, providerName, providerSegments));
|
|
10935
10951
|
|
|
10936
10952
|
// 切换到该提供商的模型
|
|
10937
10953
|
const currentModels = readCurrentModels();
|
|
@@ -11072,6 +11088,7 @@ base_url = "${safeBaseUrl}"
|
|
|
11072
11088
|
wire_api = "responses"
|
|
11073
11089
|
requires_openai_auth = true
|
|
11074
11090
|
preferred_auth_method = "${safeApiKey}"
|
|
11091
|
+
experimental_bearer_token = "${safeApiKey}"
|
|
11075
11092
|
request_max_retries = 4
|
|
11076
11093
|
stream_max_retries = 10
|
|
11077
11094
|
stream_idle_timeout_ms = 300000
|
|
@@ -11373,6 +11390,7 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
|
|
|
11373
11390
|
updatedBlock = replaceTomlStringField(updatedBlock, 'base_url', computeLocalOpenaiBridgeBaseUrl());
|
|
11374
11391
|
updatedBlock = replaceTomlBooleanField(updatedBlock, 'requires_openai_auth', true);
|
|
11375
11392
|
updatedBlock = replaceTomlStringField(updatedBlock, 'preferred_auth_method', 'codexmate');
|
|
11393
|
+
updatedBlock = replaceTomlStringField(updatedBlock, 'experimental_bearer_token', 'codexmate');
|
|
11376
11394
|
updatedBlock = replaceTomlStringField(updatedBlock, 'codexmate_bridge', 'openai');
|
|
11377
11395
|
if (hasOpenaiBridgeMaxRetries) {
|
|
11378
11396
|
updatedBlock = replaceTomlNumberField(updatedBlock, 'codexmate_bridge_max_retries', openaiBridgeMaxRetries);
|
|
@@ -11383,6 +11401,7 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
|
|
|
11383
11401
|
}
|
|
11384
11402
|
if (apiKey !== undefined) {
|
|
11385
11403
|
updatedBlock = replaceTomlStringField(updatedBlock, 'preferred_auth_method', apiKey);
|
|
11404
|
+
updatedBlock = replaceTomlStringField(updatedBlock, 'experimental_bearer_token', apiKey);
|
|
11386
11405
|
}
|
|
11387
11406
|
}
|
|
11388
11407
|
|
|
@@ -11397,12 +11416,6 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
|
|
|
11397
11416
|
}
|
|
11398
11417
|
writeConfig(finalContent);
|
|
11399
11418
|
|
|
11400
|
-
// 如果更新了 API Key 且该提供商是当前激活的,同步更新 auth.json
|
|
11401
|
-
const currentProvider = config.model_provider;
|
|
11402
|
-
if (apiKey !== undefined && name === currentProvider) {
|
|
11403
|
-
updateAuthJson(apiKey);
|
|
11404
|
-
}
|
|
11405
|
-
|
|
11406
11419
|
if (!silent) {
|
|
11407
11420
|
console.log('✓ 已更新提供商:', name);
|
|
11408
11421
|
console.log();
|
package/package.json
CHANGED
package/web-ui/app.js
CHANGED
|
@@ -575,6 +575,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
575
575
|
kilocodeAutoSaveSignature: '',
|
|
576
576
|
forceCompactLayout: false,
|
|
577
577
|
sidebarCollapsed: false,
|
|
578
|
+
theme: 'light',
|
|
578
579
|
sessionLoadNativeDialog: false,
|
|
579
580
|
starPrompted: false,
|
|
580
581
|
webhookConfig: { enabled: false, url: '', events: ['provider-switch', 'claude-md-edit'] },
|
|
@@ -604,6 +605,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
604
605
|
if (typeof this.initI18n === 'function') {
|
|
605
606
|
this.initI18n();
|
|
606
607
|
}
|
|
608
|
+
if (typeof this.applyDocumentTheme === 'function') {
|
|
609
|
+
this.applyDocumentTheme();
|
|
610
|
+
}
|
|
607
611
|
if (typeof this.loadWebhookSettings === 'function') {
|
|
608
612
|
this.loadWebhookSettings();
|
|
609
613
|
}
|
package/web-ui/index.html
CHANGED
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
<script src="/res/marked.min.js"></script>
|
|
12
12
|
<script src="/res/purify.min.js"></script>
|
|
13
13
|
<link rel="stylesheet" href="/web-ui/styles.css">
|
|
14
|
+
<script>
|
|
15
|
+
// Render hint: skip the light flash for dark-theme users before the
|
|
16
|
+
// server preference store loads. Kept in sync by applyDocumentTheme().
|
|
17
|
+
(function () {
|
|
18
|
+
try {
|
|
19
|
+
if (localStorage.getItem('codexmateThemeCache') === 'dark') {
|
|
20
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
21
|
+
}
|
|
22
|
+
} catch (_) {}
|
|
23
|
+
})();
|
|
24
|
+
</script>
|
|
14
25
|
</head>
|
|
15
26
|
<body>
|
|
16
27
|
<!-- @include ./partials/index/layout-header.html -->
|
|
@@ -38,6 +38,7 @@ import { createPluginsMethods } from './plugins.methods.mjs';
|
|
|
38
38
|
import { createI18nMethods } from './i18n.mjs';
|
|
39
39
|
import { createWebhookMethods } from './app.methods.webhook.mjs';
|
|
40
40
|
import { createWebUiPreferencesMethods } from './app.methods.web-ui-preferences.mjs';
|
|
41
|
+
import { createThemeMethods } from './app.methods.theme.mjs';
|
|
41
42
|
import {
|
|
42
43
|
CONFIG_MODE_SET,
|
|
43
44
|
getProviderConfigModeMeta
|
|
@@ -92,6 +93,7 @@ export function createAppMethods() {
|
|
|
92
93
|
...createProvidersMethods({ api }),
|
|
93
94
|
...createProviderCacheMethods({ api }),
|
|
94
95
|
...createWebUiPreferencesMethods({ api }),
|
|
96
|
+
...createThemeMethods(),
|
|
95
97
|
...createClaudeConfigMethods({ api }),
|
|
96
98
|
...createToolConfigPermissionMethods({ api }),
|
|
97
99
|
...createOpenclawCoreMethods(),
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const THEME_STORAGE_CACHE_KEY = 'codexmateThemeCache';
|
|
2
|
+
|
|
3
|
+
export function createThemeMethods() {
|
|
4
|
+
return {
|
|
5
|
+
normalizeTheme(value) {
|
|
6
|
+
return value === 'dark' ? 'dark' : 'light';
|
|
7
|
+
},
|
|
8
|
+
applyDocumentTheme() {
|
|
9
|
+
try {
|
|
10
|
+
if (typeof document === 'undefined' || !document.documentElement) return;
|
|
11
|
+
const next = this.normalizeTheme(this.theme);
|
|
12
|
+
if (next === 'dark') {
|
|
13
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
14
|
+
} else {
|
|
15
|
+
document.documentElement.removeAttribute('data-theme');
|
|
16
|
+
}
|
|
17
|
+
// Render hint only: lets the inline head script skip the light
|
|
18
|
+
// flash before server preferences arrive. Source of truth is
|
|
19
|
+
// the server-side webUi preference store.
|
|
20
|
+
const storage = typeof globalThis !== 'undefined' ? globalThis.localStorage : null;
|
|
21
|
+
if (storage && typeof storage.setItem === 'function' && typeof storage.removeItem === 'function') {
|
|
22
|
+
if (next === 'dark') storage.setItem(THEME_STORAGE_CACHE_KEY, 'dark');
|
|
23
|
+
else storage.removeItem(THEME_STORAGE_CACHE_KEY);
|
|
24
|
+
}
|
|
25
|
+
} catch (_) {}
|
|
26
|
+
},
|
|
27
|
+
setTheme(theme) {
|
|
28
|
+
const next = this.normalizeTheme(theme);
|
|
29
|
+
if (next !== this.theme) {
|
|
30
|
+
this.theme = next;
|
|
31
|
+
this.applyDocumentTheme();
|
|
32
|
+
}
|
|
33
|
+
if (typeof this.persistWebUiPreferences === 'function') {
|
|
34
|
+
this.persistWebUiPreferences({ theme: next });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -352,6 +352,9 @@ export function createWebUiPreferencesMethods(options = {}) {
|
|
|
352
352
|
? source.projectClaudeMdPath
|
|
353
353
|
: (typeof this.projectClaudeMdPath === 'string' ? this.projectClaudeMdPath : ''),
|
|
354
354
|
sidebarCollapsed: normalizeBoolean(hasOwn(source, 'sidebarCollapsed') ? source.sidebarCollapsed : this.sidebarCollapsed, false),
|
|
355
|
+
theme: typeof this.normalizeTheme === 'function'
|
|
356
|
+
? this.normalizeTheme(hasOwn(source, 'theme') ? source.theme : this.theme)
|
|
357
|
+
: 'light',
|
|
355
358
|
configModeVisibility: typeof this.normalizeConfigModeVisibility === 'function'
|
|
356
359
|
? this.normalizeConfigModeVisibility(hasOwn(source, 'configModeVisibility') ? source.configModeVisibility : this.configModeVisibility)
|
|
357
360
|
: { codex: true, claude: true, openclaw: true, opencode: true, kilocode: true },
|
|
@@ -423,6 +426,12 @@ export function createWebUiPreferencesMethods(options = {}) {
|
|
|
423
426
|
if (hasOwn(source, 'sidebarCollapsed')) {
|
|
424
427
|
this.sidebarCollapsed = normalizeBoolean(source.sidebarCollapsed, false);
|
|
425
428
|
}
|
|
429
|
+
if (typeof source.theme === 'string') {
|
|
430
|
+
this.theme = typeof this.normalizeTheme === 'function'
|
|
431
|
+
? this.normalizeTheme(source.theme)
|
|
432
|
+
: 'light';
|
|
433
|
+
if (typeof this.applyDocumentTheme === 'function') this.applyDocumentTheme();
|
|
434
|
+
}
|
|
426
435
|
if (hasOwn(source, 'configModeVisibility') && typeof this.normalizeConfigModeVisibility === 'function') {
|
|
427
436
|
this.configModeVisibility = this.normalizeConfigModeVisibility(source.configModeVisibility);
|
|
428
437
|
}
|
|
@@ -706,7 +706,7 @@ const en = Object.freeze({
|
|
|
706
706
|
'sessions.preview.loading': 'Loading...',
|
|
707
707
|
'sessions.batch.select': 'Batch delete',
|
|
708
708
|
'sessions.batch.exit': 'Exit selection',
|
|
709
|
-
'sessions.batch.
|
|
709
|
+
'sessions.batch.selectAllVisibleN': 'Select {count} visible',
|
|
710
710
|
'sessions.batch.unselectAll': 'Clear selection',
|
|
711
711
|
'sessions.batch.deleteSelected': 'Delete selected',
|
|
712
712
|
'sessions.batch.deleteSelectedN': 'Delete selected ({count})',
|
|
@@ -1133,6 +1133,11 @@ const en = Object.freeze({
|
|
|
1133
1133
|
'settings.language.meta': 'Choose the Web UI display language',
|
|
1134
1134
|
'settings.language.label': 'Interface language',
|
|
1135
1135
|
'settings.language.hint': 'Language preference is saved in this browser.',
|
|
1136
|
+
'settings.theme.title': 'Appearance',
|
|
1137
|
+
'settings.theme.meta': 'Choose the Web UI color theme',
|
|
1138
|
+
'settings.theme.light': 'Light',
|
|
1139
|
+
'settings.theme.dark': 'Dark',
|
|
1140
|
+
'settings.theme.hint': 'Theme preference is saved automatically and persists across reloads.',
|
|
1136
1141
|
'settings.sharePrefix.title': 'Share command prefix',
|
|
1137
1142
|
'settings.sharePrefix.meta': 'Used as the prefix for “Copy share command” in the Web UI',
|
|
1138
1143
|
'settings.sharePrefix.label': 'Prefix',
|
|
@@ -708,7 +708,7 @@ const ja = Object.freeze({
|
|
|
708
708
|
'sessions.preview.loading': 'プレビュー読み込み中...',
|
|
709
709
|
'sessions.batch.select': '一括削除',
|
|
710
710
|
'sessions.batch.exit': '選択を終了',
|
|
711
|
-
'sessions.batch.
|
|
711
|
+
'sessions.batch.selectAllVisibleN': '表示中の {count} 件を選択',
|
|
712
712
|
'sessions.batch.unselectAll': '選択を解除',
|
|
713
713
|
'sessions.batch.deleteSelected': '選択を削除',
|
|
714
714
|
'sessions.batch.deleteSelectedN': '選択を削除({count})',
|
|
@@ -1136,6 +1136,11 @@ const ja = Object.freeze({
|
|
|
1136
1136
|
'settings.language.meta': 'Web UI の表示言語を選択',
|
|
1137
1137
|
'settings.language.label': 'インターフェース言語',
|
|
1138
1138
|
'settings.language.hint': '言語設定はこのブラウザに保存されます。',
|
|
1139
|
+
'settings.theme.title': '外観テーマ',
|
|
1140
|
+
'settings.theme.meta': 'Web UI のカラーテーマを選択します',
|
|
1141
|
+
'settings.theme.light': 'ライト',
|
|
1142
|
+
'settings.theme.dark': 'ダーク',
|
|
1143
|
+
'settings.theme.hint': 'テーマ設定は自動保存され、再読み込み後も保持されます。',
|
|
1139
1144
|
'settings.sharePrefix.title': '共有コマンドプレフィックス',
|
|
1140
1145
|
'settings.sharePrefix.meta': 'Web UI の「共有コマンドをコピー」のプレフィックスに影響',
|
|
1141
1146
|
'settings.sharePrefix.label': 'プレフィックス',
|
|
@@ -397,6 +397,11 @@ const vi = Object.freeze({
|
|
|
397
397
|
'settings.language.meta': 'Chọn ngôn ngữ hiển thị cho Web UI',
|
|
398
398
|
'settings.language.label': 'Ngôn ngữ giao diện',
|
|
399
399
|
'settings.language.hint': 'Tùy chọn ngôn ngữ được lưu trong trình duyệt này.',
|
|
400
|
+
'settings.theme.title': 'Chủ đề giao diện',
|
|
401
|
+
'settings.theme.meta': 'Chọn chủ đề màu cho Web UI',
|
|
402
|
+
'settings.theme.light': 'Sáng',
|
|
403
|
+
'settings.theme.dark': 'Tối',
|
|
404
|
+
'settings.theme.hint': 'Tùy chọn chủ đề được tự động lưu và giữ nguyên sau khi tải lại.',
|
|
400
405
|
|
|
401
406
|
'settings.webhook.title': 'Webhook',
|
|
402
407
|
'settings.webhook.meta': 'Gửi thông báo sự kiện tới dịch vụ bên ngoài',
|
|
@@ -881,7 +886,7 @@ const vi = Object.freeze({
|
|
|
881
886
|
'sessions.preview.loading': 'Đang tải...',
|
|
882
887
|
'sessions.batch.select': 'Xóa hàng loạt',
|
|
883
888
|
'sessions.batch.exit': 'Thoát chọn',
|
|
884
|
-
'sessions.batch.
|
|
889
|
+
'sessions.batch.selectAllVisibleN': 'Chọn {count} mục hiển thị',
|
|
885
890
|
'sessions.batch.unselectAll': 'Bỏ chọn tất cả',
|
|
886
891
|
'sessions.batch.deleteSelected': 'Xóa đã chọn',
|
|
887
892
|
'sessions.batch.deleteSelectedN': 'Xóa đã chọn ({count})',
|
|
@@ -705,7 +705,7 @@ const zhTw = Object.freeze({
|
|
|
705
705
|
'sessions.preview.loading': '載入中...',
|
|
706
706
|
'sessions.batch.select': '批次刪除',
|
|
707
707
|
'sessions.batch.exit': '退出選擇',
|
|
708
|
-
'sessions.batch.
|
|
708
|
+
'sessions.batch.selectAllVisibleN': '全選可見 {count} 項',
|
|
709
709
|
'sessions.batch.unselectAll': '取消全選',
|
|
710
710
|
'sessions.batch.deleteSelected': '刪除選中',
|
|
711
711
|
'sessions.batch.deleteSelectedN': '刪除選中({count})',
|
|
@@ -1132,6 +1132,11 @@ const zhTw = Object.freeze({
|
|
|
1132
1132
|
'settings.language.meta': '選擇 Web UI 的顯示語言',
|
|
1133
1133
|
'settings.language.label': '界面語言',
|
|
1134
1134
|
'settings.language.hint': '語言偏好會保存在目前瀏覽器。',
|
|
1135
|
+
'settings.theme.title': '外觀主題',
|
|
1136
|
+
'settings.theme.meta': '選擇 Web UI 的配色主題',
|
|
1137
|
+
'settings.theme.light': '淺色',
|
|
1138
|
+
'settings.theme.dark': '深色',
|
|
1139
|
+
'settings.theme.hint': '主題偏好會自動儲存,重新整理後保持不變。',
|
|
1135
1140
|
'settings.sharePrefix.title': '分享命令前綴',
|
|
1136
1141
|
'settings.sharePrefix.meta': '影響 Web UI 裡“複製分享命令”的前綴',
|
|
1137
1142
|
'settings.sharePrefix.label': '前綴',
|
|
@@ -705,7 +705,7 @@ const zh = Object.freeze({
|
|
|
705
705
|
'sessions.preview.loading': '加载中...',
|
|
706
706
|
'sessions.batch.select': '批量删除',
|
|
707
707
|
'sessions.batch.exit': '退出选择',
|
|
708
|
-
'sessions.batch.
|
|
708
|
+
'sessions.batch.selectAllVisibleN': '全选可见 {count} 项',
|
|
709
709
|
'sessions.batch.unselectAll': '取消全选',
|
|
710
710
|
'sessions.batch.deleteSelected': '删除选中',
|
|
711
711
|
'sessions.batch.deleteSelectedN': '删除选中({count})',
|
|
@@ -1132,6 +1132,11 @@ const zh = Object.freeze({
|
|
|
1132
1132
|
'settings.language.meta': '选择 Web UI 的显示语言',
|
|
1133
1133
|
'settings.language.label': '界面语言',
|
|
1134
1134
|
'settings.language.hint': '语言偏好会保存在当前浏览器。',
|
|
1135
|
+
'settings.theme.title': '外观主题',
|
|
1136
|
+
'settings.theme.meta': '选择 Web UI 的配色主题',
|
|
1137
|
+
'settings.theme.light': '浅色',
|
|
1138
|
+
'settings.theme.dark': '深色',
|
|
1139
|
+
'settings.theme.hint': '主题偏好会自动保存,刷新后保持不变。',
|
|
1135
1140
|
'settings.sharePrefix.title': '分享命令前缀',
|
|
1136
1141
|
'settings.sharePrefix.meta': '影响 Web UI 里“复制分享命令”的前缀',
|
|
1137
1142
|
'settings.sharePrefix.label': '前缀',
|
|
@@ -105,8 +105,8 @@
|
|
|
105
105
|
<span>{{ t('sessions.batch.select') }}</span>
|
|
106
106
|
</button>
|
|
107
107
|
<template v-else>
|
|
108
|
-
<button class="btn-tool btn-tool-compact" type="button" @click="toggleSelectAllVisibleForBatch" :disabled="sessionsLoading" :title="isAllVisibleBatchSelected() ? t('sessions.batch.unselectAll') : t('sessions.batch.
|
|
109
|
-
{{ isAllVisibleBatchSelected() ? t('sessions.batch.unselectAll') : t('sessions.batch.
|
|
108
|
+
<button class="btn-tool btn-tool-compact" type="button" @click="toggleSelectAllVisibleForBatch" :disabled="sessionsLoading" :title="isAllVisibleBatchSelected() ? t('sessions.batch.unselectAll') : t('sessions.batch.selectAllVisibleN', { count: getVisibleBatchSelectableCount() })">
|
|
109
|
+
{{ isAllVisibleBatchSelected() ? t('sessions.batch.unselectAll') : t('sessions.batch.selectAllVisibleN', { count: getVisibleBatchSelectableCount() }) }}
|
|
110
110
|
</button>
|
|
111
111
|
<button class="btn-tool btn-tool-compact btn-tool-batch-delete" type="button" @click="deleteSelectedSessions" :disabled="sessionsLoading || sessionDeletingSelected || getSelectedBatchCount() === 0" :title="getSelectedBatchCount() > 0 ? t('sessions.batch.deleteSelectedN', { count: getSelectedBatchCount() }) : t('sessions.batch.emptySelection')">
|
|
112
112
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M7 6V4a2 2 0 012-2h6a2 2 0 012 2v2M10 11v6M14 11v6"/><path d="M5 6l1 14a2 2 0 002 2h8a2 2 0 002-2l1-14"/></svg>
|
|
@@ -61,6 +61,30 @@
|
|
|
61
61
|
</div>
|
|
62
62
|
</section>
|
|
63
63
|
|
|
64
|
+
<section class="settings-card" :aria-label="t('settings.theme.title')">
|
|
65
|
+
<div class="settings-card-main">
|
|
66
|
+
<div class="settings-card-content">
|
|
67
|
+
<div class="settings-card-title">{{ t('settings.theme.title') }}</div>
|
|
68
|
+
<p class="settings-card-desc">{{ t('settings.theme.meta') }}</p>
|
|
69
|
+
<div class="settings-toggle-group">
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
:class="['toggle-btn', { active: theme === 'light' }]"
|
|
73
|
+
@click="setTheme('light')">
|
|
74
|
+
{{ t('settings.theme.light') }}
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
:class="['toggle-btn', { active: theme === 'dark' }]"
|
|
79
|
+
@click="setTheme('dark')">
|
|
80
|
+
{{ t('settings.theme.dark') }}
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
<p class="settings-card-hint">{{ t('settings.theme.hint') }}</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</section>
|
|
87
|
+
|
|
64
88
|
<section class="settings-card" :aria-label="t('settings.sharePrefix.title')">
|
|
65
89
|
<div class="settings-card-main">
|
|
66
90
|
<div class="settings-card-content">
|
|
@@ -4052,8 +4052,8 @@ return function render(_ctx, _cache) {
|
|
|
4052
4052
|
type: "button",
|
|
4053
4053
|
onClick: _ctx.toggleSelectAllVisibleForBatch,
|
|
4054
4054
|
disabled: _ctx.sessionsLoading,
|
|
4055
|
-
title: _ctx.isAllVisibleBatchSelected() ? _ctx.t('sessions.batch.unselectAll') : _ctx.t('sessions.batch.
|
|
4056
|
-
}, _toDisplayString(_ctx.isAllVisibleBatchSelected() ? _ctx.t('sessions.batch.unselectAll') : _ctx.t('sessions.batch.
|
|
4055
|
+
title: _ctx.isAllVisibleBatchSelected() ? _ctx.t('sessions.batch.unselectAll') : _ctx.t('sessions.batch.selectAllVisibleN', { count: _ctx.getVisibleBatchSelectableCount() })
|
|
4056
|
+
}, _toDisplayString(_ctx.isAllVisibleBatchSelected() ? _ctx.t('sessions.batch.unselectAll') : _ctx.t('sessions.batch.selectAllVisibleN', { count: _ctx.getVisibleBatchSelectableCount() })), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]),
|
|
4057
4057
|
_createElementVNode("button", {
|
|
4058
4058
|
class: "btn-tool btn-tool-compact btn-tool-batch-delete",
|
|
4059
4059
|
type: "button",
|
|
@@ -5535,6 +5535,30 @@ return function render(_ctx, _cache) {
|
|
|
5535
5535
|
])
|
|
5536
5536
|
])
|
|
5537
5537
|
], 8 /* PROPS */, ["aria-label"]),
|
|
5538
|
+
_createElementVNode("section", {
|
|
5539
|
+
class: "settings-card",
|
|
5540
|
+
"aria-label": _ctx.t('settings.theme.title')
|
|
5541
|
+
}, [
|
|
5542
|
+
_createElementVNode("div", { class: "settings-card-main" }, [
|
|
5543
|
+
_createElementVNode("div", { class: "settings-card-content" }, [
|
|
5544
|
+
_createElementVNode("div", { class: "settings-card-title" }, _toDisplayString(_ctx.t('settings.theme.title')), 1 /* TEXT */),
|
|
5545
|
+
_createElementVNode("p", { class: "settings-card-desc" }, _toDisplayString(_ctx.t('settings.theme.meta')), 1 /* TEXT */),
|
|
5546
|
+
_createElementVNode("div", { class: "settings-toggle-group" }, [
|
|
5547
|
+
_createElementVNode("button", {
|
|
5548
|
+
type: "button",
|
|
5549
|
+
class: _normalizeClass(['toggle-btn', { active: _ctx.theme === 'light' }]),
|
|
5550
|
+
onClick: $event => (_ctx.setTheme('light'))
|
|
5551
|
+
}, _toDisplayString(_ctx.t('settings.theme.light')), 11 /* TEXT, CLASS, PROPS */, ["onClick"]),
|
|
5552
|
+
_createElementVNode("button", {
|
|
5553
|
+
type: "button",
|
|
5554
|
+
class: _normalizeClass(['toggle-btn', { active: _ctx.theme === 'dark' }]),
|
|
5555
|
+
onClick: $event => (_ctx.setTheme('dark'))
|
|
5556
|
+
}, _toDisplayString(_ctx.t('settings.theme.dark')), 11 /* TEXT, CLASS, PROPS */, ["onClick"])
|
|
5557
|
+
]),
|
|
5558
|
+
_createElementVNode("p", { class: "settings-card-hint" }, _toDisplayString(_ctx.t('settings.theme.hint')), 1 /* TEXT */)
|
|
5559
|
+
])
|
|
5560
|
+
])
|
|
5561
|
+
], 8 /* PROPS */, ["aria-label"]),
|
|
5538
5562
|
_createElementVNode("section", {
|
|
5539
5563
|
class: "settings-card",
|
|
5540
5564
|
"aria-label": _ctx.t('settings.sharePrefix.title')
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
--color-surface-alt: #F7EFE8;
|
|
16
16
|
--color-surface-elevated: #FFFFFF;
|
|
17
17
|
--color-surface-tint: rgba(255, 253, 250, 0.86);
|
|
18
|
+
--color-surface-chip: rgba(255, 255, 255, 0.76);
|
|
18
19
|
--color-text-primary: #241F1C;
|
|
19
20
|
--color-text-secondary: #5A504A;
|
|
20
21
|
--color-text-tertiary: #82746A;
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
line-height: 1 !important;
|
|
158
158
|
border-radius: var(--radius-sm) !important;
|
|
159
159
|
border: 1px solid var(--color-border-soft) !important;
|
|
160
|
-
background:
|
|
160
|
+
background: var(--color-surface-chip) !important;
|
|
161
161
|
font-size: 12px !important;
|
|
162
162
|
font-weight: var(--font-weight-secondary) !important;
|
|
163
163
|
color: var(--color-text-secondary) !important;
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
left: 3px;
|
|
172
172
|
width: 20px;
|
|
173
173
|
height: 20px;
|
|
174
|
-
background:
|
|
174
|
+
background: #fff;
|
|
175
175
|
border-radius: 50%;
|
|
176
176
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
177
177
|
transition: all 0.25s cubic-bezier(0.25, 1, 0.5, 1);
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
flex: 1;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
.settings-toggle-group .toggle-btn:hover:not(:disabled) {
|
|
232
|
+
.settings-toggle-group .toggle-btn:hover:not(:disabled):not(.active) {
|
|
233
233
|
color: var(--color-text-secondary);
|
|
234
234
|
background: var(--color-surface);
|
|
235
235
|
}
|