@xpert-ai/plugin-live-dashboard 0.2.0 → 0.3.0
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 +73 -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 +2 -0
- package/dist/lib/prompt-workflows.d.ts +3 -0
- package/dist/lib/prompt-workflows.js +118 -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 +16 -15
- 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 +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sources": {
|
|
3
|
-
".xpertai-plugin/plugin.json": "
|
|
4
|
-
"package.json": "
|
|
5
|
-
"scripts/build-action.mjs": "
|
|
3
|
+
".xpertai-plugin/plugin.json": "e83bedba6954e48497f37638f59eae5c7214c23ca16c9c18806b02ccf74b1500",
|
|
4
|
+
"package.json": "9543695d680f899db6082ae376c1653c613ac99f0eb61ddaba2e968cd6ecd8ea",
|
|
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",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"src/lib/dashboard-receipt.ts": "d02b7e4e1be0a075b7f27e887ebb555684f17036ebb9442982e6725ab37a53ab",
|
|
25
25
|
"src/lib/dashboard-search.ts": "0cd14c0004fb078b54c095faeab3195fb7e03a20c3400b5e67aec7cb4b2442e2",
|
|
26
26
|
"src/lib/dashboard-sharing.ts": "61199f135cc53b6b9f0bef0f75f14c4d40bff6a3986e62e2823ab80c564534d3",
|
|
27
|
-
"src/lib/dashboard-view.schema.ts": "
|
|
27
|
+
"src/lib/dashboard-view.schema.ts": "4db0f043e836a1e050e8621074e5370e00507f5c6862ab9e2d624001c0452f26",
|
|
28
28
|
"src/lib/dashboard.schema.ts": "982e15591e70102ea506951cd10de9ec4864281157b0b95a7b591a8fff3da813",
|
|
29
|
-
"src/lib/dashboard.service.ts": "
|
|
29
|
+
"src/lib/dashboard.service.ts": "8b61384e2e655821dcbd427f3e393c51b90d5d023ee50e078f1038617ef26dcb",
|
|
30
30
|
"src/lib/definition-tools.ts": "ba48842396e57499331a52ca7cf5e4e1eb72e1e519b5dabfff2743fddf15947d",
|
|
31
31
|
"src/lib/document-host.ts": "3543ddb443006b90cd4689b65446b0e3b3305362d3d24d93209c094967921ffe",
|
|
32
32
|
"src/lib/document-libraries.ts": "9c228995c509dc7f606cdc70d972ddb22df0164df4e0070583d0ab901f6605d0",
|
|
@@ -36,24 +36,25 @@
|
|
|
36
36
|
"src/lib/filter-evidence.ts": "594ea9065e5c8d32c6bd4fc055e78c2df7ad2418c2019106136917c89c8ac331",
|
|
37
37
|
"src/lib/html-guidance.ts": "0bd0b830b33045357a070c2079b2aedaadb57807ba366be56b767b7d55120325",
|
|
38
38
|
"src/lib/html-validation.ts": "9f342f6716d457ba1a46c9d228f3f7858dcae2c0f64a3c98bc1d28d35a8f43ac",
|
|
39
|
-
"src/lib/middleware.ts": "
|
|
39
|
+
"src/lib/middleware.ts": "962c6c40e41995899099aaf0977787a85533c9071be20d91d52041e676961a07",
|
|
40
40
|
"src/lib/plugin.ts": "c2788944df7697802350cde7fcc292a49b433b5176c7f8eec6a9809fbdf4ced7",
|
|
41
41
|
"src/lib/presentation.provider.ts": "3b37387636d9e7317ee6e36c9e607fadb20002c08c24d54e424342e913b2405f",
|
|
42
42
|
"src/lib/preview-retry.ts": "55fa28cc61f11adac4dce5a2066cbdd6d529ffd5ffd3cca7dcb552c6b7299500",
|
|
43
|
+
"src/lib/prompt-workflows.ts": "2782167c3e28fbda9be7a7312a68621a98bc120e652f0f985f57160df8110ce6",
|
|
43
44
|
"src/lib/remote-entry.ts": "4c760963ff099b4d9a8961c933b1a586c0ac57d85a88c745c2578e2f66bf1539",
|
|
44
45
|
"src/lib/renderer.ts": "418ac7cc5f37f9d638c0b2dd3a5361464674f21dbfb801b4414a9d68f1c9edc8",
|
|
45
46
|
"src/lib/resource-card.ts": "d64f91e640a3db220b941adef4a582e011a7aea253d57cc5d70a5bdcdacd8f71",
|
|
46
47
|
"src/lib/save-evidence.ts": "458f8fe2ef74fd4809fb9543d8c31bb42e0e16b10550b2a7808ca92ec3662bda",
|
|
47
48
|
"src/lib/sharing.service.ts": "e52c98d967f27a2f3bc80614f31da15e373dd4450b80aea250156efe24c77f3b",
|
|
48
|
-
"src/lib/view.provider.ts": "
|
|
49
|
-
"src/remote/bridge.ts": "
|
|
50
|
-
"src/remote/dashboard-picker.tsx": "
|
|
49
|
+
"src/lib/view.provider.ts": "5bafa599cb1d618fc8256db0eb3360533f888b2cd63cd012cdd3ae45edc4da41",
|
|
50
|
+
"src/remote/bridge.ts": "6c213ac74daf8efee87100a94396fb13728027f23cfc189e65c524985235aea7",
|
|
51
|
+
"src/remote/dashboard-picker.tsx": "ffd5ddf54469a439bcdbabe974c0764de967a1f2716ea1304625fb6c54f67461",
|
|
51
52
|
"src/remote/html-dashboard.tsx": "3e7eba0f8f760cd5334735a790278eea51beaa33ad9ff36a31b560bf7a0ed20d",
|
|
52
53
|
"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": "
|
|
54
|
+
"src/remote/locales/en.json": "8564f0f61fa0e4739e7ee2d55a23b7ff23cbfd40a8a18eeb2c32bdf15abe9a2e",
|
|
55
|
+
"src/remote/locales/zh-Hans.json": "5060899f01c9da602d0de4a84548de0115246c75f2a1e2f690beefa0f44f9913",
|
|
56
|
+
"src/remote/locales/zh-Hant.json": "123c66ae7f1b4ff4cb0001e225164193b0eb3a5069d0768879284ee96a926855",
|
|
57
|
+
"src/remote/main.tsx": "33ec783a2087a37126aca5792bdbb95bd656cecd034674e7c5bf29175d218125",
|
|
57
58
|
"src/remote/share-dialog.tsx": "f4a78a3bbf99d49c84e10631be9a9bcd3dc11b42130ce79e00010e53fbc2af84",
|
|
58
59
|
"src/remote/style.css": "8491fc393bacecd73cff2295526769cf73495d7842ca1ce57fb1abd0b7b5b60f",
|
|
59
60
|
"src/remote/viewer-controls.tsx": "b55e27a8a7932575a994fe30645b535c3dd5ba7e27df1f18a1f86462a6354ca3",
|
|
@@ -61,8 +62,8 @@
|
|
|
61
62
|
"tsconfig.remote.json": "e0b173f885c7439dfce09d411ff97fb7511bd77f6f1d7fdd66b5adb477a1f411"
|
|
62
63
|
},
|
|
63
64
|
"outputs": {
|
|
64
|
-
"app.js": "
|
|
65
|
-
"app.css": "
|
|
65
|
+
"app.js": "06a6ef10aa3a96cb333c6a7772a8cbf03571e1edf0aba34b92ec8024420d7cfa",
|
|
66
|
+
"app.css": "3584fe18b974747f90b1adefcb9fe4519870f2d04e194723fb83a15dff36ce11",
|
|
66
67
|
"renderer.html": "30abe65cd0d32a7596e02c5e94f2e20879fa73644507fefa35c7119a018ff7d2"
|
|
67
68
|
},
|
|
68
69
|
"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