codexmate 0.0.14 → 0.0.16

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/doc/CHANGELOG.md CHANGED
@@ -1,12 +1,17 @@
1
- # Changelog
2
-
3
- ## 0.0.14
4
-
5
- - Skills Manager: polish modal layout with overview counters and clearer section structure
6
- - Skills Manager: unify status select style and refine list scrollbar density
7
- - Docs: sync README / README.en release notes for 0.0.14
8
-
9
- ## 0.0.13
1
+ # Changelog
2
+
3
+ ## 0.0.15
4
+
5
+ - Release: bump package version to 0.0.15
6
+ - Docs: sync README / README.en with current release marker
7
+
8
+ ## 0.0.14
9
+
10
+ - Skills Manager: polish modal layout with overview counters and clearer section structure
11
+ - Skills Manager: unify status select style and refine list scrollbar density
12
+ - Docs: sync README / README.en release notes for 0.0.14
13
+
14
+ ## 0.0.13
10
15
 
11
16
  - Web UI: switch to IDE-style three-column layout with a fixed status inspector panel
12
17
  - AGENTS editor: add "Export" action to download current content as `agent-<timestamp>.txt`
@@ -1,5 +1,12 @@
1
1
  # 更新日志
2
2
 
3
+ ## 0.0.15
4
+
5
+ - 发版:版本提升至 0.0.15
6
+ - 文档:同步 README / README.en 当前版本标记
7
+
8
+
9
+
3
10
  ## 0.0.14
4
11
 
5
12
  - Skills 管理:打磨弹窗信息层级,新增统计概览与分区结构
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "codexmate",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Codex/Claude Code 配置与会话管理 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  "dependencies": {
38
38
  "@iarna/toml": "^2.2.5",
39
39
  "json5": "^2.2.3",
40
+ "yauzl": "^3.2.1",
40
41
  "zip-lib": "^1.2.1"
41
42
  },
42
43
  "engines": {
@@ -60,4 +61,3 @@
60
61
  "vitepress": "^1.6.4"
61
62
  }
62
63
  }
63
-
package/web-ui/app.js CHANGED
@@ -14,6 +14,13 @@
14
14
  buildSessionTimelineNodes,
15
15
  normalizeSessionMessageRole
16
16
  } from './logic.mjs';
17
+ import {
18
+ CONFIG_MODE_SET,
19
+ getProviderConfigModeMeta,
20
+ createConfigModeComputed
21
+ } from './modules/config-mode.computed.mjs';
22
+ import { createSkillsComputed } from './modules/skills.computed.mjs';
23
+ import { createSkillsMethods } from './modules/skills.methods.mjs';
17
24
 
18
25
  document.addEventListener('DOMContentLoaded', () => {
19
26
  if (typeof Vue === 'undefined') {
@@ -111,6 +118,8 @@
111
118
  skillsImportSelectedKeys: [],
112
119
  skillsScanningImports: false,
113
120
  skillsImporting: false,
121
+ skillsZipImporting: false,
122
+ skillsExporting: false,
114
123
  sessionsList: [],
115
124
  sessionsLoading: false,
116
125
  sessionFilterSource: 'all',
@@ -400,141 +409,10 @@
400
409
  installRegistryPreview() {
401
410
  return this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom);
402
411
  },
403
- filteredSkillsList() {
404
- const list = Array.isArray(this.skillsList) ? this.skillsList : [];
405
- const keyword = typeof this.skillsKeyword === 'string' ? this.skillsKeyword.trim().toLowerCase() : '';
406
- const status = typeof this.skillsStatusFilter === 'string' ? this.skillsStatusFilter : 'all';
407
- return list.filter((item) => {
408
- const safe = item && typeof item === 'object' ? item : {};
409
- const hasSkillFile = !!safe.hasSkillFile;
410
- if (status === 'with-skill-file' && !hasSkillFile) return false;
411
- if (status === 'missing-skill-file' && hasSkillFile) return false;
412
- if (!keyword) return true;
413
- const fields = [
414
- safe.name,
415
- safe.displayName,
416
- safe.description,
417
- safe.path
418
- ];
419
- return fields.some((value) => typeof value === 'string' && value.toLowerCase().includes(keyword));
420
- });
421
- },
422
- skillsSelectableNames() {
423
- const list = Array.isArray(this.filteredSkillsList) ? this.filteredSkillsList : [];
424
- return list
425
- .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
426
- .filter(Boolean);
427
- },
428
- skillsConfiguredCount() {
429
- const list = Array.isArray(this.skillsList) ? this.skillsList : [];
430
- return list.filter((item) => !!(item && item.hasSkillFile)).length;
431
- },
432
- skillsMissingSkillFileCount() {
433
- const list = Array.isArray(this.skillsList) ? this.skillsList : [];
434
- return list.filter((item) => !(item && item.hasSkillFile)).length;
435
- },
436
- skillsFilterDirty() {
437
- const keyword = typeof this.skillsKeyword === 'string' ? this.skillsKeyword.trim() : '';
438
- const status = typeof this.skillsStatusFilter === 'string' ? this.skillsStatusFilter : 'all';
439
- return keyword.length > 0 || status !== 'all';
440
- },
441
- skillsSelectedCount() {
442
- const selected = Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : [];
443
- return Array.from(new Set(selected.map((item) => String(item || '').trim()).filter(Boolean))).length;
444
- },
445
- skillsVisibleSelectedCount() {
446
- const selectable = this.skillsSelectableNames;
447
- const selectedSet = new Set(Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : []);
448
- return selectable.filter((name) => selectedSet.has(name)).length;
449
- },
450
- skillsAllSelected() {
451
- const selectable = this.skillsSelectableNames;
452
- if (!selectable.length) return false;
453
- const selectedSet = new Set(Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : []);
454
- return selectable.every((name) => selectedSet.has(name));
455
- },
456
- skillsImportSelectableKeys() {
457
- const list = Array.isArray(this.skillsImportList) ? this.skillsImportList : [];
458
- return list
459
- .map((item) => this.buildSkillImportKey(item))
460
- .filter(Boolean);
461
- },
462
- skillsImportSelectedCount() {
463
- const selectable = this.skillsImportSelectableKeys;
464
- const selectedSet = new Set(Array.isArray(this.skillsImportSelectedKeys) ? this.skillsImportSelectedKeys : []);
465
- return selectable.filter((key) => selectedSet.has(key)).length;
466
- },
467
- skillsImportAllSelected() {
468
- const selectable = this.skillsImportSelectableKeys;
469
- if (!selectable.length) return false;
470
- const selectedSet = new Set(Array.isArray(this.skillsImportSelectedKeys) ? this.skillsImportSelectedKeys : []);
471
- return selectable.every((key) => selectedSet.has(key));
472
- },
473
- skillsImportConfiguredCount() {
474
- const list = Array.isArray(this.skillsImportList) ? this.skillsImportList : [];
475
- return list.filter((item) => !!(item && item.hasSkillFile)).length;
476
- },
477
- skillsImportMissingSkillFileCount() {
478
- const list = Array.isArray(this.skillsImportList) ? this.skillsImportList : [];
479
- return list.filter((item) => !(item && item.hasSkillFile)).length;
480
- },
481
- inspectorMainTabLabel() {
482
- if (this.mainTab === 'config') return '配置中心';
483
- if (this.mainTab === 'sessions') return '会话浏览';
484
- if (this.mainTab === 'settings') return '设置';
485
- return '未知';
486
- },
487
- inspectorConfigModeLabel() {
488
- if (this.mainTab !== 'config') return '--';
489
- if (this.configMode === 'codex') return 'Codex';
490
- if (this.configMode === 'claude') return 'Claude Code';
491
- if (this.configMode === 'openclaw') return 'OpenClaw';
492
- return '未选择';
493
- },
494
- inspectorCurrentConfigLabel() {
495
- if (this.mainTab !== 'config') return '--';
496
- if (this.configMode === 'codex') {
497
- const provider = typeof this.currentProvider === 'string' ? this.currentProvider.trim() : '';
498
- return provider || '未选择';
499
- }
500
- if (this.configMode === 'claude') {
501
- const config = typeof this.currentClaudeConfig === 'string' ? this.currentClaudeConfig.trim() : '';
502
- return config || '未选择';
503
- }
504
- const openclaw = typeof this.currentOpenclawConfig === 'string' ? this.currentOpenclawConfig.trim() : '';
505
- return openclaw || '未选择';
506
- },
507
- inspectorCurrentModelLabel() {
508
- if (this.mainTab !== 'config') return '--';
509
- if (this.configMode === 'codex') {
510
- const model = typeof this.currentModel === 'string' ? this.currentModel.trim() : '';
511
- return model || '未选择';
512
- }
513
- if (this.configMode === 'claude') {
514
- const model = typeof this.currentClaudeModel === 'string' ? this.currentClaudeModel.trim() : '';
515
- return model || '未选择';
516
- }
517
- const model = this.openclawStructured && typeof this.openclawStructured.agentPrimary === 'string'
518
- ? this.openclawStructured.agentPrimary.trim()
519
- : '';
520
- return model || '按配置文件';
521
- },
522
- inspectorTemplateStatus() {
523
- if (this.mainTab !== 'config') return '--';
524
- if (this.configMode === 'codex') {
525
- if (this.configTemplateApplying || this.codexApplying) {
526
- return '模板应用中';
527
- }
528
- return '模板可编辑(手动确认应用)';
529
- }
530
- if (this.configMode === 'claude') {
531
- return '即时写入 Claude settings';
532
- }
533
- if (this.openclawApplying || this.openclawSaving) {
534
- return 'OpenClaw 保存/应用中';
535
- }
536
- return 'JSON5 可保存并应用';
537
- },
412
+ ...createSkillsComputed(),
413
+
414
+ ...createConfigModeComputed(),
415
+
538
416
  inspectorBusyStatus() {
539
417
  const tasks = [];
540
418
  if (this.loading) tasks.push('初始化');
@@ -542,7 +420,7 @@
542
420
  if (this.codexModelsLoading || this.claudeModelsLoading) tasks.push('模型加载');
543
421
  if (this.codexApplying || this.configTemplateApplying || this.openclawApplying) tasks.push('配置应用');
544
422
  if (this.agentsSaving) tasks.push('AGENTS 保存');
545
- if (this.skillsLoading || this.skillsDeleting || this.skillsScanningImports || this.skillsImporting) tasks.push('Skills 管理');
423
+ if (this.skillsLoading || this.skillsDeleting || this.skillsScanningImports || this.skillsImporting || this.skillsZipImporting || this.skillsExporting) tasks.push('Skills 管理');
546
424
  if (this.proxySaving || this.proxyApplying || this.proxyStarting || this.proxyStopping) tasks.push('代理更新');
547
425
  return tasks.length ? tasks.join(' / ') : '空闲';
548
426
  },
@@ -846,9 +724,12 @@
846
724
  },
847
725
 
848
726
  switchConfigMode(mode) {
727
+ const normalizedMode = typeof mode === 'string'
728
+ ? mode.trim().toLowerCase()
729
+ : '';
849
730
  this.mainTab = 'config';
850
- this.configMode = mode;
851
- if (mode === 'claude') {
731
+ this.configMode = CONFIG_MODE_SET.has(normalizedMode) ? normalizedMode : 'codex';
732
+ if (this.configMode === 'claude') {
852
733
  this.refreshClaudeModelContext();
853
734
  }
854
735
  },
@@ -1876,7 +1757,9 @@
1876
1757
  if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
1877
1758
  this.currentModel = this.models[0];
1878
1759
  }
1879
- await this.applyCodexConfigDirect({ silent: true });
1760
+ if (getProviderConfigModeMeta(this.configMode)) {
1761
+ await this.applyCodexConfigDirect({ silent: true });
1762
+ }
1880
1763
  },
1881
1764
 
1882
1765
  async onModelChange() {
@@ -2080,191 +1963,8 @@
2080
1963
  }
2081
1964
  },
2082
1965
 
2083
- async openSkillsManager() {
2084
- this.skillsSelectedNames = [];
2085
- this.skillsKeyword = '';
2086
- this.skillsStatusFilter = 'all';
2087
- this.skillsImportList = [];
2088
- this.skillsImportSelectedKeys = [];
2089
- this.showSkillsModal = true;
2090
- await this.refreshSkillsList({ silent: false });
2091
- },
2092
-
2093
- closeSkillsModal() {
2094
- this.showSkillsModal = false;
2095
- this.skillsSelectedNames = [];
2096
- this.skillsImportSelectedKeys = [];
2097
- },
2098
-
2099
- async refreshSkillsList(options = {}) {
2100
- this.skillsLoading = true;
2101
- try {
2102
- const res = await api('list-codex-skills');
2103
- if (res.error) {
2104
- this.showMessage(res.error, 'error');
2105
- return;
2106
- }
2107
- this.skillsRootPath = res.root || '';
2108
- this.skillsList = Array.isArray(res.items) ? res.items : [];
2109
- const currentNames = new Set((Array.isArray(this.skillsList) ? this.skillsList : [])
2110
- .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
2111
- .filter(Boolean));
2112
- this.skillsSelectedNames = (Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : [])
2113
- .filter((name) => currentNames.has(name));
2114
- if (!options.silent) {
2115
- const exists = res.exists !== false;
2116
- if (!exists) {
2117
- this.showMessage('skills 目录不存在,已按空列表显示', 'info');
2118
- }
2119
- }
2120
- } catch (e) {
2121
- this.showMessage('加载 skills 失败', 'error');
2122
- } finally {
2123
- this.skillsLoading = false;
2124
- }
2125
- },
2126
-
2127
- resetSkillsFilters() {
2128
- this.skillsKeyword = '';
2129
- this.skillsStatusFilter = 'all';
2130
- },
2131
-
2132
- toggleAllSkillsSelection() {
2133
- const selectable = this.skillsSelectableNames;
2134
- if (this.skillsAllSelected) {
2135
- const selectedSet = new Set(Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : []);
2136
- selectable.forEach((name) => selectedSet.delete(name));
2137
- this.skillsSelectedNames = Array.from(selectedSet);
2138
- return;
2139
- }
2140
- const selectedSet = new Set(Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : []);
2141
- selectable.forEach((name) => selectedSet.add(name));
2142
- this.skillsSelectedNames = Array.from(selectedSet);
2143
- },
2144
-
2145
- buildSkillImportKey(item) {
2146
- const safe = item && typeof item === 'object' ? item : {};
2147
- const sourceApp = typeof safe.sourceApp === 'string' ? safe.sourceApp.trim().toLowerCase() : '';
2148
- const name = typeof safe.name === 'string' ? safe.name.trim() : '';
2149
- if (!sourceApp || !name) return '';
2150
- return `${sourceApp}:${name}`;
2151
- },
2152
-
2153
- toggleAllSkillsImportSelection() {
2154
- const selectable = this.skillsImportSelectableKeys;
2155
- if (this.skillsImportAllSelected) {
2156
- this.skillsImportSelectedKeys = [];
2157
- return;
2158
- }
2159
- this.skillsImportSelectedKeys = [...selectable];
2160
- },
2161
-
2162
- async scanImportableSkills() {
2163
- if (this.skillsScanningImports || this.skillsImporting) return;
2164
- this.skillsScanningImports = true;
2165
- try {
2166
- const res = await api('scan-unmanaged-codex-skills');
2167
- if (res.error) {
2168
- this.showMessage(res.error, 'error');
2169
- return;
2170
- }
2171
- this.skillsImportList = Array.isArray(res.items) ? res.items : [];
2172
- const availableKeys = new Set(this.skillsImportSelectableKeys);
2173
- this.skillsImportSelectedKeys = (Array.isArray(this.skillsImportSelectedKeys) ? this.skillsImportSelectedKeys : [])
2174
- .filter((key) => availableKeys.has(key));
2175
- if (this.skillsImportList.length === 0) {
2176
- this.showMessage('未扫描到可导入 skill', 'info');
2177
- } else {
2178
- this.showMessage(`扫描到 ${this.skillsImportList.length} 个可导入 skill`, 'success');
2179
- }
2180
- } catch (e) {
2181
- this.showMessage('扫描可导入 skill 失败', 'error');
2182
- } finally {
2183
- this.skillsScanningImports = false;
2184
- }
2185
- },
2186
-
2187
- async importSelectedSkills() {
2188
- if (this.skillsImporting) return;
2189
- const selectedSet = new Set(Array.isArray(this.skillsImportSelectedKeys) ? this.skillsImportSelectedKeys : []);
2190
- const selectedItems = (Array.isArray(this.skillsImportList) ? this.skillsImportList : [])
2191
- .filter((item) => selectedSet.has(this.buildSkillImportKey(item)))
2192
- .map((item) => ({
2193
- name: item.name,
2194
- sourceApp: item.sourceApp
2195
- }));
2196
- if (!selectedItems.length) {
2197
- this.showMessage('请先选择要导入的 skill', 'error');
2198
- return;
2199
- }
2200
-
2201
- this.skillsImporting = true;
2202
- try {
2203
- const res = await api('import-codex-skills', { items: selectedItems });
2204
- if (res.error) {
2205
- this.showMessage(res.error, 'error');
2206
- return;
2207
- }
2208
- const importedCount = Array.isArray(res.imported) ? res.imported.length : 0;
2209
- const failedCount = Array.isArray(res.failed) ? res.failed.length : 0;
2210
- if (failedCount > 0 && importedCount > 0) {
2211
- this.showMessage(`已导入 ${importedCount} 个,失败 ${failedCount} 个`, 'error');
2212
- } else if (failedCount > 0) {
2213
- const first = res.failed[0] && res.failed[0].error ? res.failed[0].error : '导入失败';
2214
- this.showMessage(first, 'error');
2215
- } else {
2216
- this.showMessage(`已导入 ${importedCount} 个 skill`, 'success');
2217
- }
2218
- await this.refreshSkillsList({ silent: true });
2219
- } catch (e) {
2220
- this.showMessage('导入 skill 失败', 'error');
2221
- } finally {
2222
- this.skillsImporting = false;
2223
- await this.scanImportableSkills();
2224
- }
2225
- },
2226
-
2227
- async deleteSelectedSkills() {
2228
- if (this.skillsDeleting) return;
2229
- const selected = Array.isArray(this.skillsSelectedNames)
2230
- ? Array.from(new Set(this.skillsSelectedNames.map((item) => String(item || '').trim()).filter(Boolean)))
2231
- : [];
2232
- if (!selected.length) {
2233
- this.showMessage('请先选择要删除的 skill', 'error');
2234
- return;
2235
- }
2236
- const confirmed = window.confirm(`确认删除 ${selected.length} 个 skill 吗?此操作不可撤销。`);
2237
- if (!confirmed) {
2238
- return;
2239
- }
2240
-
2241
- this.skillsDeleting = true;
2242
- try {
2243
- const res = await api('delete-codex-skills', { names: selected });
2244
- if (res.error) {
2245
- this.showMessage(res.error, 'error');
2246
- return;
2247
- }
2248
-
2249
- const deletedCount = Array.isArray(res.deleted) ? res.deleted.length : 0;
2250
- const failedList = Array.isArray(res.failed) ? res.failed : [];
2251
- const failedCount = failedList.length;
2252
- if (failedCount > 0 && deletedCount > 0) {
2253
- this.showMessage(`已删除 ${deletedCount} 个,失败 ${failedCount} 个`, 'error');
2254
- } else if (failedCount > 0) {
2255
- const first = failedList[0] && failedList[0].error ? failedList[0].error : '删除失败';
2256
- this.showMessage(first, 'error');
2257
- } else {
2258
- this.showMessage(`已删除 ${deletedCount} 个 skill`, 'success');
2259
- }
2260
- await this.refreshSkillsList({ silent: true });
2261
- } catch (e) {
2262
- this.showMessage('删除 skill 失败', 'error');
2263
- } finally {
2264
- this.skillsDeleting = false;
2265
- }
2266
- },
2267
-
1966
+ ...createSkillsMethods({ api }),
1967
+
2268
1968
  async openOpenclawAgentsEditor() {
2269
1969
  this.setAgentsModalContext('openclaw');
2270
1970
  this.agentsLoading = true;
package/web-ui/index.html CHANGED
@@ -59,7 +59,7 @@
59
59
  :tabindex="mainTab === 'config' && configMode === 'codex' ? 0 : -1"
60
60
  :aria-selected="mainTab === 'config' && configMode === 'codex'"
61
61
  :aria-pressed="mainTab === 'config' && configMode === 'codex'"
62
- aria-controls="panel-config-codex"
62
+ aria-controls="panel-config-provider"
63
63
  :class="{ active: mainTab === 'config' && configMode === 'codex' }"
64
64
  @click="switchConfigMode('codex')">Codex 配置</button>
65
65
  <button class="top-tab"
@@ -137,7 +137,7 @@
137
137
  <button
138
138
  role="tab"
139
139
  id="side-tab-config-codex"
140
- aria-controls="panel-config-codex"
140
+ aria-controls="panel-config-provider"
141
141
  :tabindex="mainTab === 'config' && configMode === 'codex' ? 0 : -1"
142
142
  :aria-selected="mainTab === 'config' && configMode === 'codex'"
143
143
  :aria-pressed="mainTab === 'config' && configMode === 'codex'"
@@ -233,13 +233,13 @@
233
233
  </div>
234
234
 
235
235
  <div class="status-strip" v-if="!sessionStandalone && mainTab === 'config'">
236
- <template v-if="configMode === 'codex'">
236
+ <template v-if="isProviderConfigMode">
237
237
  <div class="status-chip">
238
- <span class="label">Codex 提供商</span>
238
+ <span class="label">{{ activeProviderConfigChipLabel }}</span>
239
239
  <span class="value">{{ currentProvider || '未选择' }}</span>
240
240
  </div>
241
241
  <div class="status-chip">
242
- <span class="label">Codex 模型</span>
242
+ <span class="label">{{ activeProviderModelChipLabel }}</span>
243
243
  <span class="value">{{ currentModel || '未选择' }}</span>
244
244
  </div>
245
245
  </template>
@@ -253,7 +253,7 @@
253
253
  <span class="value">{{ currentClaudeModel || '未选择' }}</span>
254
254
  </div>
255
255
  </template>
256
- <template v-else>
256
+ <template v-else-if="configMode === 'openclaw'">
257
257
  <div class="status-chip">
258
258
  <span class="label">OpenClaw 配置</span>
259
259
  <span class="value">{{ currentOpenclawConfig || '未选择' }}</span>
@@ -263,6 +263,12 @@
263
263
  <span class="value">{{ openclawWorkspaceFileName || '未选择' }}</span>
264
264
  </div>
265
265
  </template>
266
+ <template v-else>
267
+ <div class="status-chip">
268
+ <span class="label">配置模式</span>
269
+ <span class="value">未选择</span>
270
+ </div>
271
+ </template>
266
272
  </div>
267
273
  <div class="status-strip" v-else-if="!sessionStandalone && mainTab === 'sessions'">
268
274
  <div class="status-chip">
@@ -291,13 +297,13 @@
291
297
 
292
298
  <!-- 内容包裹器 - 稳定布局 -->
293
299
  <div class="content-wrapper">
294
- <!-- Codex 配置模式 -->
300
+ <!-- Provider 配置模式(Codex -->
295
301
  <div
296
- v-show="mainTab === 'config' && configMode === 'codex'"
302
+ v-show="mainTab === 'config' && isProviderConfigMode"
297
303
  class="mode-content mode-cards"
298
- id="panel-config-codex"
304
+ id="panel-config-provider"
299
305
  role="tabpanel"
300
- :aria-labelledby="'tab-config-codex'">
306
+ :aria-labelledby="'tab-config-' + configMode">
301
307
  <!-- 添加提供商按钮 -->
302
308
  <button class="btn-add" @click="showAddModal = true" v-if="!loading && !initError">
303
309
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
@@ -330,7 +336,7 @@
330
336
  class="model-input"
331
337
  v-model="currentModel"
332
338
  @blur="onModelChange"
333
- placeholder="例如: gpt-5.3-codex"
339
+ :placeholder="activeProviderModelPlaceholder"
334
340
  >
335
341
  <div class="config-template-hint" v-if="modelsSource === 'unlimited'">
336
342
  当前提供商未提供模型列表,视为不限。模型可手动输入。
@@ -339,16 +345,20 @@
339
345
  模型列表获取失败,请检查接口或手动输入。
340
346
  </div>
341
347
  <div class="config-template-hint" v-if="modelsSource === 'remote' && !modelsHasCurrent">
342
- 当前模型不在接口列表中,请手动输入或在模板中调整。
348
+ {{ isCodexConfigMode ? '当前模型不在接口列表中,请手动输入或在模板中调整。' : '当前模型不在接口列表中,请手动输入。' }}
343
349
  </div>
344
- <div class="config-template-hint">
350
+ <div class="config-template-hint" v-if="isCodexConfigMode">
345
351
  Codex 配置需先改模板,再手动应用。
346
352
  </div>
347
- <button class="btn-tool btn-template-editor" @click="openConfigTemplateEditor" :disabled="loading || !!initError">
353
+ <div class="config-template-hint" v-else-if="activeProviderBridgeHint">
354
+ {{ activeProviderBridgeHint }} 模板、认证和代理仅在 Codex 模式下可编辑。
355
+ </div>
356
+ <button class="btn-tool btn-template-editor" v-if="isCodexConfigMode" @click="openConfigTemplateEditor" :disabled="loading || !!initError">
348
357
  打开 Config 模板编辑器
349
358
  </button>
350
359
  </div>
351
360
 
361
+ <template v-if="isCodexConfigMode">
352
362
  <div class="selector-section">
353
363
  <div class="selector-header">
354
364
  <span class="selector-title">服务档位</span>
@@ -390,8 +400,8 @@
390
400
  <div class="selector-header">
391
401
  <span class="selector-title">Skills 管理</span>
392
402
  </div>
393
- <div class="config-template-hint skills-hint-line">管理 <code>~/.codex/skills</code> 自定义 skills,弹窗提供统计概览、筛选检索、多选删除与跨应用导入。</div>
394
- <button class="btn-tool" @click="openSkillsManager" :disabled="loading || !!initError || skillsLoading || skillsDeleting || skillsScanningImports || skillsImporting">
403
+ <div class="config-template-hint skills-hint-line">管理 <code>~/.codex/skills</code> 自定义 skills,弹窗提供统计概览、筛选检索、多选删除、ZIP 导入与导出。</div>
404
+ <button class="btn-tool" @click="openSkillsManager" :disabled="loading || !!initError || skillsLoading || skillsDeleting || skillsScanningImports || skillsImporting || skillsZipImporting || skillsExporting">
395
405
  {{ skillsLoading ? '加载中...' : '打开 Skills 管理' }}
396
406
  </button>
397
407
  </div>
@@ -502,6 +512,7 @@
502
512
  <span v-if="proxyRuntime.upstreamProvider">(上游 <code>{{ proxyRuntime.upstreamProvider }}</code>)</span>
503
513
  </div>
504
514
  </div>
515
+ </template>
505
516
 
506
517
  <div class="selector-section">
507
518
  <div class="selector-header">
@@ -1672,10 +1683,10 @@
1672
1683
  <div class="modal-header skills-modal-header">
1673
1684
  <div>
1674
1685
  <div class="modal-title">Skills 管理</div>
1675
- <div class="skills-modal-subtitle">集中管理本地技能目录,支持检索筛选、多选删除与跨应用导入。</div>
1686
+ <div class="skills-modal-subtitle">集中管理本地技能目录,支持检索筛选、多选删除、跨应用导入、ZIP 导入与导出。</div>
1676
1687
  </div>
1677
1688
  <div class="modal-header-actions skills-modal-actions">
1678
- <button class="btn-mini" @click="refreshSkillsList({ silent: false })" :disabled="skillsLoading || skillsDeleting || skillsScanningImports || skillsImporting">
1689
+ <button class="btn-mini" @click="refreshSkillsList({ silent: false })" :disabled="skillsLoading || skillsDeleting || skillsScanningImports || skillsImporting || skillsZipImporting || skillsExporting">
1679
1690
  {{ skillsLoading ? '刷新中...' : '刷新' }}
1680
1691
  </button>
1681
1692
  </div>
@@ -1771,7 +1782,7 @@
1771
1782
  <div class="skills-import-title">跨应用导入(对齐 cc-switch 能力)</div>
1772
1783
  <div class="skills-panel-note">从其他应用扫描并导入未托管技能,支持多选批量导入。</div>
1773
1784
  </div>
1774
- <button class="btn-mini" @click="scanImportableSkills" :disabled="skillsLoading || skillsDeleting || skillsScanningImports || skillsImporting">
1785
+ <button class="btn-mini" @click="scanImportableSkills" :disabled="skillsLoading || skillsDeleting || skillsScanningImports || skillsImporting || skillsZipImporting || skillsExporting">
1775
1786
  {{ skillsScanningImports ? '扫描中...' : '扫描可导入' }}
1776
1787
  </button>
1777
1788
  </div>
@@ -1806,14 +1817,26 @@
1806
1817
  </div>
1807
1818
 
1808
1819
  <div class="btn-group">
1809
- <button class="btn btn-cancel" @click="closeSkillsModal" :disabled="skillsDeleting || skillsImporting || skillsScanningImports">关闭</button>
1810
- <button class="btn btn-confirm" @click="importSelectedSkills" :disabled="skillsImporting || skillsImportSelectedCount === 0">
1820
+ <button class="btn btn-cancel" @click="closeSkillsModal" :disabled="skillsLoading || skillsDeleting || skillsImporting || skillsScanningImports || skillsZipImporting || skillsExporting">关闭</button>
1821
+ <button class="btn btn-cancel" @click="triggerSkillsZipImport" :disabled="skillsZipImporting || skillsDeleting || skillsImporting || skillsScanningImports || skillsExporting">
1822
+ {{ skillsZipImporting ? 'ZIP 导入中...' : '导入 ZIP' }}
1823
+ </button>
1824
+ <button class="btn btn-confirm" @click="exportSelectedSkills" :disabled="skillsExporting || skillsSelectedCount === 0 || skillsDeleting || skillsImporting || skillsScanningImports || skillsZipImporting">
1825
+ {{ skillsExporting ? '导出中...' : '导出选中' }}
1826
+ </button>
1827
+ <button class="btn btn-confirm" @click="importSelectedSkills" :disabled="skillsImporting || skillsImportSelectedCount === 0 || skillsZipImporting || skillsExporting || skillsDeleting">
1811
1828
  {{ skillsImporting ? '导入中...' : '导入选中' }}
1812
1829
  </button>
1813
- <button class="btn btn-confirm btn-danger" @click="deleteSelectedSkills" :disabled="skillsDeleting || skillsSelectedCount === 0">
1830
+ <button class="btn btn-confirm btn-danger" @click="deleteSelectedSkills" :disabled="skillsDeleting || skillsSelectedCount === 0 || skillsImporting || skillsZipImporting || skillsExporting">
1814
1831
  {{ skillsDeleting ? '删除中...' : '删除选中' }}
1815
1832
  </button>
1816
1833
  </div>
1834
+ <input
1835
+ ref="skillsZipImportInput"
1836
+ type="file"
1837
+ accept=".zip,application/zip"
1838
+ style="display:none"
1839
+ @change="handleSkillsZipImportChange">
1817
1840
  </div>
1818
1841
  </div>
1819
1842