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,228 @@
1
+ // API helpers + shared UI state (connection id survives page switches).
2
+
3
+ export const store = {
4
+ connId: null,
5
+ target: null,
6
+ tables: []
7
+ };
8
+ export async function api(path, options = {}) {
9
+ const res = await fetch(path, {
10
+ headers: {
11
+ 'Content-Type': 'application/json'
12
+ },
13
+ ...options
14
+ });
15
+ const body = await res.json().catch(() => ({}));
16
+ if (!res.ok) {
17
+ const detail = body.detail;
18
+ const message = httpErrorMessage(detail, res.status, true);
19
+ const error = new Error(message);
20
+ error.status = res.status;
21
+ error.detail = detail;
22
+ throw error;
23
+ }
24
+ return body;
25
+ }
26
+ export const post = (path, data) => api(path, {
27
+ method: 'POST',
28
+ body: JSON.stringify(data)
29
+ });
30
+ export const send = (path, data, method = 'POST') => api(path, {
31
+ method,
32
+ body: JSON.stringify(data)
33
+ });
34
+ export const get = path => api(path);
35
+ export const del = path => api(path, {
36
+ method: 'DELETE'
37
+ });
38
+
39
+ // ---- tiny DOM helpers -----------------------------------------------------
40
+
41
+ export const h = (tag, attrs = {}, ...children) => {
42
+ const el = document.createElement(tag);
43
+ applyAttributes(el, attrs);
44
+ for (const c of children.flat(Infinity)) {
45
+ if (c == null) {
46
+ continue;
47
+ }
48
+ el.append(c.nodeType ? c : document.createTextNode(c));
49
+ }
50
+ return el;
51
+ };
52
+ export const clear = el => {
53
+ while (el.firstChild) {
54
+ el.firstChild.remove();
55
+ }
56
+ };
57
+ export function table(headers, rows, {
58
+ monoCols = []
59
+ } = {}) {
60
+ const thead = h('thead', {}, h('tr', {}, ...headers.map(t => h('th', {}, t))));
61
+ const tbody = h('tbody');
62
+ for (const row of rows) {
63
+ tbody.append(h('tr', {}, ...row.map((cell, i) => h('td', monoCols.includes(i) ? {
64
+ class: 'mono'
65
+ } : {}, cell ?? ''))));
66
+ }
67
+ return h('table', {}, thead, tbody);
68
+ }
69
+ export function msg(text, kind = 'err') {
70
+ return h('div', {
71
+ class: `msg ${kind}`
72
+ }, text);
73
+ }
74
+ export function fmt(v) {
75
+ if (v === null || v === undefined) {
76
+ return '';
77
+ }
78
+ if (typeof v === 'object') {
79
+ return JSON.stringify(v);
80
+ }
81
+ return String(v);
82
+ }
83
+ export function setConnBadge() {
84
+ const label = document.getElementById('connection-label');
85
+ if (label) {
86
+ label.textContent = store.connId ? safeTargetLabel(store.target) : '连接数据库';
87
+ }
88
+ const badge = document.getElementById('conn-badge');
89
+ if (!badge) {
90
+ return;
91
+ }
92
+ if (store.connId) {
93
+ badge.className = 'badge ok';
94
+ badge.textContent = store.target || store.connId;
95
+ } else {
96
+ badge.className = 'badge empty';
97
+ badge.textContent = '未连接';
98
+ }
99
+ }
100
+
101
+ /** Display identity only; omit URL userinfo and query parameters. */
102
+ export function safeTargetLabel(target) {
103
+ if (!target) {
104
+ return '已连接数据库';
105
+ }
106
+ const text = String(target);
107
+ if (text.includes('://')) {
108
+ try {
109
+ const url = new URL(text);
110
+ return `${url.hostname}${url.port ? ":" + url.port : ''}${decodeURIComponent(url.pathname)}`;
111
+ } catch {
112
+ return '已连接数据库';
113
+ }
114
+ }
115
+ return /^(?:[\\/]|[A-Za-z]:[\\/])/.test(text) ? text.split(/[\\/]/).findLast(Boolean) || text : text;
116
+ }
117
+
118
+ // ---- 跨刷新恢复 ------------------------------------------------------------
119
+ // store 是模块级内存状态,浏览器一刷新就清空;但服务端的连接对象仍然活着。
120
+ // 用 localStorage 记住上次用的 connId,恢复时先找它,找不到再回退到主连接
121
+ // (group_index 1)。服务重启导致连接全丢时,恢复失败并清除记录。
122
+
123
+ const CONN_KEY = 'sqlseed.connId';
124
+ let connectionChoiceVersion = 0;
125
+ let explicitlyDisconnected = false;
126
+ export function rememberConnId(connId) {
127
+ connectionChoiceVersion++;
128
+ explicitlyDisconnected = connId === '';
129
+ try {
130
+ localStorage.setItem(CONN_KEY, connId);
131
+ } catch {
132
+ /* 隐私模式等下 localStorage 不可用——恢复失败也只是退回手动连接,不值得报错 */
133
+ }
134
+ }
135
+ export function forgetConnId() {
136
+ connectionChoiceVersion++;
137
+ explicitlyDisconnected = false;
138
+ try {
139
+ localStorage.removeItem(CONN_KEY);
140
+ } catch {
141
+ /* 同上 */
142
+ }
143
+ }
144
+
145
+ /**
146
+ * 尝试把上次会话的连接恢复进 store。
147
+ * @returns {Promise<boolean>} 恢复成功与否(失败时 store 保持原样)
148
+ */
149
+ export async function restoreConnection() {
150
+ if (explicitlyDisconnected) {
151
+ return false;
152
+ }
153
+ const originalConnection = store.connId;
154
+ const originalChoiceVersion = connectionChoiceVersion;
155
+ const current = () => store.connId === originalConnection && connectionChoiceVersion === originalChoiceVersion;
156
+ let remembered = null;
157
+ try {
158
+ remembered = localStorage.getItem(CONN_KEY);
159
+ } catch {
160
+ return false;
161
+ }
162
+ // An empty stored id is an explicit user disconnect, not a missing history.
163
+ // Preserve that choice even when other server-side sessions remain open.
164
+ if (remembered === '') {
165
+ return false;
166
+ }
167
+ try {
168
+ const res = await get('/api/connections');
169
+ if (!current()) {
170
+ return Boolean(store.connId);
171
+ }
172
+ const list = res.connections || [];
173
+ const pick = list.find(c => c.conn_id === remembered) || list.find(c => c.group_index === 1) || list[0];
174
+ if (!pick) {
175
+ forgetConnId();
176
+ return false;
177
+ }
178
+ const detail = await get(`/api/connections/${pick.conn_id}/tables`);
179
+ if (!current()) {
180
+ return Boolean(store.connId);
181
+ }
182
+ store.connId = pick.conn_id;
183
+ store.target = String(detail.target || '').includes('://') ? safeTargetLabel(detail.target) : detail.target;
184
+ store.tables = detail.tables;
185
+ setConnBadge();
186
+ return true;
187
+ } catch {
188
+ if (!current()) {
189
+ return Boolean(store.connId);
190
+ }
191
+ forgetConnId();
192
+ return false;
193
+ }
194
+ }
195
+ function applyAttributes(el, attrs) {
196
+ for (const [k, v] of Object.entries(attrs)) {
197
+ if (k === 'class') {
198
+ el.className = v;
199
+ } else if (k === 'onclick') {
200
+ el.onclick = v;
201
+ } else if (k.startsWith('on')) {
202
+ el.addEventListener(k.slice(2), v);
203
+ } else if (k === 'value') {
204
+ el.value = v;
205
+ } else if (k === 'checked') {
206
+ el.checked = v;
207
+ } else if (typeof v === 'boolean') {
208
+ // 布尔属性(disabled 等):false 必须移除属性——
209
+ // setAttribute('disabled', false) 会因属性存在而仍判定为禁用。
210
+ if (v) {
211
+ el.setAttribute(k, '');
212
+ } else {
213
+ el.removeAttribute(k);
214
+ }
215
+ } else {
216
+ el.setAttribute(k, v);
217
+ }
218
+ }
219
+ }
220
+ export function httpErrorMessage(detail, status, stringifyUnknown = false) {
221
+ if (typeof detail === 'string') { return detail; }
222
+ if (Array.isArray(detail)) {
223
+ return detail.map(item => `${(item.loc || []).join('.')}: ${item.msg || JSON.stringify(item)}`).join(';');
224
+ }
225
+ if (detail?.message) { return detail.message; }
226
+ if (stringifyUnknown && detail) { return JSON.stringify(detail); }
227
+ return `HTTP ${status}`;
228
+ }
@@ -0,0 +1,97 @@
1
+ import { setConnBadge } from './api.js';
2
+ import { openConnectionDialog } from './workbench/connection.js';
3
+
4
+ // Retired connect/wizard/browse/heal/meta modules remain historical source only.
5
+ // Product navigation mounts the unified workbench or durable run history.
6
+ const pages = {
7
+ workbench: () => import('./pages/workbench.js'),
8
+ configs: () => import('./pages/configs.js'),
9
+ runs: () => import('./pages/runs.js'),
10
+ settings: () => import('./pages/settings.js')
11
+ };
12
+ let currentModule = null;
13
+ let routeVersion = 0;
14
+ let committedPage = null;
15
+ const maintenance = document.documentElement?.dataset.pluginMaintenance === 'true';
16
+ const initialRecovery = document.documentElement?.dataset.pluginSupervisedMaintenance === 'true';
17
+ async function render() {
18
+ const version = ++routeVersion;
19
+ currentModule?.unmount?.();
20
+ currentModule = null;
21
+ const requested = location.hash.replace('#/', '').split('?')[0];
22
+ const recovering = initialRecovery && committedPage === null;
23
+ let page;
24
+ if (maintenance || recovering) {
25
+ page = 'settings';
26
+ } else if (Object.hasOwn(pages, requested)) {
27
+ page = requested;
28
+ } else {
29
+ page = 'workbench';
30
+ }
31
+ if ((maintenance || recovering) && location.hash !== '#/settings?section=plugins') {
32
+ location.hash = '#/settings?section=plugins';
33
+ }
34
+ document.title = maintenance ? 'sqlseed · 插件维护' : `sqlseed · ${{
35
+ workbench: '工作台',
36
+ configs: '配置管理',
37
+ runs: '运行记录',
38
+ settings: '设置'
39
+ }[page]}`;
40
+ const main = document.getElementById('app');
41
+ document.querySelectorAll('#nav button').forEach(button => {
42
+ const active = button.dataset.page === page;
43
+ button.classList.toggle('active', active);
44
+ if (active) {
45
+ button.setAttribute('aria-current', 'page');
46
+ } else {
47
+ button.removeAttribute('aria-current');
48
+ }
49
+ });
50
+ try {
51
+ const module = await pages[page]();
52
+ if (version !== routeVersion) {
53
+ return;
54
+ }
55
+ currentModule = module;
56
+ const content = module.render();
57
+ if (committedPage !== null && committedPage !== page) {
58
+ content.classList.add('page-enter');
59
+ }
60
+ main.replaceChildren(content);
61
+ committedPage = page;
62
+ await module.mount?.();
63
+ if (version === routeVersion && !maintenance) {
64
+ setConnBadge();
65
+ }
66
+ } catch (error) {
67
+ if (version === routeVersion) {
68
+ main.textContent = `页面加载失败:${error.message}`;
69
+ }
70
+ }
71
+ }
72
+ document.querySelectorAll('#nav button').forEach(button => {
73
+ const unavailable = maintenance && button.dataset.page !== 'settings';
74
+ button.hidden = unavailable;
75
+ button.disabled = unavailable;
76
+ button.onclick = () => {
77
+ if (!unavailable) {
78
+ location.hash = maintenance ? '#/settings?section=plugins' : `#/${button.dataset.page}`;
79
+ }
80
+ };
81
+ });
82
+ const connectionButton = document.getElementById('connection-button');
83
+ connectionButton.hidden = maintenance;
84
+ connectionButton.disabled = maintenance;
85
+ connectionButton.onclick = () => {
86
+ if (!maintenance) {
87
+ openConnectionDialog({});
88
+ }
89
+ };
90
+ window.addEventListener('hashchange', render);
91
+ window.addEventListener('sqlseed:connection-changed', () => {
92
+ if (!maintenance) {
93
+ setConnBadge();
94
+ render();
95
+ }
96
+ });
97
+ await render();