Fix sandbox MCP path resolution for session policy sync.
Resolve mindspace-sandbox-mcp.mjs from the code module location instead of h5Root, and copy it into the portal runtime artifact so goosed can spawn sandbox-fs on production. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+374
-43
@@ -1,5 +1,6 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
import { Algorithm as Argon2Algorithm, hashRawSync as argon2HashRawSync } from '@node-rs/argon2';
|
||||
import { computeDeltaCostCents, loadBillingConfig, normalizeTokenState } from './billing.mjs';
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
DEFAULT_USER_CAPABILITIES,
|
||||
isValidCapabilityKey,
|
||||
normalizeCapabilityPatch,
|
||||
resolveSandboxMcpServerPath,
|
||||
USER_NON_GRANTABLE_CAPABILITIES,
|
||||
} from './capabilities.mjs';
|
||||
import {
|
||||
@@ -25,9 +27,11 @@ import {
|
||||
import {
|
||||
ensurePublishSkillInstalled,
|
||||
ensureUserPublishLayout,
|
||||
ensureWorkspaceHintsInstalled,
|
||||
PUBLISH_ROOT_DIR,
|
||||
PUBLISH_SKILL_NAME,
|
||||
resolvePublicBaseUrl,
|
||||
resolveLegacyPublishDir,
|
||||
} from './user-publish.mjs';
|
||||
import {
|
||||
ensureUserSpaceLayout,
|
||||
@@ -128,6 +132,9 @@ export function createUserAuth(pool, options = {}) {
|
||||
const loginMaxFailures = Number(options.loginMaxFailures ?? 5);
|
||||
const loginFailureWindowMs = Number(options.loginFailureWindowMs ?? 5 * 60 * 1000);
|
||||
const persistSessions = options.persistSessions !== false && Boolean(pool);
|
||||
let rechargeNotifier =
|
||||
typeof options.onRechargeNotification === 'function' ? options.onRechargeNotification : null;
|
||||
const subscriptionService = options.subscriptionService ?? null;
|
||||
const sessions = new Map();
|
||||
const loginFailures = new Map();
|
||||
|
||||
@@ -170,23 +177,19 @@ export function createUserAuth(pool, options = {}) {
|
||||
fs.mkdirSync(workspaceRoot, { recursive: true });
|
||||
};
|
||||
|
||||
const resolveAdminWorkspaceRoot = () => {
|
||||
const configured = env.H5_ADMIN_WORKSPACE_ROOT?.trim();
|
||||
if (configured) return path.resolve(configured);
|
||||
return path.dirname(usersRoot);
|
||||
};
|
||||
|
||||
const isAdminRole = (user) => user?.role === 'admin';
|
||||
|
||||
const getUserById = async (userId) => {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT u.id, u.username, u.slug, u.email, u.display_name, u.role, u.status,
|
||||
u.plan_type, u.workspace_root,
|
||||
s.quota_bytes, s.used_bytes, s.reserved_bytes,
|
||||
w.balance_cents, w.tokens_used,
|
||||
(SELECT COALESCE(SUM(ABS(amount_cents)), 0)
|
||||
FROM h5_billing_ledger l
|
||||
WHERE l.user_id = u.id AND l.type = 'deduct') AS spent_cents
|
||||
FROM h5_users u
|
||||
LEFT JOIN h5_user_spaces s ON s.user_id = u.id
|
||||
LEFT JOIN h5_user_wallets w ON w.user_id = u.id
|
||||
WHERE u.id = ?`,
|
||||
[userId],
|
||||
@@ -210,8 +213,14 @@ export function createUserAuth(pool, options = {}) {
|
||||
balanceCents,
|
||||
totalCreditCents: balanceCents + spentCents,
|
||||
tokensUsed: Number(row.tokens_used ?? 0),
|
||||
spaceQuotaBytes: Number(row.quota_bytes ?? 0),
|
||||
spaceUsedBytes: Number(row.used_bytes ?? 0),
|
||||
spaceReservedBytes: Number(row.reserved_bytes ?? 0),
|
||||
spaceAvailableBytes: Math.max(
|
||||
0,
|
||||
Number(row.quota_bytes ?? 0) - Number(row.used_bytes ?? 0) - Number(row.reserved_bytes ?? 0),
|
||||
),
|
||||
};
|
||||
if (row.role === 'admin') return base;
|
||||
const publishKey = row.id;
|
||||
return {
|
||||
...base,
|
||||
@@ -238,13 +247,19 @@ export function createUserAuth(pool, options = {}) {
|
||||
slug: web.slug,
|
||||
workspaceRoot: web.publishDir,
|
||||
});
|
||||
ensurePublishSkillInstalled(web.publishDir, {
|
||||
const hintsContext = {
|
||||
slug: web.slug,
|
||||
username: user.username ?? web.username,
|
||||
displayName: user.displayName,
|
||||
publicBaseUrl,
|
||||
publishDir: web.publishDir,
|
||||
});
|
||||
};
|
||||
ensurePublishSkillInstalled(web.publishDir, hintsContext);
|
||||
ensureWorkspaceHintsInstalled(web.publishDir, hintsContext);
|
||||
const legacyPublishDir = resolveLegacyPublishDir(h5Root, user);
|
||||
if (legacyPublishDir && legacyPublishDir !== web.publishDir && fs.existsSync(legacyPublishDir)) {
|
||||
ensureWorkspaceHintsInstalled(legacyPublishDir, { ...hintsContext, publishDir: legacyPublishDir });
|
||||
}
|
||||
ensureUserMemoryProfile(web.publishDir, {
|
||||
userId: user.id,
|
||||
displayName: user.displayName ?? user.display_name,
|
||||
@@ -293,7 +308,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
};
|
||||
|
||||
const syncUserPublishWorkspace = async (user) => {
|
||||
if (!user || user.role === 'admin') return null;
|
||||
if (!user) return null;
|
||||
const layout = await publishLayoutFor(user);
|
||||
const current = path.resolve(user.workspace_root);
|
||||
const target = path.resolve(layout.publishDir);
|
||||
@@ -386,6 +401,9 @@ export function createUserAuth(pool, options = {}) {
|
||||
username: normalized,
|
||||
slug: normalized,
|
||||
});
|
||||
if (subscriptionService) {
|
||||
subscriptionService.grantSubscription(userId, 'free', null, null, '注册赠送免费套餐').catch(() => {});
|
||||
}
|
||||
const user = await getUserById(userId);
|
||||
return { ok: true, user: publicUser(user) };
|
||||
} catch (err) {
|
||||
@@ -582,18 +600,13 @@ export function createUserAuth(pool, options = {}) {
|
||||
const resolveWorkingDir = async (userId) => {
|
||||
const user = await getUserById(userId);
|
||||
if (!user) throw new Error('用户不存在');
|
||||
if (isAdminRole(user)) {
|
||||
const root = resolveAdminWorkspaceRoot();
|
||||
ensureWorkspace(root);
|
||||
return root;
|
||||
}
|
||||
const layout = await syncUserPublishWorkspace(user);
|
||||
return layout.publishDir;
|
||||
};
|
||||
|
||||
const getUserPublishLayout = async (userId) => {
|
||||
const user = await getUserById(userId);
|
||||
if (!user || user.role === 'admin') return null;
|
||||
if (!user) return null;
|
||||
return syncUserPublishWorkspace(user);
|
||||
};
|
||||
|
||||
@@ -678,6 +691,29 @@ export function createUserAuth(pool, options = {}) {
|
||||
if (isAdminRole(user)) {
|
||||
return { ok: true, balanceCents: Number(user.balance_cents ?? 0) };
|
||||
}
|
||||
|
||||
// Subscription check: active subscription with remaining quota bypasses balance requirement.
|
||||
if (subscriptionService) {
|
||||
const sub = await subscriptionService.getActiveSubscription(userId);
|
||||
if (sub) {
|
||||
const unlimited = sub.periodTokensLimit === 0;
|
||||
const hasQuota = unlimited || sub.periodTokensUsed < sub.periodTokensLimit;
|
||||
const balanceCents = Number(user.balance_cents ?? 0);
|
||||
if (hasQuota) {
|
||||
return { ok: true, balanceCents, subscription: sub };
|
||||
}
|
||||
// Quota exhausted — allow if balance covers overage, otherwise block.
|
||||
if (balanceCents > 0) {
|
||||
return { ok: true, balanceCents, subscription: sub, overQuota: true };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
message: '本月额度已用完,余额不足,请充值或升级套餐',
|
||||
...buildInsufficientBalancePayload(balanceCents, loadRechargeConfig()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const balanceCents = Number(user.balance_cents ?? 0);
|
||||
if (balanceCents <= 0) {
|
||||
return {
|
||||
@@ -709,8 +745,10 @@ export function createUserAuth(pool, options = {}) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT u.id, u.username, u.slug, u.email, u.display_name, u.role, u.status,
|
||||
u.plan_type, u.workspace_root,
|
||||
s.quota_bytes, s.used_bytes, s.reserved_bytes,
|
||||
u.created_at, u.updated_at, w.balance_cents, w.tokens_used
|
||||
FROM h5_users u
|
||||
LEFT JOIN h5_user_spaces s ON s.user_id = u.id
|
||||
LEFT JOIN h5_user_wallets w ON w.user_id = u.id
|
||||
${where}
|
||||
ORDER BY u.created_at DESC
|
||||
@@ -725,6 +763,11 @@ export function createUserAuth(pool, options = {}) {
|
||||
};
|
||||
};
|
||||
|
||||
const getUserPublic = async (userId) => {
|
||||
const user = await getUserById(userId);
|
||||
return user ? publicUser(user) : null;
|
||||
};
|
||||
|
||||
const createUser = async ({
|
||||
username,
|
||||
password,
|
||||
@@ -744,9 +787,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
|
||||
const isAdmin = role === 'admin';
|
||||
const userId = crypto.randomUUID();
|
||||
const root = isAdmin
|
||||
? path.resolve(workspaceRoot || path.join(usersRoot, normalized))
|
||||
: (await publishLayoutFor({ id: userId, username: normalized })).publishDir;
|
||||
const root = (await publishLayoutFor({ id: userId, username: normalized })).publishDir;
|
||||
const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password);
|
||||
const now = Date.now();
|
||||
|
||||
@@ -790,6 +831,9 @@ export function createUserAuth(pool, options = {}) {
|
||||
}
|
||||
await conn.commit();
|
||||
ensureWorkspace(root);
|
||||
if (subscriptionService && !isAdmin) {
|
||||
subscriptionService.grantSubscription(userId, 'free', null, null, '注册赠送免费套餐').catch(() => {});
|
||||
}
|
||||
const user = await getUserById(userId);
|
||||
return { ok: true, user: publicUser(user) };
|
||||
} catch (err) {
|
||||
@@ -854,10 +898,156 @@ export function createUserAuth(pool, options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
if (patch.spaceQuotaBytes !== undefined) {
|
||||
const quotaBytes = Math.floor(Number(patch.spaceQuotaBytes));
|
||||
if (!Number.isFinite(quotaBytes) || quotaBytes <= 0) {
|
||||
return { ok: false, message: '空间大小无效' };
|
||||
}
|
||||
const [spaceRows] = await pool.query(
|
||||
`SELECT id, quota_bytes, used_bytes, reserved_bytes
|
||||
FROM h5_user_spaces
|
||||
WHERE user_id = ?
|
||||
LIMIT 1`,
|
||||
[userId],
|
||||
);
|
||||
const currentSpace = spaceRows[0];
|
||||
const occupiedBytes = Number(currentSpace?.used_bytes ?? 0) + Number(currentSpace?.reserved_bytes ?? 0);
|
||||
if (quotaBytes < occupiedBytes) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `空间不能小于已使用容量 ${Math.ceil(occupiedBytes / 1024 / 1024)} MB`,
|
||||
};
|
||||
}
|
||||
if (currentSpace?.id) {
|
||||
await pool.query(
|
||||
`UPDATE h5_user_spaces
|
||||
SET quota_bytes = ?, updated_at = ?
|
||||
WHERE user_id = ?`,
|
||||
[quotaBytes, now, userId],
|
||||
);
|
||||
} else {
|
||||
await initializeDefaultSpace(pool, userId, {
|
||||
quotaBytes,
|
||||
now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await getUserById(userId);
|
||||
return { ok: true, user: publicUser(updated) };
|
||||
};
|
||||
|
||||
const purchaseSpaceQuota = async (userId, sizeMb) => {
|
||||
const purchaseMb = Math.floor(Number(sizeMb));
|
||||
if (!Number.isFinite(purchaseMb) || purchaseMb <= 0) {
|
||||
return { ok: false, message: '扩容大小无效' };
|
||||
}
|
||||
|
||||
const deltaBytes = purchaseMb * 1024 * 1024;
|
||||
const costCents = purchaseMb * 200;
|
||||
const now = Date.now();
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
const [spaceRows] = await conn.query(
|
||||
`SELECT id, quota_bytes, used_bytes, reserved_bytes
|
||||
FROM h5_user_spaces
|
||||
WHERE user_id = ?
|
||||
LIMIT 1
|
||||
FOR UPDATE`,
|
||||
[userId],
|
||||
);
|
||||
if (!spaceRows[0]) {
|
||||
await initializeDefaultSpace(conn, userId, { now });
|
||||
}
|
||||
|
||||
const [walletRows] = await conn.query(
|
||||
`SELECT balance_cents
|
||||
FROM h5_user_wallets
|
||||
WHERE user_id = ?
|
||||
FOR UPDATE`,
|
||||
[userId],
|
||||
);
|
||||
const balanceCents = Number(walletRows[0]?.balance_cents ?? 0);
|
||||
if (balanceCents < costCents) {
|
||||
await conn.rollback();
|
||||
return {
|
||||
ok: false,
|
||||
code: 'INSUFFICIENT_BALANCE',
|
||||
message: '余额不足,请先充值后再购买空间',
|
||||
balanceCents,
|
||||
minRechargeCents: Math.max(500, costCents - balanceCents),
|
||||
suggestedTiers: loadRechargeConfig().tiersCents,
|
||||
};
|
||||
}
|
||||
|
||||
await conn.query(
|
||||
`UPDATE h5_user_wallets
|
||||
SET balance_cents = balance_cents - ?, updated_at = ?
|
||||
WHERE user_id = ?`,
|
||||
[costCents, now, userId],
|
||||
);
|
||||
await conn.query(
|
||||
`UPDATE h5_user_spaces
|
||||
SET quota_bytes = quota_bytes + ?, updated_at = ?
|
||||
WHERE user_id = ?`,
|
||||
[deltaBytes, now, userId],
|
||||
);
|
||||
await conn.query(
|
||||
`INSERT INTO h5_billing_ledger
|
||||
(user_id, type, amount_cents, tokens, note, operator_id, created_at)
|
||||
VALUES (?, 'deduct', ?, 0, ?, NULL, ?)`,
|
||||
[userId, costCents, `space_purchase:${purchaseMb}MB`, now],
|
||||
);
|
||||
await conn.query(
|
||||
`INSERT INTO h5_user_notifications
|
||||
(id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at)
|
||||
VALUES (?, ?, 'web', 'space_purchase', ?, ?, ?, 'unread', NULL, ?, ?)`,
|
||||
[
|
||||
crypto.randomUUID(),
|
||||
userId,
|
||||
'空间扩容成功',
|
||||
`已购买 ${purchaseMb} MB 空间,支付 ¥${(costCents / 100).toFixed(2)}。`,
|
||||
JSON.stringify({ purchaseMb, deltaBytes, costCents }),
|
||||
now,
|
||||
now,
|
||||
],
|
||||
);
|
||||
|
||||
await conn.commit();
|
||||
const [updatedSpaceRows] = await pool.query(
|
||||
`SELECT quota_bytes, used_bytes, reserved_bytes
|
||||
FROM h5_user_spaces
|
||||
WHERE user_id = ?
|
||||
LIMIT 1`,
|
||||
[userId],
|
||||
);
|
||||
const updatedSpace = updatedSpaceRows[0] ?? {};
|
||||
const updatedUser = await getUserById(userId);
|
||||
return {
|
||||
ok: true,
|
||||
balanceCents: Number(updatedUser?.balance_cents ?? Math.max(0, balanceCents - costCents)),
|
||||
quota: {
|
||||
quotaBytes: Number(updatedSpace.quota_bytes ?? 0),
|
||||
usedBytes: Number(updatedSpace.used_bytes ?? 0),
|
||||
reservedBytes: Number(updatedSpace.reserved_bytes ?? 0),
|
||||
availableBytes: Math.max(
|
||||
0,
|
||||
Number(updatedSpace.quota_bytes ?? 0)
|
||||
- Number(updatedSpace.used_bytes ?? 0)
|
||||
- Number(updatedSpace.reserved_bytes ?? 0),
|
||||
),
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
const recharge = async (userId, amountCents, operatorId, note = '', options = {}) => {
|
||||
const amount = Number(amountCents);
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
@@ -870,6 +1060,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
const ledgerNote = paymentOrderId
|
||||
? `order:${paymentOrderId}`
|
||||
: note || (operatorId ? '管理员充值' : '账户充值');
|
||||
const rechargeType = paymentOrderId ? 'self_recharge' : operatorId ? 'admin_recharge' : 'recharge';
|
||||
const now = Date.now();
|
||||
const ownsConnection = !options.conn;
|
||||
const conn = options.conn ?? (await pool.getConnection());
|
||||
@@ -889,6 +1080,31 @@ export function createUserAuth(pool, options = {}) {
|
||||
VALUES (?, 'recharge', ?, 0, ?, ?, ?)`,
|
||||
[userId, amount, ledgerNote, operatorId, now],
|
||||
);
|
||||
const title = paymentOrderId ? '充值成功' : operatorId ? '管理员已充值' : '账户充值成功';
|
||||
const body = paymentOrderId
|
||||
? `你已成功充值 ¥${(amount / 100).toFixed(2)},余额已更新。`
|
||||
: operatorId
|
||||
? `管理员已为你充值 ¥${(amount / 100).toFixed(2)},余额已更新。`
|
||||
: `你的账户已充值 ¥${(amount / 100).toFixed(2)},余额已更新。`;
|
||||
await conn.query(
|
||||
`INSERT INTO h5_user_notifications
|
||||
(id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at)
|
||||
VALUES (?, ?, 'web', ?, ?, ?, ?, 'unread', NULL, ?, ?)`,
|
||||
[
|
||||
crypto.randomUUID(),
|
||||
userId,
|
||||
rechargeType,
|
||||
title,
|
||||
body,
|
||||
JSON.stringify({
|
||||
amountCents: amount,
|
||||
operatorId: operatorId ?? null,
|
||||
paymentOrderId,
|
||||
}),
|
||||
now,
|
||||
now,
|
||||
],
|
||||
);
|
||||
if (user.status === 'suspended') {
|
||||
await conn.query(`UPDATE h5_users SET status = 'active', updated_at = ? WHERE id = ?`, [
|
||||
now,
|
||||
@@ -897,6 +1113,25 @@ export function createUserAuth(pool, options = {}) {
|
||||
}
|
||||
if (ownsConnection) await conn.commit();
|
||||
const updated = await getUserById(userId);
|
||||
if (rechargeNotifier) {
|
||||
try {
|
||||
await rechargeNotifier({
|
||||
userId,
|
||||
amountCents: amount,
|
||||
operatorId: operatorId ?? null,
|
||||
paymentOrderId,
|
||||
notificationType: rechargeType,
|
||||
title,
|
||||
body,
|
||||
user: publicUser(updated),
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Recharge notifier failed:',
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
}
|
||||
}
|
||||
return { ok: true, user: publicUser(updated) };
|
||||
} catch (err) {
|
||||
if (ownsConnection) await conn.rollback();
|
||||
@@ -956,7 +1191,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
};
|
||||
}
|
||||
const config = loadBillingConfig();
|
||||
const costCents = computeDeltaCostCents(previous, tokenState, config);
|
||||
let costCents = computeDeltaCostCents(previous, tokenState, config);
|
||||
const deltaIn = Math.max(
|
||||
0,
|
||||
tokenState.accumulatedInputTokens - Number(previous?.lastInputTokens ?? 0),
|
||||
@@ -991,6 +1226,16 @@ export function createUserAuth(pool, options = {}) {
|
||||
],
|
||||
);
|
||||
|
||||
// Subscription quota check: consume tokens from active plan before touching balance.
|
||||
if (costCents > 0 && subscriptionService) {
|
||||
const coverage = await subscriptionService.consumeQuota(userId, deltaTokens, conn);
|
||||
if (coverage.fullyCovers) {
|
||||
costCents = 0;
|
||||
} else if (coverage.overageRate < 1.0) {
|
||||
costCents = Math.max(1, Math.ceil(costCents * coverage.overageRate));
|
||||
}
|
||||
}
|
||||
|
||||
let balanceAfter = null;
|
||||
let tokensUsedAfter = null;
|
||||
if (costCents > 0) {
|
||||
@@ -1339,9 +1584,12 @@ export function createUserAuth(pool, options = {}) {
|
||||
const resolveUserCapabilities = async (user) => {
|
||||
if (!user) return { unrestricted: true, capabilities: {} };
|
||||
if (user.role === 'admin') {
|
||||
const skillMap = Object.fromEntries(skillCatalog.map((item) => [item.name, true]));
|
||||
return {
|
||||
unrestricted: true,
|
||||
capabilities: Object.fromEntries(catalogKeys().map((key) => [key, true])),
|
||||
skills: skillMap,
|
||||
grantedSkills: grantedSkillNames(skillMap),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1384,15 +1632,17 @@ export function createUserAuth(pool, options = {}) {
|
||||
capabilityState.capabilities,
|
||||
policyState.policies,
|
||||
);
|
||||
// For static_publish users, wire up the sandbox MCP so file operations are
|
||||
// enforced at the OS level rather than relying on text prompt constraints.
|
||||
// Wire up the sandbox MCP for user workspace tools and the private data space.
|
||||
// File operations are OS-bound to the user's workspace; private_data_* tools
|
||||
// only touch the user's single SQLite data space inside that workspace.
|
||||
let sandboxMcp = null;
|
||||
if (effectiveCapabilities.static_publish) {
|
||||
if (effectiveCapabilities.static_publish || effectiveCapabilities.private_data_space) {
|
||||
try {
|
||||
const layout = await publishLayoutFor(user, { migrateLegacy: false });
|
||||
sandboxMcp = {
|
||||
serverPath: path.join(h5Root, 'mindspace-sandbox-mcp.mjs'),
|
||||
serverPath: resolveSandboxMcpServerPath(),
|
||||
sandboxRoot: layout.publishDir,
|
||||
userId: user.id,
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[getAgentSessionPolicy] sandbox MCP setup failed, falling back:', err?.message);
|
||||
@@ -1621,40 +1871,45 @@ export function createUserAuth(pool, options = {}) {
|
||||
const ensureAdminUser = async () => {
|
||||
const adminUsername = normalizeUsername(process.env.H5_ADMIN_USERNAME ?? 'admin');
|
||||
const adminPassword = process.env.H5_ADMIN_PASSWORD;
|
||||
if (!adminPassword) return;
|
||||
|
||||
const [rows] = await pool.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [
|
||||
adminUsername,
|
||||
]);
|
||||
const adminWorkspace = resolveAdminWorkspaceRoot();
|
||||
if (rows.length === 0) {
|
||||
if (!adminPassword) return;
|
||||
await createUser({
|
||||
username: adminUsername,
|
||||
password: adminPassword,
|
||||
displayName: '管理员',
|
||||
workspaceRoot: adminWorkspace,
|
||||
balanceCents: 9_999_999_99,
|
||||
role: 'admin',
|
||||
});
|
||||
} else {
|
||||
const adminId = rows[0].id;
|
||||
const now = Date.now();
|
||||
const adminLayout = await publishLayoutFor({
|
||||
id: adminId,
|
||||
username: adminUsername,
|
||||
displayName: '管理员',
|
||||
});
|
||||
await pool.query(`UPDATE h5_users SET workspace_root = ?, updated_at = ? WHERE id = ?`, [
|
||||
adminWorkspace,
|
||||
adminLayout.publishDir,
|
||||
now,
|
||||
adminId,
|
||||
]);
|
||||
await pool.query(`DELETE FROM h5_user_path_grants WHERE user_id = ?`, [adminId]);
|
||||
await pool.query(
|
||||
`INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
|
||||
[adminId, adminWorkspace],
|
||||
[adminId, adminLayout.publishDir],
|
||||
);
|
||||
await pool.query(
|
||||
`UPDATE h5_user_wallets SET balance_cents = GREATEST(balance_cents, ?), updated_at = ? WHERE user_id = ?`,
|
||||
[9_999_999_99, now, adminId],
|
||||
);
|
||||
}
|
||||
await syncAdminPassword();
|
||||
if (adminPassword) {
|
||||
await syncAdminPassword();
|
||||
}
|
||||
await seedRoleCapabilityDefaults();
|
||||
await upgradeMemoryStoreCapability();
|
||||
await upgradeDefaultUserCapabilities();
|
||||
@@ -1768,24 +2023,20 @@ export function createUserAuth(pool, options = {}) {
|
||||
status = 'active',
|
||||
now = Date.now(),
|
||||
}) => {
|
||||
const existing = await getWechatAgentRoute(appId, openid);
|
||||
if (existing) {
|
||||
await pool.query(
|
||||
`UPDATE h5_wechat_agent_routes
|
||||
SET user_id = ?, agent_session_id = ?, status = ?, updated_at = ?
|
||||
WHERE app_id = ? AND openid = ?`,
|
||||
[userId, agentSessionId, status, now, appId, openid],
|
||||
);
|
||||
return existing.id;
|
||||
}
|
||||
const id = crypto.randomUUID();
|
||||
await pool.query(
|
||||
`INSERT INTO h5_wechat_agent_routes
|
||||
(id, user_id, app_id, openid, agent_session_id, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
user_id = VALUES(user_id),
|
||||
agent_session_id = VALUES(agent_session_id),
|
||||
status = VALUES(status),
|
||||
updated_at = VALUES(updated_at)`,
|
||||
[id, userId, appId, openid, agentSessionId, status, now, now],
|
||||
);
|
||||
return id;
|
||||
const route = await getWechatAgentRoute(appId, openid);
|
||||
return route?.id ?? id;
|
||||
};
|
||||
|
||||
const clearWechatAgentRoute = async (appId, openid) => {
|
||||
@@ -1842,6 +2093,62 @@ export function createUserAuth(pool, options = {}) {
|
||||
);
|
||||
};
|
||||
|
||||
const insertWechatMpMessageDetail = async ({
|
||||
appId,
|
||||
openid,
|
||||
userId = null,
|
||||
msgId = null,
|
||||
msgType,
|
||||
displayText = '',
|
||||
agentText = '',
|
||||
mediaId = null,
|
||||
mediaUrl = null,
|
||||
mediaPublicUrl = null,
|
||||
mediaFormat = null,
|
||||
locationLat = null,
|
||||
locationLng = null,
|
||||
locationLabel = null,
|
||||
linkUrl = null,
|
||||
linkTitle = null,
|
||||
rawXmlHash = null,
|
||||
rawJson = null,
|
||||
now = Date.now(),
|
||||
}) => {
|
||||
if (!appId || !openid || !msgType) return null;
|
||||
const id = crypto.randomUUID();
|
||||
await pool.query(
|
||||
`INSERT INTO h5_wechat_mp_message_details
|
||||
(id, app_id, openid, user_id, msg_id, msg_type, display_text, agent_text,
|
||||
media_id, media_url, media_public_url, media_format,
|
||||
location_lat, location_lng, location_label,
|
||||
link_url, link_title, raw_xml_hash, raw_json, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
id,
|
||||
appId,
|
||||
openid,
|
||||
userId,
|
||||
msgId ? String(msgId) : null,
|
||||
String(msgType),
|
||||
displayText || null,
|
||||
agentText || null,
|
||||
mediaId,
|
||||
mediaUrl,
|
||||
mediaPublicUrl,
|
||||
mediaFormat,
|
||||
locationLat,
|
||||
locationLng,
|
||||
locationLabel,
|
||||
linkUrl,
|
||||
linkTitle,
|
||||
rawXmlHash,
|
||||
rawJson ? JSON.stringify(rawJson) : null,
|
||||
now,
|
||||
],
|
||||
);
|
||||
return id;
|
||||
};
|
||||
|
||||
const bindWechatToUser = async ({
|
||||
userId,
|
||||
appId,
|
||||
@@ -2082,6 +2389,9 @@ export function createUserAuth(pool, options = {}) {
|
||||
username: normalized,
|
||||
slug: normalized,
|
||||
});
|
||||
if (subscriptionService) {
|
||||
subscriptionService.grantSubscription(userId, 'free', null, null, '注册赠送免费套餐').catch(() => {});
|
||||
}
|
||||
const user = await getUserById(userId);
|
||||
return { ok: true, user: publicUser(user) };
|
||||
} catch (err) {
|
||||
@@ -2313,12 +2623,16 @@ export function createUserAuth(pool, options = {}) {
|
||||
getWechatPendingBind,
|
||||
getWechatBindingStatus,
|
||||
getWechatOpenidForUser,
|
||||
setRechargeNotifier(callback) {
|
||||
rechargeNotifier = typeof callback === 'function' ? callback : null;
|
||||
},
|
||||
findWechatUserByOpenid,
|
||||
getWechatAgentRoute,
|
||||
upsertWechatAgentRoute,
|
||||
clearWechatAgentRoute,
|
||||
recordWechatMpMessage,
|
||||
finishWechatMpMessage,
|
||||
insertWechatMpMessageDetail,
|
||||
resetPassword,
|
||||
verify,
|
||||
revoke,
|
||||
@@ -2335,9 +2649,12 @@ export function createUserAuth(pool, options = {}) {
|
||||
ownsSession,
|
||||
listOwnedSessionIds,
|
||||
canUseChat,
|
||||
getUserById,
|
||||
getUserPublic,
|
||||
listUsers,
|
||||
createUser,
|
||||
updateUser,
|
||||
purchaseSpaceQuota,
|
||||
recharge,
|
||||
billSessionUsage,
|
||||
listUsageRecords,
|
||||
@@ -2449,12 +2766,26 @@ export function resolveCookieDomainForRequest(req) {
|
||||
if (hostname.endsWith('.localhost')) {
|
||||
return '.localhost';
|
||||
}
|
||||
if (
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname === '::1' ||
|
||||
net.isIP(hostname)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const hostname = String(req?.get?.('host') ?? req?.hostname ?? '')
|
||||
.split(':')[0]
|
||||
.toLowerCase();
|
||||
if (!hostname || hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
|
||||
if (
|
||||
!hostname ||
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname === '::1' ||
|
||||
net.isIP(hostname)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return resolveCookieDomain();
|
||||
|
||||
Reference in New Issue
Block a user