feat: improve mindspace asset handling and local runtime paths

This commit is contained in:
john
2026-06-28 12:18:26 +08:00
parent 4a9bc710f1
commit ea19ffb5fa
36 changed files with 867 additions and 100 deletions
+18 -16
View File
@@ -48,7 +48,8 @@ const CHAT_PLACEHOLDER_PROMPTS = [
type PendingChatImage = {
id: string;
url: string;
local: boolean;
previewUrl: string;
uploading: boolean;
};
function FileIcon() {
@@ -87,7 +88,7 @@ export function ChatPanel({
session: Session | null;
capabilities?: CapabilityMap;
grantedSkills?: string[];
onSubmit: (text: string, imageUrls?: string[]) => void | Promise<void>;
onSubmit: (text: string, imageUrls?: string[], previewImageUrls?: string[]) => void | Promise<void>;
onUploadImage?: (file: File) => Promise<string>;
onStop: () => void | Promise<void>;
onApproveTool: (allow: boolean) => void | Promise<void>;
@@ -124,7 +125,7 @@ export function ChatPanel({
useEffect(() => {
return () => {
pendingImagesRef.current.forEach((item) => {
if (item.local) URL.revokeObjectURL(item.url);
if (item.previewUrl.startsWith('blob:')) URL.revokeObjectURL(item.previewUrl);
});
};
}, []);
@@ -183,17 +184,18 @@ export function ChatPanel({
const imageAttachmentDisabled = !onUploadImage || busy || uploadingImage || offlineBlocked || !!pendingTool;
const revokePendingImage = (item: PendingChatImage) => {
if (item.local) URL.revokeObjectURL(item.url);
if (item.previewUrl.startsWith('blob:')) URL.revokeObjectURL(item.previewUrl);
};
const handleSubmit = async () => {
const trimmed = input.trim();
if ((!trimmed && pendingImages.length === 0) || voiceDisabled) return;
if (uploadingImage || pendingImages.some((item) => item.local)) {
if (uploadingImage || pendingImages.some((item) => item.uploading)) {
setImageError('图片仍在上传中,请稍候再发送');
return;
}
const imagesToSend = pendingImages.map((item) => item.url);
const previewImagesToSend = pendingImages.map((item) => item.previewUrl);
const sentImages = [...pendingImages];
suppressVoiceUpdateRef.current = true;
@@ -205,7 +207,7 @@ export function ChatPanel({
setVoiceNotice(null);
setImageError(null);
try {
await onSubmit(trimmed, imagesToSend);
await onSubmit(trimmed, imagesToSend, previewImagesToSend);
sentImages.forEach(revokePendingImage);
setPendingImages([]);
} catch (err) {
@@ -245,8 +247,9 @@ export function ChatPanel({
const placeholders = toUpload.map((file, index) => ({
id: `${Date.now()}-${index}-${file.name}`,
url: URL.createObjectURL(file),
local: true,
previewUrl: URL.createObjectURL(file),
url: '',
uploading: true,
}));
setPendingImages((prev) => [...prev, ...placeholders]);
@@ -258,8 +261,7 @@ export function ChatPanel({
const next = prev.map((item) => {
const index = placeholderIds.indexOf(item.id);
if (index < 0 || !uploaded[index]) return item;
revokePendingImage(item);
return { ...item, url: uploaded[index], local: false };
return { ...item, url: uploaded[index], uploading: false };
});
const seen = new Set<string>();
return next.filter((item) => {
@@ -310,8 +312,9 @@ export function ChatPanel({
const placeholders = toUpload.map((file, index) => ({
id: `${Date.now()}-${index}-${file.name || 'pasted-image'}`,
url: URL.createObjectURL(file),
local: true,
previewUrl: URL.createObjectURL(file),
url: '',
uploading: true,
}));
setPendingImages((prev) => [...prev, ...placeholders]);
@@ -323,8 +326,7 @@ export function ChatPanel({
const next = prev.map((item) => {
const index = placeholderIds.indexOf(item.id);
if (index < 0 || !uploaded[index]) return item;
revokePendingImage(item);
return { ...item, url: uploaded[index], local: false };
return { ...item, url: uploaded[index], uploading: false };
});
const seen = new Set<string>();
return next.filter((item) => {
@@ -419,8 +421,8 @@ export function ChatPanel({
<div className="chat-image-attachment-grid">
{pendingImages.map((item, index) => (
<div key={item.id} className="chat-image-attachment-item">
<a href={item.url} target="_blank" rel="noreferrer">
<img src={item.url} alt={`上传图片 ${index + 1}`} className="chat-image-attachment-thumb" />
<a href={item.url || item.previewUrl} target="_blank" rel="noreferrer">
<img src={item.previewUrl} alt={`上传图片 ${index + 1}`} className="chat-image-attachment-thumb" />
</a>
<button
type="button"
+3 -1
View File
@@ -297,7 +297,9 @@ export function ChatView({
session={session}
capabilities={capabilities}
grantedSkills={grantedSkills}
onSubmit={(text, imageUrls) => void submit(text, undefined, imageUrls)}
onSubmit={(text, imageUrls, previewImageUrls) =>
void submit(text, undefined, imageUrls, previewImageUrls)
}
onUploadImage={uploadChatImage}
onStop={stop}
onApproveTool={approveTool}
+6 -5
View File
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useUserAvatar } from '../hooks/useUserAvatar';
import type { Message } from '../types';
import { getDisplayText, getImageUrls, getThinking } from '../utils/message';
import { getDisplayText, getImageUrls, getRenderableImageUrls, getThinking } from '../utils/message';
import { getMessageSaveActions } from '../utils/messageSave';
import { renderMarkdown } from '../utils/markdown';
import { filterText } from '../utils/wordFilter';
@@ -189,6 +189,7 @@ function MessageRow({
const thinking = getThinking(message);
const isUser = message.role === 'user';
const imageUrls = getImageUrls(message);
const renderableImageUrls = getRenderableImageUrls(message);
const copyText = [thinking ? `【思考】\n${thinking}` : '', text].filter(Boolean).join('\n\n');
if (!text && !thinking && message.role === 'assistant') {
@@ -221,16 +222,16 @@ function MessageRow({
)}
{imageUrls.length > 0 && (
<div className="msg-image-gallery">
{imageUrls.map((imageUrl, index) => (
{renderableImageUrls.map((image, index) => (
<a
key={`${imageUrl}-${index}`}
href={imageUrl}
key={`${image.href}-${index}`}
href={image.href}
target="_blank"
rel="noreferrer"
className="msg-image-link"
>
<img
src={imageUrl}
src={image.src}
alt={`图片 ${index + 1}`}
className="msg-image-thumb"
loading="lazy"
+6 -1
View File
@@ -182,7 +182,12 @@ export function PageSaveDialog({
setSaveNotice(`已保存到${CATEGORY_LABELS[result.categoryCode]}`);
onSaved({ kind: 'asset', categoryCode: result.categoryCode });
} catch (err) {
setSaveError(err instanceof Error ? err.message : '保存失败');
const message = err instanceof Error ? err.message : '保存失败';
if (message.includes('用户不存在') || message.includes('未授权') || message.includes('登录已过期')) {
setSaveError('当前账号未登录或已失效,请重新登录后再保存');
} else {
setSaveError(message);
}
} finally {
setSaving(false);
}
+1 -1
View File
@@ -112,7 +112,7 @@ export function PageSavePreviewPanel({
title="页面预览"
src={pagePreviewUrl}
className="page-save-mini-page-frame"
sandbox="allow-scripts"
sandbox="allow-scripts allow-same-origin"
scrolling="yes"
onError={() => setPreviewFailed(true)}
/>
+3 -3
View File
@@ -123,10 +123,10 @@ export function SpaceChatPanel({
chatState={chatState}
pendingTool={pendingTool}
session={session}
onSubmit={(text, imageUrls) =>
onSubmit={(text, imageUrls, previewImageUrls) =>
chatBridge
? void submit(text, context, imageUrls)
: void submit(text, { mindspaceContext: context }, imageUrls)
? void submit(text, context, imageUrls, previewImageUrls)
: void submit(text, { mindspaceContext: context }, imageUrls, previewImageUrls)
}
onUploadImage={uploadChatImage}
onStop={stop}