codexmate 0.1.17 → 0.1.19

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.
@@ -1,9 +1,37 @@
1
1
  import { api } from './api.mjs';
2
+ import { PI_PROVIDER_TEMPLATE_GROUPS } from './pi-provider-templates.mjs';
2
3
 
3
4
  function isPiPlainObject(value) {
4
5
  return value && typeof value === 'object' && !Array.isArray(value);
5
6
  }
6
7
 
8
+ function normalizePiModelInput(value) {
9
+ if (!Array.isArray(value)) return [];
10
+ return value.filter((item) => item === 'text' || item === 'image');
11
+ }
12
+
13
+ function normalizePiModelNumber(value) {
14
+ if (typeof value === 'number') return Number.isFinite(value) ? value : '';
15
+ if (typeof value === 'string' && value.trim() !== '') {
16
+ const num = Number(value);
17
+ return Number.isFinite(num) ? num : '';
18
+ }
19
+ return '';
20
+ }
21
+
22
+ function normalizePiModelRecord(model) {
23
+ const record = model && typeof model === 'object' ? model : {};
24
+ const contextWindow = record.contextWindow !== undefined ? record.contextWindow : record.context_window;
25
+ const maxTokens = record.maxTokens !== undefined ? record.maxTokens : record.max_tokens;
26
+ return {
27
+ id: record.id == null ? '' : String(record.id),
28
+ name: record.name == null ? '' : String(record.name),
29
+ reasoning: !!record.reasoning,
30
+ contextWindow: normalizePiModelNumber(contextWindow),
31
+ maxTokens: normalizePiModelNumber(maxTokens),
32
+ input: normalizePiModelInput(record.input)
33
+ };
34
+ }
7
35
  export function createPiConfigMethods({ api: apiClient }) {
8
36
  return {
9
37
  async loadPiProviders() {
@@ -79,9 +107,18 @@ export function createPiConfigMethods({ api: apiClient }) {
79
107
  }
80
108
  },
81
109
  async savePiProvider() {
110
+ if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
111
+ const editor = this.editingPiProvider;
112
+ if (!editor || !editor.form) return;
113
+ const isNew = !!editor.isNew;
114
+ const baseUrl = (editor.form.baseUrl || '').trim();
115
+ if (isNew && !baseUrl) {
116
+ this.message = 'Base URL 不能为空';
117
+ this.messageType = 'error';
118
+ return;
119
+ }
82
120
  this.piSaving = true;
83
121
  try {
84
- const providerId = this.editingPiProvider.id;
85
122
  const { merged, ok, error } = this.piBuildMergedEditorRecord();
86
123
  if (!ok || !merged) {
87
124
  this.piEditorJsonError = error || '配置 JSON 不合法';
@@ -89,12 +126,23 @@ export function createPiConfigMethods({ api: apiClient }) {
89
126
  this.messageType = 'error';
90
127
  return;
91
128
  }
92
-
129
+ const providerId = isNew ? this.derivePiProviderId(baseUrl) : editor.id;
130
+ if (isNew) {
131
+ merged.id = providerId;
132
+ if (merged.name === undefined) merged.name = providerId;
133
+ }
93
134
  await this.persistPiProvider(providerId, merged);
94
135
  this.piProviders = { ...this.piProviders, [providerId]: merged };
95
136
  this.piProviderIds = Object.keys(this.piProviders).sort();
96
137
  this.resetPiProviderEditing();
97
- this.message = '配置已保存';
138
+ if (isNew) {
139
+ this.showAddPiProviderModal = false;
140
+ this.piProviderPickerQuery = '';
141
+ this.piSelectedProviderTemplate = '';
142
+ this.message = '供应商已添加';
143
+ } else {
144
+ this.message = '配置已保存';
145
+ }
98
146
  this.messageType = 'success';
99
147
  } catch (e) {
100
148
  this.message = e && e.message ? e.message : '保存 Pi 配置失败';
@@ -103,45 +151,6 @@ export function createPiConfigMethods({ api: apiClient }) {
103
151
  this.piSaving = false;
104
152
  }
105
153
  },
106
- async addPiProviderFromModal() {
107
- let createdId = null;
108
- this.piSaving = true;
109
- try {
110
- const baseUrl = (this.addingPiProviderBaseUrl || '').trim();
111
- if (!baseUrl) throw new Error('Base URL 不能为空');
112
- const providerId = this.derivePiProviderId(baseUrl);
113
- const modelId = (this.addingPiProviderModel || '').trim();
114
- const values = {
115
- id: providerId,
116
- name: providerId,
117
- baseUrl,
118
- api: this.addingPiProviderApi || 'openai-completions',
119
- apiKey: (this.addingPiProviderApiKey || '').trim(),
120
- models: modelId ? [{
121
- id: modelId,
122
- name: modelId,
123
- reasoning: false,
124
- contextWindow: 128000,
125
- maxTokens: 32000,
126
- input: ''
127
- }] : []
128
- };
129
-
130
- await this.persistPiProvider(providerId, values);
131
- this.piProviders = { ...this.piProviders, [providerId]: values };
132
- this.piProviderIds = Object.keys(this.piProviders).sort();
133
- this.cancelAddPiProviderModal();
134
- this.message = '供应商已添加';
135
- this.messageType = 'success';
136
- createdId = providerId;
137
- } catch (e) {
138
- this.message = e && e.message ? e.message : '添加 Pi 供应商失败';
139
- this.messageType = 'error';
140
- } finally {
141
- this.piSaving = false;
142
- if (createdId) this.openEditPiProvider(createdId);
143
- }
144
- },
145
154
  derivePiProviderId(baseUrl) {
146
155
  let candidate = String(Date.now());
147
156
  while (this.piProviders[candidate]) candidate = String(Number(candidate) + 1);
@@ -150,79 +159,156 @@ export function createPiConfigMethods({ api: apiClient }) {
150
159
  piProviderUrlTitle() {
151
160
  return '';
152
161
  },
162
+ resetPiEditorTransientState() {
163
+ this.piRemoteModels = [];
164
+ this.piRemoteModelError = '';
165
+ this.piRemoteChecked = {};
166
+ this.piCatalogFillError = '';
167
+ this.piCatalogFillIndex = -1;
168
+ this.piModelSearch = '';
169
+ this.piEditorJsonError = '';
170
+ },
153
171
  startAddPiProvider() {
154
172
  if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
155
- this.addingPiProviderId = '';
156
- this.addingPiProviderName = '';
157
- this.addingPiProviderBaseUrl = '';
158
- this.addingPiProviderApi = 'openai-completions';
159
- this.addingPiProviderApiKey = '';
160
- this.addingPiProviderModel = '';
161
- this.addingPiRemoteModels = [];
162
- this.addingPiRemoteLoading = false;
163
- this.addingPiRemoteError = '';
164
- this.showAddPiProviderModal = true;
165
173
  this.editingPiProvider = null;
174
+ this.showAddPiProviderModal = true;
175
+ this.piProviderPickerQuery = '';
176
+ this.piSelectedProviderTemplate = '';
177
+ this.resetPiEditorTransientState();
166
178
  },
167
179
  cancelAddPiProviderModal() {
168
180
  this.showAddPiProviderModal = false;
169
- this.addingPiProviderId = '';
170
- this.addingPiProviderName = '';
171
- this.addingPiProviderBaseUrl = '';
172
- this.addingPiProviderApi = '';
173
- this.addingPiProviderApiKey = '';
174
- this.addingPiProviderModel = '';
175
- this.addingPiRemoteModels = [];
176
- this.addingPiRemoteLoading = false;
177
- this.addingPiRemoteError = '';
178
- },
179
- async fetchAddingPiRemoteModels() {
180
- const baseUrl = (this.addingPiProviderBaseUrl || '').trim();
181
- if (!baseUrl || !/^https?:\/\//i.test(baseUrl)) {
182
- this.addingPiRemoteModels = [];
183
- this.addingPiRemoteError = '';
184
- return;
181
+ this.piProviderPickerQuery = '';
182
+ this.piSelectedProviderTemplate = '';
183
+ if (this.editingPiProvider && this.editingPiProvider.isNew) this.editingPiProvider = null;
184
+ this.resetPiEditorTransientState();
185
+ },
186
+ piProviderTemplateGroups() {
187
+ const query = (this.piProviderPickerQuery || '').trim().toLowerCase();
188
+ if (!query) return PI_PROVIDER_TEMPLATE_GROUPS;
189
+ return PI_PROVIDER_TEMPLATE_GROUPS
190
+ .map((group) => ({
191
+ id: group.id,
192
+ i18nKey: group.i18nKey,
193
+ templates: group.templates.filter((tpl) =>
194
+ tpl.name.toLowerCase().includes(query) || tpl.id.toLowerCase().includes(query))
195
+ }))
196
+ .filter((group) => group.templates.length > 0);
197
+ },
198
+ selectPiProviderTemplate(tplId) {
199
+ if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
200
+ const tpl = PI_PROVIDER_TEMPLATE_GROUPS
201
+ .flatMap((group) => group.templates)
202
+ .find((item) => item.id === tplId);
203
+ if (!tpl) return;
204
+ this.piSelectedProviderTemplate = tpl.id;
205
+ this.startPiProviderDraft(tpl);
206
+ },
207
+ startPiProviderDraft(tpl) {
208
+ const template = isPiPlainObject(tpl) ? tpl : {};
209
+ this.resetPiEditorTransientState();
210
+ this.editingPiProvider = {
211
+ id: '',
212
+ isNew: true,
213
+ form: {
214
+ id: '',
215
+ baseUrl: template.baseUrl || '',
216
+ api: template.api || 'openai-completions',
217
+ apiKey: '',
218
+ configJsonDraft: '',
219
+ models: []
220
+ },
221
+ extras: {},
222
+ original: null
223
+ };
224
+ this.fetchPiRemoteModels();
225
+ },
226
+ resetPiProviderTemplateSelection() {
227
+ this.piSelectedProviderTemplate = '';
228
+ if (this.editingPiProvider && this.editingPiProvider.isNew) this.editingPiProvider = null;
229
+ },
230
+ piRemoteSelectableModels() {
231
+ const base = Array.isArray(this.piRemoteModels) ? this.piRemoteModels : [];
232
+ const form = this.editingPiProvider && this.editingPiProvider.form;
233
+ const existing = Array.isArray(form && form.models) ? form.models : [];
234
+ const added = new Set(existing.map((m) => String(m && m.id || '').trim().toLowerCase()));
235
+ const q = (this.piModelSearch || '').trim().toLowerCase();
236
+ return base
237
+ .filter((id) => {
238
+ if (added.has(String(id).trim().toLowerCase())) return false;
239
+ return !q || String(id).toLowerCase().includes(q);
240
+ })
241
+ .slice(0, 50);
242
+ },
243
+ isPiRemoteChecked(id) {
244
+ return !!this.piRemoteChecked[String(id)];
245
+ },
246
+ togglePiRemoteChecked(id) {
247
+ const key = String(id);
248
+ const next = { ...this.piRemoteChecked };
249
+ if (next[key]) delete next[key];
250
+ else next[key] = true;
251
+ this.piRemoteChecked = next;
252
+ },
253
+ piCheckedRemoteCount() {
254
+ return this.piRemoteSelectableModels().filter((id) => this.isPiRemoteChecked(id)).length;
255
+ },
256
+ piCreateModelRecord(id) {
257
+ const t = String(id || '').trim();
258
+ return {
259
+ id: t,
260
+ name: t,
261
+ reasoning: false,
262
+ contextWindow: 128000,
263
+ maxTokens: 32000,
264
+ input: []
265
+ };
266
+ },
267
+ addSelectedPiRemoteModels() {
268
+ if (!this.isToolConfigWriteAllowed('pi') || !this.editingPiProvider) return;
269
+ const form = this.editingPiProvider.form;
270
+ if (!form || !Array.isArray(form.models)) return;
271
+ const existing = new Set(form.models.map((m) => String(m && m.id || '').trim().toLowerCase()));
272
+ for (const id of this.piRemoteSelectableModels()) {
273
+ if (!this.isPiRemoteChecked(id)) continue;
274
+ const key = String(id).trim().toLowerCase();
275
+ if (existing.has(key)) continue;
276
+ form.models.push(this.piCreateModelRecord(id));
277
+ existing.add(key);
185
278
  }
186
- this.addingPiRemoteLoading = true;
187
- this.addingPiRemoteError = '';
279
+ this.piRemoteChecked = {};
280
+ this.piModelSearch = '';
281
+ },
282
+ async fillPiModelFromCatalog(index) {
283
+ if (!this.isToolConfigWriteAllowed('pi') || !this.editingPiProvider) return;
284
+ const models = this.editingPiProvider.form && this.editingPiProvider.form.models;
285
+ const model = Array.isArray(models) ? models[index] : null;
286
+ if (!model || !String(model.id || '').trim()) return;
287
+ if (this.piCatalogFillIndex !== -1) return;
288
+ this.piCatalogFillIndex = index;
289
+ this.piCatalogFillError = '';
188
290
  try {
189
- const result = await apiClient('fetch-pi-remote-models', {
190
- baseUrl,
191
- apiKey: (this.addingPiProviderApiKey || '').trim()
291
+ const result = await apiClient('pi-models-catalog', {
292
+ modelId: String(model.id).trim(),
293
+ providerId: this.editingPiProvider.id,
294
+ baseUrl: this.editingPiProvider.form.baseUrl || ''
192
295
  });
193
- if (!this.showAddPiProviderModal) return;
194
- if (result && Array.isArray(result.models) && result.models.length > 0) {
195
- this.addingPiRemoteModels = result.models;
296
+ if (result && result.ok && isPiPlainObject(result.model)) {
297
+ const info = result.model;
298
+ if (model.name === '' && typeof info.name === 'string') model.name = info.name;
299
+ if (model.contextWindow === '' && Number.isFinite(info.contextWindow)) model.contextWindow = info.contextWindow;
300
+ if (model.maxTokens === '' && Number.isFinite(info.maxTokens)) model.maxTokens = info.maxTokens;
301
+ model.reasoning = !!info.reasoning;
196
302
  } else {
197
- this.addingPiRemoteModels = [];
198
- this.addingPiRemoteError = (result && result.error) || '未获取到可用模型';
303
+ this.piCatalogFillError = (result && result.error) || 'models.dev 目录中未找到该模型';
199
304
  }
200
- } catch (e) {
201
- if (!this.showAddPiProviderModal) return;
202
- this.addingPiRemoteModels = [];
203
- this.addingPiRemoteError = e && e.message ? e.message : '模型列表获取失败';
204
305
  } finally {
205
- if (this.showAddPiProviderModal) this.addingPiRemoteLoading = false;
306
+ this.piCatalogFillIndex = -1;
206
307
  }
207
308
  },
208
- onAddingPiEndpointChange() {
209
- if (this.addingPiRemoteDebounce) clearTimeout(this.addingPiRemoteDebounce);
210
- this.addingPiRemoteDebounce = setTimeout(() => {
211
- this.addingPiRemoteDebounce = null;
212
- this.fetchAddingPiRemoteModels();
213
- }, 600);
214
- },
215
- piFilteredAddingRemoteModels() {
216
- const query = (this.addingPiProviderModel || '').trim().toLowerCase();
217
- if (!query) return [];
218
- return this.addingPiRemoteModels.filter((id) => id.toLowerCase().includes(query)).slice(0, 50);
219
- },
220
309
  openEditPiProvider(providerId) {
221
310
  if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
222
- this.piRemoteModels = [];
223
- this.piRemoteModelError = '';
224
- this.piModelSearch = '';
225
- this.piEditorJsonError = '';
311
+ this.resetPiEditorTransientState();
226
312
  const provider = this.piProviders[providerId] || {};
227
313
  const knownKeys = ['id', 'name', 'title', 'baseUrl', 'api', 'apiKey', 'models', 'headers', 'configJson'];
228
314
  const extras = {};
@@ -236,14 +322,7 @@ export function createPiConfigMethods({ api: apiClient }) {
236
322
  apiKey: provider.apiKey || '',
237
323
  configJsonDraft: JSON.stringify(extras, null, 2),
238
324
  models: Array.isArray(provider.models) && provider.models.length
239
- ? provider.models.map((model) => ({
240
- id: model?.id || '',
241
- name: model?.name || '',
242
- reasoning: model?.reasoning || '',
243
- contextWindow: model?.contextWindow ?? model?.context_window ?? '',
244
- maxTokens: model?.maxTokens ?? model?.max_tokens ?? '',
245
- input: model?.input || ''
246
- }))
325
+ ? provider.models.map((model) => ({ ...model, ...normalizePiModelRecord(model) }))
247
326
  : []
248
327
  };
249
328
  this.editingPiProvider = {
@@ -259,9 +338,19 @@ export function createPiConfigMethods({ api: apiClient }) {
259
338
  this.editingPiProvider = null;
260
339
  this.piRemoteModels = [];
261
340
  this.piRemoteModelError = '';
341
+ this.piRemoteChecked = {};
342
+ this.piCatalogFillError = '';
343
+ this.piCatalogFillIndex = -1;
262
344
  this.piModelSearch = '';
263
345
  this.piEditorJsonError = '';
264
346
  },
347
+ closePiProviderEditor() {
348
+ const editor = this.editingPiProvider;
349
+ if (!editor) return;
350
+ const wasNew = !!editor.isNew;
351
+ this.resetPiProviderEditing();
352
+ if (wasNew) this.piSelectedProviderTemplate = '';
353
+ },
265
354
  removePiProviderModel(index) {
266
355
  if (!this.editingPiProvider) return;
267
356
  const list = this.editingPiProvider.form.models;
@@ -280,13 +369,16 @@ export function createPiConfigMethods({ api: apiClient }) {
280
369
  if (this.editingPiProvider !== provider) return;
281
370
  if (result && Array.isArray(result.models) && result.models.length > 0) {
282
371
  this.piRemoteModels = result.models;
372
+ this.piRemoteChecked = {};
283
373
  } else {
284
374
  this.piRemoteModels = [];
375
+ this.piRemoteChecked = {};
285
376
  this.piRemoteModelError = (result && result.error) || '未获取到可用模型';
286
377
  }
287
378
  } catch (e) {
288
379
  if (this.editingPiProvider !== provider) return;
289
380
  this.piRemoteModels = [];
381
+ this.piRemoteChecked = {};
290
382
  this.piRemoteModelError = e && e.message ? e.message : '模型列表获取失败';
291
383
  } finally {
292
384
  if (this.editingPiProvider === provider) this.piRemoteModelsLoading = false;
@@ -303,14 +395,10 @@ export function createPiConfigMethods({ api: apiClient }) {
303
395
  if (!trimmed) return;
304
396
  const list = this.editingPiProvider.form.models;
305
397
  if (!Array.isArray(list)) return;
306
- list.splice(0, list.length, {
307
- id: trimmed,
308
- name: trimmed,
309
- reasoning: false,
310
- contextWindow: 128000,
311
- maxTokens: 32000,
312
- input: ''
313
- });
398
+ const key = trimmed.toLowerCase();
399
+ const duplicated = list.some((m) => String(m && m.id || '').trim().toLowerCase() === key);
400
+ if (duplicated) return;
401
+ list.push(this.piCreateModelRecord(trimmed));
314
402
  this.piModelSearch = '';
315
403
  },
316
404
  async confirmDeletePiProvider() {
@@ -447,11 +535,11 @@ export function createPiConfigMethods({ api: apiClient }) {
447
535
  const values = provider.form || {};
448
536
  const extras = draft.extras || {};
449
537
  const orig = this.piProviders[provider.id] || {};
450
- const mapModels = (xs) => (Array.isArray(xs) ? xs : []).map((model) => ({
451
- ...model,
452
- id: (model && model.id) || '',
453
- name: (model && (model.name || model.id)) || ''
454
- }));
538
+ const mapModels = (xs) => (Array.isArray(xs) ? xs : []).map((model) => {
539
+ const record = { ...(model && typeof model === 'object' ? model : {}), ...normalizePiModelRecord(model) };
540
+ if (!record.name) record.name = record.id;
541
+ return record;
542
+ });
455
543
  const merged = { ...extras };
456
544
  if (merged.baseUrl === undefined) merged.baseUrl = values.baseUrl || '';
457
545
  if (merged.api === undefined) merged.api = values.api || '';
@@ -268,6 +268,7 @@ const en = Object.freeze({
268
268
  'dashboard.doctor.export.json': 'Export JSON',
269
269
  'dashboard.doctor.export.md': 'Export Markdown',
270
270
  'dashboard.doctor.open': 'Open',
271
+ 'dashboard.doctor.empty': 'No checks have been run yet. Run checks to diagnose the current setup.',
271
272
  'doctor.action.openConfig': 'Open Config',
272
273
  'doctor.action.checkProvider': 'Check provider config',
273
274
  'doctor.action.openUsage': 'Open Usage',
@@ -1374,6 +1375,19 @@ const en = Object.freeze({
1374
1375
  'pi.providers.models.remoteLoading': 'Fetching remote model list…',
1375
1376
  'pi.providers.models.remoteFailed': 'You can still type model IDs manually.',
1376
1377
  'pi.providers.models.noMatch': 'No matching models',
1378
+ 'pi.providers.models.importTitle': 'Import models from endpoint',
1379
+ 'pi.providers.models.addSelected': 'Add selected ({count})',
1380
+ 'pi.providers.models.name': 'Name',
1381
+ 'pi.providers.models.reasoning': 'Supports reasoning',
1382
+ 'pi.providers.models.contextWindow': 'Context window',
1383
+ 'pi.providers.models.maxTokens': 'Max output',
1384
+ 'pi.providers.models.catalogFill': 'Auto-fill',
1385
+ 'pi.providers.models.catalogFilling': 'Filling…',
1386
+ 'pi.picker.searchPlaceholder': 'Search providers…',
1387
+ 'pi.picker.group.custom': 'Custom endpoint',
1388
+ 'pi.picker.group.apiKey': 'API key providers',
1389
+ 'pi.picker.empty': 'No matching provider templates',
1390
+ 'pi.picker.back': 'Back to template selection',
1377
1391
  'pi.providers.delete': 'Delete Provider',
1378
1392
  'pi.providers.addModal.title': 'Add Provider',
1379
1393
  'pi.providers.addModal.subtitle': 'Create a new Piper provider',
@@ -270,6 +270,7 @@ const ja = Object.freeze({
270
270
  'dashboard.doctor.export.json': 'JSON エクスポート',
271
271
  'dashboard.doctor.export.md': 'Markdown エクスポート',
272
272
  'dashboard.doctor.open': '開く',
273
+ 'dashboard.doctor.empty': 'まだチェックは実行されていません。「チェックを実行」で現在のセットアップを診断します。',
273
274
  'doctor.action.openConfig': 'Config を開く',
274
275
  'doctor.action.checkProvider': 'Provider 設定をチェック',
275
276
  'doctor.action.openUsage': 'Usage を開く',
@@ -1375,6 +1376,19 @@ const ja = Object.freeze({
1375
1376
  'pi.providers.models.remoteLoading': 'リモートモデル一覧を取得中…',
1376
1377
  'pi.providers.models.remoteFailed': 'モデル ID は手動入力できます。',
1377
1378
  'pi.providers.models.noMatch': '一致するモデルがありません',
1379
+ 'pi.providers.models.importTitle': 'エンドポイントからモデルをインポート',
1380
+ 'pi.providers.models.addSelected': '選択済みを追加({count})',
1381
+ 'pi.providers.models.name': '名前',
1382
+ 'pi.providers.models.reasoning': '推論をサポート',
1383
+ 'pi.providers.models.contextWindow': 'コンテキストウィンドウ',
1384
+ 'pi.providers.models.maxTokens': '最大出力',
1385
+ 'pi.providers.models.catalogFill': '自動入力',
1386
+ 'pi.providers.models.catalogFilling': '入力中…',
1387
+ 'pi.picker.searchPlaceholder': 'プロバイダーを検索…',
1388
+ 'pi.picker.group.custom': 'カスタムエンドポイント',
1389
+ 'pi.picker.group.apiKey': 'API キープロバイダー',
1390
+ 'pi.picker.empty': '一致するプロバイダーテンプレートがありません',
1391
+ 'pi.picker.back': 'テンプレート選択に戻る',
1378
1392
  'pi.providers.delete': 'プロバイダを削除',
1379
1393
  'pi.providers.addModal.title': 'プロバイダを追加',
1380
1394
  'pi.providers.addModal.subtitle': '新しい Piper プロバイダを作成',
@@ -539,6 +539,7 @@ const vi = Object.freeze({
539
539
  'dashboard.doctor.export.json': 'Xuất JSON',
540
540
  'dashboard.doctor.export.md': 'Xuất Markdown',
541
541
  'dashboard.doctor.open': 'Mở',
542
+ 'dashboard.doctor.empty': 'Chưa chạy lần kiểm tra nào. Nhấn "Chạy kiểm tra" để chẩn đoán cấu hình hiện tại.',
542
543
  'doctor.action.openConfig': 'Mở cấu hình',
543
544
  'doctor.action.checkProvider': 'Kiểm tra cấu hình nhà cung cấp',
544
545
  'doctor.action.openUsage': 'Mở thống kê',
@@ -1343,6 +1344,19 @@ const vi = Object.freeze({
1343
1344
  'pi.providers.models.remoteLoading': 'Đang tải danh sách mô hình từ xa…',
1344
1345
  'pi.providers.models.remoteFailed': 'Bạn vẫn có thể nhập ID mô hình thủ công.',
1345
1346
  'pi.providers.models.noMatch': 'Không có mô hình phù hợp',
1347
+ 'pi.providers.models.importTitle': 'Nhập mô hình từ endpoint',
1348
+ 'pi.providers.models.addSelected': 'Thêm đã chọn ({count})',
1349
+ 'pi.providers.models.name': 'Tên',
1350
+ 'pi.providers.models.reasoning': 'Hỗ trợ suy luận',
1351
+ 'pi.providers.models.contextWindow': 'Cửa sổ ngữ cảnh',
1352
+ 'pi.providers.models.maxTokens': 'Đầu ra tối đa',
1353
+ 'pi.providers.models.catalogFill': 'Tự động điền',
1354
+ 'pi.providers.models.catalogFilling': 'Đang điền…',
1355
+ 'pi.picker.searchPlaceholder': 'Tìm kiếm nhà cung cấp…',
1356
+ 'pi.picker.group.custom': 'Endpoint tùy chỉnh',
1357
+ 'pi.picker.group.apiKey': 'Nhà cung cấp API key',
1358
+ 'pi.picker.empty': 'Không có mẫu nhà cung cấp phù hợp',
1359
+ 'pi.picker.back': 'Quay lại chọn mẫu',
1346
1360
  'pi.providers.delete': 'Xóa nhà cung cấp',
1347
1361
  'pi.providers.addModal.title': 'Thêm nhà cung cấp',
1348
1362
  'pi.providers.addModal.subtitle': 'Tạo nhà cung cấp Piper mới',
@@ -268,6 +268,7 @@ const zhTw = Object.freeze({
268
268
  'dashboard.doctor.export.json': '匯出 JSON',
269
269
  'dashboard.doctor.export.md': '匯出 Markdown',
270
270
  'dashboard.doctor.open': '打開',
271
+ 'dashboard.doctor.empty': '尚未執行檢查。點擊「運行檢查」診斷目前設定。',
271
272
  'doctor.action.openConfig': '打開 Config',
272
273
  'doctor.action.checkProvider': '檢查 Provider 設定',
273
274
  'doctor.action.openUsage': '打開 Usage',
@@ -1372,6 +1373,19 @@ const zhTw = Object.freeze({
1372
1373
  'pi.providers.models.remoteLoading': '正在取得遠端模型清單…',
1373
1374
  'pi.providers.models.remoteFailed': '可手動輸入模型 ID。',
1374
1375
  'pi.providers.models.noMatch': '無符合模型',
1376
+ 'pi.providers.models.importTitle': '從端點匯入模型',
1377
+ 'pi.providers.models.addSelected': '新增選取({count})',
1378
+ 'pi.providers.models.name': '名稱',
1379
+ 'pi.providers.models.reasoning': '支援推理',
1380
+ 'pi.providers.models.contextWindow': '上下文視窗',
1381
+ 'pi.providers.models.maxTokens': '最大輸出',
1382
+ 'pi.providers.models.catalogFill': '自動填寫',
1383
+ 'pi.providers.models.catalogFilling': '填寫中…',
1384
+ 'pi.picker.searchPlaceholder': '搜尋供應商…',
1385
+ 'pi.picker.group.custom': '自訂端點',
1386
+ 'pi.picker.group.apiKey': 'API Key 服務商',
1387
+ 'pi.picker.empty': '沒有符合的供應商範本',
1388
+ 'pi.picker.back': '返回選擇範本',
1375
1389
  'pi.providers.delete': '刪除供應商',
1376
1390
  'pi.providers.addModal.title': '新增供應商',
1377
1391
  'pi.providers.addModal.subtitle': '建立新的 Piper 供應商',
@@ -268,6 +268,7 @@ const zh = Object.freeze({
268
268
  'dashboard.doctor.export.json': '导出 JSON',
269
269
  'dashboard.doctor.export.md': '导出 Markdown',
270
270
  'dashboard.doctor.open': '打开',
271
+ 'dashboard.doctor.empty': '尚未运行检查。点击上方“运行检查”诊断当前配置。',
271
272
  'doctor.action.openConfig': '打开 Config',
272
273
  'doctor.action.checkProvider': '检查 Provider 配置',
273
274
  'doctor.action.openUsage': '打开 Usage',
@@ -1372,6 +1373,19 @@ const zh = Object.freeze({
1372
1373
  'pi.providers.models.remoteLoading': '正在获取远端模型列表…',
1373
1374
  'pi.providers.models.remoteFailed': '可手动输入模型 ID。',
1374
1375
  'pi.providers.models.noMatch': '无匹配模型',
1376
+ 'pi.providers.models.importTitle': '从端点导入模型',
1377
+ 'pi.providers.models.addSelected': '添加选中({count})',
1378
+ 'pi.providers.models.name': '名称',
1379
+ 'pi.providers.models.reasoning': '支持推理',
1380
+ 'pi.providers.models.contextWindow': '上下文窗口',
1381
+ 'pi.providers.models.maxTokens': '最大输出',
1382
+ 'pi.providers.models.catalogFill': '自动填充',
1383
+ 'pi.providers.models.catalogFilling': '填充中…',
1384
+ 'pi.picker.searchPlaceholder': '搜索供应商…',
1385
+ 'pi.picker.group.custom': '自定义端点',
1386
+ 'pi.picker.group.apiKey': 'API Key 服务商',
1387
+ 'pi.picker.empty': '没有匹配的供应商模板',
1388
+ 'pi.picker.back': '返回选择模板',
1375
1389
  'pi.providers.delete': '删除供应商',
1376
1390
  'pi.providers.addModal.title': '添加供应商',
1377
1391
  'pi.providers.addModal.subtitle': '创建新的 Piper 供应商',
@@ -0,0 +1,26 @@
1
+ export const PI_PROVIDER_TEMPLATE_GROUPS = [
2
+ {
3
+ id: 'custom',
4
+ i18nKey: 'pi.picker.group.custom',
5
+ templates: [
6
+ { id: 'custom-openai', name: 'OpenAI Compatible', baseUrl: '', api: 'openai-completions' },
7
+ { id: 'custom-anthropic', name: 'Anthropic Compatible', baseUrl: '', api: 'anthropic-messages' }
8
+ ]
9
+ },
10
+ {
11
+ id: 'apiKey',
12
+ i18nKey: 'pi.picker.group.apiKey',
13
+ templates: [
14
+ { id: 'openai', name: 'OpenAI', baseUrl: 'https://api.openai.com/v1', api: 'openai-responses' },
15
+ { id: 'anthropic', name: 'Anthropic', baseUrl: 'https://api.anthropic.com', api: 'anthropic-messages' },
16
+ { id: 'google-gemini', name: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', api: 'google-generative-ai' },
17
+ { id: 'deepseek', name: 'DeepSeek', baseUrl: 'https://api.deepseek.com', api: 'openai-completions' },
18
+ { id: 'moonshot-kimi', name: 'Moonshot Kimi', baseUrl: 'https://api.moonshot.cn/v1', api: 'openai-completions' },
19
+ { id: 'zhipu-glm', name: 'Zhipu GLM', baseUrl: 'https://open.bigmodel.cn/api/paas/v4', api: 'openai-completions' },
20
+ { id: 'openrouter', name: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1', api: 'openai-completions' },
21
+ { id: 'xai-grok', name: 'xAI Grok', baseUrl: 'https://api.x.ai/v1', api: 'openai-completions' },
22
+ { id: 'groq', name: 'Groq', baseUrl: 'https://api.groq.com/openai/v1', api: 'openai-completions' },
23
+ { id: 'mistral', name: 'Mistral', baseUrl: 'https://api.mistral.ai/v1', api: 'openai-completions' }
24
+ ]
25
+ }
26
+ ];