371900bae5
Memind CI / Test, build, and release guards (pull_request) Failing after 18s
Centralize page HTML quota checks with grace-write semantics across page services and workspace tools, keep localhost MindSpace links clickable in chat display, and expand Page Data/static-page skill plus local dev docs for quota, delivery URLs, and native Aider/OpenHands tooling. Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
3.2 KiB
JavaScript
111 lines
3.2 KiB
JavaScript
function asQuotaNumber(value) {
|
|
return Number(value ?? 0);
|
|
}
|
|
|
|
export function computeUserSpaceQuotaStats(space = {}) {
|
|
const quotaBytes = asQuotaNumber(space.quota_bytes);
|
|
const usedBytes = asQuotaNumber(space.used_bytes);
|
|
const reservedBytes = asQuotaNumber(space.reserved_bytes);
|
|
const occupiedBytes = usedBytes + reservedBytes;
|
|
const availableBytes = quotaBytes - occupiedBytes;
|
|
return {
|
|
quotaBytes,
|
|
usedBytes,
|
|
reservedBytes,
|
|
occupiedBytes,
|
|
availableBytes,
|
|
isOverQuota: occupiedBytes >= quotaBytes,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Page write grace policy:
|
|
* - Already at/above quota: block the next page write entirely.
|
|
* - Below quota but insufficient headroom: allow this write (may exceed quota).
|
|
* - Sufficient headroom: allow normally.
|
|
*/
|
|
export function evaluatePageWriteQuota(space, requiredBytes) {
|
|
const stats = computeUserSpaceQuotaStats(space);
|
|
const required = Math.max(0, asQuotaNumber(requiredBytes));
|
|
if (stats.isOverQuota) {
|
|
return {
|
|
allowed: false,
|
|
grace: false,
|
|
stats,
|
|
requiredBytes: required,
|
|
reason: 'over_quota',
|
|
};
|
|
}
|
|
if (stats.availableBytes >= required) {
|
|
return {
|
|
allowed: true,
|
|
grace: false,
|
|
stats,
|
|
requiredBytes: required,
|
|
reason: 'sufficient',
|
|
};
|
|
}
|
|
return {
|
|
allowed: true,
|
|
grace: true,
|
|
stats,
|
|
requiredBytes: required,
|
|
reason: 'grace_write',
|
|
};
|
|
}
|
|
|
|
export function buildPageWriteQuotaError(
|
|
evaluation,
|
|
{ messagePrefix = '剩余空间不足' } = {},
|
|
) {
|
|
const { stats, requiredBytes, reason } = evaluation;
|
|
const message =
|
|
reason === 'over_quota'
|
|
? '空间已满,请先清理后再创建或修改页面'
|
|
: messagePrefix;
|
|
return Object.assign(new Error(message), {
|
|
code: 'quota_exceeded',
|
|
details: {
|
|
requiredBytes,
|
|
availableBytes: Math.max(0, stats.availableBytes),
|
|
usedBytes: stats.usedBytes,
|
|
quotaBytes: stats.quotaBytes,
|
|
overQuota: reason === 'over_quota',
|
|
graceWrite: reason === 'grace_write',
|
|
},
|
|
});
|
|
}
|
|
|
|
export function assertPageWriteQuota(space, requiredBytes) {
|
|
const evaluation = evaluatePageWriteQuota(space, requiredBytes);
|
|
if (!evaluation.allowed) {
|
|
throw buildPageWriteQuotaError(evaluation);
|
|
}
|
|
return evaluation;
|
|
}
|
|
|
|
export function isMindSpacePageWriteRelativePath(relativePath) {
|
|
const normalized = String(relativePath ?? '').replace(/\\/g, '/').trim();
|
|
if (!normalized.toLowerCase().endsWith('.html')) return false;
|
|
const zone = normalized.split('/')[0]?.toLowerCase();
|
|
return zone === 'public' || zone === 'draft';
|
|
}
|
|
|
|
export async function loadUserSpaceForQuota(pool, userId) {
|
|
if (!pool || !userId) return null;
|
|
const [rows] = await pool.query(
|
|
`SELECT id, quota_bytes, used_bytes, reserved_bytes, status
|
|
FROM h5_user_spaces WHERE user_id = ? LIMIT 1`,
|
|
[userId],
|
|
);
|
|
return rows[0] ?? null;
|
|
}
|
|
|
|
export async function assertPageWriteQuotaForUser(pool, userId, requiredBytes) {
|
|
const space = await loadUserSpaceForQuota(pool, userId);
|
|
if (!space || space.status !== 'active') {
|
|
throw Object.assign(new Error('用户空间不可用'), { code: 'space_unavailable' });
|
|
}
|
|
return assertPageWriteQuota(space, requiredBytes);
|
|
}
|