@yesdgq/claude-buddy 1.1.3000 → 1.1.3011

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/assets/config.js CHANGED
@@ -1,9 +1,349 @@
1
1
  const { createApp } = Vue;
2
2
 
3
- createApp({
3
+ const ENV_NODE_TYPES = ['String', 'Boolean', 'Number', 'Array', 'Dictionary'];
4
+ const CONFIG_FILE_ITEMS = Object.freeze({
5
+ Claude: [{ key: 'claude-settings', path: '~/.claude/settings.json' }],
6
+ Codex: [{ key: 'codex-config', path: '~/.codex/config.toml' }],
7
+ WorkBuddy: [{ key: 'workbuddy-models', path: '~/.workbuddy/models.json' }]
8
+ });
9
+ const CONFIG_FILE_REFRESH_STORAGE_KEY = 'ccby-config-file-refresh';
10
+
11
+ function getConfigFileItems(cliName) {
12
+ return CONFIG_FILE_ITEMS[cliName] || [];
13
+ }
14
+
15
+ let envNodeSequence = 0;
16
+
17
+ function createEnvNodeId() {
18
+ envNodeSequence += 1;
19
+ return `env-node-${Date.now()}-${envNodeSequence}`;
20
+ }
21
+
22
+ function inferEnvNodeType(value) {
23
+ if (Array.isArray(value)) return 'Array';
24
+ if (value !== null && typeof value === 'object') return 'Dictionary';
25
+ if (typeof value === 'boolean') return 'Boolean';
26
+ if (typeof value === 'number') return 'Number';
27
+ return 'String';
28
+ }
29
+
30
+ function createEnvNode(type = 'String', key = '') {
31
+ const normalizedType = ENV_NODE_TYPES.includes(type) ? type : 'String';
32
+ return {
33
+ id: createEnvNodeId(),
34
+ key,
35
+ type: normalizedType,
36
+ valueKind: null,
37
+ value: normalizedType === 'Boolean' ? false : (normalizedType === 'Number' ? 0 : ''),
38
+ children: [],
39
+ // Expansion is presentation state only; serialization omits it.
40
+ expanded: false
41
+ };
42
+ }
43
+
44
+ function normalizeEnvNode(node, fallbackKey = '') {
45
+ const source = node && typeof node === 'object' && !Array.isArray(node)
46
+ ? node
47
+ : { value: node };
48
+ const rawValue = Object.prototype.hasOwnProperty.call(source, 'value') ? source.value : '';
49
+ const inferredType = inferEnvNodeType(rawValue);
50
+ const normalizedType = ENV_NODE_TYPES.includes(source.type) ? source.type : inferredType;
51
+ const normalized = createEnvNode(normalizedType, typeof source.key === 'string' ? source.key : fallbackKey);
52
+ normalized.valueKind = source.valueKind || (
53
+ rawValue === null ? 'null' : (typeof rawValue === 'number' ? 'number' : null)
54
+ );
55
+ // Loaded nodes start collapsed unless an explicit in-memory expanded state is provided.
56
+ normalized.expanded = source.expanded === true;
57
+
58
+ if (normalizedType === 'String') {
59
+ normalized.value = rawValue === undefined || rawValue === null ? '' : String(rawValue);
60
+ return normalized;
61
+ }
62
+
63
+ if (normalizedType === 'Boolean') {
64
+ normalized.value = rawValue === true || rawValue === 'true' || rawValue === 'YES' || rawValue === 'yes';
65
+ return normalized;
66
+ }
67
+
68
+ if (normalizedType === 'Number') {
69
+ const numericValue = Number(rawValue);
70
+ normalized.value = rawValue === '' || rawValue === null || rawValue === undefined
71
+ ? 0
72
+ : (Number.isFinite(numericValue) ? numericValue : 0);
73
+ return normalized;
74
+ }
75
+
76
+ let childSources = Array.isArray(source.children) ? source.children : null;
77
+ if (!childSources && normalizedType === 'Array' && Array.isArray(rawValue)) {
78
+ childSources = rawValue.map(value => ({ value }));
79
+ }
80
+ if (!childSources && normalizedType === 'Dictionary' && rawValue && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
81
+ childSources = Object.entries(rawValue).map(([key, value]) => ({ key, value }));
82
+ }
83
+ normalized.value = '';
84
+ normalized.children = (childSources || []).map((child, index) =>
85
+ normalizeEnvNode(child, normalizedType === 'Array' ? `Item ${index}` : '')
86
+ );
87
+ return normalized;
88
+ }
89
+
90
+ function serializeEnvNode(node) {
91
+ const normalizedType = ENV_NODE_TYPES.includes(node && node.type) ? node.type : 'String';
92
+
93
+ if (normalizedType === 'String') {
94
+ if (node && node.valueKind === 'null' && (node.value === undefined || node.value === null || node.value === '')) {
95
+ return null;
96
+ }
97
+ if (node && node.valueKind === 'number') {
98
+ const numericValue = Number(node.value);
99
+ if (node.value !== undefined && node.value !== null && node.value !== '' && Number.isFinite(numericValue)) {
100
+ return numericValue;
101
+ }
102
+ }
103
+ return node && node.value !== undefined && node.value !== null
104
+ ? String(node.value)
105
+ : '';
106
+ }
107
+ if (normalizedType === 'Boolean') {
108
+ return node && (node.value === true || node.value === 'true' || node.value === 'YES' || node.value === 'yes');
109
+ }
110
+ if (normalizedType === 'Number') {
111
+ const numericValue = Number(node && node.value);
112
+ return Number.isFinite(numericValue) ? numericValue : 0;
113
+ }
114
+ if (normalizedType === 'Array') {
115
+ return Array.isArray(node && node.children)
116
+ ? node.children.map(child => serializeEnvNode(child))
117
+ : [];
118
+ }
119
+
120
+ return Array.isArray(node && node.children)
121
+ ? node.children.reduce((result, child) => {
122
+ const key = child && typeof child.key === 'string' ? child.key.trim() : '';
123
+ if (key) {
124
+ result[key] = serializeEnvNode(child);
125
+ }
126
+ return result;
127
+ }, {})
128
+ : {};
129
+ }
130
+
131
+ function normalizeEnvNodes(env) {
132
+ if (Array.isArray(env)) {
133
+ // Compatibility with the previous cfg.json node-array format.
134
+ return env.map(node => normalizeEnvNode(node));
135
+ }
136
+ if (env && typeof env === 'object') {
137
+ return Object.entries(env).map(([key, value]) => normalizeEnvNode({ key, value }));
138
+ }
139
+ return [];
140
+ }
141
+
142
+ function serializeEnvVariables(nodes) {
143
+ if (!Array.isArray(nodes)) {
144
+ return {};
145
+ }
146
+
147
+ return nodes.reduce((result, node) => {
148
+ const key = node && typeof node.key === 'string' ? node.key.trim() : '';
149
+ if (key) {
150
+ result[key] = serializeEnvNode(node);
151
+ }
152
+ return result;
153
+ }, {});
154
+ }
155
+
156
+ const EnvNodeEditor = {
157
+ name: 'EnvNodeEditor',
158
+ props: {
159
+ node: { type: Object, required: true },
160
+ index: { type: Number, required: true },
161
+ depth: { type: Number, required: true },
162
+ labels: { type: Object, required: true },
163
+ canDelete: { type: Boolean, default: true },
164
+ parentType: { type: String, default: 'Dictionary' },
165
+ confirmDelete: { type: Function, default: null }
166
+ },
167
+ data() {
168
+ return {
169
+ typeOptions: ENV_NODE_TYPES
170
+ };
171
+ },
172
+ emits: ['delete-node'],
173
+ computed: {
174
+ isContainer() {
175
+ return this.node.type === 'Array' || this.node.type === 'Dictionary';
176
+ },
177
+ isArrayItem() {
178
+ return this.parentType === 'Array';
179
+ },
180
+ itemLabel() {
181
+ return `${this.labels.arrayItem || 'Item'} ${this.index}`;
182
+ },
183
+ childParentType() {
184
+ return this.node.type;
185
+ },
186
+ isExpanded() {
187
+ return this.node.expanded !== false;
188
+ },
189
+ disclosureLabel() {
190
+ return this.isExpanded
191
+ ? (this.labels.collapse || 'Collapse')
192
+ : (this.labels.expand || 'Expand');
193
+ }
194
+ },
195
+ methods: {
196
+ toggleExpanded() {
197
+ this.node.expanded = !this.isExpanded;
198
+ },
199
+ changeType(event) {
200
+ const nextType = event.target.value;
201
+ this.node.type = nextType;
202
+ this.node.valueKind = null;
203
+ if (nextType === 'Boolean') {
204
+ this.node.value = false;
205
+ this.node.children = [];
206
+ } else if (nextType === 'Number') {
207
+ this.node.value = 0;
208
+ this.node.children = [];
209
+ } else if (nextType === 'String') {
210
+ this.node.value = '';
211
+ this.node.children = [];
212
+ } else {
213
+ this.node.value = '';
214
+ this.node.children = [];
215
+ }
216
+ // A newly selected container starts open so its add-child action is immediately visible.
217
+ if (nextType === 'Array' || nextType === 'Dictionary') {
218
+ this.node.expanded = true;
219
+ }
220
+ },
221
+ addChild() {
222
+ const childKey = this.node.type === 'Array' ? `Item ${this.node.children.length}` : '';
223
+ this.node.expanded = true;
224
+ this.node.children.push(createEnvNode('String', childKey));
225
+ },
226
+ requestDelete() {
227
+ const removeNode = () => this.$emit('delete-node', []);
228
+ if (this.confirmDelete) {
229
+ this.confirmDelete(this.node, removeNode);
230
+ return;
231
+ }
232
+ removeNode();
233
+ },
234
+ handleChildDelete(childIndex, childPath) {
235
+ const normalizedPath = Array.isArray(childPath) ? childPath : [];
236
+ this.$emit('delete-node', [childIndex, ...normalizedPath]);
237
+ },
238
+ },
239
+ template: `
240
+ <div class="env-node" :class="{ 'env-node-container': isContainer }">
241
+ <div class="env-item env-node-row" :style="{ '--env-depth': depth }">
242
+ <button
243
+ v-if="isContainer"
244
+ type="button"
245
+ class="env-node-action env-node-disclosure"
246
+ :class="{ 'is-expanded': isExpanded }"
247
+ :title="disclosureLabel"
248
+ :aria-label="disclosureLabel"
249
+ :aria-expanded="isExpanded"
250
+ @click.stop="toggleExpanded"
251
+ >
252
+ <i aria-hidden="true" :class="isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'"></i>
253
+ </button>
254
+ <span v-else class="env-node-disclosure-spacer" aria-hidden="true"></span>
255
+ <span v-if="isArrayItem" class="env-array-key">{{ itemLabel }}</span>
256
+ <input
257
+ v-else
258
+ v-model="node.key"
259
+ class="env-key-input"
260
+ type="text"
261
+ :placeholder="labels.variableNamePlaceholder"
262
+ >
263
+ <select
264
+ :value="node.type"
265
+ class="env-type-select"
266
+ :aria-label="labels.valueType"
267
+ @change="changeType"
268
+ >
269
+ <option v-for="type in typeOptions" :key="type" :value="type">{{ type }}</option>
270
+ </select>
271
+ <input
272
+ v-if="node.type === 'String'"
273
+ :class="{ 'env-value-input': true, 'env-input-with-key': !isArrayItem }"
274
+ v-model="node.value"
275
+ type="text"
276
+ :placeholder="labels.variableValuePlaceholder"
277
+ >
278
+ <input
279
+ v-else-if="node.type === 'Number'"
280
+ :class="{ 'env-value-input': true, 'env-input-with-key': !isArrayItem }"
281
+ v-model.number="node.value"
282
+ type="number"
283
+ step="any"
284
+ :placeholder="labels.variableValuePlaceholder"
285
+ >
286
+ <select
287
+ v-else-if="node.type === 'Boolean'"
288
+ v-model="node.value"
289
+ :class="{ 'env-value-input': true, 'env-input-with-key': !isArrayItem, 'env-boolean-select': true }"
290
+ :aria-label="labels.variableValuePlaceholder"
291
+ >
292
+ <option :value="true">YES</option>
293
+ <option :value="false">NO</option>
294
+ </select>
295
+ <span v-else class="env-container-summary">
296
+ {{ node.children.length }} {{ node.children.length === 1 ? labels.item : labels.items }}
297
+ </span>
298
+ <button
299
+ v-if="isContainer"
300
+ type="button"
301
+ class="env-node-action env-node-add"
302
+ :title="labels.addChildItem"
303
+ @click="addChild"
304
+ >
305
+ <i class="fa fa-plus"></i>
306
+ </button>
307
+ <span v-else class="env-node-add-spacer" aria-hidden="true"></span>
308
+ <button
309
+ v-if="canDelete"
310
+ type="button"
311
+ class="env-node-action env-node-delete"
312
+ :title="labels.deleteThisEnvVar"
313
+ @click="requestDelete"
314
+ >
315
+ <i class="fa fa-times"></i>
316
+ </button>
317
+ </div>
318
+ <div
319
+ v-if="isContainer && isExpanded && node.children.length"
320
+ class="env-node-children"
321
+ :style="{ '--env-guide-depth': depth }"
322
+ >
323
+ <env-node-editor
324
+ v-for="(child, childIndex) in node.children"
325
+ :key="child.id"
326
+ :node="child"
327
+ :index="childIndex"
328
+ :depth="depth + 1"
329
+ :labels="labels"
330
+ :parent-type="childParentType"
331
+ :confirm-delete="confirmDelete"
332
+ @delete-node="handleChildDelete(childIndex, $event)"
333
+ ></env-node-editor>
334
+ </div>
335
+ </div>
336
+ `
337
+ };
338
+
339
+ const app = createApp({
4
340
  data() {
5
341
  return {
6
342
  config: { AUTH: [], activeIndex: 0 },
343
+ pendingDeletedItems: {
344
+ modelKeys: [],
345
+ envKeys: []
346
+ },
7
347
  loading: true,
8
348
  saving: false,
9
349
  error: null,
@@ -71,6 +411,8 @@ createApp({
71
411
  translations: {
72
412
  en: {
73
413
  helpTitle: 'User Guide',
414
+ configFileLabel: 'Config file',
415
+ openConfigFile: 'Open file editor',
74
416
  helpDescription: 'Claude Buddy helps you quickly configure API keys, models, and environment variables for Claude Code and other CLI tools through a visual interface.',
75
417
  feature1Title: 'API Key Management',
76
418
  feature1Desc: 'Store and switch between multiple API keys easily',
@@ -101,7 +443,7 @@ createApp({
101
443
  modelManagement: 'Model Management',
102
444
  defaultModelTitle: 'Default Model',
103
445
  defaultModelPlaceholder: 'Enter default GPT model',
104
- workbuddyModelSectionTitle: 'Model',
446
+ workbuddyModelSectionTitle: 'Model Management',
105
447
  workbuddyModelName: 'Model Name',
106
448
  workbuddyModelPlaceholder: 'Enter model parameter value, e.g. gpt-4o',
107
449
  collapse: 'Collapse',
@@ -109,11 +451,20 @@ createApp({
109
451
  modelNamePlaceholder: 'Model name',
110
452
  modelValuePlaceholder: 'Model value',
111
453
  deleteThisModel: 'Delete this model',
454
+ confirmDeleteModel: 'Delete model "{name}"? This action cannot be undone.',
455
+ modelDeleted: 'Model deleted',
112
456
  addModel: 'Add Model',
113
457
  envVariables: 'Environment Variables',
114
458
  variableNamePlaceholder: 'Variable name',
115
459
  variableValuePlaceholder: 'Variable value',
460
+ valueType: 'Value type',
461
+ addChildItem: 'Add child item',
462
+ arrayItem: 'Item',
463
+ item: 'item',
464
+ items: 'items',
116
465
  deleteThisEnvVar: 'Delete this environment variable',
466
+ confirmDeleteEnvVariable: 'Delete environment variable "{name}"? This action cannot be undone.',
467
+ envVariableDeleted: 'Environment variable deleted',
117
468
  addEnvVariable: 'Add Environment Variable',
118
469
  addNewConfig: 'Add New Configuration',
119
470
  saveConfig: 'Save Configuration',
@@ -145,6 +496,8 @@ createApp({
145
496
  },
146
497
  zh: {
147
498
  helpTitle: '使用说明',
499
+ configFileLabel: '配置文件',
500
+ openConfigFile: '打开文件编辑器',
148
501
  helpDescription: 'Claude Buddy 是一款简单易用的 CLI 工具,通过可视化界面帮助您快速配置 Claude Code 等 CLI 工具的 API 密钥、模型和环境变量。',
149
502
  feature1Title: 'API 密钥管理',
150
503
  feature1Desc: '轻松存储和切换多组 API 密钥',
@@ -175,7 +528,7 @@ createApp({
175
528
  modelManagement: '模型管理',
176
529
  defaultModelTitle: '默认模型',
177
530
  defaultModelPlaceholder: '输入默认gpt模型',
178
- workbuddyModelSectionTitle: '模型',
531
+ workbuddyModelSectionTitle: '模型管理',
179
532
  workbuddyModelName: '模型名称',
180
533
  workbuddyModelPlaceholder: '输入模型参数值,例如gpt-4o',
181
534
  collapse: '收起',
@@ -183,11 +536,20 @@ createApp({
183
536
  modelNamePlaceholder: '模型名称',
184
537
  modelValuePlaceholder: '模型值',
185
538
  deleteThisModel: '删除此模型',
539
+ confirmDeleteModel: '确定要删除模型“{name}”吗?此操作无法撤销。',
540
+ modelDeleted: '模型已删除',
186
541
  addModel: '添加模型',
187
542
  envVariables: '环境变量',
188
543
  variableNamePlaceholder: '变量名',
189
544
  variableValuePlaceholder: '变量值',
545
+ valueType: '值类型',
546
+ addChildItem: '添加下级项目',
547
+ arrayItem: 'Item',
548
+ item: '项',
549
+ items: '项',
190
550
  deleteThisEnvVar: '删除此环境变量',
551
+ confirmDeleteEnvVariable: '确定要删除环境变量“{name}”吗?此操作无法撤销。',
552
+ envVariableDeleted: '环境变量已删除',
191
553
  addEnvVariable: '添加环境变量',
192
554
  addNewConfig: '添加新配置',
193
555
  saveConfig: '保存配置',
@@ -224,6 +586,9 @@ createApp({
224
586
  t() {
225
587
  return this.translations[this.currentLang];
226
588
  },
589
+ currentConfigFileItems() {
590
+ return getConfigFileItems(this.currentCli);
591
+ },
227
592
  funLoadingMessage() {
228
593
  const messages = {
229
594
  en: [
@@ -369,6 +734,17 @@ createApp({
369
734
  }
370
735
  },
371
736
 
737
+ openConfigFile(fileItem) {
738
+ const editorLink = document.createElement('a');
739
+ editorLink.href = `/editor.html?fileKey=${encodeURIComponent(fileItem.key)}`;
740
+ editorLink.target = '_blank';
741
+ editorLink.rel = 'noopener noreferrer';
742
+ document.body.appendChild(editorLink);
743
+ editorLink.click();
744
+ editorLink.remove();
745
+ this.resetTimer();
746
+ },
747
+
372
748
  // 动态调整输入框宽度
373
749
  adjustInputWidth(event, index) {
374
750
  const input = event.target;
@@ -492,6 +868,7 @@ createApp({
492
868
  if (!auth.ENV) {
493
869
  auth.ENV = [];
494
870
  }
871
+ auth.ENV = normalizeEnvNodes(auth.ENV);
495
872
  if (!auth.MODELS) {
496
873
  auth.MODELS = [];
497
874
  }
@@ -519,6 +896,7 @@ createApp({
519
896
 
520
897
  // 最后更新 config
521
898
  this.config = config;
899
+ this.pendingDeletedItems = { modelKeys: [], envKeys: [] };
522
900
 
523
901
  this.showToast(this.t.configLoadSuccess);
524
902
  // 重置服务器倒计时
@@ -542,15 +920,15 @@ createApp({
542
920
  try {
543
921
  this.normalizeCodexDefaults();
544
922
  // 创建配置副本,过滤掉空配置项
545
- const configToSave = {
546
- ...this.config,
547
- AUTH: this.config.AUTH.filter(auth => !this.isEmptyConfig(auth))
548
- };
923
+ const saveAuthList = this.config.AUTH.filter(auth => !this.isEmptyConfig(auth));
924
+ const configToSave = this.prepareConfigForSave(
925
+ saveAuthList
926
+ );
549
927
 
550
928
  // WorkBuddy 过滤空卡片后修正索引,确保同步到 models.json 的仍是当前卡片。
551
929
  if (this.currentCli === 'WorkBuddy') {
552
930
  const activeAuth = this.config.AUTH[this.config.activeIndex];
553
- const nextActiveIndex = activeAuth ? configToSave.AUTH.indexOf(activeAuth) : -1;
931
+ const nextActiveIndex = activeAuth ? saveAuthList.indexOf(activeAuth) : -1;
554
932
  configToSave.activeIndex = nextActiveIndex >= 0 ? nextActiveIndex : 0;
555
933
  }
556
934
 
@@ -560,11 +938,14 @@ createApp({
560
938
  return;
561
939
  }
562
940
 
941
+ const deletedItems = this.getPendingDeletedItems();
563
942
  const response = await axios.post('/api/config', {
564
943
  config: configToSave,
565
- cliType: this.currentCli.toLowerCase()
944
+ cliType: this.currentCli.toLowerCase(),
945
+ deletedItems
566
946
  });
567
- this.applyWorkBuddySaveResponse(response, configToSave.AUTH);
947
+ this.applyWorkBuddySaveResponse(response, this.currentCli === 'WorkBuddy' ? saveAuthList : configToSave.AUTH);
948
+ this.clearPendingDeletedItems(deletedItems);
568
949
  this.showToast(this.t.configSaveSuccess);
569
950
  // 彩带庆祝
570
951
  this.showConfetti();
@@ -610,6 +991,65 @@ createApp({
610
991
  });
611
992
  },
612
993
 
994
+ prepareConfigForSave(authList) {
995
+ const sourceAuth = Array.isArray(authList) ? authList : this.config.AUTH;
996
+ return {
997
+ ...this.config,
998
+ AUTH: sourceAuth.map(auth => ({
999
+ ...auth,
1000
+ ENV: serializeEnvVariables(auth.ENV)
1001
+ }))
1002
+ };
1003
+ },
1004
+
1005
+ getPendingDeletedItems() {
1006
+ return {
1007
+ modelKeys: Array.from(new Set(
1008
+ (this.pendingDeletedItems?.modelKeys || [])
1009
+ .filter(key => typeof key === 'string' && key.trim())
1010
+ .map(key => key.trim())
1011
+ )),
1012
+ envKeys: Array.from(new Set(
1013
+ (this.pendingDeletedItems?.envKeys || [])
1014
+ .filter(key => typeof key === 'string' && key.trim())
1015
+ .map(key => key.trim())
1016
+ ))
1017
+ };
1018
+ },
1019
+
1020
+ recordDeletedItem(type, key) {
1021
+ const normalizedKey = typeof key === 'string' ? key.trim() : '';
1022
+ if (!normalizedKey || !['modelKeys', 'envKeys'].includes(type)) {
1023
+ return false;
1024
+ }
1025
+ if (!Array.isArray(this.pendingDeletedItems[type])) {
1026
+ this.pendingDeletedItems[type] = [];
1027
+ }
1028
+ if (this.pendingDeletedItems[type].includes(normalizedKey)) {
1029
+ return false;
1030
+ }
1031
+ this.pendingDeletedItems[type].push(normalizedKey);
1032
+ return true;
1033
+ },
1034
+
1035
+ removePendingDeletedItem(type, key) {
1036
+ if (!Array.isArray(this.pendingDeletedItems?.[type])) {
1037
+ return;
1038
+ }
1039
+ const normalizedKey = typeof key === 'string' ? key.trim() : '';
1040
+ this.pendingDeletedItems[type] = this.pendingDeletedItems[type].filter(item => item !== normalizedKey);
1041
+ },
1042
+
1043
+ clearPendingDeletedItems(deletedItems = {}) {
1044
+ ['modelKeys', 'envKeys'].forEach(type => {
1045
+ const sentKeys = new Set(Array.isArray(deletedItems[type]) ? deletedItems[type] : []);
1046
+ if (sentKeys.size > 0) {
1047
+ this.pendingDeletedItems[type] = (this.pendingDeletedItems[type] || [])
1048
+ .filter(key => !sentKeys.has(key));
1049
+ }
1050
+ });
1051
+ },
1052
+
613
1053
  addAuth() {
614
1054
  const auth = {
615
1055
  name: '',
@@ -695,16 +1135,20 @@ createApp({
695
1135
  try {
696
1136
  this.normalizeCodexDefaults();
697
1137
  const filteredAuth = this.config.AUTH.filter(auth => !this.isEmptyConfig(auth));
698
- const configToSave = {
699
- ...this.config,
700
- AUTH: filteredAuth.length > 0 ? filteredAuth : this.config.AUTH
701
- };
1138
+ const configToSave = this.prepareConfigForSave(
1139
+ filteredAuth.length > 0 ? filteredAuth : this.config.AUTH
1140
+ );
702
1141
 
703
1142
  const response = await axios.post('/api/config', {
704
1143
  config: configToSave,
705
1144
  cliType: this.currentCli.toLowerCase()
706
1145
  });
707
- this.applyWorkBuddySaveResponse(response, configToSave.AUTH);
1146
+ this.applyWorkBuddySaveResponse(
1147
+ response,
1148
+ this.currentCli === 'WorkBuddy'
1149
+ ? (filteredAuth.length > 0 ? filteredAuth : this.config.AUTH)
1150
+ : configToSave.AUTH
1151
+ );
708
1152
  this.showToast(this.t.configDeleted);
709
1153
  // 重置服务器倒计时
710
1154
  this.resetTimer();
@@ -733,16 +1177,15 @@ createApp({
733
1177
  // 立即保存配置
734
1178
  try {
735
1179
  this.normalizeCodexDefaults();
736
- const configToSave = {
737
- ...this.config,
738
- AUTH: this.config.AUTH.filter(auth => !this.isEmptyConfig(auth))
739
- };
1180
+ const configToSave = this.prepareConfigForSave(
1181
+ this.config.AUTH.filter(auth => !this.isEmptyConfig(auth))
1182
+ );
740
1183
 
741
1184
  const response = await axios.post('/api/config', {
742
1185
  config: configToSave,
743
1186
  cliType: this.currentCli.toLowerCase()
744
1187
  });
745
- this.applyWorkBuddySaveResponse(response, configToSave.AUTH);
1188
+ this.applyWorkBuddySaveResponse(response, this.currentCli === 'WorkBuddy' ? this.config.AUTH.filter(auth => !this.isEmptyConfig(auth)) : configToSave.AUTH);
746
1189
  this.showToast(this.t.configActivated);
747
1190
  // 彩带庆祝
748
1191
  this.showConfetti();
@@ -773,14 +1216,76 @@ createApp({
773
1216
  },
774
1217
 
775
1218
  addEnv(authIndex) {
776
- this.config.AUTH[authIndex].ENV.push({
777
- key: '',
778
- value: ''
1219
+ this.config.AUTH[authIndex].ENV.push(createEnvNode());
1220
+ },
1221
+
1222
+ confirmDeleteEnvNode(node, onConfirm) {
1223
+ const nodeName = node && typeof node.key === 'string' && node.key.trim()
1224
+ ? node.key.trim()
1225
+ : (this.t.arrayItem || 'Item');
1226
+ const message = this.t.confirmDeleteEnvVariable.replace('{name}', nodeName);
1227
+ this.showConfirmDialog(message, onConfirm);
1228
+ },
1229
+
1230
+ async persistInlineConfigChange(deletedItems = {}) {
1231
+ const configToSave = this.prepareConfigForSave(this.config.AUTH);
1232
+ const response = await axios.post('/api/config', {
1233
+ config: configToSave,
1234
+ cliType: this.currentCli.toLowerCase(),
1235
+ deletedItems
779
1236
  });
1237
+ this.applyWorkBuddySaveResponse(response, configToSave.AUTH);
1238
+ this.resetTimer();
1239
+ return response;
780
1240
  },
781
1241
 
782
- deleteEnv(authIndex, envIndex) {
783
- this.config.AUTH[authIndex].ENV.splice(envIndex, 1);
1242
+ async deleteEnv(authIndex, envIndex, childPath = []) {
1243
+ const auth = this.config.AUTH[authIndex];
1244
+ const rootNode = auth && Array.isArray(auth.ENV) ? auth.ENV[envIndex] : null;
1245
+ const path = Array.isArray(childPath) ? childPath : [];
1246
+ if (!rootNode) {
1247
+ return;
1248
+ }
1249
+
1250
+ let targetList = auth.ENV;
1251
+ let targetIndex = envIndex;
1252
+ if (path.length > 0) {
1253
+ let parentNode = rootNode;
1254
+ for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex += 1) {
1255
+ if (!parentNode || !Array.isArray(parentNode.children)) {
1256
+ return;
1257
+ }
1258
+ parentNode = parentNode.children[path[pathIndex]];
1259
+ }
1260
+ if (!parentNode || !Array.isArray(parentNode.children)) {
1261
+ return;
1262
+ }
1263
+ targetList = parentNode.children;
1264
+ targetIndex = path[path.length - 1];
1265
+ }
1266
+
1267
+ const deletedNode = targetList[targetIndex];
1268
+ if (!deletedNode) {
1269
+ return;
1270
+ }
1271
+ const rootKey = path.length === 0 && typeof rootNode.key === 'string'
1272
+ ? rootNode.key.trim()
1273
+ : '';
1274
+ targetList.splice(targetIndex, 1);
1275
+ const recordedDeletion = this.recordDeletedItem('envKeys', rootKey);
1276
+
1277
+ try {
1278
+ const deletedItems = this.getPendingDeletedItems();
1279
+ await this.persistInlineConfigChange(deletedItems);
1280
+ this.clearPendingDeletedItems(deletedItems);
1281
+ this.showToast(this.t.envVariableDeleted);
1282
+ } catch (err) {
1283
+ targetList.splice(targetIndex, 0, deletedNode);
1284
+ if (recordedDeletion) {
1285
+ this.removePendingDeletedItem('envKeys', rootKey);
1286
+ }
1287
+ this.showToast(this.t.deleteFailed + ': ' + (err.response?.data?.error || err.message), 'error');
1288
+ }
784
1289
  },
785
1290
 
786
1291
  addModel(authIndex) {
@@ -790,14 +1295,43 @@ createApp({
790
1295
  });
791
1296
  },
792
1297
 
793
- deleteModel(authIndex, modelIndex) {
794
- this.config.AUTH[authIndex].MODELS.splice(modelIndex, 1);
1298
+ async deleteModel(authIndex, modelIndex) {
1299
+ const auth = this.config.AUTH[authIndex];
1300
+ const model = auth && Array.isArray(auth.MODELS) ? auth.MODELS[modelIndex] : null;
1301
+ if (!model) {
1302
+ return;
1303
+ }
1304
+ const modelKey = typeof model.key === 'string' ? model.key.trim() : '';
1305
+ const modelName = modelKey || `#${modelIndex + 1}`;
1306
+ const message = this.t.confirmDeleteModel.replace('{name}', modelName);
1307
+
1308
+ this.showConfirmDialog(message, async () => {
1309
+ auth.MODELS.splice(modelIndex, 1);
1310
+ const recordedDeletion = this.recordDeletedItem('modelKeys', modelKey);
1311
+ try {
1312
+ const deletedItems = this.getPendingDeletedItems();
1313
+ await this.persistInlineConfigChange(deletedItems);
1314
+ this.clearPendingDeletedItems(deletedItems);
1315
+ this.showToast(this.t.modelDeleted);
1316
+ } catch (err) {
1317
+ auth.MODELS.splice(modelIndex, 0, model);
1318
+ if (recordedDeletion) {
1319
+ this.removePendingDeletedItem('modelKeys', modelKey);
1320
+ }
1321
+ this.showToast(this.t.deleteFailed + ': ' + (err.response?.data?.error || err.message), 'error');
1322
+ }
1323
+ });
795
1324
  },
796
1325
 
797
1326
  // 判断是否为空配置
798
1327
  isEmptyConfig(auth) {
799
1328
  const hasDefaultModel = (this.currentCli === 'Codex' || this.currentCli === 'WorkBuddy') && auth.DEFAULT_MODEL && auth.DEFAULT_MODEL.trim();
800
- return !auth.name && !auth.BASE_URL && !auth.TOKEN && !hasDefaultModel && (!auth.ENV || auth.ENV.length === 0) && (!auth.MODELS || auth.MODELS.length === 0);
1329
+ const hasEnv = Array.isArray(auth.ENV) && auth.ENV.some(env => this.hasEnvRootKey(env));
1330
+ return !auth.name && !auth.BASE_URL && !auth.TOKEN && !hasDefaultModel && !hasEnv && (!auth.MODELS || auth.MODELS.length === 0);
1331
+ },
1332
+
1333
+ hasEnvRootKey(node) {
1334
+ return Boolean(node && typeof node.key === 'string' && node.key.trim());
801
1335
  },
802
1336
 
803
1337
  createAuthKey() {
@@ -1033,16 +1567,15 @@ createApp({
1033
1567
  // 保存配置
1034
1568
  try {
1035
1569
  this.normalizeCodexDefaults();
1036
- const configToSave = {
1037
- ...this.config,
1038
- AUTH: this.config.AUTH.filter(auth => !this.isEmptyConfig(auth))
1039
- };
1570
+ const configToSave = this.prepareConfigForSave(
1571
+ this.config.AUTH.filter(auth => !this.isEmptyConfig(auth))
1572
+ );
1040
1573
 
1041
1574
  const response = await axios.post('/api/config', {
1042
1575
  config: configToSave,
1043
1576
  cliType: this.currentCli.toLowerCase()
1044
1577
  });
1045
- this.applyWorkBuddySaveResponse(response, configToSave.AUTH);
1578
+ this.applyWorkBuddySaveResponse(response, this.currentCli === 'WorkBuddy' ? this.config.AUTH.filter(auth => !this.isEmptyConfig(auth)) : configToSave.AUTH);
1046
1579
  // 重置服务器倒计时
1047
1580
  this.resetTimer();
1048
1581
  } catch (err) {
@@ -1058,6 +1591,11 @@ createApp({
1058
1591
  }
1059
1592
  },
1060
1593
  mounted() {
1594
+ window.addEventListener('storage', event => {
1595
+ if (event.key === CONFIG_FILE_REFRESH_STORAGE_KEY && event.newValue) {
1596
+ window.location.reload();
1597
+ }
1598
+ });
1061
1599
  // 从 localStorage 读取语言设置
1062
1600
  const savedLang = localStorage.getItem('ccby-lang');
1063
1601
  if (savedLang && (savedLang === 'en' || savedLang === 'zh')) {
@@ -1082,4 +1620,7 @@ createApp({
1082
1620
  });
1083
1621
  });
1084
1622
  }
1085
- }).mount('#app');
1623
+ });
1624
+
1625
+ app.component('env-node-editor', EnvNodeEditor);
1626
+ app.mount('#app');