@axiom-lattice/gateway 3.0.2 → 3.0.3

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 (155) hide show
  1. package/dist/index.js +31 -9
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +31 -9
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +9 -6
  6. package/.eslintrc.json +0 -22
  7. package/.turbo/turbo-build.log +0 -46
  8. package/AGENTS.md +0 -112
  9. package/CHANGELOG.md +0 -1190
  10. package/LOGGER_CONFIG.md +0 -135
  11. package/RESUME_STREAM_CONTENT_BASED.md +0 -326
  12. package/RESUME_STREAM_README.md +0 -388
  13. package/jest.config.js +0 -72
  14. package/public/sdk/README.md +0 -695
  15. package/public/sdk/dashboard-engine-skill.md +0 -1122
  16. package/public/sdk/dashboard-single-file-spec.md +0 -357
  17. package/public/sdk/data-query-sdk-skill.md +0 -307
  18. package/public/sdk/data-query-sdk.d.ts +0 -252
  19. package/public/sdk/data-query-sdk.js +0 -970
  20. package/public/sdk/occupancy-dashboard.html +0 -363
  21. package/public/sdk/test-dashboard.html +0 -690
  22. package/scripts/seed-lark-installation.ts +0 -54
  23. package/src/__tests__/__mocks__/e2b.ts +0 -1
  24. package/src/__tests__/a2a.test.ts +0 -428
  25. package/src/__tests__/channel-installations.test.ts +0 -199
  26. package/src/__tests__/data-query.test.ts +0 -77
  27. package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
  28. package/src/__tests__/setup.ts +0 -5
  29. package/src/__tests__/workspace.test.ts +0 -400
  30. package/src/channels/__tests__/routes.test.ts +0 -50
  31. package/src/channels/lark/LarkChannelAdapter.ts +0 -217
  32. package/src/channels/lark/README.md +0 -187
  33. package/src/channels/lark/__tests__/controller.test.ts +0 -136
  34. package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
  35. package/src/channels/lark/__tests__/parser.test.ts +0 -72
  36. package/src/channels/lark/__tests__/sender.test.ts +0 -37
  37. package/src/channels/lark/__tests__/verification.test.ts +0 -157
  38. package/src/channels/lark/controller.ts +0 -48
  39. package/src/channels/lark/mapping-service.ts +0 -138
  40. package/src/channels/lark/parser.ts +0 -68
  41. package/src/channels/lark/routes.ts +0 -22
  42. package/src/channels/lark/sender.ts +0 -58
  43. package/src/channels/lark/types.ts +0 -20
  44. package/src/channels/lark/verification.ts +0 -32
  45. package/src/channels/registry.ts +0 -20
  46. package/src/channels/routes.ts +0 -30
  47. package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
  48. package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
  49. package/src/channels/wechat/context-store.ts +0 -54
  50. package/src/channels/wechat/controller.ts +0 -59
  51. package/src/channels/wechat/routes.ts +0 -7
  52. package/src/channels/wechat/types.ts +0 -62
  53. package/src/channels/wechat/wechat-client.ts +0 -162
  54. package/src/config/local-a2a-templates.ts +0 -59
  55. package/src/config.ts +0 -124
  56. package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
  57. package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
  58. package/src/controllers/__tests__/mime.test.ts +0 -74
  59. package/src/controllers/__tests__/resources.test.ts +0 -122
  60. package/src/controllers/__tests__/run.test.ts +0 -115
  61. package/src/controllers/__tests__/tasks.test.ts +0 -215
  62. package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
  63. package/src/controllers/agent_task.ts +0 -72
  64. package/src/controllers/assistant.ts +0 -490
  65. package/src/controllers/auth.ts +0 -524
  66. package/src/controllers/channel-bindings.ts +0 -135
  67. package/src/controllers/channel-installations.ts +0 -409
  68. package/src/controllers/collections.ts +0 -176
  69. package/src/controllers/config.ts +0 -126
  70. package/src/controllers/connections.ts +0 -181
  71. package/src/controllers/data-query.ts +0 -236
  72. package/src/controllers/database-configs.ts +0 -440
  73. package/src/controllers/embeddings.ts +0 -22
  74. package/src/controllers/entries.ts +0 -167
  75. package/src/controllers/eval.ts +0 -513
  76. package/src/controllers/export-import.ts +0 -166
  77. package/src/controllers/health.ts +0 -36
  78. package/src/controllers/local-a2a-templates.ts +0 -18
  79. package/src/controllers/local-a2a.ts +0 -149
  80. package/src/controllers/mcp-configs.ts +0 -673
  81. package/src/controllers/memory.ts +0 -257
  82. package/src/controllers/menu-items.ts +0 -114
  83. package/src/controllers/metrics-configs.ts +0 -1061
  84. package/src/controllers/middleware-types.ts +0 -15
  85. package/src/controllers/models.ts +0 -199
  86. package/src/controllers/personal-assistant.ts +0 -191
  87. package/src/controllers/resources.ts +0 -477
  88. package/src/controllers/run.ts +0 -338
  89. package/src/controllers/sandbox.ts +0 -255
  90. package/src/controllers/schedules.ts +0 -336
  91. package/src/controllers/skills.ts +0 -256
  92. package/src/controllers/tasks.ts +0 -213
  93. package/src/controllers/tenants.ts +0 -121
  94. package/src/controllers/thread_status.ts +0 -66
  95. package/src/controllers/threads.ts +0 -260
  96. package/src/controllers/tools.ts +0 -139
  97. package/src/controllers/users.ts +0 -135
  98. package/src/controllers/workflow-tracking.ts +0 -693
  99. package/src/controllers/workspace.ts +0 -879
  100. package/src/export_registrations/a2a-api-key.registration.ts +0 -89
  101. package/src/export_registrations/agent.registration.ts +0 -80
  102. package/src/export_registrations/binding.registration.ts +0 -106
  103. package/src/export_registrations/channel-installation.registration.ts +0 -88
  104. package/src/export_registrations/collection.registration.ts +0 -85
  105. package/src/export_registrations/connection.registration.ts +0 -97
  106. package/src/export_registrations/database-config.registration.ts +0 -94
  107. package/src/export_registrations/eval.registration.ts +0 -334
  108. package/src/export_registrations/index.ts +0 -31
  109. package/src/export_registrations/mcp-config.registration.ts +0 -97
  110. package/src/export_registrations/menu.registration.ts +0 -91
  111. package/src/export_registrations/metrics-config.registration.ts +0 -94
  112. package/src/export_registrations/skill.registration.ts +0 -88
  113. package/src/export_registrations/task.registration.ts +0 -99
  114. package/src/index.ts +0 -497
  115. package/src/logger/Logger.ts +0 -186
  116. package/src/router/MessageContext.ts +0 -14
  117. package/src/router/MessageRouter.ts +0 -490
  118. package/src/router/middlewares/auditLogger.ts +0 -34
  119. package/src/router/middlewares/deduplication.ts +0 -38
  120. package/src/router/middlewares/index.ts +0 -3
  121. package/src/router/middlewares/rateLimit.ts +0 -46
  122. package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
  123. package/src/routes/a2a-bridge.ts +0 -266
  124. package/src/routes/a2a-test-connection.ts +0 -126
  125. package/src/routes/a2a.ts +0 -851
  126. package/src/routes/channel-bindings.ts +0 -11
  127. package/src/routes/channel-installations.ts +0 -33
  128. package/src/routes/index.ts +0 -640
  129. package/src/routes/menu-items.ts +0 -10
  130. package/src/routes/resource-routes.ts +0 -35
  131. package/src/routes/stt.ts +0 -102
  132. package/src/schemas/data-query.ts +0 -69
  133. package/src/schemas/index.ts +0 -852
  134. package/src/services/ExportImportService.ts +0 -296
  135. package/src/services/__tests__/ExportImportService.test.ts +0 -130
  136. package/src/services/__tests__/eval-runner.test.ts +0 -131
  137. package/src/services/__tests__/local-a2a.test.ts +0 -124
  138. package/src/services/agent_task_consumer.ts +0 -348
  139. package/src/services/collection_service.ts +0 -258
  140. package/src/services/eval-runner.ts +0 -283
  141. package/src/services/local-a2a.ts +0 -354
  142. package/src/services/queue_service.ts +0 -104
  143. package/src/services/sandbox_service.ts +0 -223
  144. package/src/services/skill_service.ts +0 -97
  145. package/src/services/supabase.ts +0 -60
  146. package/src/swagger.ts +0 -77
  147. package/src/types/index.ts +0 -118
  148. package/src/types/koa.d.ts +0 -7
  149. package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
  150. package/src/utils/__tests__/path-containment.test.ts +0 -35
  151. package/src/utils/a2a-url.ts +0 -20
  152. package/src/utils/local-a2a-runtime.ts +0 -107
  153. package/src/utils/mime.ts +0 -44
  154. package/src/utils/path-containment.ts +0 -38
  155. package/tsconfig.json +0 -26
@@ -1,970 +0,0 @@
1
- /**
2
- * Data Query SDK for Axiom Lattice Gateway
3
- *
4
- * 用于在浏览器中调用 /api/data/query 接口进行数据查询
5
- * 支持语义查询和 SQL 查询,返回原始数据格式
6
- *
7
- * @version 3.0.0 - 支持状态机版本
8
- * @author Axiom Lattice
9
- */
10
-
11
- (function (global, factory) {
12
- var result;
13
- if (typeof exports === 'object' && typeof module !== 'undefined') {
14
- result = factory();
15
- module.exports = result.DataQuerySDK;
16
- module.exports.DashboardEngine = result.DashboardEngine;
17
- module.exports.StateManager = result.StateManager;
18
- module.exports.validateDashboardConfig = result.validateDashboardConfig;
19
- module.exports.globalState = result.globalState;
20
- } else if (typeof define === 'function' && define.amd) {
21
- define(factory);
22
- } else {
23
- global = typeof globalThis !== 'undefined' ? globalThis : global || self;
24
- result = factory();
25
- global.DataQuerySDK = result.DataQuerySDK;
26
- global.DashboardEngine = result.DashboardEngine;
27
- global.StateManager = result.StateManager;
28
- global.validateDashboardConfig = result.validateDashboardConfig;
29
- global.globalState = result.globalState;
30
- }
31
- }(this, function () {
32
- 'use strict';
33
-
34
- // ============================================
35
- // StateManager - 全局状态管理器
36
- // ============================================
37
- class StateManager {
38
- constructor() {
39
- this.state = new Map();
40
- this.listeners = new Map();
41
- }
42
-
43
- set(key, value) {
44
- const oldValue = this.state.get(key);
45
- this.state.set(key, value);
46
-
47
- if (this.listeners.has(key)) {
48
- this.listeners.get(key).forEach(callback => {
49
- try {
50
- callback(value, oldValue);
51
- } catch (error) {
52
- console.error(`StateManager: Error in listener for ${key}:`, error);
53
- }
54
- });
55
- }
56
- }
57
-
58
- get(key) {
59
- return this.state.get(key);
60
- }
61
-
62
- subscribe(key, callback) {
63
- if (!this.listeners.has(key)) {
64
- this.listeners.set(key, new Set());
65
- }
66
- this.listeners.get(key).add(callback);
67
-
68
- return () => {
69
- if (this.listeners.has(key)) {
70
- this.listeners.get(key).delete(callback);
71
- }
72
- };
73
- }
74
-
75
- clear() {
76
- this.state.clear();
77
- this.listeners.clear();
78
- }
79
- }
80
-
81
- const globalState = new StateManager();
82
-
83
- // ============================================
84
- // ConfigValidator - 配置验证器
85
- // ============================================
86
- const VALID_WIDGET_TYPES = ['bar', 'line', 'pie', 'scatter', 'kpi', 'table'];
87
- const VALID_OPERATORS = ['EQ', 'NE', 'GT', 'LT', 'GTE', 'LTE', 'BETWEEN', 'IN'];
88
-
89
- function validateDashboardConfig(config) {
90
- const errors = [];
91
-
92
- if (!config || typeof config !== 'object') {
93
- return { valid: false, errors: ['配置必须是对象'] };
94
- }
95
-
96
- if (config.filters) {
97
- if (!Array.isArray(config.filters)) {
98
- errors.push('filters 必须是数组');
99
- } else {
100
- config.filters.forEach((filter, index) => {
101
- validateFilter(filter, index, errors);
102
- });
103
- }
104
- }
105
-
106
- if (!Array.isArray(config.widgets)) {
107
- errors.push('widgets 必须是数组');
108
- } else if (config.widgets.length === 0) {
109
- errors.push('widgets 数组不能为空');
110
- } else {
111
- config.widgets.forEach((widget, index) => {
112
- validateWidget(widget, index, errors, config);
113
- });
114
- }
115
-
116
- if (config.widgets) {
117
- validateStateLoop(config.widgets, errors);
118
- }
119
-
120
- return { valid: errors.length === 0, errors };
121
- }
122
-
123
- function validateFilter(filter, index, errors) {
124
- const prefix = `filters[${index}]`;
125
- if (!filter.id) errors.push(`${prefix} 缺少 id`);
126
- if (!filter.dimension) errors.push(`${prefix} 缺少 dimension`);
127
- if (!filter.operator) {
128
- errors.push(`${prefix} 缺少 operator`);
129
- } else if (!VALID_OPERATORS.includes(filter.operator)) {
130
- errors.push(`${prefix} operator "${filter.operator}" 无效,可选: ${VALID_OPERATORS.join(', ')}`);
131
- }
132
- if (!filter.emitsToState) errors.push(`${prefix} 缺少 emitsToState`);
133
- if (!filter.options || !Array.isArray(filter.options)) {
134
- errors.push(`${prefix} 缺少 options 数组`);
135
- }
136
- }
137
-
138
- function validateWidget(widget, index, errors, config) {
139
- const prefix = `widgets[${index}]`;
140
- if (!widget.id) errors.push(`${prefix} 缺少 id`);
141
- if (!widget.type) {
142
- errors.push(`${prefix} 缺少 type`);
143
- } else if (!VALID_WIDGET_TYPES.includes(widget.type)) {
144
- errors.push(`${prefix} type "${widget.type}" 无效,可选: ${VALID_WIDGET_TYPES.join(', ')}`);
145
- }
146
- if (!widget.title) errors.push(`${prefix} 缺少 title`);
147
-
148
- if (!widget.query) {
149
- errors.push(`${prefix} 缺少 query`);
150
- } else {
151
- // serverKey 和 datasourceId 可以从 config 或 widget.query 获取,不在 widget.query 层强制要求
152
- if (!widget.query.metrics || !Array.isArray(widget.query.metrics)) {
153
- errors.push(`${prefix}.query.metrics 必须是数组`);
154
- }
155
- }
156
-
157
- // table 类型不需要 mapping,其他类型需要
158
- if (widget.type !== 'table') {
159
- if (!widget.mapping) {
160
- errors.push(`${prefix} 缺少 mapping`);
161
- } else {
162
- validateMapping(widget.mapping, widget.type, prefix, errors);
163
- }
164
- }
165
-
166
- if (widget.receiveFilters) {
167
- if (!Array.isArray(widget.receiveFilters)) {
168
- errors.push(`${prefix}.receiveFilters 必须是数组`);
169
- } else {
170
- widget.receiveFilters.forEach((rf, rfIndex) => {
171
- validateReceiveFilter(rf, `${prefix}.receiveFilters[${rfIndex}]`, errors);
172
- });
173
- }
174
- }
175
-
176
- if (widget.emit) {
177
- if (!Array.isArray(widget.emit)) {
178
- errors.push(`${prefix}.emit 必须是数组`);
179
- } else {
180
- widget.emit.forEach((em, emIndex) => {
181
- validateEmit(em, `${prefix}.emit[${emIndex}]`, errors);
182
- });
183
- }
184
- }
185
- }
186
-
187
- function validateMapping(mapping, type, prefix, errors) {
188
- if (type === 'pie') {
189
- if (!mapping.name) errors.push(`${prefix}.mapping 缺少 name`);
190
- if (!mapping.value) errors.push(`${prefix}.mapping 缺少 value`);
191
- } else if (type === 'kpi') {
192
- if (!mapping.value) errors.push(`${prefix}.mapping 缺少 value`);
193
- } else if (type === 'table') {
194
- // table 类型不需要特定的 mapping 字段
195
- return;
196
- } else {
197
- if (!mapping.xAxis) errors.push(`${prefix}.mapping 缺少 xAxis`);
198
- if (!mapping.yAxis || !Array.isArray(mapping.yAxis)) {
199
- errors.push(`${prefix}.mapping.yAxis 必须是数组`);
200
- }
201
- }
202
- }
203
-
204
- function validateReceiveFilter(rf, prefix, errors) {
205
- if (!rf.fromState) errors.push(`${prefix} 缺少 fromState`);
206
- if (!rf.mapToDimension) errors.push(`${prefix} 缺少 mapToDimension`);
207
- if (!rf.operator) {
208
- errors.push(`${prefix} 缺少 operator`);
209
- } else if (!VALID_OPERATORS.includes(rf.operator)) {
210
- errors.push(`${prefix} operator "${rf.operator}" 无效`);
211
- }
212
- }
213
-
214
- function validateEmit(em, prefix, errors) {
215
- if (!em.event) errors.push(`${prefix} 缺少 event`);
216
- if (!em.extractValueFrom) errors.push(`${prefix} 缺少 extractValueFrom`);
217
- if (!em.updateState) errors.push(`${prefix} 缺少 updateState`);
218
- }
219
-
220
- function validateStateLoop(widgets, errors) {
221
- const emittedStates = new Set();
222
- const receivedStates = new Set();
223
-
224
- widgets.forEach(widget => {
225
- if (widget.emit) {
226
- widget.emit.forEach(em => {
227
- if (em.updateState) emittedStates.add(em.updateState);
228
- });
229
- }
230
- if (widget.receiveFilters) {
231
- widget.receiveFilters.forEach(rf => {
232
- if (rf.fromState) receivedStates.add(rf.fromState);
233
- });
234
- }
235
- });
236
-
237
- emittedStates.forEach(state => {
238
- if (!receivedStates.has(state)) {
239
- errors.push(`闭环校验失败: emit 状态 "${state}" 没有对应的 receiveFilters 消费`);
240
- }
241
- });
242
- }
243
-
244
- // ============================================
245
- // DataQuerySDK
246
- // ============================================
247
- class DataQuerySDK {
248
- constructor(config = {}) {
249
- if (!config.baseURL) {
250
- throw new Error('DataQuerySDK: baseURL is required');
251
- }
252
-
253
- this.baseURL = config.baseURL.replace(/\/$/, '');
254
- this.defaultServerKey = config.serverKey || null;
255
- this.defaultDatasourceId = config.datasourceId || null;
256
-
257
- const globalContext = typeof window !== 'undefined' && window.__AI2APP_CONTEXT__;
258
- this.tenantId = config.tenantId || (globalContext && globalContext.tenantId) || null;
259
- this.workspaceId = config.workspaceId || (globalContext && globalContext.workspaceId) || null;
260
- this.projectId = config.projectId || (globalContext && globalContext.projectId) || null;
261
-
262
- this.headers = config.headers || {};
263
- this.timeout = config.timeout || 30000;
264
- }
265
-
266
- async query(params) {
267
- const serverKey = params.serverKey || this.defaultServerKey;
268
- const datasourceId = params.datasourceId || this.defaultDatasourceId;
269
-
270
- if (!serverKey) {
271
- throw new Error('DataQuerySDK: serverKey is required');
272
- }
273
- if (!datasourceId) {
274
- throw new Error('DataQuerySDK: datasourceId is required');
275
- }
276
-
277
- const body = {
278
- ...params,
279
- serverKey: serverKey,
280
- datasourceId: datasourceId,
281
- limit: params.limit || 1000
282
- };
283
-
284
- const headers = {
285
- 'Content-Type': 'application/json',
286
- ...this.headers
287
- };
288
-
289
- if (this.tenantId) {
290
- headers['x-tenant-id'] = this.tenantId;
291
- }
292
-
293
- try {
294
- const controller = new AbortController();
295
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
296
-
297
- const response = await fetch(`${this.baseURL}/api/data/query`, {
298
- method: 'POST',
299
- headers,
300
- body: JSON.stringify(body),
301
- signal: controller.signal
302
- });
303
-
304
- clearTimeout(timeoutId);
305
- const result = await response.json();
306
-
307
- if (!response.ok) {
308
- throw new Error(`HTTP ${response.status}: ${result.message || 'Request failed'}`);
309
- }
310
-
311
- if (!result.success) {
312
- throw new Error(result.message || 'Query failed');
313
- }
314
-
315
- // Add rowsObject for easier data access
316
- if (result.data && result.data.columns && result.data.rows) {
317
- const columns = result.data.columns;
318
- result.data.rowsObject = result.data.rows.map(row => {
319
- const obj = {};
320
- columns.forEach((col, index) => {
321
- obj[col.name] = row[index];
322
- });
323
- return obj;
324
- });
325
- }
326
-
327
- return result;
328
- } catch (error) {
329
- if (error.name === 'AbortError') {
330
- throw new Error('Request timeout');
331
- }
332
- throw error;
333
- }
334
- }
335
-
336
- async semanticQuery(params) {
337
- if (!params.metrics || !Array.isArray(params.metrics) || params.metrics.length === 0) {
338
- throw new Error('DataQuerySDK: metrics array is required for semantic query');
339
- }
340
- return this.query(params);
341
- }
342
-
343
- async sqlQuery(params) {
344
- if (!params.sql) {
345
- throw new Error('DataQuerySDK: sql is required for SQL query');
346
- }
347
- return this.query({ ...params, customSql: params.sql });
348
- }
349
- }
350
-
351
- // ============================================
352
- // DashboardEngine - 状态机版本
353
- // ============================================
354
- class DashboardEngine {
355
- constructor(sdk, config, stateManager) {
356
- if (!sdk || typeof sdk.query !== 'function') {
357
- throw new Error('DashboardEngine: sdk instance is required');
358
- }
359
-
360
- const validation = validateDashboardConfig(config);
361
- if (!validation.valid) {
362
- throw new Error('配置错误:\n' + validation.errors.map(e => ' - ' + e).join('\n'));
363
- }
364
-
365
- this.sdk = sdk;
366
- this.config = config;
367
- this.stateManager = stateManager || globalState;
368
- this.widgetConfigs = config.widgets || [];
369
- this.filterConfigs = config.filters || [];
370
- this.instances = new Map();
371
- this.unsubscribers = new Map();
372
- this.resizeObservers = new Map();
373
- this.eventListeners = new Map();
374
- }
375
-
376
- on(event, callback) {
377
- if (!this.eventListeners.has(event)) {
378
- this.eventListeners.set(event, new Set());
379
- }
380
- this.eventListeners.get(event).add(callback);
381
- return () => {
382
- this.eventListeners.get(event).delete(callback);
383
- };
384
- }
385
-
386
- emit(event, data) {
387
- if (this.eventListeners.has(event)) {
388
- this.eventListeners.get(event).forEach(callback => {
389
- try {
390
- callback(data);
391
- } catch (error) {
392
- console.error(`DashboardEngine: Error in event listener for ${event}:`, error);
393
- }
394
- });
395
- }
396
- }
397
-
398
- async init() {
399
- this.renderFilters();
400
- this.renderLayout();
401
- this.setupStateSubscriptions();
402
- await this.refreshAll();
403
- }
404
-
405
- renderFilters() {
406
- const container = document.getElementById('filters-container');
407
- if (!container || this.filterConfigs.length === 0) return;
408
-
409
- container.innerHTML = '';
410
-
411
- this.filterConfigs.forEach(filterConfig => {
412
- const wrapper = document.createElement('div');
413
- wrapper.className = 'inline-block mr-4 mb-2';
414
-
415
- const label = document.createElement('label');
416
- label.className = 'block text-sm font-medium text-gray-700 mb-1';
417
- label.textContent = filterConfig.label;
418
-
419
- const select = document.createElement('select');
420
- select.id = `filter-${filterConfig.id}`;
421
- select.className = 'block w-48 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm';
422
-
423
- filterConfig.options.forEach(opt => {
424
- const option = document.createElement('option');
425
- option.value = opt.value;
426
- option.textContent = opt.text;
427
- if (opt.value === filterConfig.defaultValue) {
428
- option.selected = true;
429
- }
430
- select.appendChild(option);
431
- });
432
-
433
- select.addEventListener('change', (e) => {
434
- const value = e.target.value;
435
- if (filterConfig.emitsToState) {
436
- this.stateManager.set(filterConfig.emitsToState, value);
437
- }
438
- });
439
-
440
- if (filterConfig.emitsToState) {
441
- this.stateManager.set(filterConfig.emitsToState, filterConfig.defaultValue);
442
- }
443
-
444
- wrapper.appendChild(label);
445
- wrapper.appendChild(select);
446
- container.appendChild(wrapper);
447
- });
448
- }
449
-
450
- renderLayout() {
451
- // Priority 1: Use external container if specified and exists
452
- if (this.config.containerId) {
453
- const externalContainer = document.getElementById(this.config.containerId);
454
- if (externalContainer) {
455
- this.renderToContainer(externalContainer);
456
- return;
457
- }
458
- }
459
-
460
- // Priority 2: Use dashboard-grid if exists
461
- const grid = document.getElementById('dashboard-grid');
462
- if (grid) {
463
- this.renderToGrid(grid);
464
- return;
465
- }
466
-
467
- // Priority 3: Auto-create container in body
468
- console.log('DashboardEngine: No container found, auto-creating dashboard container');
469
- this.autoCreateContainer();
470
- }
471
-
472
- autoCreateContainer() {
473
- // Create a default container
474
- const container = document.createElement('div');
475
- container.id = 'dashboard-auto-container';
476
- container.className = 'max-w-7xl mx-auto px-4 py-8';
477
-
478
- // Add title if specified in config
479
- if (this.config.title) {
480
- const title = document.createElement('h1');
481
- title.className = 'text-3xl font-bold text-gray-900 mb-8';
482
- title.textContent = this.config.title;
483
- container.appendChild(title);
484
- }
485
-
486
- // Add filters container
487
- const filtersContainer = document.createElement('div');
488
- filtersContainer.id = 'filters-container';
489
- filtersContainer.className = 'mb-4';
490
- container.appendChild(filtersContainer);
491
-
492
- // Add grid container
493
- const grid = document.createElement('div');
494
- grid.id = 'dashboard-grid';
495
- grid.className = 'grid grid-cols-12 gap-4';
496
- container.appendChild(grid);
497
-
498
- // Append to body
499
- document.body.appendChild(container);
500
-
501
- // Now render to the grid
502
- this.renderToGrid(grid);
503
- }
504
-
505
- renderToContainer(container) {
506
- // Render widgets to a specific external container
507
- // Each widget should have containerId pointing to existing DOM elements
508
- this.widgetConfigs.forEach(config => {
509
- const widgetContainer = document.getElementById(config.containerId);
510
- if (!widgetContainer) {
511
- console.warn(`DashboardEngine: Container "${config.containerId}" not found for widget "${config.id}"`);
512
- return;
513
- }
514
-
515
- // Initialize echarts for this widget
516
- if (config.type !== 'kpi' && config.type !== 'table' && typeof echarts !== 'undefined') {
517
- const chart = echarts.init(widgetContainer);
518
- this.instances.set(config.id, chart);
519
-
520
- if (config.emit) {
521
- chart.on('click', (params) => {
522
- this.handleChartClick(config, params);
523
- });
524
- }
525
-
526
- const resizeObserver = new ResizeObserver(() => chart.resize());
527
- resizeObserver.observe(widgetContainer);
528
- this.resizeObservers.set(config.id, resizeObserver);
529
- }
530
- });
531
- }
532
-
533
- renderToGrid(grid) {
534
- grid.innerHTML = '';
535
-
536
- this.widgetConfigs.forEach(config => {
537
- const wrapper = document.createElement('div');
538
- wrapper.className = config.gridClass || 'col-span-12';
539
-
540
- const card = document.createElement('div');
541
- card.className = 'bg-white p-5 rounded-xl shadow-sm border border-gray-100';
542
-
543
- const title = document.createElement('h3');
544
- title.className = 'text-sm font-bold text-gray-700 mb-4 flex items-center gap-2';
545
- title.innerHTML = `
546
- <span class="w-1 h-4 bg-indigo-500 rounded-full"></span>
547
- ${config.title}
548
- `;
549
-
550
- const chartContainer = document.createElement('div');
551
- chartContainer.id = `chart-${config.id}`;
552
-
553
- if (config.type === 'kpi') {
554
- chartContainer.className = 'flex items-center justify-center py-8';
555
- chartContainer.innerHTML = '<div class="text-4xl font-black text-indigo-600">-</div>';
556
- } else if (config.type === 'table') {
557
- chartContainer.className = 'overflow-auto';
558
- chartContainer.style.height = '320px';
559
- chartContainer.style.width = '100%';
560
- } else {
561
- chartContainer.className = 'h-80';
562
- chartContainer.style.height = '320px';
563
- chartContainer.style.width = '100%';
564
- }
565
-
566
- card.appendChild(title);
567
- card.appendChild(chartContainer);
568
- wrapper.appendChild(card);
569
- grid.appendChild(wrapper);
570
-
571
- if (config.type !== 'kpi' && config.type !== 'table' && typeof echarts !== 'undefined') {
572
- const chart = echarts.init(chartContainer);
573
- this.instances.set(config.id, chart);
574
-
575
- if (config.emit) {
576
- chart.on('click', (params) => {
577
- this.handleChartClick(config, params);
578
- });
579
- }
580
-
581
- const resizeObserver = new ResizeObserver(() => chart.resize());
582
- resizeObserver.observe(chartContainer);
583
- this.resizeObservers.set(config.id, resizeObserver);
584
- }
585
- });
586
- }
587
-
588
- setupStateSubscriptions() {
589
- this.widgetConfigs.forEach(config => {
590
- if (!config.receiveFilters) return;
591
-
592
- const unsubscribes = [];
593
- config.receiveFilters.forEach(rf => {
594
- const unsubscribe = this.stateManager.subscribe(rf.fromState, () => {
595
- this.refreshWidget(config);
596
- });
597
- unsubscribes.push(unsubscribe);
598
- });
599
-
600
- this.unsubscribers.set(config.id, unsubscribes);
601
- });
602
- }
603
-
604
- handleChartClick(config, params) {
605
- if (!config.emit) return;
606
-
607
- config.emit.forEach(em => {
608
- if (em.event === 'click') {
609
- const value = params.data[em.extractValueFrom];
610
- if (value !== undefined) {
611
- this.stateManager.set(em.updateState, value);
612
- }
613
- }
614
- });
615
- }
616
-
617
- buildQueryFilters(config) {
618
- const baseFilters = [...(config.query.filters || [])];
619
- const filters = [];
620
- const processedDimensions = new Set();
621
-
622
- // 先处理 receiveFilters(global filter),优先级更高
623
- if (config.receiveFilters) {
624
- config.receiveFilters.forEach(rf => {
625
- const stateValue = this.stateManager.get(rf.fromState);
626
-
627
- if (rf.ignoreIfEmpty && (!stateValue || stateValue === 'ALL')) {
628
- return;
629
- }
630
-
631
- if (stateValue !== undefined) {
632
- let values;
633
- if (rf.operator === 'BETWEEN' && typeof stateValue === 'string' && stateValue.includes(',')) {
634
- // BETWEEN 操作符,值是逗号分隔的字符串,需要拆分成数组
635
- values = stateValue.split(',').map(v => v.trim());
636
- } else {
637
- values = [stateValue];
638
- }
639
- filters.push({
640
- dimension: rf.mapToDimension,
641
- operator: rf.operator,
642
- values: values
643
- });
644
- processedDimensions.add(rf.mapToDimension);
645
- }
646
- });
647
- }
648
-
649
- // 再添加 baseFilters 中未被覆盖的 dimension
650
- baseFilters.forEach(f => {
651
- if (!processedDimensions.has(f.dimension)) {
652
- filters.push(f);
653
- }
654
- });
655
-
656
- return filters;
657
- }
658
-
659
- async refreshWidget(config) {
660
- const instance = this.instances.get(config.id);
661
- if (instance) instance.showLoading();
662
-
663
- try {
664
- const filters = this.buildQueryFilters(config);
665
-
666
- const result = await this.sdk.semanticQuery({
667
- serverKey: config.query.serverKey || this.config.serverKey,
668
- datasourceId: config.query.datasourceId || this.config.datasourceId,
669
- metrics: config.query.metrics,
670
- filters
671
- });
672
-
673
- const columns = result.data?.columns || [];
674
- const rows = result.data?.rows || [];
675
- const rowsObject = rows.map(row => {
676
- const obj = {};
677
- columns.forEach((col, index) => {
678
- obj[col.name] = row[index];
679
- });
680
- return obj;
681
- });
682
-
683
- // Emit data event for external listeners
684
- this.emit(`widget:${config.id}:data`, rowsObject);
685
-
686
- if (config.type === 'kpi') {
687
- this.renderKPI(config, rowsObject);
688
- } else if (config.type === 'table') {
689
- this.renderTable(config, columns, rows);
690
- } else if (instance) {
691
- const option = this.generateOption(config, rowsObject);
692
- if (option) instance.setOption(option, true);
693
- instance.hideLoading();
694
- }
695
- } catch (error) {
696
- console.error(`DashboardEngine: Failed to refresh widget "${config.id}":`, error);
697
- if (instance) instance.hideLoading();
698
- }
699
- }
700
-
701
- renderKPI(config, rowsObject) {
702
- const value = rowsObject[0]?.[config.mapping?.value] || 0;
703
- const container = document.getElementById(config.containerId || `chart-${config.id}`);
704
- if (container) {
705
- const formattedValue = this.formatValue(value, config.mapping?.valueFormat);
706
- container.innerHTML = `<div class="text-4xl font-black text-indigo-600">${formattedValue}</div>`;
707
- }
708
- }
709
-
710
- renderTable(config, columns, rows) {
711
- const container = document.getElementById(config.containerId || `chart-${config.id}`);
712
- if (!container) return;
713
-
714
- const tableConfig = config.tableConfig || {};
715
- const pageSize = tableConfig.pageSize || 10;
716
- const showPagination = rows.length > pageSize;
717
-
718
- // 生成表头
719
- const headerHtml = columns.map(col => {
720
- const align = tableConfig.align || 'left';
721
- return `<th class="px-4 py-2 text-${align} text-xs font-medium text-gray-500 uppercase tracking-wider bg-gray-50 border-b">${col.name}</th>`;
722
- }).join('');
723
-
724
- // 生成表格行
725
- const rowsHtml = rows.map((row, rowIndex) => {
726
- const cellsHtml = row.map((cell, cellIndex) => {
727
- const col = columns[cellIndex];
728
- const align = tableConfig.align || 'left';
729
- const formattedValue = this.formatTableCell(cell, col.type, tableConfig);
730
- return `<td class="px-4 py-2 text-${align} text-sm text-gray-900 border-b">${formattedValue}</td>`;
731
- }).join('');
732
- return `<tr class="hover:bg-gray-50 ${rowIndex >= pageSize ? 'hidden' : ''}" data-page="${Math.floor(rowIndex / pageSize)}">${cellsHtml}</tr>`;
733
- }).join('');
734
-
735
- // 生成分页控件
736
- let paginationHtml = '';
737
- if (showPagination) {
738
- const totalPages = Math.ceil(rows.length / pageSize);
739
- paginationHtml = `
740
- <div class="flex items-center justify-between px-4 py-3 bg-white border-t">
741
- <div class="text-sm text-gray-500">
742
- 共 ${rows.length} 条记录
743
- </div>
744
- <div class="flex space-x-1" id="pagination-${config.id}">
745
- ${Array.from({ length: totalPages }, (_, i) => `
746
- <button class="px-3 py-1 text-sm rounded ${i === 0 ? 'bg-indigo-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'}" data-page="${i}">
747
- ${i + 1}
748
- </button>
749
- `).join('')}
750
- </div>
751
- </div>
752
- `;
753
- }
754
-
755
- container.innerHTML = `
756
- <div class="w-full">
757
- <table class="min-w-full divide-y divide-gray-200" id="table-${config.id}">
758
- <thead>
759
- <tr>${headerHtml}</tr>
760
- </thead>
761
- <tbody class="bg-white divide-y divide-gray-200">
762
- ${rowsHtml}
763
- </tbody>
764
- </table>
765
- ${paginationHtml}
766
- </div>
767
- `;
768
-
769
- // 绑定分页事件
770
- if (showPagination) {
771
- const paginationEl = document.getElementById(`pagination-${config.id}`);
772
- if (paginationEl) {
773
- paginationEl.addEventListener('click', (e) => {
774
- if (e.target.tagName === 'BUTTON') {
775
- const page = parseInt(e.target.dataset.page);
776
- this.switchTablePage(config.id, page, pageSize);
777
-
778
- // 更新按钮样式
779
- paginationEl.querySelectorAll('button').forEach(btn => {
780
- btn.className = 'px-3 py-1 text-sm rounded bg-gray-200 text-gray-700 hover:bg-gray-300';
781
- });
782
- e.target.className = 'px-3 py-1 text-sm rounded bg-indigo-600 text-white';
783
- }
784
- });
785
- }
786
- }
787
-
788
- // 绑定行点击事件(用于 emit)
789
- if (config.emit) {
790
- const tableEl = document.getElementById(`table-${config.id}`);
791
- if (tableEl) {
792
- tableEl.addEventListener('click', (e) => {
793
- const row = e.target.closest('tr');
794
- if (row && row.parentElement.tagName === 'TBODY') {
795
- const rowIndex = Array.from(row.parentElement.children).indexOf(row);
796
- const rowData = rows[rowIndex];
797
- const rowObject = {};
798
- columns.forEach((col, index) => {
799
- rowObject[col.name] = rowData[index];
800
- });
801
- this.handleTableRowClick(config, rowObject);
802
- }
803
- });
804
- }
805
- }
806
- }
807
-
808
- switchTablePage(widgetId, page, pageSize) {
809
- const tableEl = document.getElementById(`table-${widgetId}`);
810
- if (!tableEl) return;
811
-
812
- const rows = tableEl.querySelectorAll('tbody tr');
813
- rows.forEach(row => {
814
- const rowPage = parseInt(row.dataset.page);
815
- row.classList.toggle('hidden', rowPage !== page);
816
- });
817
- }
818
-
819
- handleTableRowClick(config, rowData) {
820
- if (!config.emit) return;
821
-
822
- config.emit.forEach(em => {
823
- if (em.event === 'click') {
824
- const value = rowData[em.extractValueFrom];
825
- if (value !== undefined) {
826
- this.stateManager.set(em.updateState, value);
827
- }
828
- }
829
- });
830
- }
831
-
832
- formatTableCell(value, type, tableConfig) {
833
- if (value === null || value === undefined) return '-';
834
-
835
- // 使用列特定的格式化配置
836
- const format = tableConfig?.columnFormats?.[type] || tableConfig?.defaultFormat;
837
-
838
- if (format) {
839
- return this.formatValue(value, format);
840
- }
841
-
842
- // 默认格式化
843
- if (typeof value === 'number') {
844
- return value.toLocaleString();
845
- }
846
-
847
- return String(value);
848
- }
849
-
850
- formatValue(value, format) {
851
- const num = Number(value);
852
- if (isNaN(num)) return value;
853
-
854
- switch (format) {
855
- case 'currency_cny':
856
- return '¥' + num.toLocaleString('zh-CN');
857
- case 'currency_usd':
858
- return '$' + num.toLocaleString('en-US');
859
- case 'percent':
860
- return (num * 100).toFixed(2) + '%';
861
- case 'compact_number':
862
- return new Intl.NumberFormat('zh-CN', { notation: 'compact' }).format(num);
863
- default:
864
- return num.toLocaleString();
865
- }
866
- }
867
-
868
- generateOption(config, rows) {
869
- const mapping = config.mapping || {};
870
- const type = config.type;
871
-
872
- const baseOption = {
873
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
874
- legend: { bottom: 0 },
875
- grid: { top: 40, right: 40, bottom: 60, left: 60 },
876
- color: ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899']
877
- };
878
-
879
- switch (type) {
880
- case 'pie':
881
- return this.generatePieOption(config, rows, baseOption);
882
- case 'scatter':
883
- return this.generateScatterOption(config, rows, baseOption);
884
- default:
885
- return this.generateStandardOption(config, rows, baseOption);
886
- }
887
- }
888
-
889
- generateStandardOption(config, rows, baseOption) {
890
- const mapping = config.mapping || {};
891
- const yAxisFields = mapping.yAxis || [];
892
-
893
- return {
894
- ...baseOption,
895
- dataset: { source: rows },
896
- xAxis: { type: 'category', name: mapping.xAxisName || '' },
897
- yAxis: { type: 'value' },
898
- series: yAxisFields.map(field => ({
899
- name: field,
900
- type: config.type || 'bar',
901
- encode: { x: mapping.xAxis, y: field },
902
- smooth: config.type === 'line'
903
- }))
904
- };
905
- }
906
-
907
- generatePieOption(config, rows, baseOption) {
908
- const mapping = config.mapping || {};
909
- const data = rows.map(row => ({
910
- name: row[mapping.name],
911
- value: row[mapping.value]
912
- }));
913
-
914
- return {
915
- ...baseOption,
916
- tooltip: { trigger: 'item' },
917
- series: [{
918
- type: 'pie',
919
- radius: ['40%', '70%'],
920
- data
921
- }]
922
- };
923
- }
924
-
925
- generateScatterOption(config, rows, baseOption) {
926
- const mapping = config.mapping || {};
927
-
928
- return {
929
- ...baseOption,
930
- dataset: { source: rows },
931
- xAxis: { type: 'value', name: mapping.xAxis },
932
- yAxis: { type: 'value', name: mapping.yAxis?.[0] },
933
- series: [{
934
- type: 'scatter',
935
- encode: { x: mapping.xAxis, y: mapping.yAxis?.[0] }
936
- }]
937
- };
938
- }
939
-
940
- async refreshAll() {
941
- await Promise.all(this.widgetConfigs.map(config => this.refreshWidget(config)));
942
- }
943
-
944
- destroy() {
945
- this.unsubscribers.forEach(unsubscribes => {
946
- unsubscribes.forEach(unsubscribe => unsubscribe());
947
- });
948
- this.unsubscribers.clear();
949
-
950
- this.resizeObservers.forEach(observer => observer.disconnect());
951
- this.resizeObservers.clear();
952
-
953
- this.instances.forEach(chart => chart.dispose());
954
- this.instances.clear();
955
- }
956
- }
957
-
958
- // ============================================
959
- // 导出
960
- // ============================================
961
- return {
962
- DataQuerySDK,
963
- DashboardEngine,
964
- StateManager,
965
- validateDashboardConfig,
966
- globalState,
967
- VALID_WIDGET_TYPES,
968
- VALID_OPERATORS
969
- };
970
- }));