@chengxianglibra/dsh-data-analysis 0.1.2 → 0.2.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/README.md +136 -99
- package/cordis.patch.yml +1 -1
- package/lib/bin/environment.js +8 -6
- package/lib/client/credentials/changes.js +74 -0
- package/lib/client/credentials/create-datasource.js +126 -22
- package/lib/client/credentials/credential-draft.js +42 -0
- package/lib/client/credentials/defaults.js +11 -0
- package/lib/client/credentials/descriptions.js +46 -0
- package/lib/client/credentials/install.js +209 -81
- package/lib/client/credentials/model.js +323 -51
- package/lib/client/credentials/report-publishing.js +96 -0
- package/lib/client/credentials/styles.js +13 -9
- package/lib/client/i18n/context.js +22 -0
- package/lib/client/i18n/copy.js +57 -0
- package/lib/client/i18n/credentials.js +475 -0
- package/lib/client/i18n/host.js +27 -0
- package/lib/client/i18n/navigation.js +41 -0
- package/lib/client/i18n/presentation.js +577 -0
- package/lib/client/i18n/python.js +36 -0
- package/lib/client/i18n/semantic.js +441 -0
- package/lib/client/input-context.js +21 -0
- package/lib/client/python-tool/card.js +35 -0
- package/lib/client/python-tool/execution-error.js +35 -0
- package/lib/client/python-tool/install.js +9 -0
- package/lib/client/python-tool/model.js +101 -0
- package/lib/client/right-tabs/install.js +535 -0
- package/lib/client/right-tabs/live-delivery.js +58 -0
- package/lib/client/right-tabs/navigation.js +47 -0
- package/lib/client/right-tabs/page.js +178 -0
- package/lib/client/right-tabs/styles.js +11 -0
- package/lib/client/rpc.js +5 -0
- package/lib/client/semantic-reference.js +10 -0
- package/lib/client/workspace-header-action.js +14 -1
- package/lib/client/workspace-shortcuts.js +26 -0
- package/lib/client.js +365 -242
- package/lib/compatibility.js +1 -1
- package/lib/datasource/authoring.js +43 -4
- package/lib/datasource/bridge-programs.js +23 -20
- package/lib/datasource/bridge.js +46 -3
- package/lib/datasource/changes-contract.js +41 -0
- package/lib/datasource/changes.js +49 -0
- package/lib/datasource/configure.js +39 -0
- package/lib/datasource/defaults.js +25 -0
- package/lib/datasource/index.js +2 -1
- package/lib/datasource/python-options.js +21 -0
- package/lib/datasource/python.js +116 -25
- package/lib/datasource/rpc.js +75 -13
- package/lib/datasource/service.js +286 -11
- package/lib/datasource/test.js +2 -1
- package/lib/disclosure/activation.js +19 -7
- package/lib/disclosure/execution-guidance.js +52 -0
- package/lib/disclosure/help.js +4 -1
- package/lib/environment/runtime.js +35 -34
- package/lib/environment/workspace.js +18 -1
- package/lib/lifecycle.js +33 -0
- package/lib/plugin-agents.js +154 -0
- package/lib/plugin.js +94 -172
- package/lib/presentation/asset.js +34 -0
- package/lib/presentation/assets/portable.js +14 -14
- package/lib/presentation/assets/static.js +344 -73
- package/lib/presentation/build/index.js +3 -2
- package/lib/presentation/commit.js +19 -12
- package/lib/presentation/contracts/asset-path.js +9 -0
- package/lib/presentation/contracts/catalog.js +62 -0
- package/lib/presentation/contracts/charts.js +0 -23
- package/lib/presentation/contracts/index.js +17 -5
- package/lib/presentation/contracts/types.js +1 -1
- package/lib/presentation/delivery.js +45 -30
- package/lib/presentation/export-html.js +214 -0
- package/lib/presentation/files.js +4 -5
- package/lib/presentation/projection/index.js +2 -1
- package/lib/presentation/projection/program.js +2 -9
- package/lib/presentation/projection/sql-code.js +13 -13
- package/lib/presentation/reports.js +114 -17
- package/lib/presentation/rpc.js +96 -58
- package/lib/presentation/tool.js +6 -10
- package/lib/report-publishing/adapters.js +111 -0
- package/lib/report-publishing/config.js +69 -0
- package/lib/report-publishing/service.js +180 -0
- package/lib/rpc.js +49 -0
- package/lib/semantic-browser/contracts.js +17 -0
- package/lib/semantic-browser/program.js +10 -0
- package/lib/semantic-browser/service.js +40 -6
- package/lib/semantic-reference/bridge.js +10 -1
- package/lib/semantic-reference/contracts.js +20 -6
- package/lib/semantic-reference/environment.js +56 -0
- package/lib/semantic-reference/labels.js +16 -0
- package/lib/semantic-reference/rpc.js +39 -18
- package/lib/semantic-reference/search.js +5 -4
- package/lib/semantic-reference/usage.js +12 -3
- package/lib/tool-lifecycle.js +22 -0
- package/lib/types/client/credentials/changes.d.ts +9 -0
- package/lib/types/client/credentials/create-datasource.d.ts +3 -1
- package/lib/types/client/credentials/credential-draft.d.ts +14 -0
- package/lib/types/client/credentials/defaults.d.ts +2 -0
- package/lib/types/client/credentials/descriptions.d.ts +1 -0
- package/lib/types/client/credentials/install.d.ts +19 -1
- package/lib/types/client/credentials/model.d.ts +31 -11
- package/lib/types/client/credentials/report-publishing.d.ts +5 -0
- package/lib/types/client/credentials/styles.d.ts +1 -1
- package/lib/types/client/i18n/context.d.ts +17 -0
- package/lib/types/client/i18n/copy.d.ts +806 -0
- package/lib/types/client/i18n/credentials.d.ts +475 -0
- package/lib/types/client/i18n/host.d.ts +16 -0
- package/lib/types/client/i18n/navigation.d.ts +41 -0
- package/lib/types/client/i18n/presentation.d.ts +577 -0
- package/lib/types/client/i18n/python.d.ts +19 -0
- package/lib/types/client/i18n/semantic.d.ts +441 -0
- package/lib/types/client/input-context.d.ts +19 -0
- package/lib/types/client/presentation/ask-dsh.d.ts +4 -8
- package/lib/types/client/presentation/catalog-model.d.ts +24 -0
- package/lib/types/client/presentation/catalog.d.ts +20 -0
- package/lib/types/client/presentation/chart-tooltip.d.ts +2 -2
- package/lib/types/client/presentation/context-reference.d.ts +12 -0
- package/lib/types/client/presentation/copy-context.d.ts +2 -2
- package/lib/types/client/presentation/delivery-model.d.ts +27 -6
- package/lib/types/client/presentation/delivery.d.ts +8 -7
- package/lib/types/client/presentation/download.d.ts +2 -0
- package/lib/types/client/presentation/export-menu.d.ts +26 -0
- package/lib/types/client/presentation/export-view.d.ts +63 -0
- package/lib/types/client/presentation/host-entry.d.ts +8 -2
- package/lib/types/client/presentation/icons.d.ts +1 -1
- package/lib/types/client/presentation/model.d.ts +14 -15
- package/lib/types/client/presentation/reader.d.ts +15 -2
- package/lib/types/client/presentation/reference-source.d.ts +9 -0
- package/lib/types/client/presentation/source-code-model.d.ts +2 -1
- package/lib/types/client/presentation/source-code.d.ts +0 -5
- package/lib/types/client/presentation/source-dialog.d.ts +1 -2
- package/lib/types/client/presentation/sources.d.ts +5 -5
- package/lib/types/client/presentation/styles.d.ts +1 -1
- package/lib/types/client/presentation/table.d.ts +5 -1
- package/lib/types/client/python-tool/card.d.ts +2 -0
- package/lib/types/client/python-tool/execution-error.d.ts +3 -0
- package/lib/types/client/python-tool/install.d.ts +2 -0
- package/lib/types/client/python-tool/model.d.ts +20 -0
- package/lib/types/client/right-tabs/install.d.ts +18 -0
- package/lib/types/client/right-tabs/live-delivery.d.ts +8 -0
- package/lib/types/client/right-tabs/navigation.d.ts +17 -0
- package/lib/types/client/right-tabs/page.d.ts +36 -0
- package/lib/types/client/right-tabs/styles.d.ts +2 -0
- package/lib/types/client/rpc.d.ts +3 -0
- package/lib/types/client/semantic-reference.d.ts +4 -0
- package/lib/types/client/workspace-header-action.d.ts +1 -1
- package/lib/types/client/workspace-shortcuts.d.ts +15 -0
- package/lib/types/client.d.ts +6 -3
- package/lib/types/compatibility.d.ts +1 -1
- package/lib/types/datasource/authoring.d.ts +10 -0
- package/lib/types/datasource/bridge.d.ts +23 -2
- package/lib/types/datasource/changes-contract.d.ts +18 -0
- package/lib/types/datasource/changes.d.ts +15 -0
- package/lib/types/datasource/configure.d.ts +7 -0
- package/lib/types/datasource/defaults.d.ts +7 -0
- package/lib/types/datasource/index.d.ts +6 -1
- package/lib/types/datasource/python-options.d.ts +9 -0
- package/lib/types/datasource/python.d.ts +17 -2
- package/lib/types/datasource/rpc.d.ts +2 -1
- package/lib/types/datasource/service.d.ts +48 -5
- package/lib/types/datasource/test.d.ts +1 -1
- package/lib/types/disclosure/activation.d.ts +2 -1
- package/lib/types/disclosure/execution-guidance.d.ts +6 -0
- package/lib/types/disclosure/help.d.ts +1 -1
- package/lib/types/environment/types.d.ts +2 -1
- package/lib/types/environment/workspace.d.ts +1 -0
- package/lib/types/lifecycle.d.ts +8 -0
- package/lib/types/plugin-agents.d.ts +27 -0
- package/lib/types/plugin.d.ts +12 -28
- package/lib/types/presentation/asset.d.ts +6 -0
- package/lib/types/presentation/build/index.d.ts +7 -2
- package/lib/types/presentation/commit.d.ts +1 -1
- package/lib/types/presentation/contracts/asset-path.d.ts +2 -0
- package/lib/types/presentation/contracts/catalog.d.ts +27 -0
- package/lib/types/presentation/contracts/charts.d.ts +0 -2
- package/lib/types/presentation/contracts/types.d.ts +7 -4
- package/lib/types/presentation/delivery.d.ts +1 -1
- package/lib/types/presentation/export-html.d.ts +13 -0
- package/lib/types/presentation/reports.d.ts +6 -1
- package/lib/types/presentation/rpc.d.ts +3 -2
- package/lib/types/presentation/tool.d.ts +2 -2
- package/lib/types/report-publishing/adapters.d.ts +6 -0
- package/lib/types/report-publishing/config.d.ts +22 -0
- package/lib/types/report-publishing/service.d.ts +68 -0
- package/lib/types/rpc.d.ts +3 -0
- package/lib/types/semantic-browser/contracts.d.ts +7 -0
- package/lib/types/semantic-browser/service.d.ts +7 -1
- package/lib/types/semantic-reference/bridge.d.ts +1 -0
- package/lib/types/semantic-reference/contracts.d.ts +5 -2
- package/lib/types/semantic-reference/environment.d.ts +22 -0
- package/lib/types/semantic-reference/labels.d.ts +16 -0
- package/lib/types/semantic-reference/rpc.d.ts +1 -1
- package/lib/types/semantic-reference/search.d.ts +1 -1
- package/lib/types/tool-lifecycle.d.ts +4 -0
- package/lib/types/workspace-identity.d.ts +6 -0
- package/lib/workspace-identity.js +9 -0
- package/package.json +83 -61
- package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
- package/skills/dsh-data-analysis-files/SKILL.md +32 -0
- package/skills/dsh-data-analysis-files/references/examples.md +93 -0
- package/skills/dsh-data-analysis-presentation/SKILL.md +10 -6
- package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +13 -4
- package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +3 -2
- package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +3 -2
- package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +12 -5
- package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +3 -2
- package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +3 -2
- package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +12 -4
- package/skills/dsh-data-analysis-presentation/references/examples.md +3 -1
- package/skills/dsh-data-analysis-presentation/references/export-html.md +7 -0
- package/skills/dsh-data-analysis-presentation/references/interaction.md +1 -1
- package/skills/dsh-data-analysis-presentation/references/narrative.md +1 -1
- package/skills/dsh-data-analysis-presentation/references/publishing.md +3 -0
- package/skills/dsh-data-analysis-presentation/references/schema.md +25 -1
- package/lib/types/client/presentation/install.d.ts +0 -15
|
@@ -12,7 +12,7 @@ def sql_code_snapshot(session, artifact):
|
|
|
12
12
|
succeeded_run = mv.SucceededRun
|
|
13
13
|
get_run = session.get_run
|
|
14
14
|
except Exception:
|
|
15
|
-
return {"snippets": [], "notices": ["
|
|
15
|
+
return {"snippets": [], "notices": ["marivo.presentation.sql-capability-unavailable"]}
|
|
16
16
|
|
|
17
17
|
pending = [(artifact.ref, artifact)]
|
|
18
18
|
scheduled, runs, seen_queries = {artifact.ref}, {}, set()
|
|
@@ -22,30 +22,30 @@ def sql_code_snapshot(session, artifact):
|
|
|
22
22
|
current = known_artifact if known_artifact is not None else session.artifact(expected_ref)
|
|
23
23
|
meta, contract = current.meta, current.contract()
|
|
24
24
|
if current.ref != expected_ref or contract.ref != expected_ref or meta.session_id != session.id or Path(meta.project_root).resolve() != Path.cwd().resolve():
|
|
25
|
-
notice("
|
|
25
|
+
notice("marivo.presentation.sql-artifact-mismatch")
|
|
26
26
|
continue
|
|
27
27
|
producer = meta.produced_by_job
|
|
28
28
|
if producer is None:
|
|
29
|
-
notice("
|
|
29
|
+
notice("marivo.presentation.sql-producer-missing")
|
|
30
30
|
continue
|
|
31
31
|
if not valid_string(producer, 512):
|
|
32
|
-
notice("
|
|
32
|
+
notice("marivo.presentation.sql-producer-invalid")
|
|
33
33
|
continue
|
|
34
34
|
run = runs.get(producer)
|
|
35
35
|
if run is None:
|
|
36
36
|
run = get_run(producer)
|
|
37
37
|
runs[producer] = run
|
|
38
38
|
if not isinstance(run, succeeded_run) or run.run_id != producer or run.output_mode != "produced" or run.output_artifact_ref != expected_ref:
|
|
39
|
-
notice("
|
|
39
|
+
notice("marivo.presentation.sql-run-mismatch")
|
|
40
40
|
continue
|
|
41
41
|
for query in run.queries:
|
|
42
42
|
if query_count >= 32:
|
|
43
|
-
notice("
|
|
43
|
+
notice("marivo.presentation.sql-query-limit")
|
|
44
44
|
break
|
|
45
45
|
query_count += 1
|
|
46
46
|
query_id = query.query_id
|
|
47
47
|
if not valid_string(query_id, 512):
|
|
48
|
-
notice("
|
|
48
|
+
notice("marivo.presentation.sql-query-invalid")
|
|
49
49
|
continue
|
|
50
50
|
query_key = (producer, query_id)
|
|
51
51
|
if query_key in seen_queries:
|
|
@@ -53,27 +53,27 @@ def sql_code_snapshot(session, artifact):
|
|
|
53
53
|
seen_queries.add(query_key)
|
|
54
54
|
sql = query.sql
|
|
55
55
|
if not valid_string(sql, 32768) or not sql.strip():
|
|
56
|
-
notice("
|
|
56
|
+
notice("marivo.presentation.sql-sql-invalid")
|
|
57
57
|
continue
|
|
58
58
|
snippets.append({"language": "sql", "text": sql, "provenance": "execution", "runId": producer, "queryId": query_id, "artifactRef": expected_ref})
|
|
59
59
|
for input_index, input_ref in enumerate(run.input_artifact_refs):
|
|
60
60
|
if input_index >= 65:
|
|
61
|
-
notice("
|
|
61
|
+
notice("marivo.presentation.sql-upstream-limit")
|
|
62
62
|
break
|
|
63
63
|
if not valid_string(input_ref, 512):
|
|
64
|
-
notice("
|
|
64
|
+
notice("marivo.presentation.sql-upstream-invalid")
|
|
65
65
|
continue
|
|
66
66
|
if input_ref in scheduled:
|
|
67
67
|
continue
|
|
68
68
|
if len(scheduled) >= 65:
|
|
69
|
-
notice("
|
|
69
|
+
notice("marivo.presentation.sql-upstream-limit")
|
|
70
70
|
break
|
|
71
71
|
scheduled.add(input_ref)
|
|
72
72
|
pending.append((input_ref, None))
|
|
73
73
|
except Exception:
|
|
74
74
|
# A missing record or capability affects code only. Never return exception text.
|
|
75
|
-
notice("
|
|
75
|
+
notice("marivo.presentation.sql-read-unavailable")
|
|
76
76
|
if not snippets and not notices:
|
|
77
|
-
notice("
|
|
77
|
+
notice("marivo.presentation.sql-not-saved")
|
|
78
78
|
return {"snippets": snippets, "notices": notices}
|
|
79
79
|
`.trim();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import { lstat, open, realpath, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { lstat, open, opendir, realpath, rename, rm } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { withFileLock } from '@deepseek-ai/dsh-atomic-write';
|
|
6
6
|
import { buildPresentation } from "./build/index.js";
|
|
7
7
|
import { commitPresentation } from "./commit.js";
|
|
8
|
+
import { parseReportHistory, REPORT_HISTORY_BYTES, REPORT_HISTORY_LIMIT, } from "./contracts/catalog.js";
|
|
8
9
|
import { parsePresentationDocument, parsePresentationReceipt, } from "./contracts/index.js";
|
|
9
10
|
import { presentationAssetPath, presentationReportPath, presentationSha256, readPresentationAsset, readReportFile, } from "./files.js";
|
|
10
11
|
export async function readReceiptDocument(root, receipt, signal) {
|
|
@@ -24,26 +25,101 @@ export async function readReceiptDocument(root, receipt, signal) {
|
|
|
24
25
|
throw new Error('asset-owner-mismatch');
|
|
25
26
|
return document;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
/** Current and its publication history become visible through one atomic rename. */
|
|
29
|
+
export async function readReportHistory(root, workspaceId, reportId, signal) {
|
|
30
|
+
const bytes = await readReportFile(root, path.join(presentationReportPath(root, reportId), 'current.json'), REPORT_HISTORY_BYTES, signal);
|
|
29
31
|
const current = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
30
|
-
if (!current ||
|
|
31
|
-
Object.keys(current).sort().join(',') !== 'receipt,reportId,schemaVersion,workspaceId' ||
|
|
32
|
-
current.schemaVersion !== 2 ||
|
|
33
|
-
current.workspaceId !== workspaceId ||
|
|
34
|
-
current.reportId !== reportId)
|
|
32
|
+
if (!current || current.workspaceId !== workspaceId || current.reportId !== reportId)
|
|
35
33
|
throw new Error('invalid-report-current');
|
|
36
34
|
const receipt = parsePresentationReceipt(current.receipt);
|
|
37
35
|
if (receipt.workspaceId !== workspaceId || receipt.reportId !== reportId)
|
|
38
36
|
throw new Error('asset-owner-mismatch');
|
|
37
|
+
if (current.schemaVersion === 2 &&
|
|
38
|
+
Object.keys(current).sort().join(',') === 'receipt,reportId,schemaVersion,workspaceId') {
|
|
39
|
+
return {
|
|
40
|
+
workspaceId,
|
|
41
|
+
reportId,
|
|
42
|
+
currentBuildId: receipt.buildId,
|
|
43
|
+
legacyHistoryUnavailable: true,
|
|
44
|
+
versions: [{ receipt, publishedAt: null, source: null }],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (current.schemaVersion !== 3 ||
|
|
48
|
+
Object.keys(current).sort().join(',') !==
|
|
49
|
+
'legacyHistoryUnavailable,receipt,reportId,schemaVersion,versions,workspaceId')
|
|
50
|
+
throw new Error('invalid-report-current');
|
|
51
|
+
const history = parseReportHistory({
|
|
52
|
+
workspaceId,
|
|
53
|
+
reportId,
|
|
54
|
+
currentBuildId: receipt.buildId,
|
|
55
|
+
legacyHistoryUnavailable: current.legacyHistoryUnavailable,
|
|
56
|
+
versions: current.versions,
|
|
57
|
+
});
|
|
58
|
+
if (JSON.stringify(history.versions[0].receipt) !== JSON.stringify(receipt))
|
|
59
|
+
throw new Error('invalid-report-history');
|
|
60
|
+
return history;
|
|
61
|
+
}
|
|
62
|
+
export async function resolvePresentation(root, workspaceId, reportId, signal) {
|
|
63
|
+
const history = await readReportHistory(root, workspaceId, reportId, signal);
|
|
64
|
+
const receipt = history.versions[0].receipt;
|
|
39
65
|
await readReceiptDocument(root, receipt, signal);
|
|
40
66
|
return receipt;
|
|
41
67
|
}
|
|
68
|
+
/** Enumerate only publication pointers, never completed-but-unpublished build directories. */
|
|
69
|
+
export async function listReports(root, workspaceId, signal) {
|
|
70
|
+
const directory = path.dirname(presentationReportPath(root, 'catalog'));
|
|
71
|
+
const parents = [root, path.dirname(directory), directory];
|
|
72
|
+
const identities = [];
|
|
73
|
+
try {
|
|
74
|
+
for (const name of parents) {
|
|
75
|
+
const stat = await lstat(name);
|
|
76
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (await realpath(name)) !== name)
|
|
77
|
+
throw new Error('asset-path-mismatch');
|
|
78
|
+
identities.push(stat);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code === 'ENOENT')
|
|
83
|
+
return { workspaceId, reports: [], unavailable: 0 };
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
const reports = [];
|
|
87
|
+
let unavailable = 0, count = 0;
|
|
88
|
+
for await (const entry of await opendir(directory)) {
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
if (++count > 4096)
|
|
91
|
+
throw new Error('report-catalog-too-large');
|
|
92
|
+
if (entry.name.startsWith('.'))
|
|
93
|
+
continue;
|
|
94
|
+
try {
|
|
95
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
96
|
+
throw new Error('asset-not-file');
|
|
97
|
+
const history = await readReportHistory(root, workspaceId, entry.name, signal);
|
|
98
|
+
reports.push(history.versions[0]);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
signal?.throwIfAborted();
|
|
102
|
+
if (error.code !== 'ENOENT')
|
|
103
|
+
unavailable++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const [index, name] of parents.entries()) {
|
|
107
|
+
const stat = await lstat(name);
|
|
108
|
+
if (stat.dev !== identities[index].dev ||
|
|
109
|
+
stat.ino !== identities[index].ino ||
|
|
110
|
+
stat.isSymbolicLink() ||
|
|
111
|
+
(await realpath(name)) !== name)
|
|
112
|
+
throw new Error('asset-path-mismatch');
|
|
113
|
+
}
|
|
114
|
+
reports.sort((a, b) => (b.publishedAt ?? '').localeCompare(a.publishedAt ?? '') ||
|
|
115
|
+
a.receipt.reportId.localeCompare(b.receipt.reportId));
|
|
116
|
+
return { workspaceId, reports, unavailable };
|
|
117
|
+
}
|
|
42
118
|
/** The mutable pointer is the publication boundary; completed builds are never overwritten. */
|
|
43
|
-
export async function publishPresentation(root, document, expectedBuildId, assertOwner, signal) {
|
|
119
|
+
export async function publishPresentation(root, document, expectedBuildId, assertOwner, signal, source = null) {
|
|
44
120
|
await assertOwner();
|
|
45
121
|
signal?.throwIfAborted();
|
|
46
|
-
const built = await buildPresentation(document);
|
|
122
|
+
const built = await buildPresentation(document, { html: false });
|
|
47
123
|
await assertOwner();
|
|
48
124
|
const receipt = await commitPresentation(root, built, assertOwner, signal);
|
|
49
125
|
const directory = presentationReportPath(root, document.reportId);
|
|
@@ -66,9 +142,12 @@ export async function publishPresentation(root, document, expectedBuildId, asser
|
|
|
66
142
|
await check();
|
|
67
143
|
await withFileLock(currentPath, async () => {
|
|
68
144
|
await check();
|
|
145
|
+
let history;
|
|
69
146
|
let current;
|
|
70
147
|
try {
|
|
71
|
-
|
|
148
|
+
history = await readReportHistory(root, document.workspaceId, document.reportId, signal);
|
|
149
|
+
current = history.versions[0].receipt;
|
|
150
|
+
await readReceiptDocument(root, current, signal);
|
|
72
151
|
}
|
|
73
152
|
catch (error) {
|
|
74
153
|
if (expectedBuildId !== null || error.code !== 'ENOENT')
|
|
@@ -85,17 +164,35 @@ export async function publishPresentation(root, document, expectedBuildId, asser
|
|
|
85
164
|
}
|
|
86
165
|
if ((current?.buildId ?? null) !== expectedBuildId)
|
|
87
166
|
throw new Error('report-save-conflict');
|
|
167
|
+
const versions = [
|
|
168
|
+
{ receipt, publishedAt: new Date().toISOString(), source },
|
|
169
|
+
...(history?.versions ?? []),
|
|
170
|
+
];
|
|
171
|
+
if (versions.length > REPORT_HISTORY_LIMIT)
|
|
172
|
+
throw new Error('report-history-full');
|
|
173
|
+
parseReportHistory({
|
|
174
|
+
workspaceId: document.workspaceId,
|
|
175
|
+
reportId: document.reportId,
|
|
176
|
+
currentBuildId: receipt.buildId,
|
|
177
|
+
legacyHistoryUnavailable: history?.legacyHistoryUnavailable ?? false,
|
|
178
|
+
versions,
|
|
179
|
+
});
|
|
180
|
+
const pointer = JSON.stringify({
|
|
181
|
+
schemaVersion: 3,
|
|
182
|
+
workspaceId: document.workspaceId,
|
|
183
|
+
reportId: document.reportId,
|
|
184
|
+
receipt,
|
|
185
|
+
versions,
|
|
186
|
+
legacyHistoryUnavailable: history?.legacyHistoryUnavailable ?? false,
|
|
187
|
+
});
|
|
188
|
+
if (Buffer.byteLength(pointer) > REPORT_HISTORY_BYTES)
|
|
189
|
+
throw new Error('report-history-full');
|
|
88
190
|
const temporary = path.join(directory, `.current-${randomUUID()}.tmp`);
|
|
89
191
|
let published = false;
|
|
90
192
|
try {
|
|
91
193
|
const file = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
92
194
|
try {
|
|
93
|
-
await file.writeFile(
|
|
94
|
-
schemaVersion: 2,
|
|
95
|
-
workspaceId: document.workspaceId,
|
|
96
|
-
reportId: document.reportId,
|
|
97
|
-
receipt,
|
|
98
|
-
}), { signal });
|
|
195
|
+
await file.writeFile(pointer, { signal });
|
|
99
196
|
await file.sync();
|
|
100
197
|
}
|
|
101
198
|
finally {
|
package/lib/presentation/rpc.js
CHANGED
|
@@ -1,48 +1,101 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { finishCleanup, PendingTasks } from "../lifecycle.js";
|
|
4
|
+
import { registerPluginRpc } from "../rpc.js";
|
|
5
|
+
import { readReceiptAsset } from "./asset.js";
|
|
3
6
|
import { applyPresentationEdits } from "./contracts/editing.js";
|
|
4
|
-
import {
|
|
5
|
-
import { presentationAssetPath, presentationSha256, readPresentationAsset } from "./files.js";
|
|
7
|
+
import { PresentationContractError, parsePresentationBuildId, parsePresentationReceipt, } from "./contracts/index.js";
|
|
6
8
|
import { MARIVO_PRESENTATION_RPC_CHANNEL } from "./receipt.js";
|
|
7
|
-
import { publishPresentation, readReceiptDocument, resolvePresentation } from "./reports.js";
|
|
9
|
+
import { listReports, publishPresentation, readReceiptDocument, readReportHistory, resolvePresentation, } from "./reports.js";
|
|
8
10
|
export class MarivoPresentationFileService {
|
|
9
11
|
#resolve;
|
|
12
|
+
#resolveId;
|
|
10
13
|
#abort = new AbortController();
|
|
11
|
-
|
|
14
|
+
#tasks = new PendingTasks();
|
|
15
|
+
#closing;
|
|
16
|
+
constructor(resolve, resolveId) {
|
|
12
17
|
this.#resolve = resolve;
|
|
18
|
+
this.#resolveId = resolveId;
|
|
13
19
|
}
|
|
14
|
-
async #workspace(
|
|
20
|
+
async #workspace(input) {
|
|
15
21
|
try {
|
|
16
|
-
|
|
22
|
+
if ('workspaceId' in input) {
|
|
23
|
+
const workspace = await this.#resolveId?.(input.workspaceId);
|
|
24
|
+
if (workspace && workspace.id !== input.workspaceId)
|
|
25
|
+
throw new Error('workspace-unavailable');
|
|
26
|
+
return workspace;
|
|
27
|
+
}
|
|
28
|
+
return await this.#resolve(input.sessionId);
|
|
17
29
|
}
|
|
18
30
|
catch {
|
|
19
31
|
throw new Error('workspace-unavailable');
|
|
20
32
|
}
|
|
21
33
|
}
|
|
34
|
+
#scope(input) {
|
|
35
|
+
const scope = 'workspaceId' in input ? 'workspaceId' : 'sessionId';
|
|
36
|
+
if (typeof input[scope] !== 'string' ||
|
|
37
|
+
!input[scope].trim() ||
|
|
38
|
+
input[scope].length > 512 ||
|
|
39
|
+
('workspaceId' in input && 'sessionId' in input))
|
|
40
|
+
throw new Error('invalid-request');
|
|
41
|
+
return scope;
|
|
42
|
+
}
|
|
43
|
+
catalog(endpoint, payload, caller = new AbortController().signal) {
|
|
44
|
+
return this.#tasks.track(this.#catalog(endpoint, payload, caller));
|
|
45
|
+
}
|
|
46
|
+
async #catalog(endpoint, payload, caller = new AbortController().signal) {
|
|
47
|
+
const signal = AbortSignal.any([caller, this.#abort.signal]);
|
|
48
|
+
signal.throwIfAborted();
|
|
49
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
50
|
+
throw new Error('invalid-request');
|
|
51
|
+
const input = payload;
|
|
52
|
+
const scope = this.#scope(input);
|
|
53
|
+
const expected = endpoint === 'reports/list' ? scope : ['reportId', scope].sort().join(',');
|
|
54
|
+
if (Object.keys(input).sort().join(',') !== expected)
|
|
55
|
+
throw new Error('invalid-request');
|
|
56
|
+
const workspace = await this.#workspace(input);
|
|
57
|
+
if (!workspace)
|
|
58
|
+
throw new Error('workspace-unavailable');
|
|
59
|
+
const root = path.resolve(workspace.path);
|
|
60
|
+
const result = endpoint === 'reports/list'
|
|
61
|
+
? await listReports(root, workspace.id, signal)
|
|
62
|
+
: await readReportHistory(root, workspace.id, parsePresentationBuildId(input.reportId), signal);
|
|
63
|
+
const current = await this.#workspace(input);
|
|
64
|
+
if (!current || current.id !== workspace.id || path.resolve(current.path) !== root)
|
|
65
|
+
throw new Error('workspace-changed');
|
|
66
|
+
signal.throwIfAborted();
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
22
69
|
close() {
|
|
23
70
|
this.#abort.abort();
|
|
71
|
+
this.#closing ??= finishCleanup([() => this.#tasks.drain()]);
|
|
72
|
+
return this.#closing;
|
|
73
|
+
}
|
|
74
|
+
report(endpoint, payload, caller = new AbortController().signal) {
|
|
75
|
+
return this.#tasks.track(this.#report(endpoint, payload, caller));
|
|
24
76
|
}
|
|
25
|
-
async report(endpoint, payload, caller = new AbortController().signal) {
|
|
77
|
+
async #report(endpoint, payload, caller = new AbortController().signal) {
|
|
26
78
|
const signal = AbortSignal.any([caller, this.#abort.signal]);
|
|
79
|
+
signal.throwIfAborted();
|
|
27
80
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
28
81
|
throw new Error('invalid-request');
|
|
29
82
|
const input = payload;
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
83
|
+
const scope = this.#scope(input);
|
|
84
|
+
const expected = (endpoint === 'reports/save'
|
|
85
|
+
? ['edits', 'expectedBuildId', 'reportId', scope]
|
|
86
|
+
: ['reportId', scope])
|
|
87
|
+
.sort()
|
|
88
|
+
.join(',');
|
|
89
|
+
if (Object.keys(input).sort().join(',') !== expected)
|
|
37
90
|
throw new Error('invalid-request');
|
|
38
91
|
const reportId = parsePresentationBuildId(input.reportId, '/reportId');
|
|
39
|
-
const workspace = await this.#workspace(input
|
|
92
|
+
const workspace = await this.#workspace(input);
|
|
40
93
|
if (!workspace)
|
|
41
94
|
throw new Error('workspace-unavailable');
|
|
42
95
|
const root = path.resolve(workspace.path);
|
|
43
96
|
const check = async () => {
|
|
44
97
|
signal.throwIfAborted();
|
|
45
|
-
const current = await this.#workspace(input
|
|
98
|
+
const current = await this.#workspace(input);
|
|
46
99
|
if (!current || current.id !== workspace.id || path.resolve(current.path) !== root)
|
|
47
100
|
throw new Error('workspace-changed');
|
|
48
101
|
};
|
|
@@ -63,46 +116,29 @@ export class MarivoPresentationFileService {
|
|
|
63
116
|
throw new Error('invalid-report-edits');
|
|
64
117
|
}
|
|
65
118
|
await check();
|
|
66
|
-
return publishPresentation(root, { ...edited, buildId: randomUUID(), generatedAt: new Date().toISOString() }, expectedBuildId, check, signal);
|
|
119
|
+
return publishPresentation(root, { ...edited, buildId: randomUUID(), generatedAt: new Date().toISOString() }, expectedBuildId, check, signal, { kind: 'reader', sessionId: typeof input.sessionId === 'string' ? input.sessionId : null });
|
|
120
|
+
}
|
|
121
|
+
read(payload, caller = new AbortController().signal) {
|
|
122
|
+
return this.#tasks.track(this.#read(payload, caller));
|
|
67
123
|
}
|
|
68
|
-
async read(payload, caller = new AbortController().signal) {
|
|
124
|
+
async #read(payload, caller = new AbortController().signal) {
|
|
69
125
|
const signal = AbortSignal.any([caller, this.#abort.signal]);
|
|
70
126
|
signal.throwIfAborted();
|
|
71
127
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
72
128
|
throw new Error('invalid-request');
|
|
73
129
|
const input = payload;
|
|
74
|
-
if (Object.keys(input).sort().join(',') !==
|
|
75
|
-
|
|
76
|
-
!input.sessionId.trim() ||
|
|
77
|
-
input.sessionId.length > 512 ||
|
|
130
|
+
if (Object.keys(input).sort().join(',') !==
|
|
131
|
+
['asset', 'receipt', this.#scope(input)].sort().join(',') ||
|
|
78
132
|
(input.asset !== 'presentation.json' && input.asset !== 'index.html'))
|
|
79
133
|
throw new Error('invalid-request');
|
|
80
134
|
const receipt = structuredClone(parsePresentationReceipt(input.receipt));
|
|
81
135
|
const asset = input.asset;
|
|
82
|
-
const workspace = await this.#workspace(input
|
|
136
|
+
const workspace = await this.#workspace(input);
|
|
83
137
|
if (!workspace || workspace.id !== receipt.workspaceId)
|
|
84
138
|
throw new Error('workspace-unavailable');
|
|
85
139
|
const root = path.resolve(workspace.path);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
throw new Error('asset-path-mismatch');
|
|
89
|
-
const documentBytes = await readPresentationAsset(root, receipt.reportId, receipt.buildId, 'presentation.json', signal);
|
|
90
|
-
if (documentBytes.length !== receipt.files.document.bytes ||
|
|
91
|
-
presentationSha256(documentBytes) !== receipt.files.document.sha256)
|
|
92
|
-
throw new Error('asset-digest-mismatch');
|
|
93
|
-
const document = parsePresentationDocument(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(documentBytes)));
|
|
94
|
-
if (document.workspaceId !== workspace.id ||
|
|
95
|
-
document.reportId !== receipt.reportId ||
|
|
96
|
-
document.buildId !== receipt.buildId ||
|
|
97
|
-
document.title !== receipt.title)
|
|
98
|
-
throw new Error('asset-owner-mismatch');
|
|
99
|
-
const bytes = asset === 'presentation.json'
|
|
100
|
-
? documentBytes
|
|
101
|
-
: await readPresentationAsset(root, receipt.reportId, receipt.buildId, asset, signal);
|
|
102
|
-
const file = asset === 'presentation.json' ? receipt.files.document : receipt.files.html;
|
|
103
|
-
if (bytes.length !== file.bytes || presentationSha256(bytes) !== file.sha256)
|
|
104
|
-
throw new Error('asset-digest-mismatch');
|
|
105
|
-
const current = await this.#workspace(input.sessionId);
|
|
140
|
+
const { bytes, sha256 } = await readReceiptAsset(root, workspace.id, receipt, asset, signal);
|
|
141
|
+
const current = await this.#workspace(input);
|
|
106
142
|
if (!current || current.id !== workspace.id || path.resolve(current.path) !== root)
|
|
107
143
|
throw new Error('workspace-changed');
|
|
108
144
|
signal.throwIfAborted();
|
|
@@ -112,17 +148,19 @@ export class MarivoPresentationFileService {
|
|
|
112
148
|
buildId: receipt.buildId,
|
|
113
149
|
asset,
|
|
114
150
|
mimeType: asset === 'presentation.json' ? 'application/json' : 'text/html',
|
|
115
|
-
sha256
|
|
151
|
+
sha256,
|
|
116
152
|
bytes: bytes.length,
|
|
117
153
|
bodyBase64: bytes.toString('base64'),
|
|
118
154
|
};
|
|
119
155
|
}
|
|
120
156
|
}
|
|
121
157
|
export function registerMarivoPresentationRpc(connection, service) {
|
|
122
|
-
const unregister = connection
|
|
158
|
+
const unregister = registerPluginRpc(connection, MARIVO_PRESENTATION_RPC_CHANNEL, ['reports/resolve', 'reports/save', 'reports/list', 'reports/history', 'files/read'], async (endpoint, payload, signal) => {
|
|
123
159
|
try {
|
|
124
160
|
if (endpoint === 'reports/resolve' || endpoint === 'reports/save')
|
|
125
161
|
return { ok: true, value: await service.report(endpoint, payload, signal) };
|
|
162
|
+
if (endpoint === 'reports/list' || endpoint === 'reports/history')
|
|
163
|
+
return { ok: true, value: await service.catalog(endpoint, payload, signal) };
|
|
126
164
|
if (endpoint === 'files/read')
|
|
127
165
|
return { ok: true, value: await service.read(payload, signal) };
|
|
128
166
|
throw new Error('unknown-endpoint');
|
|
@@ -130,19 +168,19 @@ export function registerMarivoPresentationRpc(connection, service) {
|
|
|
130
168
|
catch (error) {
|
|
131
169
|
const message = signal.aborted
|
|
132
170
|
? 'cancelled'
|
|
133
|
-
: error
|
|
134
|
-
?
|
|
135
|
-
: error
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
?
|
|
140
|
-
:
|
|
171
|
+
: error instanceof PresentationContractError
|
|
172
|
+
? error.code
|
|
173
|
+
: error?.code === 'ENOENT'
|
|
174
|
+
? 'asset-missing'
|
|
175
|
+
: error instanceof Error &&
|
|
176
|
+
/^(invalid-request|workspace-unavailable|workspace-changed|asset-path-mismatch|asset-not-file|asset-too-large|asset-changed|asset-digest-mismatch|asset-owner-mismatch|unknown-endpoint|invalid-report-current|invalid-report-history|report-history-full|report-catalog-too-large|invalid-report-edits|report-save-conflict|presentation-directory-changed)$/.test(error.message)
|
|
177
|
+
? error.message
|
|
178
|
+
: error instanceof Error && /lock.*timed out|timed out.*lock/i.test(error.message)
|
|
179
|
+
? 'report-save-busy'
|
|
180
|
+
: 'presentation-operation-failed';
|
|
141
181
|
return { ok: false, error: { code: 'internal', message, details: {} } };
|
|
142
182
|
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
await unregister();
|
|
147
|
-
};
|
|
183
|
+
});
|
|
184
|
+
let closing;
|
|
185
|
+
return () => (closing ??= finishCleanup([unregister, () => service.close()]));
|
|
148
186
|
}
|
package/lib/presentation/tool.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
3
|
+
import { registerMarivoTool } from "../tool-lifecycle.js";
|
|
3
4
|
import { parsePresentationBuildId } from "./contracts/index.js";
|
|
4
5
|
import { MARIVO_PRESENT_TOOL_NAME, MARIVO_PRESENTATION_DELIVERY_KIND, parsePresentationDelivery, presentationReceiptText, } from "./receipt.js";
|
|
5
6
|
import { publishPresentation, resolvePresentation } from "./reports.js";
|
|
6
|
-
/** One call projects a draft, builds
|
|
7
|
+
/** One call projects a draft, builds the document, commits them, then emits the receipt. */
|
|
7
8
|
export function createMarivoPresentTool(source, session, lifecycleSignal) {
|
|
8
9
|
return defineTool({
|
|
9
10
|
name: MARIVO_PRESENT_TOOL_NAME,
|
|
10
|
-
description: 'Present a complete Workspace-relative analysis draft as one saved document
|
|
11
|
+
description: 'Present a complete Workspace-relative analysis draft as one saved document, automatically opened in the right-side report tab. HTML is generated only when explicitly exporting or publishing the report. Omit report_id and expected_build_id to create a Report; provide both to replace an existing Report with a new immutable Build based on the version you have read. A report-save-conflict requires reading the current report and reconciling the draft; never retry by only changing expected_build_id. Returns the exact published receipt, file locations, for the report reader. Only saved Artifact and computed snapshots are read; sources are declarations, not verification of calculations.',
|
|
11
12
|
parameters: {
|
|
12
13
|
draft_path: {
|
|
13
14
|
type: 'string',
|
|
@@ -58,7 +59,7 @@ export function createMarivoPresentTool(source, session, lifecycleSignal) {
|
|
|
58
59
|
if (!owner || owner.session !== session)
|
|
59
60
|
throw new Error('presentation-session-mismatch');
|
|
60
61
|
const rootCallId = String(exec.rootCallId ?? exec.callId);
|
|
61
|
-
const call = [...session.
|
|
62
|
+
const call = [...session.snapshotEvents()]
|
|
62
63
|
.reverse()
|
|
63
64
|
.find((event) => event.type === 'tool/call' && String(event.data.callId) === rootCallId);
|
|
64
65
|
if (call?.type !== 'tool/call' || !Number.isSafeInteger(call.data.turn) || call.data.turn < 0)
|
|
@@ -99,7 +100,7 @@ export function createMarivoPresentTool(source, session, lifecycleSignal) {
|
|
|
99
100
|
signal,
|
|
100
101
|
});
|
|
101
102
|
await check();
|
|
102
|
-
const receipt = await publishPresentation(identity.projectRoot, document, expectedBuildId, check, signal);
|
|
103
|
+
const receipt = await publishPresentation(identity.projectRoot, document, expectedBuildId, check, signal, { kind: 'agent', sessionId: String(session.id) });
|
|
103
104
|
return {
|
|
104
105
|
deliveryJson: JSON.stringify(parsePresentationDelivery({
|
|
105
106
|
kind: MARIVO_PRESENTATION_DELIVERY_KIND,
|
|
@@ -113,10 +114,5 @@ export function createMarivoPresentTool(source, session, lifecycleSignal) {
|
|
|
113
114
|
});
|
|
114
115
|
}
|
|
115
116
|
export function registerMarivoPresentTool(ctx, source, session) {
|
|
116
|
-
|
|
117
|
-
const unregister = ctx.tools.register(createMarivoPresentTool(source, session, controller.signal));
|
|
118
|
-
return () => {
|
|
119
|
-
controller.abort();
|
|
120
|
-
unregister();
|
|
121
|
-
};
|
|
117
|
+
return registerMarivoTool(ctx, createMarivoPresentTool(source, session));
|
|
122
118
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { PRESENTATION_BUDGETS, parsePresentationBuildId } from "../presentation/contracts/index.js";
|
|
4
|
+
import { registerPluginRpc } from "../rpc.js";
|
|
5
|
+
import { registerMarivoTool } from "../tool-lifecycle.js";
|
|
6
|
+
export const PUBLISHING_CHANNEL = '/dsh-report-publishing';
|
|
7
|
+
const identity = z.string().transform((value) => parsePresentationBuildId(value));
|
|
8
|
+
const field = z.enum(['accessKeyId', 'secretAccessKey', 'sessionToken']);
|
|
9
|
+
export function registerPublishingCredentials(connection, service, workspaceExists) {
|
|
10
|
+
return registerPluginRpc(connection, PUBLISHING_CHANNEL, ['describe', 'set', 'unset', 'publish'], async (endpoint, payload, signal) => {
|
|
11
|
+
try {
|
|
12
|
+
const base = { workspaceId: z.string().min(1).max(512) };
|
|
13
|
+
const { workspaceId } = z.object(base).parse(payload);
|
|
14
|
+
if (!workspaceExists(workspaceId))
|
|
15
|
+
throw new Error('workspace-unavailable');
|
|
16
|
+
signal.throwIfAborted();
|
|
17
|
+
const mutation = { ...base, configId: z.string().uuid() };
|
|
18
|
+
if (endpoint !== 'describe')
|
|
19
|
+
service.assertConfig(z.object(mutation).parse(payload).configId);
|
|
20
|
+
if (endpoint === 'publish') {
|
|
21
|
+
const input = z
|
|
22
|
+
.object({
|
|
23
|
+
...mutation,
|
|
24
|
+
reportId: identity,
|
|
25
|
+
buildId: identity,
|
|
26
|
+
viewHtml: z.string().max(PRESENTATION_BUDGETS.htmlBytes).optional(),
|
|
27
|
+
})
|
|
28
|
+
.strict()
|
|
29
|
+
.parse(payload);
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
value: await service.publish({ workspaceId }, input.reportId, input.buildId, AbortSignal.any([signal, AbortSignal.timeout(110_000)]), input.viewHtml),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (endpoint === 'set') {
|
|
36
|
+
const input = z
|
|
37
|
+
.object({ ...mutation, field, value: z.string().min(1).max(65536) })
|
|
38
|
+
.strict()
|
|
39
|
+
.parse(payload);
|
|
40
|
+
await service.change(input.field, input.value, signal);
|
|
41
|
+
}
|
|
42
|
+
else if (endpoint === 'unset') {
|
|
43
|
+
const input = z
|
|
44
|
+
.object({ ...mutation, field })
|
|
45
|
+
.strict()
|
|
46
|
+
.parse(payload);
|
|
47
|
+
await service.change(input.field, undefined, signal);
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
z.object(base).strict().parse(payload);
|
|
51
|
+
return { ok: true, value: await service.describe() };
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const message = publishingError(error);
|
|
55
|
+
return { ok: false, error: { code: 'internal', message, details: {} } };
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function registerReportPublishTool(ctx, service, sessionId) {
|
|
60
|
+
return registerMarivoTool(ctx, defineTool({
|
|
61
|
+
name: 'marivo_publish_report',
|
|
62
|
+
description: 'Publish a saved report Build as self-contained HTML to the configured object storage only when the user requests publication. Returns the immutable URL. Does not run analysis or include unsaved edits or temporary filters. Credentials are configured under 数据源与凭证 → 报告发布凭证. An unconfirmed upload may already exist; retry the same Build safely. Never send credentials or storage settings as tool arguments.',
|
|
63
|
+
parameters: {
|
|
64
|
+
report_id: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
required: true,
|
|
67
|
+
description: 'Saved Report ID from the presentation receipt.',
|
|
68
|
+
},
|
|
69
|
+
build_id: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
required: true,
|
|
72
|
+
description: 'Exact saved Build ID to publish.',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
output: {
|
|
76
|
+
schema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: { publicationJson: { type: 'string', required: true } },
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
},
|
|
81
|
+
render: (_args, value) => [{ type: 'text', text: value.publicationJson }],
|
|
82
|
+
},
|
|
83
|
+
timeoutMs: 120_000,
|
|
84
|
+
async execute(args, exec) {
|
|
85
|
+
if (!exec.agent || exec.agent.session.id !== sessionId)
|
|
86
|
+
throw new Error('presentation-session-mismatch');
|
|
87
|
+
const input = z.object({ report_id: identity, build_id: identity }).strict().parse(args);
|
|
88
|
+
try {
|
|
89
|
+
return {
|
|
90
|
+
publicationJson: JSON.stringify(await service.publish({ sessionId }, input.report_id, input.build_id, AbortSignal.any([exec.signal, AbortSignal.timeout(110_000)]))),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw new Error(publishingError(error));
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
function publishingError(error) {
|
|
100
|
+
return error instanceof Error &&
|
|
101
|
+
[
|
|
102
|
+
'report-publishing-config-changed',
|
|
103
|
+
'report-publishing-disabled',
|
|
104
|
+
'report-publishing-build-unavailable',
|
|
105
|
+
'report-publishing-credentials-missing',
|
|
106
|
+
'report-publishing-upload-unconfirmed',
|
|
107
|
+
'report-publishing-view-invalid',
|
|
108
|
+
].includes(error.message)
|
|
109
|
+
? error.message
|
|
110
|
+
: 'report-publishing-operation-failed';
|
|
111
|
+
}
|