Add MindSpace page live edit, chat skills, and H5 deploy tooling.

Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-06-15 22:09:38 -07:00
parent 3cd322ccfe
commit 6ee6fd64dd
94 changed files with 7015 additions and 2136 deletions
+97 -4
View File
@@ -33,6 +33,7 @@ import {
isPathInsideUserWorkspace,
resolveMindspaceStorageRoot,
} from './user-space.mjs';
import { ensureUserMemoryProfile } from './user-memory-profile.mjs';
import {
applySkillGrantsToCapabilities,
DEFAULT_USER_SKILLS,
@@ -123,7 +124,7 @@ export function createUserAuth(pool, options = {}) {
const storageRoot = resolveMindspaceStorageRoot(h5Root, env);
const publicBaseUrl = resolvePublicBaseUrl(env);
const skillCatalog = listPlatformSkillCatalog(h5Root);
const defaultSignupBalanceCents = Number(options.defaultSignupBalanceCents ?? 1000);
const defaultSignupBalanceCents = Number(options.defaultSignupBalanceCents ?? 500);
const sessionTtlMs = Number(options.sessionTtlMs ?? 7 * 24 * 60 * 60 * 1000);
const loginMaxFailures = Number(options.loginMaxFailures ?? 5);
const loginFailureWindowMs = Number(options.loginFailureWindowMs ?? 5 * 60 * 1000);
@@ -245,6 +246,12 @@ export function createUserAuth(pool, options = {}) {
publicBaseUrl,
publishDir: web.publishDir,
});
ensureUserMemoryProfile(web.publishDir, {
userId: user.id,
displayName: user.displayName ?? user.display_name,
username: user.username ?? web.username,
slug: web.slug,
});
return {
...web,
...space,
@@ -307,6 +314,17 @@ export function createUserAuth(pool, options = {}) {
return layout;
};
const recordSignupBonus = async (conn, userId, amountCents, now) => {
const amount = Number(amountCents);
if (!Number.isFinite(amount) || amount <= 0) return;
await conn.query(
`INSERT INTO h5_billing_ledger
(user_id, type, amount_cents, tokens, note, operator_id, created_at)
VALUES (?, 'adjust', ?, 0, '新用户赠送', NULL, ?)`,
[userId, amount, now],
);
};
const register = async ({ username, password, displayName, email }) => {
const normalized = normalizeUsername(username);
if (!isValidUsername(normalized)) {
@@ -352,6 +370,7 @@ export function createUserAuth(pool, options = {}) {
VALUES (?, ?, 0, ?)`,
[userId, defaultSignupBalanceCents, now],
);
await recordSignupBonus(conn, userId, defaultSignupBalanceCents, now);
await conn.query(
`INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
[userId, workspaceRoot],
@@ -362,6 +381,12 @@ export function createUserAuth(pool, options = {}) {
});
await conn.commit();
ensureWorkspace(workspaceRoot);
ensureUserMemoryProfile(workspaceRoot, {
userId,
displayName: displayName?.trim() || normalized,
username: normalized,
slug: normalized,
});
const user = await getUserById(userId);
return { ok: true, user: publicUser(user) };
} catch (err) {
@@ -627,6 +652,13 @@ export function createUserAuth(pool, options = {}) {
return new Set(rows.map((row) => row.agent_session_id));
};
const unregisterAgentSession = async (userId, agentSessionId) => {
await pool.query(
`DELETE FROM h5_user_sessions WHERE agent_session_id = ? AND user_id = ?`,
[agentSessionId, userId],
);
};
const canUseChat = async (userId) => {
const user = await getUserById(userId);
if (!user) return { ok: false, message: '用户不存在' };
@@ -874,6 +906,7 @@ export function createUserAuth(pool, options = {}) {
ok: true,
costCents: 0,
balanceCents: Number(user?.balance_cents ?? 0),
tokensUsed: Number(user?.tokens_used ?? 0),
deltaInputTokens: 0,
deltaOutputTokens: 0,
};
@@ -890,6 +923,7 @@ export function createUserAuth(pool, options = {}) {
ok: true,
costCents: 0,
balanceCents: user ? Number(user.balance_cents) : null,
tokensUsed: user ? Number(user.tokens_used ?? 0) : null,
deltaInputTokens: 0,
deltaOutputTokens: 0,
};
@@ -931,6 +965,7 @@ export function createUserAuth(pool, options = {}) {
);
let balanceAfter = null;
let tokensUsedAfter = null;
if (costCents > 0) {
const [walletRows] = await conn.query(
`SELECT balance_cents, tokens_used FROM h5_user_wallets WHERE user_id = ? FOR UPDATE`,
@@ -945,6 +980,7 @@ export function createUserAuth(pool, options = {}) {
const currentBalance = Number(wallet.balance_cents ?? 0);
const nextBalance = Math.max(0, currentBalance - costCents);
balanceAfter = nextBalance;
tokensUsedAfter = Number(wallet.tokens_used ?? 0) + deltaTokens;
await conn.query(
`UPDATE h5_user_wallets
@@ -983,6 +1019,7 @@ export function createUserAuth(pool, options = {}) {
} else {
const user = await getUserById(userId);
balanceAfter = user ? Number(user.balance_cents) : null;
tokensUsedAfter = user ? Number(user.tokens_used ?? 0) : null;
}
await conn.commit();
@@ -990,6 +1027,7 @@ export function createUserAuth(pool, options = {}) {
ok: true,
costCents,
balanceCents: balanceAfter,
tokensUsed: tokensUsedAfter,
deltaInputTokens: deltaIn,
deltaOutputTokens: deltaOut,
};
@@ -1033,14 +1071,19 @@ export function createUserAuth(pool, options = {}) {
}));
};
const listBillingLedger = async ({ userId = null, limit = 50 } = {}) => {
const listBillingLedger = async ({ userId = null, limit = 50, types = null } = {}) => {
const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), 200);
const params = [];
let where = '';
const clauses = [];
if (userId) {
where = 'WHERE l.user_id = ?';
clauses.push('l.user_id = ?');
params.push(userId);
}
if (Array.isArray(types) && types.length) {
clauses.push(`l.type IN (${types.map(() => '?').join(', ')})`);
params.push(...types);
}
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
const [rows] = await pool.query(
`SELECT l.id, l.user_id, u.username, l.type, l.amount_cents, l.tokens,
l.session_id, l.note, l.created_at
@@ -1150,6 +1193,45 @@ export function createUserAuth(pool, options = {}) {
}
};
/** Enable L3 memory_store for existing role defaults without touching per-user overrides. */
const upgradeMemoryStoreCapability = async () => {
const now = Date.now();
await pool.query(
`INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
VALUES ('role', 'user', 'memory_store', 1, ?)
ON DUPLICATE KEY UPDATE allowed = 1, updated_at = VALUES(updated_at)`,
[now],
);
};
/** Enable platform skill loading + chat recall for existing role defaults. */
const upgradeDefaultUserCapabilities = async () => {
const now = Date.now();
for (const key of ['skills', 'chat_recall']) {
if (!DEFAULT_USER_CAPABILITIES[key]) continue;
await pool.query(
`INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
VALUES ('role', 'user', ?, 1, ?)
ON DUPLICATE KEY UPDATE allowed = 1, updated_at = VALUES(updated_at)`,
[key, now],
);
}
};
/** Enable default platform skills for existing role defaults. */
const upgradeDefaultUserSkills = async () => {
const now = Date.now();
for (const [name, enabled] of Object.entries(DEFAULT_USER_SKILLS)) {
if (!enabled) continue;
await pool.query(
`INSERT INTO h5_user_skill_grants (subject_type, subject_id, skill_name, enabled, updated_at)
VALUES ('role', 'user', ?, 1, ?)
ON DUPLICATE KEY UPDATE enabled = 1, updated_at = VALUES(updated_at)`,
[name, now],
);
}
};
const serializePolicyValue = (key, value) => {
const def = POLICY_CATALOG.find((item) => item.key === key);
if (def?.type === 'boolean') return value ? 'true' : 'false';
@@ -1514,8 +1596,11 @@ export function createUserAuth(pool, options = {}) {
}
await syncAdminPassword();
await seedRoleCapabilityDefaults();
await upgradeMemoryStoreCapability();
await upgradeDefaultUserCapabilities();
await seedRolePolicyDefaults();
await seedRoleSkillDefaults();
await upgradeDefaultUserSkills();
await repairAllUserPublishDirs();
};
@@ -1769,6 +1854,7 @@ export function createUserAuth(pool, options = {}) {
VALUES (?, ?, 0, ?)`,
[userId, defaultSignupBalanceCents, now],
);
await recordSignupBonus(conn, userId, defaultSignupBalanceCents, now);
await conn.query(
`INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
[userId, workspaceRoot],
@@ -1796,6 +1882,12 @@ export function createUserAuth(pool, options = {}) {
});
await conn.commit();
ensureWorkspace(workspaceRoot);
ensureUserMemoryProfile(workspaceRoot, {
userId,
displayName,
username: normalized,
slug: normalized,
});
const user = await getUserById(userId);
return { ok: true, user: publicUser(user) };
} catch (err) {
@@ -2037,6 +2129,7 @@ export function createUserAuth(pool, options = {}) {
isPathAllowed,
repairAllUserPublishDirs,
registerAgentSession,
unregisterAgentSession,
ownsSession,
listOwnedSessionIds,
canUseChat,