pmx-canvas 0.1.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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/Readme.md +865 -0
- package/dist/canvas/global.css +3173 -0
- package/dist/canvas/index.js +183 -0
- package/dist/json-render/index.css +2 -0
- package/dist/json-render/index.js +389 -0
- package/dist/types/cli/agent.d.ts +13 -0
- package/dist/types/cli/index.d.ts +2 -0
- package/dist/types/cli/watch.d.ts +5 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
- package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
- package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
- package/dist/types/client/canvas/DockedNode.d.ts +4 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
- package/dist/types/client/canvas/Minimap.d.ts +23 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
- package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
- package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
- package/dist/types/client/canvas/snap-guides.d.ts +23 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
- package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
- package/dist/types/client/ext-app/bridge.d.ts +161 -0
- package/dist/types/client/icons.d.ts +70 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/nodes/ContextNode.d.ts +34 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
- package/dist/types/client/nodes/FileNode.d.ts +5 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -0
- package/dist/types/client/nodes/ImageNode.d.ts +10 -0
- package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
- package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
- package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
- package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
- package/dist/types/client/nodes/PromptNode.d.ts +5 -0
- package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
- package/dist/types/client/nodes/StatusNode.d.ts +4 -0
- package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
- package/dist/types/client/nodes/TraceNode.d.ts +4 -0
- package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
- package/dist/types/client/nodes/image-warnings.d.ts +6 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
- package/dist/types/client/nodes/md-format.d.ts +25 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/attention-store.d.ts +25 -0
- package/dist/types/client/state/canvas-store.d.ts +74 -0
- package/dist/types/client/state/intent-bridge.d.ts +158 -0
- package/dist/types/client/state/sse-bridge.d.ts +5 -0
- package/dist/types/client/theme/tokens.d.ts +27 -0
- package/dist/types/client/types.d.ts +40 -0
- package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
- package/dist/types/client/utils/placement.d.ts +1 -0
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/json-render/catalog.d.ts +815 -0
- package/dist/types/json-render/charts/components.d.ts +54 -0
- package/dist/types/json-render/charts/definitions.d.ts +103 -0
- package/dist/types/json-render/charts/extra-components.d.ts +58 -0
- package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
- package/dist/types/json-render/renderer/index.d.ts +16 -0
- package/dist/types/json-render/schema.d.ts +46 -0
- package/dist/types/json-render/server.d.ts +55 -0
- package/dist/types/mcp/server.d.ts +22 -0
- package/dist/types/server/agent-context.d.ts +21 -0
- package/dist/types/server/artifact-paths.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +154 -0
- package/dist/types/server/canvas-provenance.d.ts +13 -0
- package/dist/types/server/canvas-schema.d.ts +49 -0
- package/dist/types/server/canvas-serialization.d.ts +25 -0
- package/dist/types/server/canvas-state.d.ts +174 -0
- package/dist/types/server/canvas-validation.d.ts +33 -0
- package/dist/types/server/chart-template.d.ts +29 -0
- package/dist/types/server/code-graph.d.ts +67 -0
- package/dist/types/server/context-cards.d.ts +24 -0
- package/dist/types/server/diagram-presets.d.ts +28 -0
- package/dist/types/server/ext-app-call-registry.d.ts +16 -0
- package/dist/types/server/ext-app-tool-result.d.ts +1 -0
- package/dist/types/server/file-watcher.d.ts +16 -0
- package/dist/types/server/index.d.ts +243 -0
- package/dist/types/server/mcp-app-candidate.d.ts +25 -0
- package/dist/types/server/mcp-app-host.d.ts +65 -0
- package/dist/types/server/mcp-app-runtime.d.ts +47 -0
- package/dist/types/server/mutation-history.d.ts +105 -0
- package/dist/types/server/placement.d.ts +37 -0
- package/dist/types/server/server.d.ts +103 -0
- package/dist/types/server/spatial-analysis.d.ts +87 -0
- package/dist/types/server/trace-manager.d.ts +48 -0
- package/dist/types/server/web-artifacts.d.ts +50 -0
- package/dist/types/server/webpage-node.d.ts +25 -0
- package/dist/types/shared/auto-arrange.d.ts +29 -0
- package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
- package/dist/types/shared/placement.d.ts +26 -0
- package/dist/types/shared/semantic-attention.d.ts +97 -0
- package/package.json +109 -0
- package/skills/data-analysis/SKILL.md +324 -0
- package/skills/doc-coauthoring/SKILL.md +375 -0
- package/skills/frontend-design/SKILL.md +45 -0
- package/skills/json-render-codegen/SKILL.md +112 -0
- package/skills/json-render-core/SKILL.md +265 -0
- package/skills/json-render-ink/SKILL.md +273 -0
- package/skills/json-render-mcp/SKILL.md +132 -0
- package/skills/json-render-react/SKILL.md +264 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/playwright-cli/SKILL.md +67 -0
- package/skills/pmx-canvas/SKILL.md +668 -0
- package/skills/pmx-canvas/evals/evals.json +186 -0
- package/skills/pmx-canvas-testing/SKILL.md +78 -0
- package/skills/published-consumer-e2e/SKILL.md +43 -0
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
- package/skills/web-artifacts-builder/SKILL.md +80 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/web-design-guidelines/SKILL.md +39 -0
- package/src/cli/agent.ts +2144 -0
- package/src/cli/index.ts +622 -0
- package/src/cli/watch.ts +88 -0
- package/src/client/App.tsx +507 -0
- package/src/client/canvas/AttentionHistory.tsx +81 -0
- package/src/client/canvas/AttentionToast.tsx +19 -0
- package/src/client/canvas/CanvasNode.tsx +363 -0
- package/src/client/canvas/CanvasViewport.tsx +590 -0
- package/src/client/canvas/CommandPalette.tsx +302 -0
- package/src/client/canvas/ContextMenu.tsx +601 -0
- package/src/client/canvas/ContextPinBar.tsx +25 -0
- package/src/client/canvas/ContextPinHud.tsx +22 -0
- package/src/client/canvas/DockedNode.tsx +66 -0
- package/src/client/canvas/EdgeLayer.tsx +280 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
- package/src/client/canvas/FocusFieldLayer.tsx +107 -0
- package/src/client/canvas/Minimap.tsx +301 -0
- package/src/client/canvas/SelectionBar.tsx +69 -0
- package/src/client/canvas/ShortcutOverlay.tsx +69 -0
- package/src/client/canvas/SnapshotPanel.tsx +236 -0
- package/src/client/canvas/snap-guides.ts +170 -0
- package/src/client/canvas/use-node-drag.ts +51 -0
- package/src/client/canvas/use-node-resize.ts +59 -0
- package/src/client/canvas/use-pan-zoom.ts +191 -0
- package/src/client/ext-app/bridge.ts +542 -0
- package/src/client/icons.tsx +424 -0
- package/src/client/index.tsx +7 -0
- package/src/client/nodes/ContextNode.tsx +412 -0
- package/src/client/nodes/ExtAppFrame.tsx +509 -0
- package/src/client/nodes/FileNode.tsx +256 -0
- package/src/client/nodes/GroupNode.tsx +39 -0
- package/src/client/nodes/ImageNode.tsx +160 -0
- package/src/client/nodes/InlineFormatBar.tsx +169 -0
- package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
- package/src/client/nodes/LedgerNode.tsx +37 -0
- package/src/client/nodes/MarkdownNode.tsx +359 -0
- package/src/client/nodes/McpAppNode.tsx +85 -0
- package/src/client/nodes/MdFormatBar.tsx +109 -0
- package/src/client/nodes/PromptNode.tsx +597 -0
- package/src/client/nodes/ResponseNode.tsx +153 -0
- package/src/client/nodes/StatusNode.tsx +84 -0
- package/src/client/nodes/StatusSummary.tsx +38 -0
- package/src/client/nodes/TraceNode.tsx +120 -0
- package/src/client/nodes/WebpageNode.tsx +288 -0
- package/src/client/nodes/image-warnings.ts +95 -0
- package/src/client/nodes/inline-editor-commands.ts +37 -0
- package/src/client/nodes/md-format.ts +206 -0
- package/src/client/state/attention-bridge.ts +328 -0
- package/src/client/state/attention-store.ts +73 -0
- package/src/client/state/canvas-store.ts +631 -0
- package/src/client/state/intent-bridge.ts +315 -0
- package/src/client/state/sse-bridge.ts +965 -0
- package/src/client/theme/global.css +3173 -0
- package/src/client/theme/tokens.ts +72 -0
- package/src/client/types-shims.d.ts +5 -0
- package/src/client/types.ts +81 -0
- package/src/client/utils/ext-app-tool-result.ts +4 -0
- package/src/client/utils/placement.ts +4 -0
- package/src/client/utils/platform.ts +2 -0
- package/src/json-render/catalog.ts +256 -0
- package/src/json-render/charts/components.tsx +198 -0
- package/src/json-render/charts/definitions.ts +81 -0
- package/src/json-render/charts/extra-components.tsx +267 -0
- package/src/json-render/charts/extra-definitions.ts +145 -0
- package/src/json-render/renderer/index.css +174 -0
- package/src/json-render/renderer/index.tsx +86 -0
- package/src/json-render/schema.ts +62 -0
- package/src/json-render/server.ts +597 -0
- package/src/mcp/server.ts +1377 -0
- package/src/server/agent-context.ts +242 -0
- package/src/server/artifact-paths.ts +17 -0
- package/src/server/canvas-operations.ts +1279 -0
- package/src/server/canvas-provenance.ts +243 -0
- package/src/server/canvas-schema.ts +432 -0
- package/src/server/canvas-serialization.ts +95 -0
- package/src/server/canvas-state.ts +1134 -0
- package/src/server/canvas-validation.ts +114 -0
- package/src/server/chart-template.ts +449 -0
- package/src/server/code-graph.ts +370 -0
- package/src/server/context-cards.ts +31 -0
- package/src/server/diagram-presets.ts +71 -0
- package/src/server/ext-app-call-registry.ts +77 -0
- package/src/server/ext-app-tool-result.ts +4 -0
- package/src/server/file-watcher.ts +121 -0
- package/src/server/index.ts +647 -0
- package/src/server/mcp-app-candidate.ts +174 -0
- package/src/server/mcp-app-host.ts +814 -0
- package/src/server/mcp-app-runtime.ts +459 -0
- package/src/server/mutation-history.ts +350 -0
- package/src/server/placement.ts +125 -0
- package/src/server/server.ts +3846 -0
- package/src/server/spatial-analysis.ts +356 -0
- package/src/server/trace-manager.ts +333 -0
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
- package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
- package/src/server/web-artifacts.ts +442 -0
- package/src/server/webpage-node.ts +328 -0
- package/src/shared/auto-arrange.ts +439 -0
- package/src/shared/ext-app-tool-result.ts +76 -0
- package/src/shared/placement.ts +81 -0
- package/src/shared/semantic-attention.ts +598 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
const FETCH_TIMEOUT_MS = 10000;
|
|
2
|
+
const MAX_HTML_LENGTH = 1_000_000;
|
|
3
|
+
const MAX_TEXT_LENGTH = 50_000;
|
|
4
|
+
const EXCERPT_LENGTH = 420;
|
|
5
|
+
|
|
6
|
+
export const WEBPAGE_NODE_DEFAULT_SIZE = {
|
|
7
|
+
width: 520,
|
|
8
|
+
height: 420,
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export interface WebpageSnapshot {
|
|
12
|
+
url: string;
|
|
13
|
+
pageTitle: string | null;
|
|
14
|
+
description: string | null;
|
|
15
|
+
imageUrl: string | null;
|
|
16
|
+
content: string;
|
|
17
|
+
excerpt: string;
|
|
18
|
+
fetchedAt: string;
|
|
19
|
+
statusCode: number;
|
|
20
|
+
contentType: string | null;
|
|
21
|
+
frameBlocked: boolean;
|
|
22
|
+
frameBlockedReason: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class WebpageFetchError extends Error {
|
|
26
|
+
readonly statusCode: number | null;
|
|
27
|
+
readonly contentType: string | null;
|
|
28
|
+
|
|
29
|
+
constructor(message: string, options?: { statusCode?: number; contentType?: string | null }) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'WebpageFetchError';
|
|
32
|
+
this.statusCode = options?.statusCode ?? null;
|
|
33
|
+
this.contentType = options?.contentType ?? null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function decodeHtmlEntities(text: string): string {
|
|
38
|
+
const named: Record<string, string> = {
|
|
39
|
+
amp: '&',
|
|
40
|
+
lt: '<',
|
|
41
|
+
gt: '>',
|
|
42
|
+
quot: '"',
|
|
43
|
+
apos: "'",
|
|
44
|
+
nbsp: ' ',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (entity, token: string) => {
|
|
48
|
+
const lower = token.toLowerCase();
|
|
49
|
+
if (lower in named) return named[lower] ?? entity;
|
|
50
|
+
if (lower.startsWith('#x')) {
|
|
51
|
+
const codePoint = Number.parseInt(lower.slice(2), 16);
|
|
52
|
+
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : entity;
|
|
53
|
+
}
|
|
54
|
+
if (lower.startsWith('#')) {
|
|
55
|
+
const codePoint = Number.parseInt(lower.slice(1), 10);
|
|
56
|
+
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : entity;
|
|
57
|
+
}
|
|
58
|
+
return entity;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeText(text: string): string {
|
|
63
|
+
return decodeHtmlEntities(text)
|
|
64
|
+
.replace(/\r\n?/g, '\n')
|
|
65
|
+
.replace(/[\t\f\v ]+/g, ' ')
|
|
66
|
+
.replace(/\n\s+/g, '\n')
|
|
67
|
+
.replace(/\s+\n/g, '\n')
|
|
68
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
69
|
+
.trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseTagAttributes(tag: string): Record<string, string> {
|
|
73
|
+
const attributes: Record<string, string> = {};
|
|
74
|
+
const pattern = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
|
|
75
|
+
let match: RegExpExecArray | null = null;
|
|
76
|
+
while ((match = pattern.exec(tag)) !== null) {
|
|
77
|
+
const [, name, doubleQuoted, singleQuoted, unquoted] = match;
|
|
78
|
+
attributes[name.toLowerCase()] = doubleQuoted ?? singleQuoted ?? unquoted ?? '';
|
|
79
|
+
}
|
|
80
|
+
return attributes;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractMetaContent(html: string, key: string): string | null {
|
|
84
|
+
const metaTags = html.match(/<meta\b[^>]*>/gi) ?? [];
|
|
85
|
+
const target = key.toLowerCase();
|
|
86
|
+
for (const tag of metaTags) {
|
|
87
|
+
const attributes = parseTagAttributes(tag);
|
|
88
|
+
const property = attributes.property?.toLowerCase();
|
|
89
|
+
const name = attributes.name?.toLowerCase();
|
|
90
|
+
if (property === target || name === target) {
|
|
91
|
+
const content = attributes.content?.trim();
|
|
92
|
+
if (content) return normalizeText(content);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractTitle(html: string): string | null {
|
|
99
|
+
const title = html.match(/<title\b[^>]*>([\s\S]*?)<\/title>/i)?.[1] ?? null;
|
|
100
|
+
if (!title) return null;
|
|
101
|
+
const normalized = normalizeText(title);
|
|
102
|
+
return normalized.length > 0 ? normalized : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveMaybeRelativeUrl(baseUrl: string, rawUrl: string | null): string | null {
|
|
106
|
+
if (!rawUrl) return null;
|
|
107
|
+
try {
|
|
108
|
+
return new URL(rawUrl, baseUrl).toString();
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractReadableText(html: string): string {
|
|
115
|
+
const body = html.match(/<body\b[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? html;
|
|
116
|
+
const primary =
|
|
117
|
+
body.match(/<main\b[^>]*>([\s\S]*?)<\/main>/i)?.[1] ??
|
|
118
|
+
body.match(/<article\b[^>]*>([\s\S]*?)<\/article>/i)?.[1] ??
|
|
119
|
+
body;
|
|
120
|
+
|
|
121
|
+
const text = primary
|
|
122
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
123
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
124
|
+
.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, ' ')
|
|
125
|
+
.replace(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi, ' ')
|
|
126
|
+
.replace(/<template\b[^>]*>[\s\S]*?<\/template>/gi, ' ')
|
|
127
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
128
|
+
.replace(/<\/(?:p|div|section|article|header|footer|aside|main|nav|li|ul|ol|h[1-6]|tr|td|blockquote|pre)>/gi, '\n')
|
|
129
|
+
.replace(/<[^>]+>/g, ' ');
|
|
130
|
+
|
|
131
|
+
return normalizeText(text).slice(0, MAX_TEXT_LENGTH);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractFrameAncestorsDirective(contentSecurityPolicy: string | null): string | null {
|
|
135
|
+
if (!contentSecurityPolicy) return null;
|
|
136
|
+
const directives = contentSecurityPolicy
|
|
137
|
+
.split(';')
|
|
138
|
+
.map((directive) => directive.trim())
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
for (const directive of directives) {
|
|
141
|
+
if (!directive.toLowerCase().startsWith('frame-ancestors')) continue;
|
|
142
|
+
return directive;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function inferFrameBlocking(headers: Headers): {
|
|
148
|
+
frameBlocked: boolean;
|
|
149
|
+
frameBlockedReason: string | null;
|
|
150
|
+
} {
|
|
151
|
+
const xFrameOptions = headers.get('x-frame-options')?.trim() ?? null;
|
|
152
|
+
if (xFrameOptions) {
|
|
153
|
+
const normalized = xFrameOptions.toLowerCase();
|
|
154
|
+
if (normalized === 'deny') {
|
|
155
|
+
return {
|
|
156
|
+
frameBlocked: true,
|
|
157
|
+
frameBlockedReason: 'Live preview blocked by X-Frame-Options: DENY.',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (normalized === 'sameorigin') {
|
|
161
|
+
return {
|
|
162
|
+
frameBlocked: true,
|
|
163
|
+
frameBlockedReason: 'Live preview blocked by X-Frame-Options: SAMEORIGIN.',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const frameAncestors = extractFrameAncestorsDirective(headers.get('content-security-policy'));
|
|
169
|
+
if (frameAncestors) {
|
|
170
|
+
const sources = frameAncestors
|
|
171
|
+
.split(/\s+/)
|
|
172
|
+
.slice(1)
|
|
173
|
+
.map((source) => source.trim().toLowerCase())
|
|
174
|
+
.filter(Boolean);
|
|
175
|
+
|
|
176
|
+
if (sources.includes("'none'")) {
|
|
177
|
+
return {
|
|
178
|
+
frameBlocked: true,
|
|
179
|
+
frameBlockedReason: "Live preview blocked by Content-Security-Policy: frame-ancestors 'none'.",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (
|
|
184
|
+
sources.length > 0 &&
|
|
185
|
+
!sources.includes('*') &&
|
|
186
|
+
!sources.includes('http:') &&
|
|
187
|
+
!sources.includes('https:') &&
|
|
188
|
+
!sources.some((source) => source.includes('localhost') || source.includes('127.0.0.1'))
|
|
189
|
+
) {
|
|
190
|
+
return {
|
|
191
|
+
frameBlocked: true,
|
|
192
|
+
frameBlockedReason: `Live preview likely blocked by Content-Security-Policy: ${frameAncestors}.`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
frameBlocked: false,
|
|
199
|
+
frameBlockedReason: null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function normalizeWebpageUrl(rawUrl: string): string {
|
|
204
|
+
const trimmed = rawUrl.trim();
|
|
205
|
+
if (!trimmed) {
|
|
206
|
+
throw new Error('Webpage nodes require a non-empty URL.');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
210
|
+
let url: URL;
|
|
211
|
+
try {
|
|
212
|
+
url = new URL(withScheme);
|
|
213
|
+
} catch {
|
|
214
|
+
throw new Error('Webpage nodes require a valid http(s) URL.');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
218
|
+
throw new Error('Webpage nodes only support http(s) URLs.');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return url.toString();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function summarizeWebpageContent(data: Record<string, unknown>, maxLength = 500): string {
|
|
225
|
+
const parts: string[] = [];
|
|
226
|
+
const url = typeof data.url === 'string' ? data.url : '';
|
|
227
|
+
const pageTitle = typeof data.pageTitle === 'string' ? data.pageTitle : '';
|
|
228
|
+
const description = typeof data.description === 'string' ? data.description : '';
|
|
229
|
+
const excerpt = typeof data.excerpt === 'string'
|
|
230
|
+
? data.excerpt
|
|
231
|
+
: typeof data.content === 'string'
|
|
232
|
+
? data.content
|
|
233
|
+
: '';
|
|
234
|
+
|
|
235
|
+
if (url) parts.push(`URL: ${url}`);
|
|
236
|
+
if (pageTitle) parts.push(`Title: ${pageTitle}`);
|
|
237
|
+
if (description) parts.push(`Description: ${description}`);
|
|
238
|
+
if (excerpt) parts.push(excerpt.slice(0, maxLength));
|
|
239
|
+
|
|
240
|
+
return parts.join('\n').trim();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function fetchWebpageSnapshot(inputUrl: string): Promise<WebpageSnapshot> {
|
|
244
|
+
const url = normalizeWebpageUrl(inputUrl);
|
|
245
|
+
const controller = new AbortController();
|
|
246
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const response = await fetch(url, {
|
|
250
|
+
redirect: 'follow',
|
|
251
|
+
signal: controller.signal,
|
|
252
|
+
headers: {
|
|
253
|
+
Accept: 'text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.1',
|
|
254
|
+
'User-Agent': 'pmx-canvas webpage node',
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const contentType = response.headers.get('content-type');
|
|
259
|
+
const responseUrl = normalizeWebpageUrl(response.url || url);
|
|
260
|
+
const body = (await response.text()).slice(0, MAX_HTML_LENGTH);
|
|
261
|
+
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
throw new WebpageFetchError(`Request failed with ${response.status} ${response.statusText}`.trim(), {
|
|
264
|
+
statusCode: response.status,
|
|
265
|
+
contentType,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const pageTitle =
|
|
270
|
+
extractMetaContent(body, 'og:title') ??
|
|
271
|
+
extractMetaContent(body, 'twitter:title') ??
|
|
272
|
+
extractTitle(body);
|
|
273
|
+
const description =
|
|
274
|
+
extractMetaContent(body, 'description') ??
|
|
275
|
+
extractMetaContent(body, 'og:description') ??
|
|
276
|
+
extractMetaContent(body, 'twitter:description');
|
|
277
|
+
const imageUrl = resolveMaybeRelativeUrl(
|
|
278
|
+
responseUrl,
|
|
279
|
+
extractMetaContent(body, 'og:image') ?? extractMetaContent(body, 'twitter:image'),
|
|
280
|
+
);
|
|
281
|
+
const content = extractReadableText(body);
|
|
282
|
+
const excerpt = content.slice(0, EXCERPT_LENGTH);
|
|
283
|
+
const { frameBlocked, frameBlockedReason } = inferFrameBlocking(response.headers);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
url: responseUrl,
|
|
287
|
+
pageTitle,
|
|
288
|
+
description,
|
|
289
|
+
imageUrl,
|
|
290
|
+
content,
|
|
291
|
+
excerpt,
|
|
292
|
+
fetchedAt: new Date().toISOString(),
|
|
293
|
+
statusCode: response.status,
|
|
294
|
+
contentType,
|
|
295
|
+
frameBlocked,
|
|
296
|
+
frameBlockedReason,
|
|
297
|
+
};
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error instanceof WebpageFetchError) throw error;
|
|
300
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
301
|
+
throw new WebpageFetchError(`Timed out after ${FETCH_TIMEOUT_MS}ms while fetching ${url}.`);
|
|
302
|
+
}
|
|
303
|
+
throw new WebpageFetchError(
|
|
304
|
+
error instanceof Error ? error.message : `Failed to fetch ${url}.`,
|
|
305
|
+
);
|
|
306
|
+
} finally {
|
|
307
|
+
clearTimeout(timeout);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function getWebpageFetchErrorDetails(error: unknown): {
|
|
312
|
+
message: string;
|
|
313
|
+
statusCode: number | null;
|
|
314
|
+
contentType: string | null;
|
|
315
|
+
} {
|
|
316
|
+
if (error instanceof WebpageFetchError) {
|
|
317
|
+
return {
|
|
318
|
+
message: error.message,
|
|
319
|
+
statusCode: error.statusCode,
|
|
320
|
+
contentType: error.contentType,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
message: error instanceof Error ? error.message : 'Unknown webpage fetch failure.',
|
|
325
|
+
statusCode: null,
|
|
326
|
+
contentType: null,
|
|
327
|
+
};
|
|
328
|
+
}
|