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,395 @@
1
+ // 配置校验与修复:L2 validate → L3 repair → L5 auto-heal,逐层可视。
2
+ // 这是缺陷收敛的主战场:贴 YAML,看违规、看修复、看最终产物。
3
+ // 未安装 sqlseed-ai 时展示 onboarding 引导卡(安装命令 + 能力说明)。
4
+
5
+ import { h, post, get, store, msg, clear, table, fmt } from '../api.js';
6
+ import { createDropdown } from '../dropdown.js';
7
+ let yamlText = '';
8
+ let healTimer = null;
9
+ const templateTableDd = createDropdown({
10
+ options: [],
11
+ placeholder: '— 选择表 —',
12
+ onChange: () => {}
13
+ });
14
+ export function render() {
15
+ const root = h('div');
16
+ templateTableDd.setOptions(store.tables.map(t => ({
17
+ value: t.name,
18
+ label: t.name
19
+ })), store.tables[0]?.name || '');
20
+ root.append(h('h2', {}, '配置助手'), renderAiPanel(), h('div', {
21
+ class: 'panel'
22
+ }, h('div', {
23
+ class: 'muted',
24
+ style: 'margin-bottom:10px; line-height:1.7'
25
+ }, '校验、修复或生成测试数据配置:', h('br'), '① 校验配置 — 检查下方 YAML 中的生成规则是否符合数据库约束;', h('br'), '② 修复配置 — 修正下方 YAML 中可自动处理的问题,输出修复后的配置;', h('br'), '③ 生成新配置 — 根据当前数据库的表结构和约束重新生成配置,需要时由 AI 辅助。此操作不使用下方 YAML。'), h('div', {
26
+ class: 'row'
27
+ }, h('label', {
28
+ class: 'genform-label'
29
+ }, '从表导入模板:'), templateTableDd.el, h('button', {
30
+ onclick: loadTemplate
31
+ }, '导入'), h('span', {
32
+ class: 'muted',
33
+ style: 'font-size:12px'
34
+ }, '粘贴 YAML,或从所选表导入模板。处理后的配置可送到数据生成向导,也可保存后用 sqlseed fill --config config.yaml 执行。')), h('textarea', {
35
+ id: 'heal-yaml',
36
+ spellcheck: 'false',
37
+ oninput: e => {
38
+ yamlText = e.target.value;
39
+ }
40
+ }, yamlText), h('div', {
41
+ class: 'row end'
42
+ }, h('button', {
43
+ onclick: doValidate
44
+ }, '① 校验此配置'), h('button', {
45
+ onclick: doRepair
46
+ }, '② 修复此配置'), h('button', {
47
+ class: 'primary',
48
+ onclick: doAutoHeal
49
+ }, '③ 生成新配置'))), h('div', {
50
+ id: 'heal-out'
51
+ }));
52
+ const ta = document.getElementById('heal-yaml');
53
+ if (ta && yamlText) ta.value = yamlText;
54
+ return root;
55
+ }
56
+ export function mount() {
57
+ if (!store.connId) {
58
+ const out = document.getElementById('heal-out');
59
+ clear(out);
60
+ out.append(msg('请先在「数据库连接」页连接数据库,以便按表结构和约束校验配置。', 'warn'));
61
+ }
62
+ }
63
+
64
+ // ---- AI 配置面板(在线/本地大模型,会话级,免重启切换) ----------------------
65
+
66
+ let aiState = null; // /api/ai/config 响应
67
+
68
+ async function loadAiConfig() {
69
+ try {
70
+ aiState = await get('/api/ai/config');
71
+ } catch {
72
+ aiState = null;
73
+ }
74
+ const holder = document.getElementById('ai-panel-body');
75
+ if (holder) renderAiBody(holder);
76
+ }
77
+ function renderAiPanel() {
78
+ const body = h('div', {
79
+ id: 'ai-panel-body'
80
+ }, h('div', {
81
+ class: 'loading'
82
+ }, '加载 AI 配置…'));
83
+ loadAiConfig().then(() => renderAiBody(body));
84
+ return h('div', {
85
+ class: 'panel'
86
+ }, h('div', {
87
+ class: 'row'
88
+ }, h('span', {
89
+ class: 'pill gen'
90
+ }, 'AI 服务设置'), h('span', {
91
+ class: 'muted'
92
+ }, '选择辅助生成配置的模型服务,保存后立即生效。')), body);
93
+ }
94
+
95
+ // 未安装 sqlseed-ai 时的 onboarding 引导卡:说清楚缺了什么、怎么装、
96
+ // 不装还能用什么(数据生成/浏览不依赖 AI)。
97
+ function renderOnboarding(holder, reason) {
98
+ holder.append(h('div', {
99
+ class: 'msg warn'
100
+ }, reason || 'sqlseed-ai 未安装'), h('div', {
101
+ class: 'muted',
102
+ style: 'margin:8px 0; line-height:1.7'
103
+ }, 'sqlseed-ai 是可选插件,提供:① 配置校验与自动修复;' + '② 按数据库约束一键生成合规配置(即数据生成向导中的「AI 一键生成配置」)。' + '未安装时,连接、数据生成、数据浏览功能完全不受影响。'), h('div', {
104
+ class: 'row'
105
+ }, h('label', {}, '安装命令'), h('code', {}, 'pip install -e ./plugins/sqlseed-web[ai]')), h('div', {
106
+ class: 'muted',
107
+ style: 'margin-top:6px'
108
+ }, '安装后刷新本页即可。本地模型(Ollama / LM Studio)无需 API Key;在线模型需要 Key。'));
109
+ }
110
+ function renderAiBody(holder) {
111
+ clear(holder);
112
+ if (!aiState?.available) {
113
+ renderOnboarding(holder, aiState?.reason);
114
+ return;
115
+ }
116
+ const eff = aiState.effective;
117
+ const ov = aiState.override || {};
118
+ // 排序:当前生效的后端浮到第一(下拉默认就选它,放最前免得被埋没),
119
+ // 其余保持 API 顺序——通用(OpenAI 兼容 / Google AI Studio)在前,本地殿后。
120
+ const currentBackend = ov.backend || eff.backend;
121
+ const orderedBackends = [...aiState.backends.filter(b => b.id === currentBackend), ...aiState.backends.filter(b => b.id !== currentBackend)];
122
+ const backendDd = createDropdown({
123
+ value: currentBackend,
124
+ options: orderedBackends.map(b => ({
125
+ value: b.id,
126
+ label: b.label
127
+ })),
128
+ onChange: v => {
129
+ const b = aiState.backends.find(x => x.id === v);
130
+ const keyInput = document.getElementById('ai-key');
131
+ const urlInput = document.getElementById('ai-url');
132
+ if (b && keyInput && urlInput) {
133
+ keyInput.disabled = b.needs_key === '0';
134
+ urlInput.disabled = b.needs_url === '0';
135
+ }
136
+ }
137
+ });
138
+ function effectiveAIConfiguration() {
139
+ if (eff.backend === 'ollama' || eff.backend === 'lm_studio') {
140
+ return `当前生效:${eff.backend} · ${eff.model} · 本地后端无需 Key`;
141
+ } else {
142
+ return `当前生效:${eff.backend} · ${eff.model} · key ${eff.api_key_present ? '已配置' : '未配置'}`;
143
+ }
144
+ }
145
+ holder.append(h('div', {
146
+ class: 'row'
147
+ }, h('label', {
148
+ class: 'genform-label'
149
+ }, '后端'), backendDd.el), h('div', {
150
+ class: 'row'
151
+ }, h('label', {
152
+ class: 'genform-label'
153
+ }, '模型'), h('input', {
154
+ id: 'ai-model',
155
+ class: 'grow',
156
+ placeholder: '留空=自动(如 gemma4:e4b / gemma-4-26b-a4b-it)',
157
+ value: ov.model || ''
158
+ })), h('div', {
159
+ class: 'row'
160
+ }, h('label', {
161
+ class: 'genform-label'
162
+ }, 'API Key'), h('input', {
163
+ id: 'ai-key',
164
+ class: 'grow',
165
+ type: 'password',
166
+ placeholder: '在线后端需要;本地后端无需密钥',
167
+ value: ov.api_key || ''
168
+ })), h('div', {
169
+ class: 'row'
170
+ }, h('label', {
171
+ class: 'genform-label'
172
+ }, 'Base URL'), h('input', {
173
+ id: 'ai-url',
174
+ class: 'grow',
175
+ placeholder: 'OpenAI 兼容服务需要;其余留空',
176
+ value: ov.base_url || ''
177
+ })), h('div', {
178
+ class: 'row'
179
+ }, h('button', {
180
+ class: 'primary',
181
+ onclick: () => saveAiConfig(backendDd)
182
+ }, '应用配置'), h('button', {
183
+ onclick: testConnection
184
+ }, '测试连接'), h('span', {
185
+ class: 'muted',
186
+ id: 'ai-effective'
187
+ }, effectiveAIConfiguration())), h('div', {
188
+ id: 'ai-test-out'
189
+ }));
190
+ // 初始化输入框禁用态
191
+ const initB = aiState.backends.find(x => x.id === backendDd.get());
192
+ if (initB) {
193
+ document.getElementById('ai-key').disabled = initB.needs_key === '0';
194
+ document.getElementById('ai-url').disabled = initB.needs_url === '0';
195
+ }
196
+ }
197
+ async function testConnection() {
198
+ const out = document.getElementById('ai-test-out');
199
+ clear(out);
200
+ out.append(h('div', {
201
+ class: 'loading'
202
+ }, '探测后端连接…'));
203
+ try {
204
+ const res = await post('/api/ai/test-connection', {});
205
+ clear(out);
206
+ if (!res.available) {
207
+ out.append(msg(res.reason || 'sqlseed-ai 未安装', 'warn'));
208
+ return;
209
+ }
210
+ out.append(msg(res.message, res.ok ? 'ok' : 'warn'));
211
+ } catch (e) {
212
+ clear(out);
213
+ out.append(msg(`测试失败:${e.message}`));
214
+ }
215
+ }
216
+ async function saveAiConfig(backendDd) {
217
+ const payload = {
218
+ backend: backendDd.get(),
219
+ model: document.getElementById('ai-model').value.trim() || null,
220
+ api_key: document.getElementById('ai-key').value.trim() || null,
221
+ base_url: document.getElementById('ai-url').value.trim() || null
222
+ };
223
+ try {
224
+ aiState = await post('/api/ai/config', payload);
225
+ const holder = document.getElementById('ai-panel-body');
226
+ renderAiBody(holder);
227
+ holder.append(msg('AI 配置已应用(本会话生效)。', 'ok'));
228
+ } catch (e) {
229
+ const holder = document.getElementById('ai-panel-body');
230
+ holder.append(msg(`保存失败:${e.message}`));
231
+ }
232
+ }
233
+ async function loadTemplate() {
234
+ if (!store.connId || !store.tables.length) return;
235
+ const t = templateTableDd.get() || store.tables[0].name;
236
+ try {
237
+ const res = await get(`/api/connections/${store.connId}/tables/${encodeURIComponent(t)}/yaml-template`);
238
+ yamlText = res.yaml;
239
+ const ta = document.getElementById('heal-yaml');
240
+ if (ta) ta.value = yamlText;
241
+ } catch (e) {
242
+ const out = document.getElementById('heal-out');
243
+ out.append(msg(e.message));
244
+ }
245
+ }
246
+ function basePayload() {
247
+ return {
248
+ yaml: yamlText
249
+ };
250
+ }
251
+ async function doValidate() {
252
+ const out = document.getElementById('heal-out');
253
+ clear(out);
254
+ out.append(h('div', {
255
+ class: 'loading'
256
+ }, '校验中…'));
257
+ try {
258
+ const res = await post(`/api/connections/${store.connId}/heal/validate`, basePayload());
259
+ clear(out);
260
+ if (!res.ok) {
261
+ out.append(msg(`校验器异常:${res.error}`));
262
+ return;
263
+ }
264
+ // renderViolations 返回数组:必须 spread,否则 append 把数组当单个参数
265
+ // 字符串化成 "[object HTMLDivElement]"(P0 渲染 bug,实测发现)。
266
+ out.append(...renderViolations(res));
267
+ } catch (e) {
268
+ clear(out);
269
+ out.append(msg(e.message));
270
+ }
271
+ }
272
+ function renderViolations(res) {
273
+ const parts = [];
274
+ parts.push(h('div', {
275
+ class: 'panel'
276
+ }, h('h3', {}, `① 校验结果 — schema_hash ${res.schema_hash?.slice(0, 12)}`), res.is_clean ? msg('配置干净:无违规。', 'ok') : msg(`发现 ${res.violation_count} 处违规:`, 'warn')));
277
+ if (res.violations?.length) {
278
+ const validationSeverityClass = v => {
279
+ if (v.severity === 'crash') {
280
+ return 'err';
281
+ } else if (v.severity === 'semantic_error') {
282
+ return 'warn';
283
+ } else {
284
+ return 'ok';
285
+ }
286
+ };
287
+ parts.push(h('div', {
288
+ class: 'panel'
289
+ }, h('h3', {}, 'ViolationReport 列表'), h('div', {
290
+ class: 'table-scroll'
291
+ }, table(['severity', 'table', 'columns', '类型', 'fix_hint', 'message'], res.violations.map(v => [h('span', {
292
+ class: `pill ${validationSeverityClass(v)}`
293
+ }, v.severity), v.table, fmt(v.columns), v.constraint_type, v.fix_hint || '—', v.message || '']), {
294
+ monoCols: [1]
295
+ }))));
296
+ }
297
+ if (res.column_groups?.length) {
298
+ parts.push(h('div', {
299
+ class: 'panel'
300
+ }, h('h3', {}, '复合 FK 协同组(ColumnGroup)'), h('div', {
301
+ class: 'table-scroll'
302
+ }, table(['group_id', 'columns', 'parent_table', 'parent_columns'], res.column_groups.map(g => [g.group_id, fmt(g.columns), g.parent_table, fmt(g.parent_columns)]), {
303
+ monoCols: [0]
304
+ }))));
305
+ }
306
+ return parts;
307
+ }
308
+ async function doRepair() {
309
+ const out = document.getElementById('heal-out');
310
+ clear(out);
311
+ out.append(h('div', {
312
+ class: 'loading'
313
+ }, '修复中…'));
314
+ try {
315
+ const res = await post(`/api/connections/${store.connId}/heal/repair`, basePayload());
316
+ clear(out);
317
+ if (!res.ok) {
318
+ out.append(msg(`修复器异常:${res.error}`));
319
+ return;
320
+ }
321
+ out.append(h('div', {
322
+ class: 'panel'
323
+ }, h('h3', {}, `② 修复结果 — ${res.fix_count} 处已修`), res.applied_fixes?.length ? h('div', {
324
+ class: 'table-scroll'
325
+ }, table(['策略', 'table', 'columns', 'before', 'after'], res.applied_fixes.map(f => [h('span', {
326
+ class: 'pill gen'
327
+ }, f.fix_strategy), f.table, fmt(f.columns), h('code', {}, JSON.stringify(f.before)), h('code', {}, JSON.stringify(f.after))]), {
328
+ monoCols: [1]
329
+ })) : h('div', {
330
+ class: 'muted'
331
+ }, '无需修复。'), res.unfixable?.length ? msg(`不可自动修复 ${res.unfixable.length} 处,见 L2 视图。`, 'warn') : null));
332
+ if (res.repaired_yaml) {
333
+ const pre = h('pre', {}, res.repaired_yaml);
334
+ out.append(h('div', {
335
+ class: 'panel'
336
+ }, h('h3', {}, '修复后 YAML'), pre, h('div', {
337
+ class: 'row end'
338
+ }, h('button', {
339
+ onclick: () => {
340
+ yamlText = res.repaired_yaml;
341
+ document.getElementById('heal-yaml').value = yamlText;
342
+ }
343
+ }, '回填到编辑区'))));
344
+ }
345
+ } catch (e) {
346
+ clear(out);
347
+ out.append(msg(e.message));
348
+ }
349
+ }
350
+ async function doAutoHeal() {
351
+ const out = document.getElementById('heal-out');
352
+ clear(out);
353
+ const status = h('div', {
354
+ class: 'muted'
355
+ }, '提交全流程生成任务(读取 schema → 按约束校验/修复 → 必要时调用大模型 → 输出 YAML)…');
356
+ out.append(h('div', {
357
+ class: 'panel'
358
+ }, h('h3', {}, '③ 全流程生成'), status));
359
+ try {
360
+ const res = await post(`/api/connections/${store.connId}/heal/auto`, {
361
+ budget_seconds: 300
362
+ });
363
+ healTimer = setInterval(async () => {
364
+ try {
365
+ const job = await get(`/api/jobs/${res.job_id}`);
366
+ if (job.status === 'running') return;
367
+ clearInterval(healTimer);
368
+ if (job.status === 'error') {
369
+ status.replaceWith(msg(`生成失败:${job.error}`));
370
+ } else {
371
+ const n = job.result.llm_calls;
372
+ const llmNote = n === 0 ? '本次未调用大模型:schema 无语义违规,全部由确定性校验/修复完成(契约驱动设计:LLM 只在必要时介入)' : `大模型调用 ${n} 次`;
373
+ status.replaceWith(h('div', {}, msg(`完成(模型 ${job.result.model} @ ${job.result.backend};${llmNote})`, 'ok'), h('pre', {}, job.result.yaml), h('div', {
374
+ class: 'row end'
375
+ }, h('button', {
376
+ onclick: () => {
377
+ yamlText = job.result.yaml;
378
+ document.getElementById('heal-yaml').value = yamlText;
379
+ }
380
+ }, '回填到编辑区'), h('button', {
381
+ class: 'primary',
382
+ onclick: () => {
383
+ store.aiYaml = job.result.yaml;
384
+ location.hash = '#/wizard';
385
+ }
386
+ }, '送到数据生成向导 →'))));
387
+ }
388
+ } catch {
389
+ clearInterval(healTimer);
390
+ }
391
+ }, 1000);
392
+ } catch (e) {
393
+ status.replaceWith(msg(e.message));
394
+ }
395
+ }
@@ -0,0 +1,110 @@
1
+ // 系统信息:生成器与参数、插件扩展点、生成引擎、AI 后端状态。
2
+ // 验收驾驶舱:这里的计数必须与代码一致(doc-sync 的 UI 版)。
3
+
4
+ import { h, get, msg, clear, table } from '../api.js';
5
+ export function render() {
6
+ const root = h('div');
7
+ root.append(h('h2', {}, '系统信息'), h('div', {
8
+ class: 'muted',
9
+ style: 'margin-bottom:12px; line-height:1.6'
10
+ }, '查看当前版本、可用生成器、插件扩展点、数据生成引擎、AI 服务状态和最近任务。'), h('div', {
11
+ id: 'meta-out'
12
+ }, h('div', {
13
+ class: 'loading'
14
+ }, '加载中…')));
15
+ return root;
16
+ }
17
+ export function mount() {
18
+ load();
19
+ }
20
+ async function load() {
21
+ const out = document.getElementById('meta-out');
22
+ try {
23
+ const [generators, hooks, providers, ai, info, jobs] = await Promise.all([get('/api/meta/generators'), get('/api/meta/hooks'), get('/api/meta/providers'), get('/api/meta/ai'), get('/api/meta/info'), get('/api/jobs')]);
24
+ clear(out);
25
+ out.append(renderStats(generators, hooks, ai, info), renderGenerators(generators), renderHooks(hooks), renderProviders(providers), renderJobs(jobs));
26
+ } catch (e) {
27
+ clear(out);
28
+ out.append(msg(e.message));
29
+ }
30
+ }
31
+ function renderStats(gen, hooks, ai, info) {
32
+ return h('div', {
33
+ class: 'panel'
34
+ }, h('div', {
35
+ class: 'stats'
36
+ }, h('div', {}, h('div', {
37
+ class: 'stat'
38
+ }, gen.count), h('div', {
39
+ class: 'stat-label'
40
+ }, '生成器类型')), h('div', {}, h('div', {
41
+ class: 'stat'
42
+ }, hooks.count), h('div', {
43
+ class: 'stat-label'
44
+ }, '插件扩展点')), h('div', {}, h('div', {
45
+ class: 'stat'
46
+ }, info.sqlseed_version || '—'), h('div', {
47
+ class: 'stat-label'
48
+ }, 'sqlseed 版本'))), h('div', {
49
+ class: 'row'
50
+ }, ai.available ? h('span', {
51
+ class: 'pill ok'
52
+ }, `AI: ${ai.model} @ ${ai.backend}(协议 ${ai.tool_calling_protocol})`) : h('span', {
53
+ class: 'pill warn'
54
+ }, `AI 不可用:${ai.reason}`)));
55
+ }
56
+ function renderGenerators(gen) {
57
+ const paramList = name => (gen.params[name] || []).join(', ');
58
+ return h('div', {
59
+ class: 'panel'
60
+ }, h('h3', {}, `生成器及参数(${gen.count})`), h('div', {
61
+ class: 'table-scroll'
62
+ }, table(['生成器', '参数'], gen.names.map(n => [h('span', {
63
+ class: 'pill gen'
64
+ }, n), paramList(n) || '—']), {
65
+ monoCols: [1]
66
+ })));
67
+ }
68
+ function renderHooks(hooks) {
69
+ return h('div', {
70
+ class: 'panel'
71
+ }, h('h3', {}, `插件扩展点(${hooks.count})`), h('div', {
72
+ class: 'table-scroll'
73
+ }, table(['扩展点', '结果处理方式'], hooks.hooks.map(hk => [hk.name, hk.firstresult ? h('span', {
74
+ class: 'pill ok'
75
+ }, '首个有效结果') : h('span', {
76
+ class: 'pill'
77
+ }, '收集所有结果')]), {
78
+ monoCols: [0]
79
+ })));
80
+ }
81
+ function renderProviders(providers) {
82
+ return h('div', {
83
+ class: 'panel'
84
+ }, h('h3', {}, '数据生成引擎优先级'), h('div', {
85
+ class: 'row'
86
+ }, ...providers.default_chain.map(p => h('span', {
87
+ class: `pill ${providers.available.includes(p) ? 'ok' : 'warn'}`
88
+ }, p)), h('span', {
89
+ class: 'muted'
90
+ }, '优先使用前面的引擎;不可用时依次尝试后续引擎。')));
91
+ }
92
+ function jobStatusClass(j) {
93
+ if (j.status === 'done') {
94
+ return 'ok';
95
+ } else if (j.status === 'error') {
96
+ return 'err';
97
+ } else {
98
+ return 'warn';
99
+ }
100
+ }
101
+ function renderJobs(jobs) {
102
+ if (!jobs.jobs.length) return h('div');
103
+ return h('div', {
104
+ class: 'panel'
105
+ }, h('h3', {}, '最近任务'), h('div', {
106
+ class: 'table-scroll'
107
+ }, table(['任务', '类型', '状态', '插入行数', '错误'], jobs.jobs.map(j => [j.label, j.kind, h('span', {
108
+ class: `pill ${jobStatusClass(j)}`
109
+ }, j.status), j.rows_inserted, j.error ? j.error.slice(0, 80) : '']))));
110
+ }