sqlseed-web 0.2.4__py3-none-any.whl

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.
Files changed (87) hide show
  1. sqlseed_web/AGENTS.md +106 -0
  2. sqlseed_web/__init__.py +24 -0
  3. sqlseed_web/__main__.py +8 -0
  4. sqlseed_web/_application.py +205 -0
  5. sqlseed_web/ai_settings.py +290 -0
  6. sqlseed_web/api.py +1102 -0
  7. sqlseed_web/app.py +26 -0
  8. sqlseed_web/managed_worker.py +184 -0
  9. sqlseed_web/operation_errors.py +42 -0
  10. sqlseed_web/plugin_environment.py +231 -0
  11. sqlseed_web/plugin_management.py +322 -0
  12. sqlseed_web/plugin_process.py +131 -0
  13. sqlseed_web/runtime_lifecycle.py +130 -0
  14. sqlseed_web/runtime_session.py +97 -0
  15. sqlseed_web/settings_environment.py +368 -0
  16. sqlseed_web/sqlite_target.py +86 -0
  17. sqlseed_web/state.py +348 -0
  18. sqlseed_web/static/AGENTS.md +180 -0
  19. sqlseed_web/static/ai.css +57 -0
  20. sqlseed_web/static/configs.css +50 -0
  21. sqlseed_web/static/date-picker.css +230 -0
  22. sqlseed_web/static/disclosure.css +149 -0
  23. sqlseed_web/static/graph-clarity.css +103 -0
  24. sqlseed_web/static/index.html +29 -0
  25. sqlseed_web/static/js/api.js +228 -0
  26. sqlseed_web/static/js/app.js +97 -0
  27. sqlseed_web/static/js/dropdown.js +432 -0
  28. sqlseed_web/static/js/filepicker.js +203 -0
  29. sqlseed_web/static/js/genform.js +1066 -0
  30. sqlseed_web/static/js/labels.js +195 -0
  31. sqlseed_web/static/js/pages/browse.js +209 -0
  32. sqlseed_web/static/js/pages/configs.js +424 -0
  33. sqlseed_web/static/js/pages/connect.js +332 -0
  34. sqlseed_web/static/js/pages/heal.js +395 -0
  35. sqlseed_web/static/js/pages/meta.js +110 -0
  36. sqlseed_web/static/js/pages/runs.js +293 -0
  37. sqlseed_web/static/js/pages/settings.js +942 -0
  38. sqlseed_web/static/js/pages/wizard.js +751 -0
  39. sqlseed_web/static/js/pages/workbench.js +3123 -0
  40. sqlseed_web/static/js/tree.js +126 -0
  41. sqlseed_web/static/js/workbench/ai-eligibility.js +33 -0
  42. sqlseed_web/static/js/workbench/ai-handoff.js +31 -0
  43. sqlseed_web/static/js/workbench/ai-stream.js +116 -0
  44. sqlseed_web/static/js/workbench/ai.js +888 -0
  45. sqlseed_web/static/js/workbench/connection.js +508 -0
  46. sqlseed_web/static/js/workbench/date-picker.js +445 -0
  47. sqlseed_web/static/js/workbench/dependency-view.js +119 -0
  48. sqlseed_web/static/js/workbench/editor.js +1236 -0
  49. sqlseed_web/static/js/workbench/focus.js +11 -0
  50. sqlseed_web/static/js/workbench/graph-layout.js +332 -0
  51. sqlseed_web/static/js/workbench/graph.js +970 -0
  52. sqlseed_web/static/js/workbench/guidance.js +29 -0
  53. sqlseed_web/static/js/workbench/model.js +124 -0
  54. sqlseed_web/static/js/workbench/plugin-management.js +512 -0
  55. sqlseed_web/static/js/workbench/preview-scroll-layout.js +94 -0
  56. sqlseed_web/static/js/workbench/preview.js +572 -0
  57. sqlseed_web/static/js/workbench/provider-guide.js +33 -0
  58. sqlseed_web/static/js/workbench/recovery.js +28 -0
  59. sqlseed_web/static/js/workbench/scroll-lock.js +26 -0
  60. sqlseed_web/static/js/workbench/session.js +174 -0
  61. sqlseed_web/static/js/workbench/table-data.js +186 -0
  62. sqlseed_web/static/js/workbench/ui.js +262 -0
  63. sqlseed_web/static/navigation.css +92 -0
  64. sqlseed_web/static/preview.css +29 -0
  65. sqlseed_web/static/runs.css +53 -0
  66. sqlseed_web/static/scrollbars.css +42 -0
  67. sqlseed_web/static/settings.css +108 -0
  68. sqlseed_web/static/style.css +3382 -0
  69. sqlseed_web/static/table-data.css +27 -0
  70. sqlseed_web/static/workbench.css +509 -0
  71. sqlseed_web/supervised_plugins.py +173 -0
  72. sqlseed_web/supervisor.py +238 -0
  73. sqlseed_web/workbench.py +381 -0
  74. sqlseed_web/workbench_ai.py +887 -0
  75. sqlseed_web/workbench_ai_relations.py +285 -0
  76. sqlseed_web/workbench_ai_stream.py +172 -0
  77. sqlseed_web/workbench_data.py +163 -0
  78. sqlseed_web/workbench_execution.py +199 -0
  79. sqlseed_web/workbench_runtime.py +1218 -0
  80. sqlseed_web/workbench_schema.py +277 -0
  81. sqlseed_web/workbench_store.py +458 -0
  82. sqlseed_web/worker_control.py +192 -0
  83. sqlseed_web-0.2.4.dist-info/METADATA +105 -0
  84. sqlseed_web-0.2.4.dist-info/RECORD +87 -0
  85. sqlseed_web-0.2.4.dist-info/WHEEL +4 -0
  86. sqlseed_web-0.2.4.dist-info/entry_points.txt +2 -0
  87. sqlseed_web-0.2.4.dist-info/licenses/LICENSE +679 -0
@@ -0,0 +1,942 @@
1
+ import { h, api, store } from '../api.js';
2
+ import { button } from '../workbench/ui.js';
3
+ import { createDropdown } from '../dropdown.js';
4
+ import { peekAIHandoff, requestAIReturn, leaveAISettings } from '../workbench/ai-handoff.js';
5
+ import { createPluginManagement, componentImpact } from '../workbench/plugin-management.js';
6
+ const prefix = '/api/workbench/ai';
7
+ const backends = [{
8
+ value: 'ollama',
9
+ label: 'Ollama'
10
+ }, {
11
+ value: 'lm_studio',
12
+ label: 'LM Studio'
13
+ }, {
14
+ value: 'openai_compat',
15
+ label: 'OpenAI 兼容服务'
16
+ }, {
17
+ value: 'google_ai_studio',
18
+ label: 'Google AI Studio'
19
+ }];
20
+ const defaults = {
21
+ ollama: 'http://localhost:11434/v1',
22
+ lm_studio: 'http://localhost:1234/v1',
23
+ google_ai_studio: 'https://generativelanguage.googleapis.com/v1beta/openai'
24
+ };
25
+ const keySources = {
26
+ session: '密钥仅在本次服务中有效,重启后需重新填写。',
27
+ environment: '密钥来自服务环境变量。',
28
+ none: '尚未配置密钥。'
29
+ };
30
+ const descriptions = {
31
+ core: '离线生成、约束处理与数据库写入',
32
+ cli: '在终端配置、预览和生成数据',
33
+ web: '当前浏览器工作台',
34
+ ai: '根据表结构与业务说明提出规则建议',
35
+ mcp: '供 MCP 客户端调用生成能力',
36
+ base: '内置基础数据与语义占位值',
37
+ faker: '姓名、地址等本地化测试数据',
38
+ mimesis: '另一种本地化数据实现,可按场景选择'
39
+ };
40
+ let root, aiPanel, pluginsPanel, sections, form, backend, endpoint, modelInput, key, clearKey;
41
+ let notice, saveState, badge, currentService, readinessNote, keyLabel, keyHint, serviceHint, testNotice, models, save, probe, reset, returnButton, returnPrompt, storageInfo;
42
+ let config = null,
43
+ loaded = false,
44
+ busy = false,
45
+ version = 0,
46
+ configController = null;
47
+ let settingsVersion = 0,
48
+ settingsStale = false;
49
+ let environmentList,
50
+ environmentNotice,
51
+ environmentSummary,
52
+ refreshButton,
53
+ environmentController = null,
54
+ environmentVersion = 0;
55
+ let installationVersion = 0;
56
+ let currentSection = 'ai';
57
+ let management,
58
+ environmentInfo = null,
59
+ environmentLoading = false;
60
+ const field = (label, input, help) => h('label', {
61
+ class: 'settings-field'
62
+ }, h('span', {}, label), input, help ? h('small', {
63
+ class: 'muted'
64
+ }, help) : null);
65
+ const currentDraft = () => ({
66
+ backend: backend.get(),
67
+ model: modelInput.value.trim(),
68
+ base_url: endpoint.value.trim(),
69
+ api_key: key.value,
70
+ clear_api_key: clearKey.checked
71
+ });
72
+ function dirty() {
73
+ if (!loaded || !config) {
74
+ return false;
75
+ }
76
+ const effective = config.effective || {},
77
+ draft = currentDraft();
78
+ return Boolean(draft.api_key || draft.clear_api_key || ['backend', 'model', 'base_url'].some(name => draft[name] !== (effective[name] || (name === 'backend' ? 'ollama' : ''))));
79
+ }
80
+ export function render() {
81
+ management?.destroy();
82
+ invalidateInstallationCopies();
83
+ version++;
84
+ loaded = false;
85
+ busy = false;
86
+ config = null;
87
+ settingsStale = false;
88
+ environmentInfo = null;
89
+ environmentLoading = false;
90
+ currentSection = new URLSearchParams(location.hash.split('?')[1] || '').get('section') === 'plugins' ? 'plugins' : 'ai';
91
+ notice = h('p', {
92
+ class: 'settings-notice',
93
+ role: 'status',
94
+ 'aria-live': 'polite',
95
+ 'data-settings-notice': ''
96
+ }, '正在读取设置…');
97
+ saveState = h('p', {
98
+ class: 'settings-save-state muted',
99
+ role: 'status',
100
+ 'data-settings-save-state': ''
101
+ });
102
+ badge = h('span', {
103
+ class: 'settings-badge'
104
+ }, '读取中');
105
+ currentService = h('p', {
106
+ class: 'settings-current-model'
107
+ }, '正在读取当前服务…');
108
+ readinessNote = h('p', {
109
+ class: 'settings-readiness muted'
110
+ });
111
+ keyLabel = h('span', {
112
+ 'data-settings-key-label': ''
113
+ }, 'API Key');
114
+ keyHint = h('p', {
115
+ class: 'muted settings-key-hint'
116
+ });
117
+ serviceHint = h('p', {
118
+ class: 'settings-service-hint muted'
119
+ });
120
+ testNotice = h('p', {
121
+ class: 'settings-test-notice',
122
+ role: 'status',
123
+ 'aria-live': 'polite',
124
+ 'data-settings-test': ''
125
+ });
126
+ models = h('div', {
127
+ class: 'settings-models',
128
+ 'aria-label': '服务中的模型'
129
+ });
130
+ modelInput = h('input', {
131
+ type: 'text',
132
+ 'aria-label': '模型名称',
133
+ placeholder: '填写服务提供的完整模型 ID',
134
+ maxlength: 200,
135
+ autocomplete: 'off',
136
+ oninput: changed
137
+ });
138
+ endpoint = h('input', {
139
+ type: 'url',
140
+ 'aria-label': 'AI 服务地址',
141
+ placeholder: 'https://example.com/v1',
142
+ maxlength: 2000,
143
+ autocomplete: 'off',
144
+ oninput: changed
145
+ });
146
+ key = h('input', {
147
+ type: 'password',
148
+ 'aria-label': 'API Key',
149
+ value: '',
150
+ placeholder: '输入新密钥',
151
+ maxlength: 4000,
152
+ autocomplete: 'new-password',
153
+ oninput: () => {
154
+ if (key.value) {
155
+ clearKey.checked = false;
156
+ }
157
+ changed();
158
+ }
159
+ });
160
+ clearKey = h('input', {
161
+ type: 'checkbox',
162
+ 'aria-label': '停用已配置密钥',
163
+ onchange: () => {
164
+ if (clearKey.checked) {
165
+ key.value = '';
166
+ }
167
+ changed();
168
+ }
169
+ });
170
+ backend = createDropdown({
171
+ label: 'AI 服务',
172
+ value: 'ollama',
173
+ options: backends,
174
+ onChange: value => {
175
+ endpoint.value = defaults[value] || '';
176
+ modelInput.value = '';
177
+ key.value = '';
178
+ clearKey.checked = false;
179
+ changed();
180
+ }
181
+ });
182
+ save = button('保存设置', () => submit(false), {
183
+ primary: true
184
+ });
185
+ probe = button('检测连接', () => submit(true));
186
+ reset = button('撤销修改', () => {
187
+ if (!busy && config) {
188
+ populate();
189
+ testNotice.textContent = '';
190
+ models.replaceChildren();
191
+ notice.textContent = '已恢复当前生效的设置。';
192
+ }
193
+ });
194
+ storageInfo = h('details', {
195
+ class: 'settings-storage'
196
+ }, h('summary', {}, '保存范围与有效期'));
197
+ form = h('div', {
198
+ class: 'settings-form'
199
+ }, h('div', {
200
+ class: 'settings-fields'
201
+ }, field('AI 服务', backend.el), field('服务地址', endpoint)), serviceHint, field('默认模型', modelInput, '检测连接可读取服务中的模型列表;不会自动开始分析。'), testNotice, models, h('div', {
202
+ class: 'settings-key'
203
+ }, field(keyLabel, key), keyHint, h('label', {
204
+ class: 'settings-clear-key'
205
+ }, clearKey, '停用本次服务的密钥')), h('div', {
206
+ class: 'settings-actions'
207
+ }, save, probe, reset), saveState, storageInfo);
208
+ const installation = h('section', {
209
+ class: 'settings-install',
210
+ 'data-ai-install': '',
211
+ hidden: true
212
+ });
213
+ aiPanel = h('section', {
214
+ id: 'settings-ai',
215
+ class: 'settings-panel',
216
+ role: 'tabpanel',
217
+ 'aria-labelledby': 'settings-tab-ai'
218
+ }, h('header', {
219
+ class: 'settings-panel-head'
220
+ }, h('div', {}, h('h2', {}, 'AI 服务'), h('p', {
221
+ class: 'muted'
222
+ }, '配置默认服务,供工作台的 AI 配置助手使用。')), badge), h('section', {
223
+ class: 'settings-current',
224
+ 'aria-label': '当前使用的 AI 服务'
225
+ }, h('span', {
226
+ class: 'muted'
227
+ }, '当前使用'), currentService, readinessNote), installation, form, notice);
228
+ environmentNotice = h('p', {
229
+ class: 'settings-notice',
230
+ role: 'status',
231
+ 'aria-live': 'polite'
232
+ });
233
+ environmentSummary = h('p', {
234
+ class: 'muted'
235
+ }, '正在读取运行环境…');
236
+ environmentList = h('div', {
237
+ class: 'settings-environment'
238
+ });
239
+ refreshButton = button('刷新状态', refreshPlugins, {
240
+ glyph: 'refresh'
241
+ });
242
+ management = createPluginManagement({
243
+ initialEnabled: document.documentElement?.dataset.pluginMaintenance === 'true',
244
+ onChange: () => {
245
+ updatePluginControls();
246
+ refreshButton.disabled = environmentLoading || management.refreshBlocked;
247
+ },
248
+ onMode: setMaintenanceMode,
249
+ onRestored: async () => {
250
+ const expected = version,
251
+ preserveDraft = dirty();
252
+ const request = ++settingsVersion;
253
+ settingsStale = true;
254
+ configController?.abort();
255
+ busy = false;
256
+ probe.textContent = '检测连接';
257
+ save.textContent = '保存设置';
258
+ testNotice.textContent = '';
259
+ models.replaceChildren();
260
+ update();
261
+ await Promise.all([refreshEnvironment({
262
+ replace: true
263
+ }), refreshCurrentConnection(expected)]);
264
+ if (expected !== version || request !== settingsVersion) {
265
+ return;
266
+ }
267
+ await loadSettings({
268
+ preserveDraft
269
+ });
270
+ if (expected === version && request === settingsVersion) {
271
+ window.dispatchEvent(new Event('sqlseed:plugins-changed'));
272
+ }
273
+ }
274
+ });
275
+ pluginsPanel = h('section', {
276
+ id: 'settings-plugins',
277
+ class: 'settings-panel',
278
+ role: 'tabpanel',
279
+ 'aria-labelledby': 'settings-tab-plugins'
280
+ }, h('header', {
281
+ class: 'settings-panel-head'
282
+ }, h('div', {}, h('h2', {}, '插件与版本'), h('p', {
283
+ class: 'muted'
284
+ }, '查看当前 Web 服务所在 Python 环境中的组件。')), refreshButton), management.el, environmentSummary, environmentNotice, environmentList);
285
+ sections = [];
286
+ for (const [id, label] of [['ai', 'AI 服务'], ['plugins', '插件与版本']]) {
287
+ const tab = button(label, () => selectSection(id), {
288
+ id: `settings-tab-${id}`,
289
+ role: 'tab',
290
+ 'aria-controls': `settings-${id}`
291
+ });
292
+ tab.onkeydown = event => {
293
+ const index = sections.findIndex(item => item.id === id);
294
+ let target;
295
+ if (event.key === 'Home') {
296
+ target = 0;
297
+ } else if (event.key === 'End') {
298
+ target = 1;
299
+ } else if (['ArrowDown', 'ArrowRight'].includes(event.key)) {
300
+ target = (index + 1) % 2;
301
+ } else if (['ArrowUp', 'ArrowLeft'].includes(event.key)) {
302
+ target = (index + 1) % 2;
303
+ } else {
304
+ target = null;
305
+ }
306
+ if (target !== null) {
307
+ event.preventDefault();
308
+ selectSection(sections[target].id);
309
+ sections[target].button.focus();
310
+ }
311
+ };
312
+ sections.push({
313
+ id,
314
+ button: tab
315
+ });
316
+ }
317
+ const returnInfo = peekAIHandoff(store.connId);
318
+ returnButton = button('返回 AI 助手', goBack, {
319
+ hidden: !returnInfo
320
+ });
321
+ returnPrompt = h('div', {
322
+ class: 'settings-return-prompt',
323
+ hidden: true,
324
+ role: 'alert'
325
+ }, h('p', {}, 'AI 服务设置尚未保存。可以继续编辑并保存,或放弃修改后返回。'), h('div', {
326
+ class: 'settings-actions'
327
+ }, button('继续编辑', () => {
328
+ returnPrompt.hidden = true;
329
+ modelInput.focus();
330
+ }), button('放弃修改并返回', returnToAssistant)));
331
+ root = h('div', {
332
+ class: 'page settings-page'
333
+ }, h('header', {
334
+ class: 'heading'
335
+ }, h('div', {}, h('h1', {}, '设置'), h('p', {
336
+ class: 'subtitle'
337
+ }, '管理 AI 服务和扩展能力。')), returnButton), returnPrompt, h('div', {
338
+ class: 'settings-layout'
339
+ }, h('div', {
340
+ class: 'settings-tabs',
341
+ role: 'tablist',
342
+ 'aria-label': '设置分类',
343
+ 'aria-orientation': 'vertical'
344
+ }, ...sections.map(item => item.button)), h('div', {
345
+ class: 'settings-panels'
346
+ }, aiPanel, pluginsPanel)));
347
+ selectSection(currentSection);
348
+ setMaintenanceMode(management.maintenance);
349
+ update();
350
+ return root;
351
+ }
352
+ export async function mount() {
353
+ const expected = version,
354
+ current = management;
355
+ await Promise.all([current.refresh(), refreshEnvironment()]);
356
+ if (expected === version && !current.maintenance) {
357
+ await loadSettings();
358
+ }
359
+ }
360
+ export function unmount() {
361
+ management?.destroy();
362
+ invalidateInstallationCopies();
363
+ version++;
364
+ environmentVersion++;
365
+ configController?.abort();
366
+ environmentController?.abort();
367
+ backend?.destroy();
368
+ if (key) {
369
+ key.value = '';
370
+ }
371
+ leaveAISettings(location.hash);
372
+ }
373
+ function selectSection(id) {
374
+ if (id === 'ai' && management?.maintenance) {
375
+ return;
376
+ }
377
+ if (currentSection !== id) {
378
+ invalidateInstallationCopies();
379
+ }
380
+ currentSection = id;
381
+ aiPanel.hidden = id !== 'ai';
382
+ pluginsPanel.hidden = id !== 'plugins';
383
+ for (const item of sections) {
384
+ item.button.setAttribute('aria-selected', String(item.id === id));
385
+ item.button.setAttribute('tabindex', item.id === id ? '0' : '-1');
386
+ }
387
+ }
388
+ function setMaintenanceMode(enabled) {
389
+ const aiTab = sections.find(item => item.id === 'ai');
390
+ aiTab.button.disabled = enabled;
391
+ aiTab.button.title = enabled ? '维护模式仅提供插件管理,重启普通模式后可配置 AI。' : '';
392
+ if (enabled) {
393
+ configController?.abort();
394
+ loaded = false;
395
+ config = null;
396
+ busy = false;
397
+ key.value = '';
398
+ selectSection('plugins');
399
+ update();
400
+ returnButton.hidden = true;
401
+ }
402
+ }
403
+ function goBack() {
404
+ if (busy) {
405
+ return;
406
+ }
407
+ if (dirty()) {
408
+ returnPrompt.hidden = false;
409
+ } else {
410
+ returnToAssistant();
411
+ }
412
+ }
413
+ function returnToAssistant() {
414
+ if (busy) {
415
+ return;
416
+ }
417
+ const destination = requestAIReturn(store.connId);
418
+ if (destination) {
419
+ location.hash = destination;
420
+ } else {
421
+ returnPrompt.hidden = true;
422
+ notice.textContent = '原来的分析上下文已失效,请从工作台重新打开 AI 助手。';
423
+ }
424
+ }
425
+ function changed() {
426
+ invalidateInstallationCopies();
427
+ if (testNotice.textContent) {
428
+ testNotice.textContent = '设置已变化,请重新检测。';
429
+ }
430
+ models.replaceChildren();
431
+ notice.textContent = '';
432
+ update();
433
+ }
434
+ function update() {
435
+ const disabled = busy || settingsStale || !loaded || !config?.available;
436
+ for (const input of [endpoint, modelInput, key, clearKey]) {
437
+ input.disabled = disabled;
438
+ }
439
+ backend.el.querySelector('button').disabled = disabled;
440
+ save.disabled = disabled || !dirty();
441
+ probe.disabled = disabled;
442
+ reset.disabled = disabled || !dirty();
443
+ if (disabled) {
444
+ saveState.textContent = '';
445
+ } else if (dirty()) {
446
+ saveState.textContent = '有未保存的修改;检测连接不会保存。';
447
+ } else {
448
+ saveState.textContent = '没有待保存的更改。可直接检测连接。';
449
+ }
450
+ save.title = !disabled && !dirty() ? '当前表单与生效设置一致,无需重复保存' : '';
451
+ returnButton.disabled = busy;
452
+ for (const option of models.querySelectorAll('button')) {
453
+ option.disabled = disabled;
454
+ }
455
+ form.setAttribute('aria-busy', String(busy));
456
+ updateConfigurationBadge();
457
+ if (backend.get() === 'ollama') {
458
+ serviceHint.textContent = 'Ollama 可连接本地或云端模型;本机服务地址不代表模型一定在本机运行。';
459
+ } else if (backend.get() === 'lm_studio') {
460
+ serviceHint.textContent = '填写 LM Studio 服务地址,并在服务中加载需要使用的模型。';
461
+ } else {
462
+ serviceHint.textContent = '使用提供方公布的 API 地址和模型 ID。OpenAI 兼容服务地址通常以 /v1 结尾。';
463
+ }
464
+ updateAuthenticationHint();
465
+ function updateConfigurationBadge() {
466
+ if (settingsStale) {
467
+ if (busy) {
468
+ badge.textContent = '读取中';
469
+ } else {
470
+ badge.textContent = '待重新读取';
471
+ }
472
+ } else if (!loaded) {
473
+ badge.textContent = '读取中';
474
+ } else if (!config) {
475
+ badge.textContent = '读取失败';
476
+ } else if (!config.available) {
477
+ if (config.availability_status === 'import_error') {
478
+ badge.textContent = '加载异常';
479
+ } else {
480
+ badge.textContent = '未安装';
481
+ }
482
+ } else if (dirty()) {
483
+ badge.textContent = '未保存';
484
+ } else if (config.ready) {
485
+ badge.textContent = '配置已填写';
486
+ } else {
487
+ badge.textContent = '待配置';
488
+ }
489
+ }
490
+ }
491
+ function updateAuthenticationHint() {
492
+ const effective = config?.effective || {};
493
+ const target = serviceIdentity(backend.get(), endpoint.value);
494
+ const sameService = Boolean(target) && target === serviceIdentity(effective.backend, effective.base_url || '');
495
+ const hasKey = sameService && effective.api_key_present;
496
+ const local = ['ollama', 'lm_studio'].includes(backend.get());
497
+ keyLabel.textContent = local ? 'API Key(通常无需填写)' : 'API Key(按服务要求填写)';
498
+ if (hasKey) {
499
+ key.placeholder = '留空保留同一服务的已配置密钥';
500
+ } else if (local) {
501
+ key.placeholder = '服务启用了认证时填写';
502
+ } else {
503
+ key.placeholder = '填写此服务的密钥';
504
+ }
505
+ if (hasKey) {
506
+ keyHint.textContent = keySources[config.sources?.api_key] || keySources.session;
507
+ } else if (local) {
508
+ keyHint.textContent = '默认本地服务无需密钥,修改地址或模型后即可保存;启用认证时按服务要求填写。';
509
+ } else {
510
+ keyHint.textContent = '按提供方要求配置认证;不同服务之间不会沿用已配置的密钥。';
511
+ }
512
+ clearKey.closest('label').hidden = !hasKey;
513
+ }
514
+ function serviceIdentity(service, address) {
515
+ let raw = address.trim() || defaults[service] || '';
516
+ let end = raw.length;
517
+ while (raw[end - 1] === '/') {
518
+ end--;
519
+ }
520
+ raw = raw.slice(0, end);
521
+ try {
522
+ const url = new URL(raw);
523
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || /[\s\\]/.test(raw)) {
524
+ return '';
525
+ }
526
+ // Keep the configured path: URL normalizes dot segments, while the server
527
+ // binds credentials to the complete configured endpoint.
528
+ const path = raw.match(/^https?:\/\/[^/]*(\/.*)?$/i)?.[1] || '';
529
+ return JSON.stringify([service, url.protocol, url.hostname, url.port || (url.protocol === 'https:' ? '443' : '80'), path]);
530
+ } catch {
531
+ return '';
532
+ }
533
+ }
534
+ function populate({
535
+ preserveDraft = false
536
+ } = {}) {
537
+ const effective = config.effective || {};
538
+ if (!preserveDraft) {
539
+ backend.set(effective.backend || 'ollama');
540
+ endpoint.value = effective.base_url || '';
541
+ modelInput.value = effective.model || '';
542
+ key.value = '';
543
+ clearKey.checked = false;
544
+ }
545
+ currentService.textContent = effective.model ? `${backends.find(item => item.value === effective.backend)?.label || effective.backend} · ${effective.model}` : '尚未选择默认模型';
546
+ const serviceCard = currentService.closest('.settings-current');
547
+ serviceCard.querySelector('span').textContent = config.available ? '当前使用' : '已保存的服务配置';
548
+ serviceCard.setAttribute('aria-label', config.available ? '当前使用的 AI 服务' : '已保存的 AI 服务');
549
+ readinessNote.textContent = config.available ? config.message || '配置状态与服务连通性分别检查。' : '';
550
+ storageInfo.replaceChildren(h('summary', {}, '保存范围与有效期'), h('p', {}, '服务类型、地址和模型保存在运行 Web 的设备上,重启后仍保留。同一 Web 服务的浏览器共享这些设置;修改后用于下一次分析。'), h('p', {}, '界面填写的密钥仅在本次服务中有效;如需重启后继续使用,可在启动环境中配置。密钥不写入生成配置或运行记录。'), ...(config.storage?.path ? [h('div', {
551
+ class: 'settings-storage-location'
552
+ }, h('strong', {}, '当前 Web 服务的配置文件位置'), h('p', {
553
+ class: 'mono'
554
+ }, config.storage.path), h('p', {}, '按启动 Web 的用户和操作系统自动选择;也可通过 SQLSEED_WEB_SETTINGS_PATH 指定。远程访问时,这里是服务器上的位置。'))] : []));
555
+ const installation = aiPanel.querySelector('[data-ai-install]');
556
+ installation.hidden = Boolean(config.available);
557
+ if (!config.available) {
558
+ const broken = config.availability_status === 'import_error';
559
+ const installationHelp = () => {
560
+ if (!managedInstallAvailable(management?.controls({
561
+ id: 'ai'
562
+ })) || broken) {
563
+ return [h('details', {
564
+ class: 'settings-package-help'
565
+ }, h('summary', {}, '管理员排查信息'), ...installationInstructions(config.installer, broken ? config.repair_command : config.install_command))];
566
+ } else {
567
+ return [];
568
+ }
569
+ };
570
+ installation.replaceChildren(h('h3', {}, broken ? 'AI 插件加载异常' : 'AI 扩展未安装'), h('p', {}, `${componentImpact('ai')} 已保存的服务设置会保留。`), ...(broken ? [h('p', {}, config.message || 'AI 插件已安装,但加载失败,请检查运行 Web 的环境依赖。')] : []), button(broken ? '查看插件状态' : '前往安装', () => selectSection('plugins'), {
571
+ primary: true
572
+ }), ...installationHelp());
573
+ }
574
+ form.hidden = !config.available;
575
+ update();
576
+ }
577
+ async function loadSettings({
578
+ preserveDraft = false
579
+ } = {}) {
580
+ if (busy || management?.maintenance) {
581
+ return;
582
+ }
583
+ preserveDraft ||= settingsStale && dirty();
584
+ const expected = version,
585
+ request = settingsVersion;
586
+ const current = () => expected === version && request === settingsVersion;
587
+ busy = true;
588
+ configController = new AbortController();
589
+ update();
590
+ try {
591
+ const response = await api(`${prefix}/config`, {
592
+ signal: configController.signal
593
+ });
594
+ if (!current() || management?.maintenance) {
595
+ return;
596
+ }
597
+ config = structuredClone(response);
598
+ loaded = true;
599
+ settingsStale = false;
600
+ populate({
601
+ preserveDraft
602
+ });
603
+ notice.textContent = '';
604
+ } catch (error) {
605
+ if (current()) {
606
+ loaded = true;
607
+ notice.replaceChildren(`无法读取 AI 设置:${error.message} `, button('重试读取', loadSettings));
608
+ }
609
+ } finally {
610
+ if (current()) {
611
+ busy = false;
612
+ update();
613
+ }
614
+ }
615
+ }
616
+ async function submit(testOnly) {
617
+ if (busy || settingsStale || management?.maintenance || !loaded || !config?.available || !testOnly && !dirty()) {
618
+ return;
619
+ }
620
+ const expected = version,
621
+ request = settingsVersion,
622
+ snapshot = currentDraft();
623
+ const current = () => expected === version && request === settingsVersion;
624
+ busy = true;
625
+ configController = new AbortController();
626
+ update();
627
+ showSubmissionProgress();
628
+ try {
629
+ const response = await api(`${prefix}/${testOnly ? 'test' : 'config'}`, {
630
+ method: 'POST',
631
+ body: JSON.stringify(snapshot),
632
+ signal: configController.signal
633
+ });
634
+ if (!current() || management?.maintenance) {
635
+ return;
636
+ }
637
+ applySettingsResponse(response);
638
+ } catch (error) {
639
+ if (current()) {
640
+ (testOnly ? testNotice : notice).textContent = `${testOnly ? '检测' : '保存'}失败:${error.message}`;
641
+ }
642
+ } finally {
643
+ snapshot.api_key = '';
644
+ if (current()) {
645
+ busy = false;
646
+ probe.textContent = '检测连接';
647
+ save.textContent = '保存设置';
648
+ update();
649
+ }
650
+ }
651
+ function showSubmissionProgress() {
652
+ probe.textContent = testOnly ? '检测中…' : '检测连接';
653
+ save.textContent = testOnly ? '保存设置' : '保存中…';
654
+ if (testOnly) {
655
+ testNotice.textContent = '正在检测服务连接…';
656
+ models.replaceChildren();
657
+ } else {
658
+ notice.textContent = '正在保存设置…';
659
+ }
660
+ }
661
+ function applySettingsResponse(response) {
662
+ if (testOnly) {
663
+ showConnectionProbe(response);
664
+ } else {
665
+ config = structuredClone(response);
666
+ populate();
667
+ returnPrompt.hidden = true;
668
+ notice.textContent = '设置已保存,下一次 AI 分析将使用此配置。';
669
+ window.dispatchEvent(new Event('sqlseed:ai-settings-changed'));
670
+ }
671
+ }
672
+ }
673
+ async function refreshPlugins() {
674
+ if (refreshButton.disabled) {
675
+ return;
676
+ }
677
+ return Promise.all([management.refresh(), refreshEnvironment()]);
678
+ }
679
+ async function refreshCurrentConnection(expected) {
680
+ const id = store.connId;
681
+ if (!id) {
682
+ return;
683
+ }
684
+ try {
685
+ const response = await api('/api/connections');
686
+ if (expected !== version || store.connId !== id || !response.connections?.some(item => item.conn_id === id)) {
687
+ return;
688
+ }
689
+ const detail = await api(`/api/connections/${encodeURIComponent(id)}/tables`);
690
+ if (expected === version && store.connId === id) {
691
+ store.tables = detail.tables || [];
692
+ }
693
+ } catch {
694
+ // Keep the selected target and its in-memory draft; never pick another database.
695
+ if (expected === version) {
696
+ environmentNotice.textContent = '组件状态已刷新;当前数据库连接信息暂时无法更新。';
697
+ }
698
+ }
699
+ }
700
+ async function refreshEnvironment({
701
+ replace = false
702
+ } = {}) {
703
+ if (environmentLoading && !replace) {
704
+ return;
705
+ }
706
+ environmentController?.abort();
707
+ invalidateInstallationCopies();
708
+ const expected = version,
709
+ request = ++environmentVersion;
710
+ environmentController = new AbortController();
711
+ environmentLoading = true;
712
+ refreshButton.disabled = true;
713
+ environmentNotice.textContent = '正在读取组件状态…';
714
+ try {
715
+ const info = await api('/api/settings/environment', {
716
+ signal: environmentController.signal
717
+ });
718
+ if (expected !== version || request !== environmentVersion) {
719
+ return;
720
+ }
721
+ environmentInfo = info;
722
+ renderEnvironment();
723
+ environmentNotice.textContent = '状态已更新。';
724
+ } catch (error) {
725
+ if (expected === version && request === environmentVersion) {
726
+ environmentNotice.textContent = `无法刷新组件状态:${error.message}`;
727
+ }
728
+ } finally {
729
+ if (expected === version && request === environmentVersion) {
730
+ environmentLoading = false;
731
+ refreshButton.disabled = management.refreshBlocked;
732
+ }
733
+ }
734
+ }
735
+ function renderEnvironment() {
736
+ if (!environmentInfo) {
737
+ return;
738
+ }
739
+ const info = environmentInfo,
740
+ packages = info.packages || [];
741
+ environmentSummary.textContent = `${info.python?.implementation || 'Python'} ${info.python?.version || '版本未知'} · 当前 Web 服务环境`;
742
+ const application = item => item.category ? item.category === 'application' : ['core', 'web'].includes(item.id);
743
+ const group = (title, items) => h('section', {
744
+ class: 'settings-package-group'
745
+ }, h('h3', {}, title), ...items.map(item => packageRow(item, info.installer)));
746
+ environmentList.replaceChildren(group('当前应用', packages.filter(application)), group('可选扩展', packages.filter(item => !application(item))), group('数据生成引擎', info.providers || []), h('p', {
747
+ class: 'muted'
748
+ }, management?.maintenance ? '版本来自当前安装环境;维护模式不加载组件。安装或卸载后需重启普通模式,再验证组件可用性。' : '版本来自当前安装环境,不表示最新发布版本。“可用”表示组件可加载;AI 服务需单独检测。生成引擎在工作台选择,可选扩展按需安装。'));
749
+ }
750
+ function updatePluginControls() {
751
+ const items = [...(environmentInfo?.packages || []), ...(environmentInfo?.providers || [])];
752
+ for (const row of environmentList.querySelectorAll('[data-package-id]')) {
753
+ const item = items.find(value => value.id === row.dataset.packageId);
754
+ const holder = row.querySelector('[data-plugin-controls]');
755
+ const controls = item && management.controls(item);
756
+ holder.replaceChildren(...(controls ? [controls] : []));
757
+ const guide = row.querySelector('.settings-package-help');
758
+ if (guide) {
759
+ guide.hidden = !item?.installed && managedInstallAvailable(controls);
760
+ }
761
+ }
762
+ }
763
+ function managedInstallAvailable(controls) {
764
+ return Boolean(management?.automatic && controls?.querySelector('[data-plugin-action="install"]'));
765
+ }
766
+ function installationInstructions(installer, command) {
767
+ const usable = installer?.available !== false && typeof command === 'string' && command;
768
+ const message = installer?.message || (!usable ? '未提供可直接执行的命令。请使用创建此 Python 环境的工具安装或修复组件。' : '');
769
+ return [...(message ? [h('p', {
770
+ class: 'muted'
771
+ }, message)] : []), ...(!usable && installer?.python_executable ? [h('p', {
772
+ class: 'mono muted'
773
+ }, `目标解释器:${installer.python_executable}`)] : []), h('p', {
774
+ class: 'muted'
775
+ }, `在运行 Web 的设备终端${installer?.shell === 'powershell' ? '(PowerShell)' : ''}操作,完成后重启 Web 并刷新状态。`), ...(usable ? [copyableCommand(command)] : [])];
776
+ }
777
+ function invalidateInstallationCopies() {
778
+ installationVersion++;
779
+ for (const status of root?.querySelectorAll('[data-install-copy-status]') || []) {
780
+ status.textContent = '';
781
+ }
782
+ }
783
+ function copyableCommand(command) {
784
+ const ownerVersion = version;
785
+ let copying = false;
786
+ const status = h('span', {
787
+ class: 'settings-copy-status muted',
788
+ role: 'status',
789
+ 'aria-live': 'polite',
790
+ 'data-install-copy-status': ''
791
+ });
792
+ const copy = button('复制命令', async () => {
793
+ if (copying || ownerVersion !== version || !container.isConnected) {
794
+ return;
795
+ }
796
+ const expected = installationVersion;
797
+ const current = () => ownerVersion === version && expected === installationVersion && container.isConnected;
798
+ copying = true;
799
+ copy.disabled = true;
800
+ copy.textContent = '复制中…';
801
+ status.textContent = '';
802
+ try {
803
+ const clipboard = globalThis.navigator?.clipboard;
804
+ if (typeof clipboard?.writeText !== 'function') {
805
+ status.textContent = '当前浏览器无法自动复制,请手动选择并复制上方命令。';
806
+ return;
807
+ }
808
+ await clipboard.writeText(command);
809
+ if (current()) {
810
+ status.textContent = '已复制命令。';
811
+ }
812
+ } catch {
813
+ if (current()) {
814
+ status.textContent = '复制失败,请手动选择并复制上方命令。';
815
+ }
816
+ } finally {
817
+ copying = false;
818
+ if (ownerVersion === version && container.isConnected) {
819
+ copy.disabled = false;
820
+ copy.textContent = '复制命令';
821
+ }
822
+ }
823
+ }, {
824
+ small: true,
825
+ 'data-install-copy': ''
826
+ });
827
+ const container = h('div', {
828
+ class: 'settings-command'
829
+ }, h('code', {}, command), h('div', {
830
+ class: 'settings-command-actions'
831
+ }, copy, status));
832
+ return container;
833
+ }
834
+ function packageRow(item, installer) {
835
+ function defaultRequirement() {
836
+ if (item.id === 'base') {
837
+ return 'builtin';
838
+ } else if (['core', 'web', 'faker'].includes(item.id)) {
839
+ return 'required';
840
+ } else {
841
+ return 'optional';
842
+ }
843
+ }
844
+ const requirement = item.requirement || defaultRequirement();
845
+ const metadataOnly = environmentInfo?.inspection === 'metadata_only';
846
+ const broken = !metadataOnly && !item.available && (item.installed || requirement === 'required' || requirement === 'builtin');
847
+ const state = packageStateLabel();
848
+ const requirementLabels = {
849
+ builtin: '内置',
850
+ required: '必需',
851
+ optional: '可选'
852
+ };
853
+ const requirementLabel = {
854
+ base: '内置',
855
+ faker: '随 sqlseed 安装',
856
+ mimesis: '按需安装'
857
+ }[item.id] || requirementLabels[requirement];
858
+ const controls = management?.controls(item);
859
+ function packageBadgeClass() {
860
+ if (broken) {
861
+ return ' settings-badge-warning';
862
+ } else if (!item.available) {
863
+ return ' settings-badge-neutral';
864
+ } else {
865
+ return '';
866
+ }
867
+ }
868
+ const row = h('article', {
869
+ class: 'settings-package',
870
+ 'data-package-id': item.id
871
+ }, h('div', {
872
+ class: 'settings-package-info'
873
+ }, h('div', {
874
+ class: 'settings-package-name'
875
+ }, h('strong', {}, item.name), h('span', {
876
+ class: 'settings-requirement'
877
+ }, requirementLabel)), item.distribution && requirement !== 'builtin' ? h('small', {
878
+ class: 'settings-distribution mono'
879
+ }, item.distribution) : null, h('p', {
880
+ class: 'muted'
881
+ }, item.description || descriptions[item.id] || item.message || ''), item.available && item.guidance ? h('p', {
882
+ class: 'settings-dependency'
883
+ }, item.guidance) : null, !metadataOnly && !item.available && componentImpact(item.id) ? h('p', {
884
+ class: 'settings-component-impact',
885
+ 'data-component-impact': ''
886
+ }, componentImpact(item.id)) : null), h('span', {
887
+ class: 'mono settings-version'
888
+ }, requirement === 'builtin' ? '随 sqlseed 提供' : item.version || '—'), h('div', {
889
+ class: 'settings-package-status'
890
+ }, h('span', {
891
+ class: `settings-badge${packageBadgeClass()}`
892
+ }, state)));
893
+ if (!item.available && (!metadataOnly || !item.installed && requirement !== 'builtin')) {
894
+ renderPackageHelp();
895
+ }
896
+ row.append(h('div', {
897
+ 'data-plugin-controls': ''
898
+ }, controls));
899
+ return row;
900
+ function packageStateLabel() {
901
+ let state;
902
+ if (metadataOnly && requirement === 'builtin') {
903
+ state = '内置';
904
+ } else if (metadataOnly && item.installed) {
905
+ state = '已安装(待验证)';
906
+ } else if (item.available) {
907
+ state = '可用';
908
+ } else if (item.installed) {
909
+ state = '加载异常';
910
+ } else if (requirement === 'optional') {
911
+ state = '未安装';
912
+ } else {
913
+ state = '必需依赖缺失';
914
+ }
915
+ return state;
916
+ }
917
+ function renderPackageHelp() {
918
+ row.append(h('details', {
919
+ class: 'settings-package-help',
920
+ hidden: !item.installed && managedInstallAvailable(controls)
921
+ }, h('summary', {}, broken ? '修复指引' : '管理员安装信息'), h('p', {}, item.guidance || item.message || '请在运行 Web 的 Python 环境中检查此组件。'), ...installationInstructions(installer, item.installed || requirement === 'builtin' ? item.repair_command : item.install_command)));
922
+ }
923
+ }
924
+ function showConnectionProbe(response) {
925
+ const checked = response.checked_at ? new Date(response.checked_at) : null;
926
+ const time = checked && Number.isFinite(checked.getTime()) ? checked.toLocaleTimeString('zh-CN', {
927
+ hour12: false
928
+ }) : '';
929
+ testNotice.textContent = `${response.message || (response.ok ? '服务连接成功,仅验证模型列表。' : '连接检测失败。')}${time ? " · " + time : ''}`;
930
+ if (response.ok) {
931
+ for (const name of (response.models || []).slice(0, 30)) {
932
+ models.append(button(name, () => {
933
+ if (!busy) {
934
+ modelInput.value = name;
935
+ changed();
936
+ }
937
+ }, {
938
+ small: true
939
+ }));
940
+ }
941
+ }
942
+ }