Add attachment text extraction, auto web news skill, and chat/voice UI updates.

Simplify asset upload temp paths, refresh deploy docs for Aliyun DNS topology, and ship MindSpace content-scan and auth improvements.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-20 15:08:10 +08:00
parent 2e5afe3bfd
commit 70492d9eba
24 changed files with 648 additions and 129 deletions
+19 -32
View File
@@ -3,12 +3,13 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import { DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs';
import { runBasicFileScan } from './mindspace-scan.mjs';
import { extractAttachmentText } from './mindspace-attachment-text.mjs';
import {
assetThumbnailKey,
ensureHtmlThumbnail,
scheduleHtmlThumbnail,
} from './mindspace-thumbnails.mjs';
import { mirrorAssetToZone, removeZoneMirror, resolveUserWorkspaceRoot } from './user-space.mjs';
import { removeZoneMirror, resolveUserWorkspaceRoot } from './user-space.mjs';
import { canPreviewAsset, renderAssetPreviewHtml } from './mindspace-asset-preview.mjs';
import { createWorkspaceAssetSync } from './mindspace-workspace-sync.mjs';
@@ -257,7 +258,7 @@ export function createAssetService(pool, options = {}) {
const uploadId = idFactory();
const now = Date.now();
const temporaryStorageKey = path.posix.join('tmp', userId, `${uploadId}.upload`);
const temporaryStorageKey = path.posix.join('users', userId, 'temp', `${uploadId}.upload`);
await conn.query(
`UPDATE h5_user_spaces
SET reserved_bytes = reserved_bytes + ?, updated_at = ?
@@ -349,9 +350,8 @@ export function createAssetService(pool, options = {}) {
};
const completeUpload = async (userId, uploadId) => {
const conn = await pool.getConnection();
let temporaryPath;
let finalPath;
const conn = await pool.getConnection();
let temporaryPath;
try {
await conn.beginTransaction();
const [rows] = await conn.query(
@@ -389,20 +389,9 @@ export function createAssetService(pool, options = {}) {
const assetId = idFactory();
const versionId = idFactory();
const finalStorageKey = path.posix.join(
'users',
userId,
'assets',
assetId,
'versions',
versionId,
);
const finalStorageKey = upload.temporary_storage_key;
temporaryPath = absoluteStoragePath(upload.temporary_storage_key);
finalPath = absoluteStoragePath(finalStorageKey);
await fs.mkdir(path.dirname(finalPath), { recursive: true });
await fs.rename(temporaryPath, finalPath);
const fileBuffer = await fs.readFile(finalPath);
const fileBuffer = await fs.readFile(temporaryPath);
const scan = runBasicFileScan(fileBuffer, {
filename: upload.filename,
mimeType: upload.detected_mime_type,
@@ -469,11 +458,6 @@ export function createAssetService(pool, options = {}) {
[now, assetId, uploadId, userId],
);
await conn.commit();
await mirrorToUserWorkspace(userId, {
categoryCode: upload.category_code,
filename: upload.filename,
sourcePath: finalPath,
});
return {
id: assetId,
categoryId: upload.category_id,
@@ -494,10 +478,6 @@ export function createAssetService(pool, options = {}) {
};
} catch (error) {
await conn.rollback();
if (finalPath && temporaryPath) {
await fs.mkdir(path.dirname(temporaryPath), { recursive: true }).catch(() => {});
await fs.rename(finalPath, temporaryPath).catch(() => {});
}
throw error;
} finally {
conn.release();
@@ -820,11 +800,6 @@ export function createAssetService(pool, options = {}) {
[buffer.length, now, category.space_id, userId],
);
await conn.commit();
await mirrorToUserWorkspace(userId, {
categoryCode: category.category_code,
filename: normalizedFilename,
sourcePath: finalPath,
});
if (detectedMimeType === 'text/html') {
scheduleHtmlThumbnail(storageRoot, assetThumbnailKey(userId, assetId), buffer.toString('utf8'), {
title: displayName || normalizedFilename,
@@ -891,6 +866,17 @@ export function createAssetService(pool, options = {}) {
return renderAssetPreviewHtml({ asset, buffer, downloadUrl });
};
const extractAssetText = async (userId, assetId) => {
const { asset, path: assetPath } = await readAsset(userId, assetId);
const buffer = await fs.readFile(assetPath);
const extracted = extractAttachmentText(buffer, asset.mimeType, asset.filename);
return {
asset: assetResponse(asset),
...extracted,
charCount: extracted.text.length,
};
};
const expireStaleUploads = async (now = Date.now()) => {
const [rows] = await pool.query(
`SELECT id, user_id, space_id, reserved_bytes, temporary_storage_key, status
@@ -952,6 +938,7 @@ export function createAssetService(pool, options = {}) {
deleteAsset,
readAsset,
renderAssetPreview,
extractAssetText,
renderAssetThumbnail,
syncWorkspaceAssets: workspaceSync.syncUserWorkspace,
expireStaleUploads,