@yesdgq/claude-buddy 1.1.2000 → 1.1.3010

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,
@@ -25,7 +365,7 @@ createApp({
25
365
  cliList: [
26
366
  { name: 'Claude', label: 'Claude', icon: 'images/claude.png' },
27
367
  { name: 'Codex', label: 'Codex', icon: 'images/gpt.png' },
28
- { name: 'Gemini', label: 'Gemini', icon: 'images/gemini.png' }
368
+ { name: 'WorkBuddy', label: 'WorkBuddy', icon: 'images/workbuddy.png' }
29
369
  ],
30
370
  // 安装命令列表
31
371
  installCommands: [
@@ -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,16 +443,28 @@ createApp({
101
443
  modelManagement: 'Model Management',
102
444
  defaultModelTitle: 'Default Model',
103
445
  defaultModelPlaceholder: 'Enter default GPT model',
446
+ workbuddyModelSectionTitle: 'Model Management',
447
+ workbuddyModelName: 'Model Name',
448
+ workbuddyModelPlaceholder: 'Enter model parameter value, e.g. gpt-4o',
104
449
  collapse: 'Collapse',
105
450
  expand: 'Expand',
106
451
  modelNamePlaceholder: 'Model name',
107
452
  modelValuePlaceholder: 'Model value',
108
453
  deleteThisModel: 'Delete this model',
454
+ confirmDeleteModel: 'Delete model "{name}"? This action cannot be undone.',
455
+ modelDeleted: 'Model deleted',
109
456
  addModel: 'Add Model',
110
457
  envVariables: 'Environment Variables',
111
458
  variableNamePlaceholder: 'Variable name',
112
459
  variableValuePlaceholder: 'Variable value',
460
+ valueType: 'Value type',
461
+ addChildItem: 'Add child item',
462
+ arrayItem: 'Item',
463
+ item: 'item',
464
+ items: 'items',
113
465
  deleteThisEnvVar: 'Delete this environment variable',
466
+ confirmDeleteEnvVariable: 'Delete environment variable "{name}"? This action cannot be undone.',
467
+ envVariableDeleted: 'Environment variable deleted',
114
468
  addEnvVariable: 'Add Environment Variable',
115
469
  addNewConfig: 'Add New Configuration',
116
470
  saveConfig: 'Save Configuration',
@@ -127,7 +481,6 @@ createApp({
127
481
  configDeleted: 'Configuration deleted',
128
482
  deleteFailed: 'Failed to delete',
129
483
  configActivated: 'Configuration activated',
130
- notAvailableYet: 'Not available yet, stay tuned',
131
484
  saveFailed: 'Save failed',
132
485
  cancel: 'Cancel',
133
486
  confirm: 'Confirm',
@@ -143,6 +496,8 @@ createApp({
143
496
  },
144
497
  zh: {
145
498
  helpTitle: '使用说明',
499
+ configFileLabel: '配置文件',
500
+ openConfigFile: '打开文件编辑器',
146
501
  helpDescription: 'Claude Buddy 是一款简单易用的 CLI 工具,通过可视化界面帮助您快速配置 Claude Code 等 CLI 工具的 API 密钥、模型和环境变量。',
147
502
  feature1Title: 'API 密钥管理',
148
503
  feature1Desc: '轻松存储和切换多组 API 密钥',
@@ -173,16 +528,28 @@ createApp({
173
528
  modelManagement: '模型管理',
174
529
  defaultModelTitle: '默认模型',
175
530
  defaultModelPlaceholder: '输入默认gpt模型',
531
+ workbuddyModelSectionTitle: '模型管理',
532
+ workbuddyModelName: '模型名称',
533
+ workbuddyModelPlaceholder: '输入模型参数值,例如gpt-4o',
176
534
  collapse: '收起',
177
535
  expand: '展开',
178
536
  modelNamePlaceholder: '模型名称',
179
537
  modelValuePlaceholder: '模型值',
180
538
  deleteThisModel: '删除此模型',
539
+ confirmDeleteModel: '确定要删除模型“{name}”吗?此操作无法撤销。',
540
+ modelDeleted: '模型已删除',
181
541
  addModel: '添加模型',
182
542
  envVariables: '环境变量',
183
543
  variableNamePlaceholder: '变量名',
184
544
  variableValuePlaceholder: '变量值',
545
+ valueType: '值类型',
546
+ addChildItem: '添加下级项目',
547
+ arrayItem: 'Item',
548
+ item: '项',
549
+ items: '项',
185
550
  deleteThisEnvVar: '删除此环境变量',
551
+ confirmDeleteEnvVariable: '确定要删除环境变量“{name}”吗?此操作无法撤销。',
552
+ envVariableDeleted: '环境变量已删除',
186
553
  addEnvVariable: '添加环境变量',
187
554
  addNewConfig: '添加新配置',
188
555
  saveConfig: '保存配置',
@@ -199,7 +566,6 @@ createApp({
199
566
  configDeleted: '配置已删除',
200
567
  deleteFailed: '删除失败',
201
568
  configActivated: '配置启用成功',
202
- notAvailableYet: '暂未开放,敬请期待',
203
569
  saveFailed: '保存失败',
204
570
  cancel: '取消',
205
571
  confirm: '确认',
@@ -220,6 +586,9 @@ createApp({
220
586
  t() {
221
587
  return this.translations[this.currentLang];
222
588
  },
589
+ currentConfigFileItems() {
590
+ return getConfigFileItems(this.currentCli);
591
+ },
223
592
  funLoadingMessage() {
224
593
  const messages = {
225
594
  en: [
@@ -365,6 +734,17 @@ createApp({
365
734
  }
366
735
  },
367
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
+
368
748
  // 动态调整输入框宽度
369
749
  adjustInputWidth(event, index) {
370
750
  const input = event.target;
@@ -468,14 +848,18 @@ createApp({
468
848
 
469
849
  // 如果配置列表为空,自动添加一个空配置项
470
850
  if (config.AUTH.length === 0) {
471
- config.AUTH = [{
851
+ const emptyAuth = {
472
852
  name: '',
473
853
  BASE_URL: '',
474
854
  TOKEN: '',
475
855
  DEFAULT_MODEL: '',
476
856
  MODELS: [],
477
857
  ENV: []
478
- }];
858
+ };
859
+ if (cliType === 'workbuddy') {
860
+ emptyAuth.__workbuddyNew = true;
861
+ }
862
+ config.AUTH = [emptyAuth];
479
863
  config.activeIndex = 0;
480
864
  }
481
865
 
@@ -484,12 +868,24 @@ createApp({
484
868
  if (!auth.ENV) {
485
869
  auth.ENV = [];
486
870
  }
871
+ auth.ENV = normalizeEnvNodes(auth.ENV);
487
872
  if (!auth.MODELS) {
488
873
  auth.MODELS = [];
489
874
  }
490
- if (cliType === 'codex' && typeof auth.DEFAULT_MODEL !== 'string') {
875
+ if ((cliType === 'codex' || cliType === 'workbuddy') && typeof auth.DEFAULT_MODEL !== 'string') {
491
876
  auth.DEFAULT_MODEL = '';
492
877
  }
878
+ if (cliType === 'workbuddy') {
879
+ // 这些字段只用于区分 models.json 中已有的模型和页面新建的卡片,页面不展示。
880
+ auth.__workbuddyModelExists = auth.__workbuddyModelExists === true;
881
+ auth.__workbuddyModelUrl = typeof auth.__workbuddyModelUrl === 'string'
882
+ ? auth.__workbuddyModelUrl
883
+ : '';
884
+ auth.__workbuddyModelIndex = Number.isInteger(auth.__workbuddyModelIndex)
885
+ ? auth.__workbuddyModelIndex
886
+ : -1;
887
+ auth.__workbuddyNew = auth.__workbuddyNew === true && !auth.__workbuddyModelExists;
888
+ }
493
889
  });
494
890
 
495
891
  // 先设置辅助数组,再设置 config,确保 Vue 响应式正确更新
@@ -500,6 +896,7 @@ createApp({
500
896
 
501
897
  // 最后更新 config
502
898
  this.config = config;
899
+ this.pendingDeletedItems = { modelKeys: [], envKeys: [] };
503
900
 
504
901
  this.showToast(this.t.configLoadSuccess);
505
902
  // 重置服务器倒计时
@@ -523,10 +920,17 @@ createApp({
523
920
  try {
524
921
  this.normalizeCodexDefaults();
525
922
  // 创建配置副本,过滤掉空配置项
526
- const configToSave = {
527
- ...this.config,
528
- AUTH: this.config.AUTH.filter(auth => !this.isEmptyConfig(auth))
529
- };
923
+ const saveAuthList = this.config.AUTH.filter(auth => !this.isEmptyConfig(auth));
924
+ const configToSave = this.prepareConfigForSave(
925
+ saveAuthList
926
+ );
927
+
928
+ // WorkBuddy 过滤空卡片后修正索引,确保同步到 models.json 的仍是当前卡片。
929
+ if (this.currentCli === 'WorkBuddy') {
930
+ const activeAuth = this.config.AUTH[this.config.activeIndex];
931
+ const nextActiveIndex = activeAuth ? saveAuthList.indexOf(activeAuth) : -1;
932
+ configToSave.activeIndex = nextActiveIndex >= 0 ? nextActiveIndex : 0;
933
+ }
530
934
 
531
935
  // 如果过滤后没有配置项,确保至少有一个空配置项在前端显示
532
936
  if (configToSave.AUTH.length === 0 && this.config.AUTH.length > 0) {
@@ -534,10 +938,14 @@ createApp({
534
938
  return;
535
939
  }
536
940
 
941
+ const deletedItems = this.getPendingDeletedItems();
537
942
  const response = await axios.post('/api/config', {
538
943
  config: configToSave,
539
- cliType: this.currentCli.toLowerCase()
944
+ cliType: this.currentCli.toLowerCase(),
945
+ deletedItems
540
946
  });
947
+ this.applyWorkBuddySaveResponse(response, this.currentCli === 'WorkBuddy' ? saveAuthList : configToSave.AUTH);
948
+ this.clearPendingDeletedItems(deletedItems);
541
949
  this.showToast(this.t.configSaveSuccess);
542
950
  // 彩带庆祝
543
951
  this.showConfetti();
@@ -550,8 +958,32 @@ createApp({
550
958
  }
551
959
  },
552
960
 
961
+ // 保存后把后端确认的模型来源标记同步回当前页面,避免同一新卡片重复插入。
962
+ applyWorkBuddySaveResponse(response, submittedAuth = this.config.AUTH) {
963
+ if (this.currentCli !== 'WorkBuddy') {
964
+ return;
965
+ }
966
+ const savedAuth = response?.data?.config?.AUTH;
967
+ if (!Array.isArray(savedAuth)) {
968
+ return;
969
+ }
970
+
971
+ submittedAuth.forEach((auth, index) => {
972
+ const match = savedAuth[index];
973
+ if (!auth || !match || typeof match !== 'object') {
974
+ return;
975
+ }
976
+ auth.__workbuddyModelExists = match.__workbuddyModelExists === true;
977
+ auth.__workbuddyModelUrl = match.__workbuddyModelUrl || '';
978
+ auth.__workbuddyModelIndex = Number.isInteger(match.__workbuddyModelIndex)
979
+ ? match.__workbuddyModelIndex
980
+ : -1;
981
+ auth.__workbuddyNew = match.__workbuddyNew === true;
982
+ });
983
+ },
984
+
553
985
  normalizeCodexDefaults() {
554
- if (this.currentCli !== 'Codex') {
986
+ if (this.currentCli !== 'Codex' && this.currentCli !== 'WorkBuddy') {
555
987
  return;
556
988
  }
557
989
  this.config.AUTH.forEach(auth => {
@@ -559,15 +991,78 @@ createApp({
559
991
  });
560
992
  },
561
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
+
562
1053
  addAuth() {
563
- this.config.AUTH.push({
1054
+ const auth = {
564
1055
  name: '',
565
1056
  BASE_URL: '',
566
1057
  TOKEN: '',
567
1058
  DEFAULT_MODEL: '',
568
1059
  MODELS: [],
569
1060
  ENV: []
570
- });
1061
+ };
1062
+ if (this.currentCli === 'WorkBuddy') {
1063
+ auth.__workbuddyNew = true;
1064
+ }
1065
+ this.config.AUTH.push(auth);
571
1066
  this.showTokens.push(false);
572
1067
  this.showEnvSections.push(false);
573
1068
  this.showModelSections.push(false);
@@ -617,14 +1112,18 @@ createApp({
617
1112
 
618
1113
  // 如果删除后列表为空,添加一个空配置项
619
1114
  if (this.config.AUTH.length === 0) {
620
- this.config.AUTH.push({
1115
+ const emptyAuth = {
621
1116
  name: '',
622
1117
  BASE_URL: '',
623
1118
  TOKEN: '',
624
1119
  DEFAULT_MODEL: '',
625
1120
  MODELS: [],
626
1121
  ENV: []
627
- });
1122
+ };
1123
+ if (this.currentCli === 'WorkBuddy') {
1124
+ emptyAuth.__workbuddyNew = true;
1125
+ }
1126
+ this.config.AUTH.push(emptyAuth);
628
1127
  this.showTokens.push(false);
629
1128
  this.showEnvSections.push(false);
630
1129
  this.showModelSections.push(false);
@@ -636,15 +1135,20 @@ createApp({
636
1135
  try {
637
1136
  this.normalizeCodexDefaults();
638
1137
  const filteredAuth = this.config.AUTH.filter(auth => !this.isEmptyConfig(auth));
639
- const configToSave = {
640
- ...this.config,
641
- AUTH: filteredAuth.length > 0 ? filteredAuth : this.config.AUTH
642
- };
1138
+ const configToSave = this.prepareConfigForSave(
1139
+ filteredAuth.length > 0 ? filteredAuth : this.config.AUTH
1140
+ );
643
1141
 
644
- await axios.post('/api/config', {
1142
+ const response = await axios.post('/api/config', {
645
1143
  config: configToSave,
646
1144
  cliType: this.currentCli.toLowerCase()
647
1145
  });
1146
+ this.applyWorkBuddySaveResponse(
1147
+ response,
1148
+ this.currentCli === 'WorkBuddy'
1149
+ ? (filteredAuth.length > 0 ? filteredAuth : this.config.AUTH)
1150
+ : configToSave.AUTH
1151
+ );
648
1152
  this.showToast(this.t.configDeleted);
649
1153
  // 重置服务器倒计时
650
1154
  this.resetTimer();
@@ -673,15 +1177,15 @@ createApp({
673
1177
  // 立即保存配置
674
1178
  try {
675
1179
  this.normalizeCodexDefaults();
676
- const configToSave = {
677
- ...this.config,
678
- AUTH: this.config.AUTH.filter(auth => !this.isEmptyConfig(auth))
679
- };
1180
+ const configToSave = this.prepareConfigForSave(
1181
+ this.config.AUTH.filter(auth => !this.isEmptyConfig(auth))
1182
+ );
680
1183
 
681
- await axios.post('/api/config', {
1184
+ const response = await axios.post('/api/config', {
682
1185
  config: configToSave,
683
1186
  cliType: this.currentCli.toLowerCase()
684
1187
  });
1188
+ this.applyWorkBuddySaveResponse(response, this.currentCli === 'WorkBuddy' ? this.config.AUTH.filter(auth => !this.isEmptyConfig(auth)) : configToSave.AUTH);
685
1189
  this.showToast(this.t.configActivated);
686
1190
  // 彩带庆祝
687
1191
  this.showConfetti();
@@ -712,14 +1216,76 @@ createApp({
712
1216
  },
713
1217
 
714
1218
  addEnv(authIndex) {
715
- this.config.AUTH[authIndex].ENV.push({
716
- key: '',
717
- 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
718
1236
  });
1237
+ this.applyWorkBuddySaveResponse(response, configToSave.AUTH);
1238
+ this.resetTimer();
1239
+ return response;
719
1240
  },
720
1241
 
721
- deleteEnv(authIndex, envIndex) {
722
- 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
+ }
723
1289
  },
724
1290
 
725
1291
  addModel(authIndex) {
@@ -729,14 +1295,43 @@ createApp({
729
1295
  });
730
1296
  },
731
1297
 
732
- deleteModel(authIndex, modelIndex) {
733
- 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
+ });
734
1324
  },
735
1325
 
736
1326
  // 判断是否为空配置
737
1327
  isEmptyConfig(auth) {
738
- const hasDefaultModel = this.currentCli === 'Codex' && auth.DEFAULT_MODEL && auth.DEFAULT_MODEL.trim();
739
- return !auth.name && !auth.BASE_URL && !auth.TOKEN && !hasDefaultModel && (!auth.ENV || auth.ENV.length === 0) && (!auth.MODELS || auth.MODELS.length === 0);
1328
+ const hasDefaultModel = (this.currentCli === 'Codex' || this.currentCli === 'WorkBuddy') && auth.DEFAULT_MODEL && auth.DEFAULT_MODEL.trim();
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());
740
1335
  },
741
1336
 
742
1337
  createAuthKey() {
@@ -785,11 +1380,6 @@ createApp({
785
1380
 
786
1381
  // 选择 CLI 类型
787
1382
  async selectCli(cliName) {
788
- if (cliName === 'Gemini') {
789
- this.showToast(this.t.notAvailableYet, 'error');
790
- this.resetTimer();
791
- return;
792
- }
793
1383
  this.currentCli = cliName;
794
1384
  await this.loadConfig(cliName.toLowerCase());
795
1385
  },
@@ -977,15 +1567,15 @@ createApp({
977
1567
  // 保存配置
978
1568
  try {
979
1569
  this.normalizeCodexDefaults();
980
- const configToSave = {
981
- ...this.config,
982
- AUTH: this.config.AUTH.filter(auth => !this.isEmptyConfig(auth))
983
- };
1570
+ const configToSave = this.prepareConfigForSave(
1571
+ this.config.AUTH.filter(auth => !this.isEmptyConfig(auth))
1572
+ );
984
1573
 
985
- await axios.post('/api/config', {
1574
+ const response = await axios.post('/api/config', {
986
1575
  config: configToSave,
987
1576
  cliType: this.currentCli.toLowerCase()
988
1577
  });
1578
+ this.applyWorkBuddySaveResponse(response, this.currentCli === 'WorkBuddy' ? this.config.AUTH.filter(auth => !this.isEmptyConfig(auth)) : configToSave.AUTH);
989
1579
  // 重置服务器倒计时
990
1580
  this.resetTimer();
991
1581
  } catch (err) {
@@ -1001,6 +1591,11 @@ createApp({
1001
1591
  }
1002
1592
  },
1003
1593
  mounted() {
1594
+ window.addEventListener('storage', event => {
1595
+ if (event.key === CONFIG_FILE_REFRESH_STORAGE_KEY && event.newValue) {
1596
+ window.location.reload();
1597
+ }
1598
+ });
1004
1599
  // 从 localStorage 读取语言设置
1005
1600
  const savedLang = localStorage.getItem('ccby-lang');
1006
1601
  if (savedLang && (savedLang === 'en' || savedLang === 'zh')) {
@@ -1025,4 +1620,7 @@ createApp({
1025
1620
  });
1026
1621
  });
1027
1622
  }
1028
- }).mount('#app');
1623
+ });
1624
+
1625
+ app.component('env-node-editor', EnvNodeEditor);
1626
+ app.mount('#app');