Fix workspace sync restore for deleted assets with orphaned storage keys.
When a workspace file was soft-deleted but its workspace:// storage_key row remains, re-sync now restores and updates the asset instead of inserting a duplicate version that breaks WeChat Agent delivery. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -154,6 +154,29 @@ export function createWorkspaceAssetSync({
|
||||
return new Map(rows.map((row) => [row.original_filename, row]));
|
||||
};
|
||||
|
||||
/** Deleted workspace assets still own workspace:// storage_key rows — restore instead of re-import. */
|
||||
const loadDeletedWorkspaceAssetsByFilename = async (userId, categoryId) => {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT a.id, a.original_filename, a.checksum, a.size_bytes, a.current_version_id, a.status
|
||||
FROM h5_assets a
|
||||
WHERE a.user_id = ? AND a.category_id = ? AND a.status = 'deleted' AND a.source_type = 'workspace'`,
|
||||
[userId, categoryId],
|
||||
);
|
||||
return new Map(rows.map((row) => [row.original_filename, row]));
|
||||
};
|
||||
|
||||
const findDeletedWorkspaceAssetByStorageKey = async (userId, storageKey) => {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT a.id, a.original_filename, a.checksum, a.size_bytes, a.current_version_id, a.status
|
||||
FROM h5_asset_versions v
|
||||
JOIN h5_assets a ON a.id = v.asset_id AND a.user_id = ?
|
||||
WHERE v.storage_key = ? AND a.status = 'deleted' AND a.source_type = 'workspace'
|
||||
LIMIT 1`,
|
||||
[userId, storageKey],
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
};
|
||||
|
||||
/** Skip re-importing workspace files the user already deleted (checksum unchanged). */
|
||||
const loadDeletedWorkspaceChecksums = async (userId, categoryId) => {
|
||||
const [rows] = await pool.query(
|
||||
@@ -307,7 +330,7 @@ export function createWorkspaceAssetSync({
|
||||
throw Object.assign(new Error('用户空间不可用'), { code: 'space_unavailable' });
|
||||
}
|
||||
const [assetRows] = await conn.query(
|
||||
`SELECT id, current_version_id, size_bytes, checksum
|
||||
`SELECT id, current_version_id, size_bytes, checksum, status
|
||||
FROM h5_assets
|
||||
WHERE id = ? AND user_id = ?
|
||||
LIMIT 1
|
||||
@@ -318,6 +341,7 @@ export function createWorkspaceAssetSync({
|
||||
if (!currentAsset) {
|
||||
throw Object.assign(new Error('工作区资产不存在'), { code: 'asset_not_found' });
|
||||
}
|
||||
const wasDeleted = currentAsset.status === 'deleted';
|
||||
|
||||
const detectedMimeType = assetInternals.detectMimeType(buffer, file.filename);
|
||||
if (!detectedMimeType) {
|
||||
@@ -375,7 +399,8 @@ export function createWorkspaceAssetSync({
|
||||
await conn.query(
|
||||
`UPDATE h5_assets
|
||||
SET size_bytes = ?, checksum = ?, mime_type = ?,
|
||||
asset_type = ?, risk_level = ?, status = ?, workspace_relative_path = ?, updated_at = ?
|
||||
asset_type = ?, risk_level = ?, status = ?, workspace_relative_path = ?,
|
||||
deleted_at = NULL, updated_at = ?
|
||||
WHERE id = ? AND user_id = ?`,
|
||||
[
|
||||
buffer.length,
|
||||
@@ -406,16 +431,17 @@ export function createWorkspaceAssetSync({
|
||||
currentAsset.id,
|
||||
],
|
||||
);
|
||||
if (sizeDelta !== 0) {
|
||||
const quotaDelta = wasDeleted ? buffer.length : sizeDelta;
|
||||
if (quotaDelta !== 0) {
|
||||
await conn.query(
|
||||
`UPDATE h5_user_spaces SET used_bytes = GREATEST(0, used_bytes + ?), updated_at = ?
|
||||
WHERE id = ? AND user_id = ?`,
|
||||
[sizeDelta, now, category.space_id, userId],
|
||||
[quotaDelta, now, category.space_id, userId],
|
||||
);
|
||||
}
|
||||
await conn.commit();
|
||||
return {
|
||||
action: 'updated',
|
||||
action: wasDeleted ? 'restored' : 'updated',
|
||||
assetId: currentAsset.id,
|
||||
filename: file.filename,
|
||||
checksum,
|
||||
@@ -490,6 +516,7 @@ export function createWorkspaceAssetSync({
|
||||
if (!category) return { imported: 0, updated: 0, skipped: 0 };
|
||||
|
||||
const existingByName = await loadExistingAssets(userId, category.id);
|
||||
const deletedByName = await loadDeletedWorkspaceAssetsByFilename(userId, category.id);
|
||||
const deletedWorkspaceChecksums = await loadDeletedWorkspaceChecksums(userId, category.id);
|
||||
let imported = 0;
|
||||
let updated = 0;
|
||||
@@ -524,6 +551,31 @@ export function createWorkspaceAssetSync({
|
||||
existing.size_bytes = buffer.length;
|
||||
updated += 1;
|
||||
} else {
|
||||
const storageKey = buildWorkspaceStorageKey(
|
||||
userId,
|
||||
category.category_code,
|
||||
file.filename,
|
||||
);
|
||||
const deletedExisting =
|
||||
deletedByName.get(file.filename)
|
||||
?? (await findDeletedWorkspaceAssetByStorageKey(userId, storageKey));
|
||||
if (deletedExisting) {
|
||||
const result = await updateWorkspaceFile(userId, category, deletedExisting, file, buffer);
|
||||
if (result.action === 'skipped') {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
await registerWorkspaceArtifactForConversation(userId, source, result);
|
||||
deletedByName.delete(deletedExisting.original_filename);
|
||||
existingByName.set(deletedExisting.original_filename, {
|
||||
id: deletedExisting.id,
|
||||
checksum,
|
||||
size_bytes: buffer.length,
|
||||
current_version_id: deletedExisting.current_version_id,
|
||||
});
|
||||
updated += 1;
|
||||
continue;
|
||||
}
|
||||
const result = await importWorkspaceFile(userId, category, file, buffer);
|
||||
await registerWorkspaceArtifactForConversation(userId, source, result);
|
||||
existingByName.set(file.filename, { checksum });
|
||||
|
||||
Reference in New Issue
Block a user