@agent-native/dispatch 0.8.20 → 0.8.23
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/dist/routes/pages/chat.d.ts +21 -2
- package/dist/routes/pages/chat.d.ts.map +1 -1
- package/dist/routes/pages/chat.js +12 -3
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/routes/pages/overview.d.ts +21 -2
- package/dist/routes/pages/overview.d.ts.map +1 -1
- package/dist/routes/pages/overview.js +13 -4
- package/dist/routes/pages/overview.js.map +1 -1
- package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
- package/dist/server/lib/dispatch-integrations.js +27 -3
- package/dist/server/lib/dispatch-integrations.js.map +1 -1
- package/dist/server/lib/thread-link-preview.d.ts +24 -0
- package/dist/server/lib/thread-link-preview.d.ts.map +1 -0
- package/dist/server/lib/thread-link-preview.js +176 -0
- package/dist/server/lib/thread-link-preview.js.map +1 -0
- package/package.json +1 -1
- package/src/routes/pages/chat.tsx +20 -3
- package/src/routes/pages/overview.tsx +21 -8
- package/src/server/lib/dispatch-integrations.spec.ts +69 -0
- package/src/server/lib/dispatch-integrations.ts +26 -3
- package/src/server/lib/thread-link-preview.spec.ts +129 -0
- package/src/server/lib/thread-link-preview.ts +187 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { ChatThread } from "@agent-native/core/server";
|
|
2
|
+
import { getRequestContext, getThread } from "@agent-native/core/server";
|
|
3
|
+
|
|
4
|
+
export interface ThreadLinkPreview {
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
imageUrl: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const IMAGE_URL_KEYS = new Set([
|
|
11
|
+
"previewUrl",
|
|
12
|
+
"thumbnailUrl",
|
|
13
|
+
"imageUrl",
|
|
14
|
+
"image",
|
|
15
|
+
"downloadUrl",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const GENERATION_TOOL_NAMES = new Set([
|
|
19
|
+
"generate-image",
|
|
20
|
+
"generate-image-batch",
|
|
21
|
+
"refine-image",
|
|
22
|
+
"rerun-generation-run",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function safeJsonParse(value: string): unknown {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(value);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cleanUrlCandidate(value: string): string {
|
|
34
|
+
return value
|
|
35
|
+
.trim()
|
|
36
|
+
.replace(/[),.;\]}]+$/g, "")
|
|
37
|
+
.replace(/^["'(<]+/g, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isAbsoluteHttpUrl(value: string): boolean {
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(value);
|
|
43
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isImageLikeUrl(value: string): boolean {
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(value);
|
|
52
|
+
return (
|
|
53
|
+
/\.(?:png|jpe?g|webp|gif|avif)(?:$|[?#])/i.test(url.pathname) ||
|
|
54
|
+
/\/api\/assets\/[^/]+\/content(?:$|[?#])/i.test(url.pathname)
|
|
55
|
+
);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validPreviewImageUrl(value: unknown, key?: string): string | null {
|
|
62
|
+
if (typeof value !== "string") return null;
|
|
63
|
+
const candidate = cleanUrlCandidate(value);
|
|
64
|
+
if (!isAbsoluteHttpUrl(candidate)) return null;
|
|
65
|
+
if (key && IMAGE_URL_KEYS.has(key)) return candidate;
|
|
66
|
+
return isImageLikeUrl(candidate) ? candidate : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function imageUrlFromStructuredValue(value: unknown): string | null {
|
|
70
|
+
if (!value || typeof value !== "object") return null;
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
for (let i = value.length - 1; i >= 0; i--) {
|
|
73
|
+
const found = imageUrlFromStructuredValue(value[i]);
|
|
74
|
+
if (found) return found;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const record = value as Record<string, unknown>;
|
|
80
|
+
for (const key of IMAGE_URL_KEYS) {
|
|
81
|
+
const found = validPreviewImageUrl(record[key], key);
|
|
82
|
+
if (found) return found;
|
|
83
|
+
}
|
|
84
|
+
for (const [key, child] of Object.entries(record).reverse()) {
|
|
85
|
+
const direct = validPreviewImageUrl(child, key);
|
|
86
|
+
if (direct) return direct;
|
|
87
|
+
if (child && typeof child === "object") {
|
|
88
|
+
const nested = imageUrlFromStructuredValue(child);
|
|
89
|
+
if (nested) return nested;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function imageUrlFromText(value: string): string | null {
|
|
96
|
+
const matches = value.match(/https?:\/\/[^\s<>"']+/g);
|
|
97
|
+
if (!matches) return null;
|
|
98
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
99
|
+
const candidate = validPreviewImageUrl(matches[i]);
|
|
100
|
+
if (candidate) return candidate;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function extractThreadPreviewImageUrl(
|
|
106
|
+
threadData: string,
|
|
107
|
+
): string | null {
|
|
108
|
+
const parsed = safeJsonParse(threadData);
|
|
109
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
110
|
+
const messages = (parsed as { messages?: unknown }).messages;
|
|
111
|
+
if (!Array.isArray(messages)) return null;
|
|
112
|
+
|
|
113
|
+
for (
|
|
114
|
+
let messageIndex = messages.length - 1;
|
|
115
|
+
messageIndex >= 0;
|
|
116
|
+
messageIndex--
|
|
117
|
+
) {
|
|
118
|
+
const entry = messages[messageIndex] as any;
|
|
119
|
+
const message = entry?.message ?? entry;
|
|
120
|
+
const content = message?.content;
|
|
121
|
+
if (!Array.isArray(content)) continue;
|
|
122
|
+
|
|
123
|
+
for (let partIndex = content.length - 1; partIndex >= 0; partIndex--) {
|
|
124
|
+
const part = content[partIndex] as Record<string, unknown>;
|
|
125
|
+
const result = typeof part.result === "string" ? part.result : "";
|
|
126
|
+
if (!result.trim()) continue;
|
|
127
|
+
|
|
128
|
+
const toolName = typeof part.toolName === "string" ? part.toolName : "";
|
|
129
|
+
const parsedResult = safeJsonParse(result);
|
|
130
|
+
if (parsedResult && GENERATION_TOOL_NAMES.has(toolName)) {
|
|
131
|
+
const structured = imageUrlFromStructuredValue(parsedResult);
|
|
132
|
+
if (structured) return structured;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const fromText = imageUrlFromText(result);
|
|
136
|
+
if (fromText) return fromText;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function previewDescription(thread: ChatThread): string {
|
|
143
|
+
const preview = thread.preview.trim();
|
|
144
|
+
if (preview) return preview.slice(0, 180);
|
|
145
|
+
return "Open this Agent-Native thread in Dispatch.";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function loadThreadLinkPreview(
|
|
149
|
+
threadId: string | null | undefined,
|
|
150
|
+
): Promise<ThreadLinkPreview | null> {
|
|
151
|
+
const id = threadId?.trim();
|
|
152
|
+
if (!id) return null;
|
|
153
|
+
const viewerEmail = getRequestContext()?.userEmail?.trim();
|
|
154
|
+
if (!viewerEmail) return null;
|
|
155
|
+
const thread = await getThread(id).catch(() => null);
|
|
156
|
+
if (!thread) return null;
|
|
157
|
+
if (thread.ownerEmail !== viewerEmail) return null;
|
|
158
|
+
const title = thread.title.trim() || "Agent-Native thread";
|
|
159
|
+
return {
|
|
160
|
+
title,
|
|
161
|
+
description: previewDescription(thread),
|
|
162
|
+
imageUrl: extractThreadPreviewImageUrl(thread.threadData),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function buildThreadLinkPreviewMeta(preview: ThreadLinkPreview | null) {
|
|
167
|
+
const title = preview?.title ? `${preview.title} - Dispatch` : "Dispatch";
|
|
168
|
+
const description =
|
|
169
|
+
preview?.description ||
|
|
170
|
+
"Open this Agent-Native thread in the Dispatch workspace.";
|
|
171
|
+
const image = preview?.imageUrl ?? null;
|
|
172
|
+
return [
|
|
173
|
+
{ title },
|
|
174
|
+
{ name: "description", content: description },
|
|
175
|
+
{ property: "og:title", content: title },
|
|
176
|
+
{ property: "og:description", content: description },
|
|
177
|
+
{ property: "og:type", content: "website" },
|
|
178
|
+
...(image ? [{ property: "og:image", content: image }] : []),
|
|
179
|
+
{
|
|
180
|
+
name: "twitter:card",
|
|
181
|
+
content: image ? "summary_large_image" : "summary",
|
|
182
|
+
},
|
|
183
|
+
{ name: "twitter:title", content: title },
|
|
184
|
+
{ name: "twitter:description", content: description },
|
|
185
|
+
...(image ? [{ name: "twitter:image", content: image }] : []),
|
|
186
|
+
];
|
|
187
|
+
}
|