feat(mindspace): add page quota grace write and local delivery guardrails.
Memind CI / Test, build, and release guards (pull_request) Failing after 18s
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>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user