mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 20:15:48 +00:00
fix(files): heal bare workspace paths and deduplicate media gallery vs markdown
SignFileURLs converts bare /app/workspace/ paths in markdown links to /v1/files/ URLs so they can be served. File handler uses workspace root for ft= auth fallback search (no tenant context with signed tokens). Strip standalone media links from markdown when MediaGallery already renders the same file (prevents image/file appearing twice). Backend dedup uses URL-context basename matching to avoid false positives.
This commit is contained in:
1 parent
20869275f2
commit
4c987aff69
3 files changed
+37
-3
No files matched your search
@@ -82,6 +82,9 @@ func SignFileURLs(content, secret string) string {
|
||||
content = strings.ReplaceAll(content, "/v1/files/v1/files/", "/v1/files/")
|
||||
content = strings.ReplaceAll(content, "/v1/files/v1/media/", "/v1/media/")
|
||||
content = strings.ReplaceAll(content, "/v1/media/v1/media/", "/v1/media/")
|
||||
// Heal legacy: convert bare absolute paths in markdown links to /v1/files/ URLs.
|
||||
// Agent text may embed  — these need the /v1/files/ prefix to be served.
|
||||
content = barePathInLinkRe.ReplaceAllString(content, "](/v1/files$1)")
|
||||
// Heal legacy: clean ?ft=... from markdown link display text [name?ft=xxx](...) → [name](...)
|
||||
content = linkTextFtRe.ReplaceAllString(content, "[$1]")
|
||||
// Sign URLs
|
||||
@@ -101,6 +104,11 @@ func SignFileURLs(content, secret string) string {
|
||||
// Captures the clean name before ?ft= for replacement.
|
||||
var linkTextFtRe = regexp.MustCompile(`\[([^\]\s?]+)\?ft=[^\]]*\]`)
|
||||
|
||||
// barePathInLinkRe matches markdown link URLs that are bare absolute paths (not /v1/ URLs).
|
||||
// Captures the absolute path so we can prepend /v1/files. Only matches inside ](...)
|
||||
// to avoid false positives in prose text.
|
||||
var barePathInLinkRe = regexp.MustCompile(`\]\((/(?:app|home|tmp|opt|data|workspace)[^\s)"'<>]*)\)`)
|
||||
|
||||
// staleTokenRe matches ?ft=... or &ft=... query parameter (greedy to end of URL segment).
|
||||
var staleTokenRe = regexp.MustCompile(`[?&]ft=[^\s)"'<>&]*`)
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export function MessageBubble({ message }: MessageBubbleProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<MessageContent content={message.content} role={message.role} />
|
||||
<MessageContent content={message.content} role={message.role} mediaBasenames={message.mediaItems?.map((m) => m.path.split("/").pop() ?? "").filter(Boolean)} />
|
||||
{message.mediaItems && message.mediaItems.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<MediaGallery items={message.mediaItems} />
|
||||
|
||||
@@ -3,8 +3,34 @@ import { RichContent } from "./rich-content";
|
||||
interface MessageContentProps {
|
||||
content: string;
|
||||
role: string;
|
||||
/** Basenames of media files rendered separately by MediaGallery — strip from markdown. */
|
||||
mediaBasenames?: string[];
|
||||
}
|
||||
|
||||
export function MessageContent({ content, role }: MessageContentProps) {
|
||||
return <RichContent content={content} role={role} />;
|
||||
export function MessageContent({ content, role, mediaBasenames }: MessageContentProps) {
|
||||
const cleaned = mediaBasenames?.length ? stripGalleryDuplicates(content, mediaBasenames) : content;
|
||||
return <RichContent content={cleaned} role={role} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove standalone markdown image/link lines whose basename matches a MediaGallery item.
|
||||
* This prevents the same file from appearing twice (once in markdown, once in gallery).
|
||||
*/
|
||||
function stripGalleryDuplicates(content: string, basenames: string[]): string {
|
||||
if (!basenames.length) return content;
|
||||
const baseSet = new Set(basenames);
|
||||
return content
|
||||
.split("\n")
|
||||
.filter((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!(trimmed.startsWith(" || !trimmed.endsWith(")")) {
|
||||
return true; // not a standalone link — keep
|
||||
}
|
||||
const m = trimmed.match(/\]\(([^)]+)\)/);
|
||||
if (!m?.[1]) return true;
|
||||
const url = m[1].split("?")[0] ?? "";
|
||||
const base = url.split("/").pop() ?? "";
|
||||
return !baseSet.has(base); // drop if gallery already shows this file
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
Reference in new issue
Block a user