@xpert-ai/plugin-live-dashboard 0.2.0 → 0.3.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 +1 -1
- package/README.md +137 -0
- package/dist/index.js +11 -1
- package/dist/lib/assistant-template.d.ts +3 -0
- package/dist/lib/assistant-template.js +56 -0
- package/dist/lib/dashboard-view.schema.d.ts +27 -0
- package/dist/lib/dashboard-view.schema.js +1 -1
- package/dist/lib/dashboard.service.d.ts +6 -1
- package/dist/lib/dashboard.service.js +22 -6
- package/dist/lib/middleware.js +8 -2
- package/dist/lib/plugin.js +2 -1
- package/dist/lib/prompt-workflows.d.ts +4 -0
- package/dist/lib/prompt-workflows.js +132 -0
- package/dist/lib/provider-options.controller.d.ts +10 -0
- package/dist/lib/provider-options.controller.js +49 -0
- package/dist/lib/view.provider.d.ts +2 -122
- package/dist/lib/view.provider.js +8 -0
- package/dist/remote/app.css +1 -1
- package/dist/remote/app.js +19 -19
- package/dist/remote/build-manifest.json +21 -17
- package/dist/sandbox-actions/action.json +1 -1
- package/dist/sandbox-actions/bundle/resource-cache.mjs +39 -0
- package/dist/sandbox-actions/bundle/validate.mjs +90 -22
- package/package.json +3 -2
- package/scripts/import-prompts.mjs +108 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sources": {
|
|
3
|
-
".xpertai-plugin/plugin.json": "
|
|
4
|
-
"package.json": "
|
|
5
|
-
"scripts/build-action.mjs": "
|
|
3
|
+
".xpertai-plugin/plugin.json": "1ee877449129cba345af770103c31745eb162df370f9502effd22adfeddf3e77",
|
|
4
|
+
"package.json": "45298d9c5530d07385f7f6724631524069b3a0efe7103e8aec89fb465892a15a",
|
|
5
|
+
"scripts/build-action.mjs": "3a3de4d7fd05d81f5fdeb9eacbd04235dcc6e5000c6fff2d07f0e4bdc7b40c30",
|
|
6
6
|
"scripts/build-libraries.mjs": "20bd34ca3f8bb6e87df2637a3cca85496962688c8ea4ceff5bc294ebd2de704d",
|
|
7
7
|
"scripts/build-remote.mjs": "492550ac5c95eb371253d863567723d8853522013a0bcdfa0e264720206fda16",
|
|
8
8
|
"scripts/dist-manifest.mjs": "8dcca9f1e74c520171649e848f9b5570ca9cc83fb5e8d2f265be3d2e5ceb970c",
|
|
9
|
+
"scripts/import-prompts.mjs": "c8c611fec92e168f63487a09ce0509d6ccef80d0b6d2c44ecf23da63e782a05d",
|
|
9
10
|
"scripts/renderer-asset.mjs": "e7edf2fb41f2384e838c27c2e5566f5d479135ad365ae2a99fb421433464d82a",
|
|
10
11
|
"scripts/verify-dist.mjs": "523d0fe9703a0f3d12099173d115b5145d5665b06551cfab2869a2db76d880e2",
|
|
11
12
|
"skills/live-dashboard/SKILL.md": "1825876a74391c2c514548b7a1b37e123d044952f9c882498dd5225950f169d0",
|
|
@@ -13,9 +14,10 @@
|
|
|
13
14
|
"skills/live-dashboard/references/data-validation.md": "4455240470b3456fcd7db8f0f58b040d1dceb548010414542dd5cc86f3abfe63",
|
|
14
15
|
"skills/live-dashboard/references/html-lifecycle.md": "4bf88730709ab45166425e1917610d1e618c9a0883e615ce76c0fe55488ffd51",
|
|
15
16
|
"skills/live-dashboard/references/html-runtime.md": "5be3dc50e618b30e83ef59c2d684ceed5a3e37d63a550bdf157b0d8953af5d38",
|
|
16
|
-
"src/index.ts": "
|
|
17
|
+
"src/index.ts": "26af7bca762bda113956697f7928971ccf694c80c4c4596f06a60ddabe5cdf86",
|
|
17
18
|
"src/lib/agent.schema.ts": "1444585eab3618b985b774937309d950df99de1a1804e4caecdff8026de70297",
|
|
18
19
|
"src/lib/analytics-source.ts": "6fb5fd394fffa5a0c12f1fe32d0c9d7dbd8597052047b6122fc5b160fa7af54f",
|
|
20
|
+
"src/lib/assistant-template.ts": "6be01da06edeff18a79c724a708396f9bd9fe7d8944c31e4f4f71db32c0b8d92",
|
|
19
21
|
"src/lib/content-capability.ts": "fa1252a539a681be9780d318b2b3710da273c4405c8e21d58ac675294cef3836",
|
|
20
22
|
"src/lib/content.provider.ts": "3cfbccec1483269c150ba3dade7aa812ee59c998c30d89a57835977027c7e622",
|
|
21
23
|
"src/lib/dashboard-icon.ts": "3f95a626e91d3ce198cdef6b05365591b6e65af4f58d6f5765ec0836a590a494",
|
|
@@ -24,9 +26,9 @@
|
|
|
24
26
|
"src/lib/dashboard-receipt.ts": "d02b7e4e1be0a075b7f27e887ebb555684f17036ebb9442982e6725ab37a53ab",
|
|
25
27
|
"src/lib/dashboard-search.ts": "0cd14c0004fb078b54c095faeab3195fb7e03a20c3400b5e67aec7cb4b2442e2",
|
|
26
28
|
"src/lib/dashboard-sharing.ts": "61199f135cc53b6b9f0bef0f75f14c4d40bff6a3986e62e2823ab80c564534d3",
|
|
27
|
-
"src/lib/dashboard-view.schema.ts": "
|
|
29
|
+
"src/lib/dashboard-view.schema.ts": "4db0f043e836a1e050e8621074e5370e00507f5c6862ab9e2d624001c0452f26",
|
|
28
30
|
"src/lib/dashboard.schema.ts": "982e15591e70102ea506951cd10de9ec4864281157b0b95a7b591a8fff3da813",
|
|
29
|
-
"src/lib/dashboard.service.ts": "
|
|
31
|
+
"src/lib/dashboard.service.ts": "8b61384e2e655821dcbd427f3e393c51b90d5d023ee50e078f1038617ef26dcb",
|
|
30
32
|
"src/lib/definition-tools.ts": "ba48842396e57499331a52ca7cf5e4e1eb72e1e519b5dabfff2743fddf15947d",
|
|
31
33
|
"src/lib/document-host.ts": "3543ddb443006b90cd4689b65446b0e3b3305362d3d24d93209c094967921ffe",
|
|
32
34
|
"src/lib/document-libraries.ts": "9c228995c509dc7f606cdc70d972ddb22df0164df4e0070583d0ab901f6605d0",
|
|
@@ -36,24 +38,26 @@
|
|
|
36
38
|
"src/lib/filter-evidence.ts": "594ea9065e5c8d32c6bd4fc055e78c2df7ad2418c2019106136917c89c8ac331",
|
|
37
39
|
"src/lib/html-guidance.ts": "0bd0b830b33045357a070c2079b2aedaadb57807ba366be56b767b7d55120325",
|
|
38
40
|
"src/lib/html-validation.ts": "9f342f6716d457ba1a46c9d228f3f7858dcae2c0f64a3c98bc1d28d35a8f43ac",
|
|
39
|
-
"src/lib/middleware.ts": "
|
|
40
|
-
"src/lib/plugin.ts": "
|
|
41
|
+
"src/lib/middleware.ts": "8f9e05fa6d44e46e89d050c441fc867c2dbdbcc037df2ac5aaf8f2bed9343e40",
|
|
42
|
+
"src/lib/plugin.ts": "715f0ce8e1c8e6b32a778d418c5c7cd1f7f516f1a47ccb8437ebe1342ea1f9e8",
|
|
41
43
|
"src/lib/presentation.provider.ts": "3b37387636d9e7317ee6e36c9e607fadb20002c08c24d54e424342e913b2405f",
|
|
42
44
|
"src/lib/preview-retry.ts": "55fa28cc61f11adac4dce5a2066cbdd6d529ffd5ffd3cca7dcb552c6b7299500",
|
|
45
|
+
"src/lib/prompt-workflows.ts": "c2aa959f7fad508415394bc0ce6e19ec48f27b82fa04105e6dc2dfd30b7bfe25",
|
|
46
|
+
"src/lib/provider-options.controller.ts": "0b329686f75182ec6a6db2bb741bedd3ade0e7a25093ddbb7b15336282270349",
|
|
43
47
|
"src/lib/remote-entry.ts": "4c760963ff099b4d9a8961c933b1a586c0ac57d85a88c745c2578e2f66bf1539",
|
|
44
48
|
"src/lib/renderer.ts": "418ac7cc5f37f9d638c0b2dd3a5361464674f21dbfb801b4414a9d68f1c9edc8",
|
|
45
49
|
"src/lib/resource-card.ts": "d64f91e640a3db220b941adef4a582e011a7aea253d57cc5d70a5bdcdacd8f71",
|
|
46
50
|
"src/lib/save-evidence.ts": "458f8fe2ef74fd4809fb9543d8c31bb42e0e16b10550b2a7808ca92ec3662bda",
|
|
47
51
|
"src/lib/sharing.service.ts": "e52c98d967f27a2f3bc80614f31da15e373dd4450b80aea250156efe24c77f3b",
|
|
48
|
-
"src/lib/view.provider.ts": "
|
|
49
|
-
"src/remote/bridge.ts": "
|
|
50
|
-
"src/remote/dashboard-picker.tsx": "
|
|
52
|
+
"src/lib/view.provider.ts": "5bafa599cb1d618fc8256db0eb3360533f888b2cd63cd012cdd3ae45edc4da41",
|
|
53
|
+
"src/remote/bridge.ts": "6c213ac74daf8efee87100a94396fb13728027f23cfc189e65c524985235aea7",
|
|
54
|
+
"src/remote/dashboard-picker.tsx": "ffd5ddf54469a439bcdbabe974c0764de967a1f2716ea1304625fb6c54f67461",
|
|
51
55
|
"src/remote/html-dashboard.tsx": "3e7eba0f8f760cd5334735a790278eea51beaa33ad9ff36a31b560bf7a0ed20d",
|
|
52
56
|
"src/remote/i18n.ts": "65bb7941903e0a0e6445836cdf25f22e30d875d709fb07d60ac2d88d591b7218",
|
|
53
|
-
"src/remote/locales/en.json": "
|
|
54
|
-
"src/remote/locales/zh-Hans.json": "
|
|
55
|
-
"src/remote/locales/zh-Hant.json": "
|
|
56
|
-
"src/remote/main.tsx": "
|
|
57
|
+
"src/remote/locales/en.json": "8564f0f61fa0e4739e7ee2d55a23b7ff23cbfd40a8a18eeb2c32bdf15abe9a2e",
|
|
58
|
+
"src/remote/locales/zh-Hans.json": "5060899f01c9da602d0de4a84548de0115246c75f2a1e2f690beefa0f44f9913",
|
|
59
|
+
"src/remote/locales/zh-Hant.json": "123c66ae7f1b4ff4cb0001e225164193b0eb3a5069d0768879284ee96a926855",
|
|
60
|
+
"src/remote/main.tsx": "33ec783a2087a37126aca5792bdbb95bd656cecd034674e7c5bf29175d218125",
|
|
57
61
|
"src/remote/share-dialog.tsx": "f4a78a3bbf99d49c84e10631be9a9bcd3dc11b42130ce79e00010e53fbc2af84",
|
|
58
62
|
"src/remote/style.css": "8491fc393bacecd73cff2295526769cf73495d7842ca1ce57fb1abd0b7b5b60f",
|
|
59
63
|
"src/remote/viewer-controls.tsx": "b55e27a8a7932575a994fe30645b535c3dd5ba7e27df1f18a1f86462a6354ca3",
|
|
@@ -61,8 +65,8 @@
|
|
|
61
65
|
"tsconfig.remote.json": "e0b173f885c7439dfce09d411ff97fb7511bd77f6f1d7fdd66b5adb477a1f411"
|
|
62
66
|
},
|
|
63
67
|
"outputs": {
|
|
64
|
-
"app.js": "
|
|
65
|
-
"app.css": "
|
|
68
|
+
"app.js": "06a6ef10aa3a96cb333c6a7772a8cbf03571e1edf0aba34b92ec8024420d7cfa",
|
|
69
|
+
"app.css": "6e5081fc763445d24f2c61f9eb0c3eea53b7e392001ee54b54d1a34424c77d3c",
|
|
66
70
|
"renderer.html": "30abe65cd0d32a7596e02c5e94f2e20879fa73644507fefa35c7119a018ff7d2"
|
|
67
71
|
},
|
|
68
72
|
"libraries": {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Reuse successful static resource bytes only within one validation. Every request
|
|
2
|
+
// still passes the document resource policy; redirects are never fetched out of band.
|
|
3
|
+
export function createResourceCache() {
|
|
4
|
+
const admitted = new WeakSet(), entries = new Map()
|
|
5
|
+
const maxResourceBytes = 8 * 1024 * 1024, maxTotalBytes = 32 * 1024 * 1024
|
|
6
|
+
let totalBytes = 0
|
|
7
|
+
const key = request => `${request.resourceType()}:${request.url().split('#')[0]}`
|
|
8
|
+
return {
|
|
9
|
+
async route(page, route) {
|
|
10
|
+
const request = route.request()
|
|
11
|
+
const allowed = request.method() === 'GET' && await page.evaluate(({ type, url }) =>
|
|
12
|
+
DocumentHost.isDashboardResourceAllowed(type, url), { type: request.resourceType(), url: request.url() })
|
|
13
|
+
if (!allowed) return route.abort('blockedbyclient')
|
|
14
|
+
admitted.add(request)
|
|
15
|
+
const cached = await entries.get(key(request))
|
|
16
|
+
if (cached) return route.fulfill(cached)
|
|
17
|
+
return route.continue()
|
|
18
|
+
},
|
|
19
|
+
capture(response) {
|
|
20
|
+
const request = response.request(), id = key(request), headers = response.headers()
|
|
21
|
+
if (!admitted.has(request) || response.status() !== 200 || entries.has(id) || entries.size >= 128 ||
|
|
22
|
+
headers['set-cookie'] || /(?:^|,)\s*no-store(?:,|$)/i.test(headers['cache-control'] ?? '') ||
|
|
23
|
+
headers.vary === '*' || Number(headers['content-length']) > maxResourceBytes) return
|
|
24
|
+
const pending = (async () => {
|
|
25
|
+
try {
|
|
26
|
+
const body = await response.body()
|
|
27
|
+
if (body.length > maxResourceBytes || totalBytes + body.length > maxTotalBytes) return
|
|
28
|
+
totalBytes += body.length
|
|
29
|
+
// Playwright exposes decoded bytes; do not replay wire compression/length.
|
|
30
|
+
const replayHeaders = { ...headers }
|
|
31
|
+
for (const name of ['content-encoding', 'content-length', 'transfer-encoding']) delete replayHeaders[name]
|
|
32
|
+
return { status: 200, headers: replayHeaders, body }
|
|
33
|
+
} catch { return undefined } // A failed load is reported by the page, never cached.
|
|
34
|
+
})()
|
|
35
|
+
entries.set(id, pending)
|
|
36
|
+
},
|
|
37
|
+
async settle() { await Promise.all(entries.values()) }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,18 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// Invariants: the SDK's per-document rate/concurrency limits also apply in preflight.
|
|
2
|
+
// Independent synthetic interactions need fresh mounts, not a larger production quota.
|
|
3
|
+
// Each scenario still validates initial load, its interaction sequence and host refresh.
|
|
4
|
+
import { createResourceCache } from './resource-cache.mjs'
|
|
5
|
+
|
|
6
|
+
export async function validateDocument(input, browser, hostPath, { timeoutMs = 55000 } = {}) {
|
|
4
7
|
const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, serviceWorkers: 'block' })
|
|
8
|
+
const resources = createResourceCache()
|
|
9
|
+
let timer
|
|
10
|
+
const validation = validateScenarios(input, context, hostPath, resources)
|
|
5
11
|
try {
|
|
12
|
+
// Leave time for browser/report cleanup within the host's 60-second Action budget.
|
|
13
|
+
return await Promise.race([validation, new Promise(resolve => {
|
|
14
|
+
timer = setTimeout(() => resolve({ ok: false, errors: ['DOCUMENT_PREFLIGHT_TIMEOUT'],
|
|
15
|
+
requestedBindings: [], renderedTextLength: 0 }), timeoutMs)
|
|
16
|
+
})])
|
|
17
|
+
} finally {
|
|
18
|
+
clearTimeout(timer)
|
|
19
|
+
await context.close()
|
|
20
|
+
await validation.catch(() => {}) // Closing the context cancels outstanding Playwright operations.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function validateScenarios(input, context, hostPath, resources) {
|
|
25
|
+
const initial = await validateScenario(input, context, hostPath, resources, { type: 'refresh' })
|
|
26
|
+
if (!initial.report.ok) return initial.report
|
|
27
|
+
const scenarios = [
|
|
28
|
+
...Array.from({ length: input.parameters ? Math.min(initial.selectCount, 24) : 0 }, (_, index) => ({ type: 'select', index })),
|
|
29
|
+
...Array.from({ length: Math.min(initial.buttonCount, 12) }, (_, index) => ({ type: 'button', index }))
|
|
30
|
+
]
|
|
31
|
+
const requestedBindings = new Set(initial.report.requestedBindings)
|
|
32
|
+
// Bound live pages while keeping the full control/button sweep within the Action deadline.
|
|
33
|
+
for (let offset = 0; offset < scenarios.length; offset += 2) {
|
|
34
|
+
const results = await Promise.allSettled(scenarios.slice(offset, offset + 2).map(scenario =>
|
|
35
|
+
validateScenario(input, context, hostPath, resources, scenario)))
|
|
36
|
+
// Settle both pages before closing the context, including on infrastructure errors.
|
|
37
|
+
for (const result of results) {
|
|
38
|
+
if (result.status === 'rejected') throw result.reason
|
|
39
|
+
const { report } = result.value
|
|
40
|
+
report.requestedBindings.forEach(id => requestedBindings.add(id))
|
|
41
|
+
if (!report.ok) return { ...report, requestedBindings: [...requestedBindings] }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { ...initial.report, requestedBindings: [...requestedBindings] }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function validateScenario(input, context, hostPath, resources, scenario) {
|
|
48
|
+
const errors = [], addError = value => { const message = String(value).slice(0, 300); if (errors.length < 16 && !errors.includes(message)) errors.push(message) }
|
|
49
|
+
const loadedResources = new Set()
|
|
6
50
|
const page = await context.newPage()
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const allowed = request.method() === 'GET' && await page.evaluate(({ type, url }) =>
|
|
10
|
-
DocumentHost.isDashboardResourceAllowed(type, url), { type: request.resourceType(), url: request.url() })
|
|
11
|
-
if (allowed) await route.continue()
|
|
12
|
-
else await route.abort('blockedbyclient')
|
|
13
|
-
})
|
|
51
|
+
try {
|
|
52
|
+
await page.route('**/*', route => resources.route(page, route))
|
|
14
53
|
page.on('requestfailed', request => addError(`DOCUMENT_RESOURCE_LOAD_FAILED: ${request.resourceType()} ${request.url().split('?')[0]} (${request.failure()?.errorText ?? 'request failed'})`))
|
|
15
54
|
page.on('response', response => {
|
|
55
|
+
resources.capture(response)
|
|
16
56
|
if (response.status() >= 400) addError(`DOCUMENT_RESOURCE_HTTP_ERROR: ${response.status()} ${response.url().split('?')[0]}`)
|
|
17
57
|
else if (response.status() < 300 || response.status() === 304) {
|
|
18
58
|
for (let request = response.request(); request; request = request.redirectedFrom()) loadedResources.add(request.url().split('#')[0])
|
|
@@ -79,9 +119,10 @@ const errors = [], addError = value => { const message = String(value).slice(0,
|
|
|
79
119
|
}
|
|
80
120
|
// Exercise bounded native selects against the saved parameter contract; data remains the real default preview.
|
|
81
121
|
// This detects invalid filter wiring, not the upstream effect of a filter or arbitrary custom controls.
|
|
82
|
-
if (frame && input.parameters) {
|
|
122
|
+
if (frame && input.parameters && scenario.type === 'select') {
|
|
83
123
|
const selects = frame.locator('select')
|
|
84
|
-
|
|
124
|
+
if (scenario.index >= await selects.count()) addError('DOCUMENT_SELECT_INTERACTION_FAILED')
|
|
125
|
+
for (let index = scenario.index; index < Math.min(await selects.count(), scenario.index + 1); index++) {
|
|
85
126
|
const select = selects.nth(index)
|
|
86
127
|
if (!(await select.isVisible()) || !(await select.isEnabled())) continue
|
|
87
128
|
const original = await select.inputValue()
|
|
@@ -138,14 +179,8 @@ const errors = [], addError = value => { const message = String(value).slice(0,
|
|
|
138
179
|
}
|
|
139
180
|
// Page buttons are untrusted executable UI too. Exercise a bounded set inside
|
|
140
181
|
// the isolated preview; do not guess which button is refresh from its label.
|
|
141
|
-
if (frame) {
|
|
142
|
-
|
|
143
|
-
for (let index = 0; index < Math.min(await buttons.count(), 12); index++) {
|
|
144
|
-
const button = buttons.nth(index)
|
|
145
|
-
if (!(await button.isVisible()) || !(await button.isEnabled())) continue
|
|
146
|
-
await button.click({ timeout: 1000 }).catch(() => addError('DOCUMENT_BUTTON_INTERACTION_FAILED'))
|
|
147
|
-
await page.waitForTimeout(200)
|
|
148
|
-
}
|
|
182
|
+
if (frame && scenario.type === 'button') {
|
|
183
|
+
await exerciseButtonPath(frame, page, scenario.index, addError)
|
|
149
184
|
}
|
|
150
185
|
// Exercise the same refresh event as the Workbench. A renderer that fails on its
|
|
151
186
|
// second Dataset must not pass merely because the first query painted a page.
|
|
@@ -156,6 +191,39 @@ const errors = [], addError = value => { const message = String(value).slice(0,
|
|
|
156
191
|
const renderedTextLength = frame ? (await frame.locator('body').innerText()).trim().length : 0
|
|
157
192
|
if (!renderedTextLength) addError('DOCUMENT_EMPTY_PAGE')
|
|
158
193
|
if (!state.requestedBindings.length) addError('DOCUMENT_INITIAL_QUERY_MISSING')
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
await resources.settle()
|
|
195
|
+
return {
|
|
196
|
+
report: { ok: !errors.length, errors, requestedBindings: [...new Set(state.requestedBindings)], renderedTextLength },
|
|
197
|
+
selectCount: frame ? await frame.locator('select').count() : 0,
|
|
198
|
+
buttonCount: frame ? await frame.locator('button').count() : 0
|
|
199
|
+
}
|
|
200
|
+
} finally { await page.close() }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function actionableButtons(frame) {
|
|
204
|
+
const buttons = (await frame.locator('button').elementHandles()).slice(0, 12)
|
|
205
|
+
const actionable = []
|
|
206
|
+
for (const button of buttons)
|
|
207
|
+
if (await button.isVisible() && await button.isEnabled()) actionable.push(button)
|
|
208
|
+
return actionable
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function exerciseButtonPath(frame, page, index, addError) {
|
|
212
|
+
const root = (await frame.locator('button').elementHandles())[index]
|
|
213
|
+
if (!root) { addError('DOCUMENT_BUTTON_INTERACTION_FAILED'); return }
|
|
214
|
+
const pending = [root], clicked = []
|
|
215
|
+
while (pending.length) {
|
|
216
|
+
const button = pending.shift()
|
|
217
|
+
if (!(await button.isVisible()) || !(await button.isEnabled()) ||
|
|
218
|
+
await button.evaluate((element, previous) => previous.includes(element), clicked)) continue
|
|
219
|
+
if (clicked.length >= 12) { addError('DOCUMENT_BUTTON_PATH_LIMIT'); return }
|
|
220
|
+
const before = await actionableButtons(frame)
|
|
221
|
+
await button.click({ timeout: 1000 }).catch(() => addError('DOCUMENT_BUTTON_INTERACTION_FAILED'))
|
|
222
|
+
clicked.push(button)
|
|
223
|
+
await page.waitForTimeout(200)
|
|
224
|
+
// Follow newly inserted, revealed or enabled controls in this same document.
|
|
225
|
+
// Compare DOM identity, not labels or indexes that can shift after a click.
|
|
226
|
+
for (const next of await actionableButtons(frame))
|
|
227
|
+
if (!await next.evaluate((element, previous) => previous.includes(element), before)) pending.push(next)
|
|
228
|
+
}
|
|
161
229
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xpert-ai/plugin-live-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Versioned live dashboards using platform analytics and Data Analytics renderer",
|
|
5
5
|
"main": "./index.cjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"docs",
|
|
20
20
|
"examples",
|
|
21
21
|
".xpertai-plugin",
|
|
22
|
-
"skills"
|
|
22
|
+
"skills",
|
|
23
|
+
"scripts/import-prompts.mjs"
|
|
23
24
|
],
|
|
24
25
|
"license": "AGPL-3.0",
|
|
25
26
|
"private": false,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { realpathSync } from 'node:fs'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { parseArgs } from 'node:util'
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
const { dashboardWorkspacePrompts } = require('../dist/lib/prompt-workflows.js')
|
|
9
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
10
|
+
|
|
11
|
+
/** Create-only import: no Assistant edits, broad associations, replacement or automatic publication. */
|
|
12
|
+
export async function importPrompts({ apiUrl, organizationId, assistantId, token, apply = false, request = fetch, log = console.log }) {
|
|
13
|
+
const base = new URL(apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`)
|
|
14
|
+
if (base.username || base.password || base.search || base.hash ||
|
|
15
|
+
!['http:', 'https:'].includes(base.protocol)) throw new Error('Use an HTTP(S) API base URL without credentials, query or fragment.')
|
|
16
|
+
if (base.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(base.hostname))
|
|
17
|
+
throw new Error('Use HTTPS for a remote API. TLS certificate verification must remain enabled.')
|
|
18
|
+
if (!uuid.test(organizationId) || !uuid.test(assistantId)) throw new Error('Organization and Assistant IDs must be UUIDs.')
|
|
19
|
+
const bearer = token?.trim().replace(/^Bearer\s+/i, '')
|
|
20
|
+
if (!bearer) throw new Error('Set XPERT_TOKEN to a platform login token.')
|
|
21
|
+
const headers = { Authorization: `Bearer ${bearer}`, 'organization-id': organizationId, 'Content-Type': 'application/json' }
|
|
22
|
+
async function api(path, body) {
|
|
23
|
+
const response = await request(new URL(path, base), {
|
|
24
|
+
method: body ? 'POST' : 'GET', headers, redirect: 'error', signal: AbortSignal.timeout(30000),
|
|
25
|
+
...(body ? { body: JSON.stringify(body) } : {})
|
|
26
|
+
})
|
|
27
|
+
if (!response.ok) throw new Error(`API ${body ? 'POST' : 'GET'} failed (${response.status}). No existing data was overwritten. Rerun to inspect partial progress.`)
|
|
28
|
+
return response.json()
|
|
29
|
+
}
|
|
30
|
+
const assistant = await api(`xpert/${assistantId}`)
|
|
31
|
+
if (assistant?.id !== assistantId || !uuid.test(assistant.workspaceId ?? '') ||
|
|
32
|
+
assistant.organizationId !== organizationId || assistant.latest === false)
|
|
33
|
+
throw new Error('The Assistant must be the current version in the selected organization and an authoring workspace.')
|
|
34
|
+
const nodes = assistant.graph?.nodes
|
|
35
|
+
if (!Array.isArray(nodes)) throw new Error('The Assistant has no published graph. Configure and publish LiveDashboard first.')
|
|
36
|
+
const middlewares = nodes.filter(node => node?.type === 'workflow' && node.entity?.type === 'middleware' && node.entity.provider === 'LiveDashboard')
|
|
37
|
+
// Required middleware is always active, so prompts need no stale per-Assistant node keys.
|
|
38
|
+
if (middlewares.length !== 1 || middlewares[0].entity.required !== true)
|
|
39
|
+
throw new Error('Configure one LiveDashboard middleware as required (System middleware), publish, then rerun.')
|
|
40
|
+
async function runtimeCommands() {
|
|
41
|
+
const runtime = await api(`xpert/${assistantId}/runtime-capabilities`)
|
|
42
|
+
if (!Array.isArray(runtime?.commands)) throw new Error('Unexpected runtime command response; import stopped.')
|
|
43
|
+
return runtime.commands
|
|
44
|
+
}
|
|
45
|
+
const commands = await runtimeCommands()
|
|
46
|
+
if (!commands.some(command => command.source?.type === 'middleware' && command.source.provider === 'LiveDashboard' && command.source.nodeKey === middlewares[0].key))
|
|
47
|
+
throw new Error('Published LiveDashboard commands are unavailable. Connect the middleware to the main Agent and load the updated plugin first.')
|
|
48
|
+
function available(workflow, existing, catalog) {
|
|
49
|
+
return catalog.some(command => command.name === workflow.name && command.kind === 'prompt_workflow' &&
|
|
50
|
+
command.source?.type === 'workspace_prompt_workflow' && command.source.workflowId === existing.id &&
|
|
51
|
+
command.action?.type === 'insert_text' && command.action.template === workflow.template)
|
|
52
|
+
}
|
|
53
|
+
const workspace = encodeURIComponent(assistant.workspaceId)
|
|
54
|
+
const creates = []
|
|
55
|
+
const skipped = []
|
|
56
|
+
const conflicts = []
|
|
57
|
+
for (const workflow of dashboardWorkspacePrompts) {
|
|
58
|
+
const data = encodeURIComponent(JSON.stringify({ where: { name: workflow.name }, take: 2 }))
|
|
59
|
+
const page = await api(`prompt-workflow/by-workspace/${workspace}?data=${data}`)
|
|
60
|
+
if (!Array.isArray(page?.items) || page.total !== page.items.length || page.total > 1 ||
|
|
61
|
+
page.items.some(item => item?.name !== workflow.name)) throw new Error('Unexpected prompt lookup result; import stopped.')
|
|
62
|
+
const existing = page.items[0]
|
|
63
|
+
if (existing) {
|
|
64
|
+
const applicable = !existing.associatedXpertIds?.length || existing.associatedXpertIds.includes(assistantId)
|
|
65
|
+
if (existing.archivedAt || existing.actionType === 'insert_invocation' || existing.actionType === 'submit_prompt' ||
|
|
66
|
+
existing.template !== workflow.template || !applicable || !available(workflow, existing, commands)) conflicts.push(workflow.name)
|
|
67
|
+
else skipped.push(workflow.name)
|
|
68
|
+
} else {
|
|
69
|
+
if (commands.some(command => command.name === workflow.name)) conflicts.push(workflow.name)
|
|
70
|
+
creates.push({ ...workflow, associatedXpertIds: [assistantId] })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (conflicts.length) throw new Error(`Resolve existing prompts in the workspace prompt library before importing (no writes made): ${[...new Set(conflicts)].join(', ')}. Associate the existing prompt with this Assistant if appropriate; user edits and archived prompts are preserved.`)
|
|
74
|
+
log(`${apply ? 'Apply' : 'Preview'}: create ${creates.length}, already available ${skipped.length}. Assistant ${assistantId}.`)
|
|
75
|
+
const created = []
|
|
76
|
+
const savedWorkflows = []
|
|
77
|
+
if (apply) for (const body of creates) {
|
|
78
|
+
const saved = await api(`prompt-workflow/workspace/${workspace}`, body)
|
|
79
|
+
if (!uuid.test(saved?.id ?? '') || saved.name !== body.name ||
|
|
80
|
+
saved.template !== body.template || !saved.associatedXpertIds?.includes(assistantId))
|
|
81
|
+
throw new Error(`Creation response did not confirm ${body.name}; inspect the prompt library before retrying.`)
|
|
82
|
+
created.push(body.name)
|
|
83
|
+
savedWorkflows.push(saved)
|
|
84
|
+
log(`Created ${body.name}`)
|
|
85
|
+
}
|
|
86
|
+
if (savedWorkflows.length) {
|
|
87
|
+
const updated = await runtimeCommands()
|
|
88
|
+
if (savedWorkflows.some(workflow => !available(workflow, workflow, updated)))
|
|
89
|
+
throw new Error('Prompts were created but runtime shortcuts are not confirmed. Inspect the Assistant command settings; existing data was not overwritten.')
|
|
90
|
+
log('Created shortcuts confirmed by the published Assistant runtime.')
|
|
91
|
+
}
|
|
92
|
+
return { planned: creates.map(item => item.name), created, skipped }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
96
|
+
try {
|
|
97
|
+
const { values } = parseArgs({ options: {
|
|
98
|
+
'api-url': { type: 'string' }, 'organization-id': { type: 'string' },
|
|
99
|
+
'assistant-id': { type: 'string' }, apply: { type: 'boolean', default: false }, help: { type: 'boolean' }
|
|
100
|
+
} })
|
|
101
|
+
if (values.help) console.log('Set XPERT_TOKEN, then run import-prompts.mjs --api-url https://host/api --organization-id UUID --assistant-id UUID [--apply]. Default: read-only preview. Requires Node 20+.')
|
|
102
|
+
else if (!values['api-url'] || !values['organization-id'] || !values['assistant-id']) throw new Error('Required: --api-url, --organization-id, --assistant-id. See --help.')
|
|
103
|
+
else await importPrompts({ apiUrl: values['api-url'], organizationId: values['organization-id'], assistantId: values['assistant-id'], token: process.env.XPERT_TOKEN, apply: values.apply })
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(error instanceof Error ? error.message : 'Prompt import failed.')
|
|
106
|
+
process.exitCode = 1
|
|
107
|
+
}
|
|
108
|
+
}
|