@xpert-ai/plugin-live-dashboard 0.0.0-stage → 0.1.1
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.
- package/.xpertai-plugin/plugin.json +8 -0
- package/README.md +108 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/lib/agent.schema.d.ts +568 -0
- package/dist/lib/agent.schema.js +52 -0
- package/dist/lib/dashboard-icon.d.ts +5 -0
- package/dist/lib/dashboard-icon.js +8 -0
- package/dist/lib/dashboard-navigation.d.ts +38 -0
- package/dist/lib/dashboard-navigation.js +18 -0
- package/dist/lib/dashboard-queries.d.ts +8 -0
- package/dist/lib/dashboard-queries.js +39 -0
- package/dist/lib/dashboard-receipt.d.ts +11 -0
- package/dist/lib/dashboard-receipt.js +35 -0
- package/dist/lib/dashboard.schema.d.ts +816 -0
- package/dist/lib/dashboard.schema.js +74 -0
- package/dist/lib/dashboard.service.d.ts +243 -0
- package/dist/lib/dashboard.service.js +185 -0
- package/dist/lib/document-libraries.d.ts +8 -0
- package/dist/lib/document-libraries.js +41 -0
- package/dist/lib/document-package.d.ts +41 -0
- package/dist/lib/document-package.js +25 -0
- package/dist/lib/editing.d.ts +403 -0
- package/dist/lib/editing.js +114 -0
- package/dist/lib/filter-evidence.d.ts +71 -0
- package/dist/lib/filter-evidence.js +60 -0
- package/dist/lib/html-guidance.d.ts +1 -0
- package/dist/lib/html-guidance.js +4 -0
- package/dist/lib/html-validation.d.ts +19 -0
- package/dist/lib/html-validation.js +136 -0
- package/dist/lib/middleware.d.ts +10 -0
- package/dist/lib/middleware.js +225 -0
- package/dist/lib/plugin.d.ts +2 -0
- package/dist/lib/plugin.js +22 -0
- package/dist/lib/presentation.provider.d.ts +122 -0
- package/dist/lib/presentation.provider.js +72 -0
- package/dist/lib/remote-entry.d.ts +4 -0
- package/dist/lib/remote-entry.js +12 -0
- package/dist/lib/renderer.d.ts +58 -0
- package/dist/lib/renderer.js +32 -0
- package/dist/lib/resource-card.d.ts +21 -0
- package/dist/lib/resource-card.js +33 -0
- package/dist/lib/save-evidence.d.ts +24 -0
- package/dist/lib/save-evidence.js +99 -0
- package/dist/lib/sharing.service.d.ts +93 -0
- package/dist/lib/sharing.service.js +71 -0
- package/dist/lib/view.provider.d.ts +235 -0
- package/dist/lib/view.provider.js +103 -0
- package/dist/libraries/ECHARTS-LICENSE.txt +222 -0
- package/dist/libraries/ECHARTS-NOTICE.txt +5 -0
- package/dist/libraries/THREE-LICENSE.txt +21 -0
- package/dist/libraries/catalog.json +1 -0
- package/dist/remote/app.css +1 -0
- package/dist/remote/app.js +253 -0
- package/dist/remote/build-manifest.json +63 -0
- package/dist/remote/renderer-source.json +5 -0
- package/dist/remote/renderer.html +4653 -0
- package/dist/sandbox-actions/action.json +10 -0
- package/dist/sandbox-actions/bundle/host.js +149 -0
- package/dist/sandbox-actions/bundle/runner.mjs +15 -0
- package/dist/sandbox-actions/bundle/validate.mjs +140 -0
- package/dist/validation/host-entry.d.ts +1 -0
- package/dist/validation/host-entry.js +6 -0
- package/docs/AGENTS.md +9 -0
- package/docs/acceptance.md +89 -0
- package/docs/agent-acceptance.md +96 -0
- package/docs/chat-resource-cards.mdx +60 -0
- package/docs/docs.json +49 -0
- package/docs/drafts-acceptance.mdx +42 -0
- package/docs/drafts.mdx +35 -0
- package/docs/editing.mdx +63 -0
- package/docs/evolution-acceptance.mdx +98 -0
- package/docs/file-authoring-acceptance.mdx +62 -0
- package/docs/file-authoring.mdx +49 -0
- package/docs/file-datasets-acceptance.mdx +65 -0
- package/docs/fixed-build-multimodel-acceptance.mdx +129 -0
- package/docs/frontend-libraries-acceptance.mdx +86 -0
- package/docs/frontend-libraries.mdx +57 -0
- package/docs/html-acceptance.md +86 -0
- package/docs/html-mode.md +76 -0
- package/docs/index.mdx +47 -0
- package/docs/retry-regression-acceptance.mdx +112 -0
- package/docs/team-sharing-acceptance.mdx +66 -0
- package/docs/team-sharing.mdx +44 -0
- package/docs/verification.mdx +91 -0
- package/examples/assistant-middleware.yaml +28 -0
- package/examples/data-xpert-sales.json +118 -0
- package/examples/postgres-sales.json +93 -0
- package/index.cjs +1 -0
- package/package.json +73 -3
- package/skills/live-dashboard/SKILL.md +71 -0
- package/skills/live-dashboard/agents/xpertai.yaml +21 -0
- package/skills/live-dashboard/references/data-validation.md +15 -0
- package/skills/live-dashboard/references/html-lifecycle.md +129 -0
- package/skills/live-dashboard/references/html-runtime.md +53 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HTML_DASHBOARD_GUIDANCE = "For free-layout HTML dashboards, read the installed live-dashboard skill's references/html-runtime.md through read_skill_file, relative to its SKILL.md path in the skill catalog. Create/edit the HTML with SandboxFile; then dashboard_create_html or dashboard_update validates and saves it. Do not use generic present_files to save or preview a live dashboard. Its preview belongs to the Live dashboards Workbench. ECharts/Three.js are available via dashboard_libraries; all data access remains declarative Binding queries through window.xpertLiveArtifact. The page MUST declare const live = window.xpertLiveArtifact in its script (live is not a host global), register live.subscribe(bindingId, render), await live.ready(), and invoke live.query(bindingId, typedControls) for initial data and filter changes. window.__datasets and window.__refresh are not platform APIs and are never populated by the host. Read the runtime reference and its html-lifecycle.md example before writing HTML; preserve that lifecycle while freely changing the layout. Use one complete Dataset for KPI, trend and details when they share the same metric and filters; do not create a redundant unfiltered total Binding. Never let filtered and unfiltered subscriptions both write the same KPI: a refresh would race and overwrite the selected-region total. Distinct global totals need distinct, clearly labeled elements. Native data selects require data-dashboard-control=\"<controlId>\"; purely visual selects use data-dashboard-presentation. A data filter must query, not only filter cached browser rows. bindSelect controls ONE binding only. For a shared dimension filter across multiple bindings, follow the skill custom-select example and query every affected binding with the same typed controls; matching control IDs do not broadcast automatically. bindSelect derives options from Dataset fields and is only for dimension filters. Fixed query enums (days, page size, granularity) need options from parameter.allowedValues and a typed change listener calling live.query; never use date rows as numeric day-count options.";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HTML_DASHBOARD_GUIDANCE = void 0;
|
|
4
|
+
exports.HTML_DASHBOARD_GUIDANCE = `For free-layout HTML dashboards, read the installed live-dashboard skill's references/html-runtime.md through read_skill_file, relative to its SKILL.md path in the skill catalog. Create/edit the HTML with SandboxFile; then dashboard_create_html or dashboard_update validates and saves it. Do not use generic present_files to save or preview a live dashboard. Its preview belongs to the Live dashboards Workbench. ECharts/Three.js are available via dashboard_libraries; all data access remains declarative Binding queries through window.xpertLiveArtifact. The page MUST declare const live = window.xpertLiveArtifact in its script (live is not a host global), register live.subscribe(bindingId, render), await live.ready(), and invoke live.query(bindingId, typedControls) for initial data and filter changes. window.__datasets and window.__refresh are not platform APIs and are never populated by the host. Read the runtime reference and its html-lifecycle.md example before writing HTML; preserve that lifecycle while freely changing the layout. Use one complete Dataset for KPI, trend and details when they share the same metric and filters; do not create a redundant unfiltered total Binding. Never let filtered and unfiltered subscriptions both write the same KPI: a refresh would race and overwrite the selected-region total. Distinct global totals need distinct, clearly labeled elements. Native data selects require data-dashboard-control="<controlId>"; purely visual selects use data-dashboard-presentation. A data filter must query, not only filter cached browser rows. bindSelect controls ONE binding only. For a shared dimension filter across multiple bindings, follow the skill custom-select example and query every affected binding with the same typed controls; matching control IDs do not broadcast automatically. bindSelect derives options from Dataset fields and is only for dimension filters. Fixed query enums (days, page size, granularity) need options from parameter.allowedValues and a typed change listener calling live.query; never use date rows as numeric day-count options.`;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AnalyticalDataset, XpertResolvedViewHostContext } from '@xpert-ai/contracts';
|
|
2
|
+
import { type ManagedQueueJob, type ManagedQueueJobContext, type ManagedQueueService, type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
|
|
3
|
+
import type { DashboardManifest } from './dashboard.schema';
|
|
4
|
+
import { type ArtifactDocumentDependencyAsset } from '@xpert-ai/plugin-sdk/artifact-document';
|
|
5
|
+
/** Browser execution belongs to the platform's bounded background pool, not the API request. */
|
|
6
|
+
export declare class HtmlDashboardValidation {
|
|
7
|
+
private readonly runtime;
|
|
8
|
+
private readonly queue;
|
|
9
|
+
constructor(runtime: RuntimeCapabilityRegistry, queue: ManagedQueueService);
|
|
10
|
+
validate(context: XpertResolvedViewHostContext, manifest: DashboardManifest, datasets: AnalyticalDataset[], dependencies?: ArtifactDocumentDependencyAsset[]): Promise<{
|
|
11
|
+
jobId: `${string}-${string}-${string}-${string}-${string}`;
|
|
12
|
+
requestedBindings: string[];
|
|
13
|
+
status: string;
|
|
14
|
+
} | null>;
|
|
15
|
+
/** Ownership comes from persisted queue context. This handler needs no HTTP request or conversation. */
|
|
16
|
+
handle(job: ManagedQueueJob<{
|
|
17
|
+
validationId: string;
|
|
18
|
+
}>, context: ManagedQueueJobContext): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.HtmlDashboardValidation = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const node_crypto_1 = require("node:crypto");
|
|
18
|
+
const promises_1 = require("node:timers/promises");
|
|
19
|
+
const v3_1 = require("zod/v3");
|
|
20
|
+
const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
|
|
21
|
+
const artifact_document_1 = require("@xpert-ai/plugin-sdk/artifact-document");
|
|
22
|
+
const PLUGIN = '@xpert-ai/plugin-live-dashboard', ACTION = 'live_dashboard.validate-html', POOL = 'sandbox-browser';
|
|
23
|
+
const reportSchema = v3_1.z.object({ ok: v3_1.z.boolean(), errors: v3_1.z.array(v3_1.z.string().max(300)).max(16),
|
|
24
|
+
requestedBindings: v3_1.z.array(v3_1.z.string().max(80)).max(60), renderedTextLength: v3_1.z.number().int().nonnegative() }).strict();
|
|
25
|
+
const recordSchema = v3_1.z.object({ tenantId: v3_1.z.string(), organizationId: v3_1.z.string(), userId: v3_1.z.string(), manifestId: v3_1.z.string(), deadline: v3_1.z.number(),
|
|
26
|
+
filePath: v3_1.z.string(), workspacePath: v3_1.z.string(), size: v3_1.z.number(), sha256: v3_1.z.string() }).strict();
|
|
27
|
+
const digest = (buffer) => (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
|
|
28
|
+
const recordKey = (id) => `live_dashboard:validation:${id}`;
|
|
29
|
+
/** Browser execution belongs to the platform's bounded background pool, not the API request. */
|
|
30
|
+
let HtmlDashboardValidation = class HtmlDashboardValidation {
|
|
31
|
+
constructor(runtime, queue) {
|
|
32
|
+
this.runtime = runtime;
|
|
33
|
+
this.queue = queue;
|
|
34
|
+
}
|
|
35
|
+
async validate(context, manifest, datasets, dependencies = []) {
|
|
36
|
+
if (!manifest.presentation)
|
|
37
|
+
return null;
|
|
38
|
+
if (!context.tenantId || !context.organizationId || !context.userId)
|
|
39
|
+
throw new common_1.BadRequestException('DASHBOARD_ACCESS_DENIED');
|
|
40
|
+
const files = this.runtime.require(plugin_sdk_1.WorkspaceFilesRuntimeCapability), jobs = this.runtime.require(plugin_sdk_1.SandboxJobsRuntimeCapability);
|
|
41
|
+
const [health, pool] = await Promise.all([jobs.getActionHealth({ pluginName: PLUGIN, action: ACTION, actionVersion: '1.0.0' }),
|
|
42
|
+
this.queue.getExecutionPoolHealth({ executionPool: POOL })]);
|
|
43
|
+
if (!health.available || !pool.available)
|
|
44
|
+
throw new common_1.BadRequestException(`DASHBOARD_HTML_VALIDATION_UNAVAILABLE: ${health.reason ?? pool.warning ?? 'Browser Runtime unavailable'}`);
|
|
45
|
+
const jobId = (0, node_crypto_1.randomUUID)(), scope = { tenantId: context.tenantId, userId: context.userId, catalog: 'users' };
|
|
46
|
+
const buffer = Buffer.from(JSON.stringify({ html: manifest.presentation.html,
|
|
47
|
+
dependencies: (0, artifact_document_1.verifyArtifactDocumentDependencies)(dependencies, manifest.presentation.dependencies ?? []),
|
|
48
|
+
context: { bindingIds: manifest.bindings.map(binding => binding.id), defaults: Object.fromEntries(manifest.controls.filter(control => control.defaultValue !== undefined).map(control => [control.controlId, control.defaultValue ?? null])), theme: 'light', locale: 'en' },
|
|
49
|
+
parameters: Object.fromEntries(manifest.bindings.map(binding => [binding.id, binding.parameters])),
|
|
50
|
+
datasets: Object.fromEntries(manifest.bindings.map(binding => [binding.id, datasets.find(dataset => dataset.datasetId === binding.output.datasetId)])) }));
|
|
51
|
+
const stored = await files.uploadBuffer({ ...scope, buffer, originalName: 'document.json', fileName: 'document.json', folder: `live-dashboard-validation/${jobId}`, mimeType: 'application/json' });
|
|
52
|
+
const reference = { source: 'platform.workspace.files', ...scope, filePath: stored.filePath, workspacePath: stored.workspacePath };
|
|
53
|
+
const redis = await this.queue.getRedis(), key = recordKey(jobId), deadline = Date.now() + 90000;
|
|
54
|
+
const record = { tenantId: context.tenantId, organizationId: context.organizationId, userId: context.userId, manifestId: manifest.id,
|
|
55
|
+
deadline, filePath: stored.filePath, workspacePath: stored.workspacePath, size: buffer.length, sha256: digest(buffer) };
|
|
56
|
+
let finished = false;
|
|
57
|
+
try {
|
|
58
|
+
// Transient plugin-owned task record; queue payloads contain only its identity, never HTML or rows.
|
|
59
|
+
await redis.set(key, JSON.stringify(record), 'EX', 300);
|
|
60
|
+
const queued = await this.queue.enqueue({ pluginName: PLUGIN, queueName: 'html-validation', jobName: 'validate', payload: { validationId: jobId },
|
|
61
|
+
tenantId: context.tenantId, organizationId: context.organizationId, userId: context.userId, scopeKey: 'system:global', jobId,
|
|
62
|
+
attempts: 1, executionPool: POOL, removeOnComplete: { age: 3600, count: 100 }, removeOnFail: { age: 86400, count: 100 } });
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
const state = await this.queue.getJob({ jobId: queued.jobId, executionPool: POOL });
|
|
65
|
+
if (state?.state === 'failed') {
|
|
66
|
+
finished = true;
|
|
67
|
+
throw new common_1.BadRequestException(`DASHBOARD_HTML_VALIDATION_FAILED: ${state.failedReason?.slice(0, 500) ?? 'Browser Job failed'}`);
|
|
68
|
+
}
|
|
69
|
+
if (state?.state === 'completed') {
|
|
70
|
+
const report = reportSchema.parse(JSON.parse(await redis.get(`${key}:report`) ?? 'null'));
|
|
71
|
+
finished = true;
|
|
72
|
+
if (!report.ok)
|
|
73
|
+
throw new common_1.BadRequestException(`DASHBOARD_HTML_PREFLIGHT_FAILED: ${JSON.stringify(report.errors)}. Repair the Workspace HTML or include every required previewId, then retry. No dashboard was saved.`);
|
|
74
|
+
return { jobId, requestedBindings: report.requestedBindings, status: 'passed' };
|
|
75
|
+
}
|
|
76
|
+
await (0, promises_1.setTimeout)(250);
|
|
77
|
+
}
|
|
78
|
+
throw new common_1.BadRequestException('DASHBOARD_HTML_VALIDATION_TIMEOUT');
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (!finished) {
|
|
82
|
+
await Promise.allSettled([this.queue.cancel({ jobId, executionPool: POOL }), jobs.cancel({ jobId })]);
|
|
83
|
+
}
|
|
84
|
+
await removeTemporary(() => files.deleteFile(reference));
|
|
85
|
+
await redis.del(key, `${key}:report`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Ownership comes from persisted queue context. This handler needs no HTTP request or conversation. */
|
|
89
|
+
async handle(job, context) {
|
|
90
|
+
const jobId = v3_1.z.string().uuid().parse(job.data.validationId), key = recordKey(jobId), redis = await this.queue.getRedis();
|
|
91
|
+
const record = recordSchema.parse(JSON.parse(await redis.get(key) ?? 'null'));
|
|
92
|
+
if (record.deadline <= Date.now() || record.tenantId !== context.tenantId || record.organizationId !== context.organizationId || record.userId !== context.userId)
|
|
93
|
+
throw new Error('DASHBOARD_HTML_VALIDATION_EXPIRED_OR_DENIED');
|
|
94
|
+
const files = this.runtime.require(plugin_sdk_1.WorkspaceFilesRuntimeCapability), jobs = this.runtime.require(plugin_sdk_1.SandboxJobsRuntimeCapability);
|
|
95
|
+
const scope = { tenantId: record.tenantId, userId: record.userId, catalog: 'users' };
|
|
96
|
+
const reference = { source: 'platform.workspace.files', ...scope, filePath: record.filePath, workspacePath: record.workspacePath };
|
|
97
|
+
try {
|
|
98
|
+
const result = await jobs.run({ jobId, action: ACTION, actionVersion: '1.0.0', idempotencyKey: `live_dashboard/${jobId}`,
|
|
99
|
+
scope: { tenantId: record.tenantId, organizationId: record.organizationId, userId: record.userId, pluginName: PLUGIN, businessResourceType: 'html-validation', businessResourceId: record.manifestId },
|
|
100
|
+
payload: {}, files: [{ reference, targetPath: 'document.json', size: record.size, sha256: record.sha256 }], timeoutMs: Math.min(60000, record.deadline - Date.now()),
|
|
101
|
+
outputs: [{ path: 'report.json', originalName: 'report.json', mimeType: 'application/json', destination: { ...scope, folder: `live-dashboard-validation/${jobId}` } }] });
|
|
102
|
+
const output = result.outputs.find(output => output.path === 'report.json');
|
|
103
|
+
if (!output)
|
|
104
|
+
throw new Error('DASHBOARD_HTML_VALIDATION_UNAVAILABLE');
|
|
105
|
+
try {
|
|
106
|
+
const bytes = (await files.readBuffer(output.reference)).buffer;
|
|
107
|
+
if (bytes.length > 16000 || digest(bytes) !== output.sha256)
|
|
108
|
+
throw new Error('DASHBOARD_HTML_VALIDATION_INVALID');
|
|
109
|
+
await redis.set(`${key}:report`, JSON.stringify(reportSchema.parse(JSON.parse(bytes.toString('utf8')))), 'EX', 300);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await removeTemporary(() => files.deleteFile(output.reference));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await removeTemporary(() => files.deleteFile(reference));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
exports.HtmlDashboardValidation = HtmlDashboardValidation;
|
|
121
|
+
exports.HtmlDashboardValidation = HtmlDashboardValidation = __decorate([
|
|
122
|
+
(0, common_1.Injectable)(),
|
|
123
|
+
(0, plugin_sdk_1.PluginJobProcessor)({ pluginName: PLUGIN, queueName: 'html-validation', jobName: 'validate', concurrency: 1 }),
|
|
124
|
+
__param(0, (0, common_1.Inject)(plugin_sdk_1.XPERT_RUNTIME_CAPABILITIES_TOKEN)),
|
|
125
|
+
__param(1, (0, common_1.Inject)(plugin_sdk_1.MANAGED_QUEUE_SERVICE_TOKEN)),
|
|
126
|
+
__metadata("design:paramtypes", [Object, Object])
|
|
127
|
+
], HtmlDashboardValidation);
|
|
128
|
+
async function removeTemporary(remove) {
|
|
129
|
+
try {
|
|
130
|
+
await remove();
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TAgentMiddlewareMeta } from '@xpert-ai/contracts';
|
|
2
|
+
import { type AgentMiddleware, type IAgentMiddlewareStrategy, type IAgentMiddlewareContext } from '@xpert-ai/plugin-sdk';
|
|
3
|
+
import { DashboardService } from './dashboard.service';
|
|
4
|
+
export declare const DASHBOARD_GUIDANCE = "For a new independent dashboard, use a new unused HTML file path. An existing filename or matching title is not authorization to edit another Artifact; do not switch from creation to dashboard_open/update because a file already exists. For a new dashboard, call dashboard_list_sources before searching raw files, reading old scripts or creating transformations. Named governed sources may already provide the required query; inspect their provider contracts first. File-backed sources do not imply that a script must be written. For creating or editing live dashboards, first read the installed live-dashboard skill using read_skill_file and follow only the references relevant to the task. Use SandboxFile tools for all source-file authoring; HTML alone is not a saved dashboard. Follow preview.validation.checks immediately before authoring HTML; each suggestedCall must actually succeed. Business definitions, real data previews and nullable-filter tests are required before saving new or changed Bindings. Do not invent business data or substitute a different metric without agreement. Report completion only from a successful dashboard_create/dashboard_create_html/dashboard_update draft receipt. Human publication is separate. Successful create, update and open tools emit a chat resource card automatically; resourceCard.status reports emission. Its Open button selects the exact dashboard in Workbench. Never fabricate a link, call present_files for dashboard HTML, or output card JSON as text. If card delivery is unavailable, report the saved receipt and use the Workbench picker.";
|
|
5
|
+
export declare class DashboardMiddleware implements IAgentMiddlewareStrategy {
|
|
6
|
+
private readonly service;
|
|
7
|
+
readonly meta: TAgentMiddlewareMeta;
|
|
8
|
+
constructor(service: DashboardService);
|
|
9
|
+
createMiddleware(_options: object, context: IAgentMiddlewareContext): AgentMiddleware;
|
|
10
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DashboardMiddleware = exports.DASHBOARD_GUIDANCE = void 0;
|
|
13
|
+
const messages_1 = require("@langchain/core/messages");
|
|
14
|
+
const tools_1 = require("@langchain/core/tools");
|
|
15
|
+
const node_crypto_1 = require("node:crypto");
|
|
16
|
+
const v3_1 = require("zod/v3");
|
|
17
|
+
const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
|
|
18
|
+
const artifact_document_1 = require("@xpert-ai/plugin-sdk/artifact-document");
|
|
19
|
+
const document_libraries_1 = require("./document-libraries");
|
|
20
|
+
const agent_schema_1 = require("./agent.schema");
|
|
21
|
+
const html_guidance_1 = require("./html-guidance");
|
|
22
|
+
const dashboard_service_1 = require("./dashboard.service");
|
|
23
|
+
const dashboard_schema_1 = require("./dashboard.schema");
|
|
24
|
+
const view_provider_1 = require("./view.provider");
|
|
25
|
+
const editing_1 = require("./editing");
|
|
26
|
+
const dashboard_receipt_1 = require("./dashboard-receipt");
|
|
27
|
+
const filter_evidence_1 = require("./filter-evidence");
|
|
28
|
+
const save_evidence_1 = require("./save-evidence");
|
|
29
|
+
const resource_card_1 = require("./resource-card");
|
|
30
|
+
const dashboard_icon_1 = require("./dashboard-icon");
|
|
31
|
+
const middlewareOptionsSchema = v3_1.z.object({ allowedFinalProviders: v3_1.z.array(v3_1.z.string().min(1)).min(1).max(20).optional() }).strict();
|
|
32
|
+
exports.DASHBOARD_GUIDANCE = `For a new independent dashboard, use a new unused HTML file path. An existing filename or matching title is not authorization to edit another Artifact; do not switch from creation to dashboard_open/update because a file already exists. For a new dashboard, call dashboard_list_sources before searching raw files, reading old scripts or creating transformations. Named governed sources may already provide the required query; inspect their provider contracts first. File-backed sources do not imply that a script must be written. For creating or editing live dashboards, first read the installed live-dashboard skill using read_skill_file and follow only the references relevant to the task. Use SandboxFile tools for all source-file authoring; HTML alone is not a saved dashboard. Follow preview.validation.checks immediately before authoring HTML; each suggestedCall must actually succeed. Business definitions, real data previews and nullable-filter tests are required before saving new or changed Bindings. Do not invent business data or substitute a different metric without agreement. Report completion only from a successful dashboard_create/dashboard_create_html/dashboard_update draft receipt. Human publication is separate. Successful create, update and open tools emit a chat resource card automatically; resourceCard.status reports emission. Its Open button selects the exact dashboard in Workbench. Never fabricate a link, call present_files for dashboard HTML, or output card JSON as text. If card delivery is unavailable, report the saved receipt and use the Workbench picker.`;
|
|
33
|
+
let DashboardMiddleware = class DashboardMiddleware {
|
|
34
|
+
constructor(service) {
|
|
35
|
+
this.service = service;
|
|
36
|
+
this.meta = {
|
|
37
|
+
name: 'LiveDashboard', label: { en_US: 'Live dashboards', zh_Hans: '实时看板' },
|
|
38
|
+
description: { en_US: 'Discover, preview, create and update governed live dashboards.', zh_Hans: '发现业务数据、验证查询并持续修改实时看板。' },
|
|
39
|
+
// Tools inherit this self-contained icon through the platform middleware metadata.
|
|
40
|
+
icon: dashboard_icon_1.dashboardIcon, features: [view_provider_1.FEATURE], configSchema: { type: 'object', properties: {
|
|
41
|
+
allowedFinalProviders: { type: 'array', items: { type: 'string' }, title: { en_US: 'Allowed final dashboard providers', zh_Hans: '最终看板允许的 Provider' },
|
|
42
|
+
description: { en_US: 'Optional provider IDs required for saved Agent dashboards. Upstream previews remain available.', zh_Hans: '可选的最终看板 Provider ID 列表,不限制上游来源预览。' } }
|
|
43
|
+
}, additionalProperties: false }
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
createMiddleware(_options, context) {
|
|
47
|
+
const options = middlewareOptionsSchema.parse(_options);
|
|
48
|
+
if (!context.xpertId)
|
|
49
|
+
throw new Error('DASHBOARD_ASSISTANT_REQUIRED');
|
|
50
|
+
const scope = {
|
|
51
|
+
tenantId: context.tenantId ?? plugin_sdk_1.RequestContext.currentTenantId(), organizationId: context.organizationId ?? plugin_sdk_1.RequestContext.getOrganizationId(),
|
|
52
|
+
userId: context.userId ?? plugin_sdk_1.RequestContext.currentUserId(), hostType: context.projectId ? 'project' : 'agent', hostId: context.projectId ?? context.xpertId, slots: []
|
|
53
|
+
};
|
|
54
|
+
// Evidence belongs to this run and actor, never a process-wide cache of business data.
|
|
55
|
+
const discovered = new Map(), previews = new Map();
|
|
56
|
+
let listedSources;
|
|
57
|
+
const filterEvidence = (0, filter_evidence_1.createFilterEvidence)(this.service, scope, previews);
|
|
58
|
+
const created = new Map();
|
|
59
|
+
const preview = async (binding) => {
|
|
60
|
+
const contract = discovered.get(`${binding.dataSourceId}/${binding.providerId}`);
|
|
61
|
+
if (!contract)
|
|
62
|
+
throw new Error('DASHBOARD_DISCOVER_REQUIRED');
|
|
63
|
+
(0, plugin_sdk_1.validateAnalyticsPayload)(binding.payload, contract.payloadSchema);
|
|
64
|
+
const controls = Object.fromEntries(binding.parameters.map((parameter) => [parameter.controlId, parameter.defaultValue ?? null]));
|
|
65
|
+
const dataset = await this.service.preview(scope, { binding, controls });
|
|
66
|
+
const previewId = previewKey(binding, controls);
|
|
67
|
+
if (previews.size >= 24 && !previews.has(previewId))
|
|
68
|
+
throw new Error('DASHBOARD_PREVIEW_LIMIT');
|
|
69
|
+
previews.set(previewId, binding);
|
|
70
|
+
const rows = dataset.rows.slice(0, 20);
|
|
71
|
+
while (Buffer.byteLength(JSON.stringify(rows)) > 32000)
|
|
72
|
+
rows.pop();
|
|
73
|
+
return JSON.stringify({ ...dataset, previewId, bindingJson: JSON.stringify(binding), rows, validation: filterEvidence.requirements(binding, previewId, rows), preview: { displayedRows: rows.length,
|
|
74
|
+
omittedRows: dataset.rows.length - rows.length, note: 'Display sample only; completeness describes the full bounded query. Verify business semantics before create.' } });
|
|
75
|
+
};
|
|
76
|
+
const resolvePreviews = (previewIds) => previewIds.map(id => {
|
|
77
|
+
const binding = previews.get(id);
|
|
78
|
+
if (!binding)
|
|
79
|
+
throw new Error('DASHBOARD_PREVIEW_REQUIRED: use a previewId returned in this run');
|
|
80
|
+
return binding;
|
|
81
|
+
});
|
|
82
|
+
const validateFinal = (manifest, requirePreviews) => {
|
|
83
|
+
const allowedProviders = options.allowedFinalProviders;
|
|
84
|
+
if (allowedProviders && manifest.bindings.some(binding => !allowedProviders.includes(binding.providerId)))
|
|
85
|
+
throw new Error(`DASHBOARD_PROVIDER_REQUIRED: final bindings must use ${allowedProviders.join(', ')}`);
|
|
86
|
+
if (requirePreviews) {
|
|
87
|
+
const controls = Object.fromEntries(manifest.controls.filter(item => item.defaultValue !== undefined).map(item => [item.controlId, item.defaultValue ?? null]));
|
|
88
|
+
for (const binding of manifest.bindings) {
|
|
89
|
+
const previewId = previewKey(binding, controls);
|
|
90
|
+
if (!previews.has(previewId))
|
|
91
|
+
throw new Error('DASHBOARD_PREVIEW_REQUIRED: preview the exact binding and default controls before saving');
|
|
92
|
+
filterEvidence.assert(binding, previewId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const save = async (input, config, html, dependencies = []) => {
|
|
97
|
+
const { previewIds, supersedesPaths, ...definition } = input;
|
|
98
|
+
const bindings = resolvePreviews(previewIds);
|
|
99
|
+
const manifest = dashboard_schema_1.dashboardSchema.parse({ ...definition, bindings, schema: 'xpert.live-dashboard/v1', id: (0, node_crypto_1.randomUUID)(),
|
|
100
|
+
...(html ? { presentation: { type: 'html', html, ...(dependencies.length ? { dependencies } : {}) } } : {}) });
|
|
101
|
+
validateFinal(manifest, true);
|
|
102
|
+
const key = hash({ input: { ...definition, previewIds }, html, dependencies });
|
|
103
|
+
const previous = created.get(key);
|
|
104
|
+
if (previous) {
|
|
105
|
+
const receipt = await this.service.save(scope, previous.manifest, previous.receipt.baseVersionId, previous.receipt.draftRevision);
|
|
106
|
+
created.set(key, { manifest: previous.manifest, receipt });
|
|
107
|
+
const resourceCard = await (0, resource_card_1.emitDashboardCard)(scope, { ...receipt, state: 'draft', revision: receipt.draftRevision }, config);
|
|
108
|
+
return JSON.stringify({ dashboard: receipt, resourceCard });
|
|
109
|
+
}
|
|
110
|
+
const sameTitle = [...created.values()].find(item => item.manifest.title === manifest.title);
|
|
111
|
+
if (sameTitle)
|
|
112
|
+
throw new Error(`DASHBOARD_ALREADY_CREATED: this run already saved this dashboard. Use dashboard_open with artifactId ${sameTitle.receipt.artifactId}, then dashboard_update with fresh previewIds for data changes; preserve the Artifact instead of creating another one.`);
|
|
113
|
+
const result = await this.service.save(scope, manifest, null);
|
|
114
|
+
created.set(key, { manifest, receipt: result });
|
|
115
|
+
const resourceCard = await (0, resource_card_1.emitDashboardCard)(scope, { ...result, state: 'draft', revision: result.draftRevision }, config);
|
|
116
|
+
return JSON.stringify({ dashboard: result, resourceCard });
|
|
117
|
+
};
|
|
118
|
+
const evidence = (0, save_evidence_1.dashboardSaveEvidence)(() => {
|
|
119
|
+
const files = context.runtime?.capabilities?.require(plugin_sdk_1.WorkspaceFilesRuntimeCapability);
|
|
120
|
+
if (!files)
|
|
121
|
+
throw new Error('DASHBOARD_WORKSPACE_REQUIRED');
|
|
122
|
+
return files;
|
|
123
|
+
});
|
|
124
|
+
let modelCalls = 0;
|
|
125
|
+
return { name: 'LiveDashboard', contextSchema: editing_1.dashboardContextSchema,
|
|
126
|
+
wrapToolCall: async (request, handler) => {
|
|
127
|
+
const name = request.toolCall.name;
|
|
128
|
+
const args = request.toolCall.args;
|
|
129
|
+
const path = (0, plugin_sdk_1.workspaceFileMutationPath)(request.tool && 'metadata' in request.tool ? request.tool.metadata : undefined, args);
|
|
130
|
+
const creation = name === 'dashboard_create_html' ? agent_schema_1.createHtmlToolSchema.safeParse(args)
|
|
131
|
+
: name === 'dashboard_create' ? agent_schema_1.createToolSchema.safeParse(args) : undefined;
|
|
132
|
+
const save = dashboard_receipt_1.DASHBOARD_MUTATION_TOOLS.includes(name) ? evidence.beginSave(creation?.success ? creation.data.supersedesPaths : undefined, name === 'dashboard_create' ? 'draft' : 'source') : undefined;
|
|
133
|
+
const result = path && (evidence.has(path) || previews.size > 0 && agent_schema_1.htmlPathSchema.safeParse(path).success)
|
|
134
|
+
? await evidence.trackWrite(path, async () => handler(request)) : await handler(request);
|
|
135
|
+
if (save && 'content' in result && (!('status' in result) || result.status !== 'error'))
|
|
136
|
+
await evidence.accept(save, result.content);
|
|
137
|
+
return result;
|
|
138
|
+
}, wrapModelCall: async (request, handler) => {
|
|
139
|
+
const exhausted = ++modelCalls > 24;
|
|
140
|
+
if (exhausted && evidence.pending)
|
|
141
|
+
throw new Error('DASHBOARD_SAVE_NOT_CONFIRMED: no successful draft save receipt; Workspace changes have not been saved to the dashboard.');
|
|
142
|
+
const result = await handler({ ...request, ...(evidence.pending ? { toolChoice: 'required' } : {}), ...(exhausted ? { tools: [] } : {}), systemMessage: new messages_1.SystemMessage({ content: [
|
|
143
|
+
...(typeof request.systemMessage?.content === 'string' ? [{ type: 'text', text: request.systemMessage.content }] : request.systemMessage?.content ?? []),
|
|
144
|
+
{ type: 'text', text: exports.DASHBOARD_GUIDANCE + '\n' + html_guidance_1.HTML_DASHBOARD_GUIDANCE + '\n' + editing_1.EDITING_GUIDANCE + (evidence.pending ? `\nDashboard changes are pending without a matching draft save receipt. Unsaved files: ${JSON.stringify(evidence.pendingPaths)}. Continue reading/repairing and call dashboard_create_html or dashboard_update with the exact file path; do not finish or claim it was saved. When abandoning earlier unsaved HTML creation candidates, explicitly pass their exact paths as supersedesPaths to dashboard_create or dashboard_create_html, matching the requested presentation mode. If a standard dashboard is already saved, repeat dashboard_create with the same definition/previewIds and supersedesPaths; do not create an unwanted HTML dashboard. This only abandons those candidates after a successful save; it never discards another Artifact editing copy.` : '') + (options.allowedFinalProviders ? `\nThis Assistant requires every saved dashboard binding to use one of these providers: ${options.allowedFinalProviders.join(', ')}. Other providers may be previewed only as upstream inputs. Do not save a direct-source substitute.` : '') + (exhausted ? '\nThe dashboard tool budget is exhausted. Stop calls; report only saved receipts or explain the concrete blocker truthfully.' : '') }
|
|
145
|
+
] }) });
|
|
146
|
+
if (evidence.pending && result && !result.tool_calls?.length)
|
|
147
|
+
throw new Error('DASHBOARD_SAVE_NOT_CONFIRMED: the model stopped without a successful draft save receipt.');
|
|
148
|
+
return result;
|
|
149
|
+
}, tools: [
|
|
150
|
+
filterEvidence.tool,
|
|
151
|
+
(0, tools_1.tool)(async () => JSON.stringify({ libraries: await (0, document_libraries_1.libraryLocks)(['echarts', 'three']),
|
|
152
|
+
pageRuntime: {
|
|
153
|
+
global: 'window.xpertLiveArtifact',
|
|
154
|
+
bindingIds: [...new Set([...previews.values()].map(binding => binding.id))],
|
|
155
|
+
startup: 'const live = window.xpertLiveArtifact; live.subscribe(bindingId, renderDataset); await live.ready(); await live.query(bindingId, typedControls); Use an async function, with the exact binding IDs from saved previews.',
|
|
156
|
+
refresh: 'live.refresh(bindingId) requires a Binding ID. live.refresh() without an ID is invalid. For a page refresh button or timer, call Promise.all(bindingIds.map(id => live.refresh(id))). Each subscription redraws its KPI, chart and details.',
|
|
157
|
+
filters: 'Mark native data selects with data-dashboard-control="<exact controlId>". Mark purely visual selects with data-dashboard-presentation instead. Changing a data select must call live.query for every affected Binding, not only filter cached rows in JS. bindSelect updates one Binding only. Read references/html-lifecycle.md before writing the page.'
|
|
158
|
+
},
|
|
159
|
+
usage: { echarts: 'window.echarts: init(element, null, {renderer:"svg"}) then setOption(completeOption, {notMerge:true}). Canvas is also available. Derive active series/legend dimensions from each received Dataset, including filtered results; never retain excluded groups as zero-valued series. Aggregate repeated categories using all grain dimensions, clear empty results, and redraw KPI, chart and details together in subscribe. Use ResizeObserver to resize and reuse the instance on filter/refresh.',
|
|
160
|
+
three: 'window.THREE: bundled core including WebGLRenderer. Requires WebGL2; catch unavailable GPU contexts and show a clear fallback. External textures/models, loaders/addons, WebGPU and workers are not bundled.' },
|
|
161
|
+
lifecycle: 'Pass libraries IDs to dashboard_create_html or dashboard_update. Actual version and bytes are pinned on save; omitted libraries on update preserves saved locks. No CDN, imports or network access.' }), { name: 'dashboard_libraries', description: 'Discover packaged frontend libraries and exact versions available to HTML dashboards. Read before using ECharts/Three.js. Returns metadata, never library source.',
|
|
162
|
+
schema: v3_1.z.object({}).strict(), verboseParsingErrors: true, metadata: { toolName: { en_US: 'Discover dashboard libraries', zh_Hans: '查看看板前端图库' } } }),
|
|
163
|
+
...(0, editing_1.createEditingTools)(this.service, scope, context, resolvePreviews, validateFinal, (path, artifactId) => evidence.opened(path, artifactId)),
|
|
164
|
+
(0, tools_1.tool)(async () => {
|
|
165
|
+
listedSources = await this.service.sources(scope);
|
|
166
|
+
return JSON.stringify(listedSources);
|
|
167
|
+
}, {
|
|
168
|
+
name: 'dashboard_list_sources', description: 'List authorized analytics sources and versioned providers. Start here; IDs must come from this list.',
|
|
169
|
+
schema: v3_1.z.object({}).strict(), verboseParsingErrors: true, metadata: { toolName: { en_US: 'List analytics sources', zh_Hans: '发现分析数据源' } }
|
|
170
|
+
}),
|
|
171
|
+
(0, tools_1.tool)(async (input) => {
|
|
172
|
+
if (listedSources && !listedSources.some(source => source.id === input.dataSourceId && source.providers.some(provider => provider.id === input.providerId)))
|
|
173
|
+
throw new Error(`DASHBOARD_SOURCE_SELECTION_INVALID: copy an exact dataSourceId/providerId pair from dashboard_list_sources; do not invent or combine IDs. Known pairs: ${JSON.stringify(listedSources.map(source => ({ dataSourceId: source.id, name: source.name, providerIds: source.providers.map(provider => provider.id) })))}. Refresh the list if the source changed. Initial requestJson is "{}"; then follow inspectRequest/nextRequest.`);
|
|
174
|
+
const parsed = { dataSourceId: input.dataSourceId, providerId: input.providerId, request: (0, agent_schema_1.parseObjectJson)(input.requestJson) };
|
|
175
|
+
const result = await this.service.discover(scope, parsed);
|
|
176
|
+
discovered.set(`${parsed.dataSourceId}/${parsed.providerId}`, result.queryContract);
|
|
177
|
+
return JSON.stringify(result);
|
|
178
|
+
}, { name: 'dashboard_discover', description: 'Discover provider-owned business metadata. Start requestJson="{}" then serialize inspectRequest/nextRequest. Inspect dataset fields, comments, measures and native query format before preview.',
|
|
179
|
+
schema: agent_schema_1.discoverToolSchema, verboseParsingErrors: true, metadata: { toolName: { en_US: 'Inspect business data', zh_Hans: '理解业务数据与指标' } } }),
|
|
180
|
+
(0, tools_1.tool)(async (input) => {
|
|
181
|
+
const contract = discovered.get(`${input.binding.dataSourceId}/${input.binding.providerId}`);
|
|
182
|
+
if (!contract)
|
|
183
|
+
throw new Error('DASHBOARD_DISCOVER_REQUIRED: discover the source before preview');
|
|
184
|
+
const binding = (0, agent_schema_1.fromAgentBinding)(input.binding, contract);
|
|
185
|
+
return preview(binding);
|
|
186
|
+
}, { name: 'dashboard_preview', description: 'Run one read-only binding using parameters[].defaultValue. Returns validated Dataset fields, first 20 rows, full query completeness and audit. Required before create; changes to binding/defaults require another preview.',
|
|
187
|
+
schema: agent_schema_1.previewToolSchema, verboseParsingErrors: true, metadata: { toolName: { en_US: 'Verify dashboard data', zh_Hans: '预览并验证看板数据' } } }),
|
|
188
|
+
(0, tools_1.tool)(async ({ bindingJson }) => preview(plugin_sdk_1.analyticsBindingSchema.parse(JSON.parse(bindingJson))), {
|
|
189
|
+
name: 'dashboard_preview_binding', description: 'Preview a complete canonical AnalyticsBinding returned by another tool. Serialize the complete canonical Binding (for example discovery items[].metadata.binding), including parameters[].controlId. bindingPayload is only the payload, not a Binding. If adding a parameter, include name, controlId, type, required and defaultValue. Do not convert it to the simplified dashboard_preview input. Discovery of the source is still required. Returns the same previewId receipt for dashboard_create.',
|
|
190
|
+
schema: v3_1.z.object({ bindingJson: v3_1.z.string().min(2).max(200000) }).strict(), verboseParsingErrors: true,
|
|
191
|
+
metadata: { toolName: { en_US: 'Verify registered dataset', zh_Hans: '验证已注册数据集' } }
|
|
192
|
+
}),
|
|
193
|
+
(0, tools_1.tool)(async (input, config) => save(input, config), { name: 'dashboard_create', description: 'Save a standard chart/table dashboard from previewId receipts. For custom HTML use dashboard_create_html. Fields and defaults must match previewed parameters. Returns a saved Artifact receipt after current access/data revalidation.',
|
|
194
|
+
schema: agent_schema_1.createToolSchema, verboseParsingErrors: true,
|
|
195
|
+
metadata: { toolName: { en_US: 'Save live dashboard', zh_Hans: '生成并保存实时看板' } } }),
|
|
196
|
+
(0, tools_1.tool)(async (input, config) => {
|
|
197
|
+
if (input.supersedesPaths?.includes(input.path))
|
|
198
|
+
throw new Error('DASHBOARD_SUPERSEDE_INVALID: the saved path cannot supersede itself');
|
|
199
|
+
const files = context.runtime?.capabilities?.require(plugin_sdk_1.WorkspaceFilesRuntimeCapability);
|
|
200
|
+
if (!files)
|
|
201
|
+
throw new Error('DASHBOARD_WORKSPACE_REQUIRED');
|
|
202
|
+
const file = await files.readRuntimeBuffer(input.path);
|
|
203
|
+
if (file.buffer.byteLength > 300000)
|
|
204
|
+
throw new Error('DASHBOARD_HTML_TOO_LARGE');
|
|
205
|
+
const html = artifact_document_1.artifactDocumentHtml.parse(new TextDecoder('utf-8', { fatal: true }).decode(file.buffer));
|
|
206
|
+
const { path, libraries, supersedesPaths, ...definition } = input;
|
|
207
|
+
const result = JSON.parse(await save({ ...definition, charts: [], tables: [] }, config, html, await (0, document_libraries_1.libraryLocks)(libraries ?? [])));
|
|
208
|
+
return JSON.stringify({ ...result, sourceFile: { path, sha256: (0, node_crypto_1.createHash)('sha256').update(file.buffer).digest('hex') } });
|
|
209
|
+
}, { name: 'dashboard_create_html', description: 'Save custom HTML from a Workspace path together with run-local preview receipts and controls in one persistent draft (human publication is separate). Requires real data preview; does not accept query payloads or cached rows.',
|
|
210
|
+
schema: agent_schema_1.createHtmlToolSchema, verboseParsingErrors: true,
|
|
211
|
+
metadata: { toolName: { en_US: 'Save HTML dashboard', zh_Hans: '保存自由样式看板' } } })
|
|
212
|
+
] };
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
exports.DashboardMiddleware = DashboardMiddleware;
|
|
216
|
+
exports.DashboardMiddleware = DashboardMiddleware = __decorate([
|
|
217
|
+
(0, plugin_sdk_1.AgentMiddlewareStrategy)('LiveDashboard'),
|
|
218
|
+
__metadata("design:paramtypes", [dashboard_service_1.DashboardService])
|
|
219
|
+
], DashboardMiddleware);
|
|
220
|
+
function previewKey(binding, controls) {
|
|
221
|
+
return hash({ binding, controls: Object.fromEntries(binding.parameters.map((parameter) => [parameter.controlId, Object.hasOwn(controls, parameter.controlId) ? controls[parameter.controlId] : parameter.defaultValue ?? null])) });
|
|
222
|
+
}
|
|
223
|
+
function hash(value) {
|
|
224
|
+
return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item)).digest('hex');
|
|
225
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.LiveDashboardPlugin = void 0;
|
|
10
|
+
const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
|
|
11
|
+
const dashboard_service_1 = require("./dashboard.service");
|
|
12
|
+
const view_provider_1 = require("./view.provider");
|
|
13
|
+
const middleware_1 = require("./middleware");
|
|
14
|
+
const html_validation_1 = require("./html-validation");
|
|
15
|
+
const presentation_provider_1 = require("./presentation.provider");
|
|
16
|
+
const sharing_service_1 = require("./sharing.service");
|
|
17
|
+
let LiveDashboardPlugin = class LiveDashboardPlugin {
|
|
18
|
+
};
|
|
19
|
+
exports.LiveDashboardPlugin = LiveDashboardPlugin;
|
|
20
|
+
exports.LiveDashboardPlugin = LiveDashboardPlugin = __decorate([
|
|
21
|
+
(0, plugin_sdk_1.XpertServerPlugin)({ providers: [dashboard_service_1.DashboardService, view_provider_1.DashboardViewProvider, middleware_1.DashboardMiddleware, html_validation_1.HtmlDashboardValidation, presentation_provider_1.DashboardPresentationProvider, sharing_service_1.DashboardSharingService] })
|
|
22
|
+
], LiveDashboardPlugin);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ArtifactPresentationQuery } from '@xpert-ai/contracts';
|
|
2
|
+
import { type ArtifactPresentationContext, type IArtifactPresentationProvider, type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
|
|
3
|
+
export declare class DashboardPresentationProvider implements IArtifactPresentationProvider {
|
|
4
|
+
private readonly runtime;
|
|
5
|
+
readonly operations: readonly ["query", "query-binding"];
|
|
6
|
+
constructor(runtime: RuntimeCapabilityRegistry);
|
|
7
|
+
getEntry(): Promise<{
|
|
8
|
+
html: string;
|
|
9
|
+
contentType: "text/html; charset=utf-8";
|
|
10
|
+
}>;
|
|
11
|
+
read(context: ArtifactPresentationContext): Promise<{
|
|
12
|
+
meta: {
|
|
13
|
+
saved: {
|
|
14
|
+
artifactId: string;
|
|
15
|
+
versionId: string;
|
|
16
|
+
versionNumber: number;
|
|
17
|
+
title: string;
|
|
18
|
+
isDraft: boolean;
|
|
19
|
+
manifest: {
|
|
20
|
+
schema: "xpert.live-dashboard/v1";
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
bindings: import("@xpert-ai/contracts").AnalyticsBinding[];
|
|
24
|
+
controls: {
|
|
25
|
+
type: "string" | "number" | "boolean" | "date";
|
|
26
|
+
controlId: string;
|
|
27
|
+
label: string;
|
|
28
|
+
defaultValue?: string | number | boolean | null | undefined;
|
|
29
|
+
allowedValues?: (string | number | boolean | null)[] | undefined;
|
|
30
|
+
}[];
|
|
31
|
+
charts: {
|
|
32
|
+
type: "bar" | "line" | "area" | "pie" | "scatter";
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
datasetId: string;
|
|
36
|
+
x: string;
|
|
37
|
+
y: string;
|
|
38
|
+
color?: string | undefined;
|
|
39
|
+
}[];
|
|
40
|
+
tables: {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
datasetId: string;
|
|
44
|
+
fields: string[];
|
|
45
|
+
}[];
|
|
46
|
+
presentation?: {
|
|
47
|
+
type: "html";
|
|
48
|
+
html: string;
|
|
49
|
+
dependencies?: import("@xpert-ai/plugin-sdk/artifact-document").ArtifactDocumentDependencyLock[] | undefined;
|
|
50
|
+
} | undefined;
|
|
51
|
+
};
|
|
52
|
+
dependencies: import("@xpert-ai/plugin-sdk/artifact-document").ArtifactDocumentDependencyAsset[];
|
|
53
|
+
};
|
|
54
|
+
dashboards: {
|
|
55
|
+
artifactId: string;
|
|
56
|
+
versionId: string;
|
|
57
|
+
title: string;
|
|
58
|
+
}[];
|
|
59
|
+
sources: never[];
|
|
60
|
+
versions: never[];
|
|
61
|
+
};
|
|
62
|
+
}>;
|
|
63
|
+
query(context: ArtifactPresentationContext, request: ArtifactPresentationQuery): Promise<import("@xpert-ai/contracts").AnalyticalDataset | {
|
|
64
|
+
payload?: {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
widget_type: string;
|
|
67
|
+
surface: string;
|
|
68
|
+
sources: never[];
|
|
69
|
+
manifest: {
|
|
70
|
+
version: number;
|
|
71
|
+
surface: string;
|
|
72
|
+
title: string;
|
|
73
|
+
cards: never[];
|
|
74
|
+
filters: never[];
|
|
75
|
+
sources: never[];
|
|
76
|
+
charts: {
|
|
77
|
+
id: string;
|
|
78
|
+
title: string;
|
|
79
|
+
type: "bar" | "line" | "area" | "pie" | "scatter";
|
|
80
|
+
dataset: string;
|
|
81
|
+
encodings: {
|
|
82
|
+
color?: {
|
|
83
|
+
field: string;
|
|
84
|
+
} | undefined;
|
|
85
|
+
x: {
|
|
86
|
+
field: string;
|
|
87
|
+
};
|
|
88
|
+
y: {
|
|
89
|
+
field: string;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
}[];
|
|
93
|
+
tables: {
|
|
94
|
+
id: string;
|
|
95
|
+
title: string;
|
|
96
|
+
dataset: string;
|
|
97
|
+
columns: {
|
|
98
|
+
field: string;
|
|
99
|
+
label: string;
|
|
100
|
+
}[];
|
|
101
|
+
}[];
|
|
102
|
+
blocks: ({
|
|
103
|
+
id: string;
|
|
104
|
+
type: string;
|
|
105
|
+
chartId: string;
|
|
106
|
+
} | {
|
|
107
|
+
id: string;
|
|
108
|
+
type: string;
|
|
109
|
+
tableId: string;
|
|
110
|
+
})[];
|
|
111
|
+
};
|
|
112
|
+
snapshot: {
|
|
113
|
+
version: number;
|
|
114
|
+
generatedAt: string;
|
|
115
|
+
status: string;
|
|
116
|
+
datasets: any;
|
|
117
|
+
};
|
|
118
|
+
} | undefined;
|
|
119
|
+
datasets: import("@xpert-ai/contracts").AnalyticalDataset[];
|
|
120
|
+
}>;
|
|
121
|
+
private open;
|
|
122
|
+
}
|