Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+193
-217
@@ -1,7 +1,6 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import argon2 from 'argon2';
|
||||
import { computeDeltaCostCents, loadBillingConfig, normalizeTokenState } from './billing.mjs';
|
||||
import { buildInsufficientBalancePayload, loadRechargeConfig } from './billing-recharge.mjs';
|
||||
import {
|
||||
@@ -65,38 +64,25 @@ function hashPasswordPbkdf2(password, salt) {
|
||||
return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
|
||||
}
|
||||
|
||||
async function hashPasswordArgon2id(password, salt) {
|
||||
if (typeof crypto.argon2Sync === 'function') {
|
||||
return crypto
|
||||
.argon2Sync(PASSWORD_ALGORITHM_ARGON2ID, {
|
||||
message: password,
|
||||
nonce: Buffer.from(salt, 'hex'),
|
||||
parallelism: ARGON2_PARALLELISM,
|
||||
tagLength: ARGON2_TAG_LENGTH,
|
||||
memory: ARGON2_MEMORY,
|
||||
passes: ARGON2_PASSES,
|
||||
})
|
||||
.toString('hex');
|
||||
}
|
||||
return (
|
||||
await argon2.hash(password, {
|
||||
type: argon2.argon2id,
|
||||
salt: Buffer.from(salt, 'hex'),
|
||||
memoryCost: ARGON2_MEMORY,
|
||||
timeCost: ARGON2_PASSES,
|
||||
function hashPasswordArgon2id(password, salt) {
|
||||
return crypto
|
||||
.argon2Sync(PASSWORD_ALGORITHM_ARGON2ID, {
|
||||
message: password,
|
||||
nonce: Buffer.from(salt, 'hex'),
|
||||
parallelism: ARGON2_PARALLELISM,
|
||||
hashLength: ARGON2_TAG_LENGTH,
|
||||
raw: true,
|
||||
tagLength: ARGON2_TAG_LENGTH,
|
||||
memory: ARGON2_MEMORY,
|
||||
passes: ARGON2_PASSES,
|
||||
})
|
||||
).toString('hex');
|
||||
.toString('hex');
|
||||
}
|
||||
|
||||
async function createPasswordRecord(password, algorithm = PASSWORD_ALGORITHM_ARGON2ID) {
|
||||
function createPasswordRecord(password, algorithm = PASSWORD_ALGORITHM_ARGON2ID) {
|
||||
const salt = crypto.randomBytes(16).toString('hex');
|
||||
if (algorithm === PASSWORD_ALGORITHM_ARGON2ID) {
|
||||
return {
|
||||
salt,
|
||||
passwordHash: await hashPasswordArgon2id(password, salt),
|
||||
passwordHash: hashPasswordArgon2id(password, salt),
|
||||
passwordAlgorithm: PASSWORD_ALGORITHM_ARGON2ID,
|
||||
};
|
||||
}
|
||||
@@ -107,10 +93,10 @@ async function createPasswordRecord(password, algorithm = PASSWORD_ALGORITHM_ARG
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyPassword(password, row) {
|
||||
function verifyPassword(password, row) {
|
||||
const algorithm = row.password_algorithm || PASSWORD_ALGORITHM_PBKDF2;
|
||||
if (algorithm === PASSWORD_ALGORITHM_ARGON2ID) {
|
||||
return safeEqual(await hashPasswordArgon2id(password, row.salt), row.password_hash);
|
||||
return safeEqual(hashPasswordArgon2id(password, row.salt), row.password_hash);
|
||||
}
|
||||
return safeEqual(hashPasswordPbkdf2(password, row.salt), row.password_hash);
|
||||
}
|
||||
@@ -351,7 +337,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
return { ok: false, message: '请输入有效邮箱' };
|
||||
}
|
||||
|
||||
const { salt, passwordHash, passwordAlgorithm } = await createPasswordRecord(password);
|
||||
const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password);
|
||||
const userId = crypto.randomUUID();
|
||||
const layout = await publishLayoutFor({ id: userId, username: normalized });
|
||||
const workspaceRoot = layout.publishDir;
|
||||
@@ -447,7 +433,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
return { ok: false, message: '用户名或密码错误' };
|
||||
}
|
||||
|
||||
if (!(await verifyPassword(password, row))) {
|
||||
if (!verifyPassword(password, row)) {
|
||||
const current =
|
||||
failure && failure.resetAt > now
|
||||
? failure
|
||||
@@ -461,7 +447,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
}
|
||||
|
||||
if ((row.password_algorithm || PASSWORD_ALGORITHM_PBKDF2) !== PASSWORD_ALGORITHM_ARGON2ID) {
|
||||
const nextPassword = await createPasswordRecord(password);
|
||||
const nextPassword = createPasswordRecord(password);
|
||||
await pool.query(
|
||||
`UPDATE h5_users
|
||||
SET salt = ?, password_hash = ?, password_algorithm = ?, updated_at = ?
|
||||
@@ -504,7 +490,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
return { ok: false, message: '账户已禁用,请联系管理员' };
|
||||
}
|
||||
|
||||
const { salt, passwordHash, passwordAlgorithm } = await createPasswordRecord(password);
|
||||
const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password);
|
||||
const now = Date.now();
|
||||
await pool.query(
|
||||
`UPDATE h5_users SET salt = ?, password_hash = ?, password_algorithm = ?, updated_at = ? WHERE id = ?`,
|
||||
@@ -696,20 +682,40 @@ export function createUserAuth(pool, options = {}) {
|
||||
return { ok: true, balanceCents };
|
||||
};
|
||||
|
||||
const listUsers = async () => {
|
||||
const listUsers = async ({ page = 1, pageSize = 20, search = '', role = '', status = '' } = {}) => {
|
||||
const safePageSize = Math.min(Math.max(Number(pageSize) || 20, 1), 100);
|
||||
const safePage = Math.max(Number(page) || 1, 1);
|
||||
const offset = (safePage - 1) * safePageSize;
|
||||
const params = [];
|
||||
const clauses = [];
|
||||
if (search) {
|
||||
clauses.push('(u.username LIKE ? OR u.display_name LIKE ?)');
|
||||
params.push(`%${search}%`, `%${search}%`);
|
||||
}
|
||||
if (role) { clauses.push('u.role = ?'); params.push(role); }
|
||||
if (status) { clauses.push('u.status = ?'); params.push(status); }
|
||||
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
||||
const [[{ total }]] = await pool.query(
|
||||
`SELECT COUNT(*) AS total FROM h5_users u ${where}`,
|
||||
params,
|
||||
);
|
||||
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,
|
||||
u.created_at, u.updated_at, w.balance_cents, w.tokens_used
|
||||
FROM h5_users u
|
||||
LEFT JOIN h5_user_wallets w ON w.user_id = u.id
|
||||
ORDER BY u.created_at DESC`,
|
||||
${where}
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT ${safePageSize} OFFSET ${offset}`,
|
||||
params,
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
...publicUser(row),
|
||||
createdAt: Number(row.created_at),
|
||||
updatedAt: Number(row.updated_at),
|
||||
}));
|
||||
return {
|
||||
users: rows.map((row) => ({ ...publicUser(row), createdAt: Number(row.created_at), updatedAt: Number(row.updated_at) })),
|
||||
total: Number(total),
|
||||
page: safePage,
|
||||
pageSize: safePageSize,
|
||||
};
|
||||
};
|
||||
|
||||
const createUser = async ({
|
||||
@@ -734,7 +740,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
const root = isAdmin
|
||||
? path.resolve(workspaceRoot || path.join(usersRoot, normalized))
|
||||
: (await publishLayoutFor({ id: userId, username: normalized })).publishDir;
|
||||
const { salt, passwordHash, passwordAlgorithm } = await createPasswordRecord(password);
|
||||
const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password);
|
||||
const now = Date.now();
|
||||
|
||||
const conn = await pool.getConnection();
|
||||
@@ -926,65 +932,55 @@ export function createUserAuth(pool, options = {}) {
|
||||
};
|
||||
}
|
||||
const tokenState = normalizeTokenState(tokenStateRaw);
|
||||
const previous = await getBillingState(agentSessionId);
|
||||
if (
|
||||
previous &&
|
||||
tokenState.accumulatedInputTokens <= Number(previous.lastInputTokens ?? 0) &&
|
||||
tokenState.accumulatedOutputTokens <= Number(previous.lastOutputTokens ?? 0)
|
||||
) {
|
||||
const user = await getUserById(userId);
|
||||
return {
|
||||
ok: true,
|
||||
costCents: 0,
|
||||
balanceCents: user ? Number(user.balance_cents) : null,
|
||||
tokensUsed: user ? Number(user.tokens_used ?? 0) : null,
|
||||
deltaInputTokens: 0,
|
||||
deltaOutputTokens: 0,
|
||||
};
|
||||
}
|
||||
const config = loadBillingConfig();
|
||||
const costCents = computeDeltaCostCents(previous, tokenState, config);
|
||||
const deltaIn = Math.max(
|
||||
0,
|
||||
tokenState.accumulatedInputTokens - Number(previous?.lastInputTokens ?? 0),
|
||||
);
|
||||
const deltaOut = Math.max(
|
||||
0,
|
||||
tokenState.accumulatedOutputTokens - Number(previous?.lastOutputTokens ?? 0),
|
||||
);
|
||||
const deltaTokens = deltaIn + deltaOut;
|
||||
|
||||
const now = Date.now();
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// Serialize concurrent Finish handlers for the same session.
|
||||
await conn.query(
|
||||
`INSERT INTO h5_session_billing_state
|
||||
(agent_session_id, user_id, last_accumulated_cost, last_input_tokens, last_output_tokens, updated_at)
|
||||
VALUES (?, ?, NULL, 0, 0, ?)
|
||||
ON DUPLICATE KEY UPDATE agent_session_id = agent_session_id`,
|
||||
[agentSessionId, userId, now],
|
||||
);
|
||||
const [stateRows] = await conn.query(
|
||||
`SELECT last_accumulated_cost, last_input_tokens, last_output_tokens
|
||||
FROM h5_session_billing_state
|
||||
WHERE agent_session_id = ?
|
||||
FOR UPDATE`,
|
||||
[agentSessionId],
|
||||
);
|
||||
const stateRow = stateRows[0];
|
||||
const previous = {
|
||||
lastAccumulatedCost: stateRow?.last_accumulated_cost ?? null,
|
||||
lastInputTokens: Number(stateRow?.last_input_tokens ?? 0),
|
||||
lastOutputTokens: Number(stateRow?.last_output_tokens ?? 0),
|
||||
};
|
||||
|
||||
if (
|
||||
tokenState.accumulatedInputTokens <= previous.lastInputTokens &&
|
||||
tokenState.accumulatedOutputTokens <= previous.lastOutputTokens
|
||||
) {
|
||||
await conn.commit();
|
||||
const fresh = await getUserById(userId);
|
||||
return {
|
||||
ok: true,
|
||||
costCents: 0,
|
||||
balanceCents: fresh ? Number(fresh.balance_cents) : null,
|
||||
tokensUsed: fresh ? Number(fresh.tokens_used ?? 0) : null,
|
||||
deltaInputTokens: 0,
|
||||
deltaOutputTokens: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const costCents = computeDeltaCostCents(previous, tokenState, config);
|
||||
const deltaIn = Math.max(0, tokenState.accumulatedInputTokens - previous.lastInputTokens);
|
||||
const deltaOut = Math.max(0, tokenState.accumulatedOutputTokens - previous.lastOutputTokens);
|
||||
const deltaTokens = deltaIn + deltaOut;
|
||||
|
||||
await conn.query(
|
||||
`UPDATE h5_session_billing_state
|
||||
SET last_accumulated_cost = ?, last_input_tokens = ?, last_output_tokens = ?, updated_at = ?
|
||||
WHERE agent_session_id = ?`,
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_accumulated_cost = VALUES(last_accumulated_cost),
|
||||
last_input_tokens = VALUES(last_input_tokens),
|
||||
last_output_tokens = VALUES(last_output_tokens),
|
||||
updated_at = VALUES(updated_at)`,
|
||||
[
|
||||
agentSessionId,
|
||||
userId,
|
||||
tokenState.accumulatedCost,
|
||||
tokenState.accumulatedInputTokens,
|
||||
tokenState.accumulatedOutputTokens,
|
||||
now,
|
||||
agentSessionId,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1063,152 +1059,89 @@ export function createUserAuth(pool, options = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageSummary = async ({ userId = null } = {}) => {
|
||||
const since24h = Date.now() - 24 * 60 * 60 * 1000;
|
||||
|
||||
if (userId) {
|
||||
const user = await getUserById(userId);
|
||||
if (!user) return null;
|
||||
|
||||
const [[usage24h]] = await pool.query(
|
||||
`SELECT COUNT(*) AS request_count,
|
||||
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
||||
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
||||
COALESCE(SUM(cost_cents), 0) AS cost_cents
|
||||
FROM h5_usage_records
|
||||
WHERE user_id = ? AND created_at >= ?`,
|
||||
[userId, since24h],
|
||||
const listUsageRecords = async ({ userId = null, page = 1, pageSize = 20, limit = null } = {}) => {
|
||||
// legacy: if limit is passed (from dashboard summary), skip pagination
|
||||
if (limit !== null) {
|
||||
const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), 200);
|
||||
const params = [];
|
||||
const where = userId ? 'WHERE r.user_id = ?' : '';
|
||||
if (userId) params.push(userId);
|
||||
const [rows] = await pool.query(
|
||||
`SELECT r.id, r.user_id, u.username, r.agent_session_id, r.request_id,
|
||||
r.input_tokens, r.output_tokens, r.cost_cents, r.balance_after_cents, r.created_at
|
||||
FROM h5_usage_records r JOIN h5_users u ON u.id = r.user_id
|
||||
${where} ORDER BY r.created_at DESC LIMIT ${safeLimit}`,
|
||||
params,
|
||||
);
|
||||
|
||||
const [[deductAll]] = await pool.query(
|
||||
`SELECT COUNT(*) AS request_count,
|
||||
COALESCE(SUM(ABS(amount_cents)), 0) AS cost_cents
|
||||
FROM h5_billing_ledger
|
||||
WHERE user_id = ? AND type = 'deduct'`,
|
||||
[userId],
|
||||
);
|
||||
|
||||
return {
|
||||
allTime: {
|
||||
requestCount: Number(deductAll.request_count),
|
||||
inputTokens: null,
|
||||
outputTokens: null,
|
||||
totalTokens: Number(user.tokens_used ?? 0),
|
||||
costCents: Number(user.spent_cents ?? deductAll.cost_cents),
|
||||
},
|
||||
last24h: {
|
||||
requestCount: Number(usage24h.request_count),
|
||||
inputTokens: Number(usage24h.input_tokens),
|
||||
outputTokens: Number(usage24h.output_tokens),
|
||||
totalTokens:
|
||||
Number(usage24h.input_tokens) + Number(usage24h.output_tokens),
|
||||
costCents: Number(usage24h.cost_cents),
|
||||
},
|
||||
};
|
||||
return rows.map((row) => ({ id: Number(row.id), userId: row.user_id, username: row.username, agentSessionId: row.agent_session_id, requestId: row.request_id, inputTokens: Number(row.input_tokens), outputTokens: Number(row.output_tokens), costCents: Number(row.cost_cents), balanceAfterCents: Number(row.balance_after_cents), createdAt: Number(row.created_at) }));
|
||||
}
|
||||
|
||||
const [[wallet]] = await pool.query(
|
||||
`SELECT COALESCE(SUM(tokens_used), 0) AS total_tokens FROM h5_user_wallets`,
|
||||
);
|
||||
const [[deductAll]] = await pool.query(
|
||||
`SELECT COUNT(*) AS request_count,
|
||||
COALESCE(SUM(ABS(amount_cents)), 0) AS cost_cents
|
||||
FROM h5_billing_ledger
|
||||
WHERE type = 'deduct'`,
|
||||
);
|
||||
const [[deduct24h]] = await pool.query(
|
||||
`SELECT COUNT(*) AS request_count,
|
||||
COALESCE(SUM(ABS(amount_cents)), 0) AS cost_cents,
|
||||
COALESCE(SUM(tokens), 0) AS total_tokens
|
||||
FROM h5_billing_ledger
|
||||
WHERE type = 'deduct' AND created_at >= ?`,
|
||||
[since24h],
|
||||
);
|
||||
|
||||
return {
|
||||
allTime: {
|
||||
requestCount: Number(deductAll.request_count),
|
||||
inputTokens: null,
|
||||
outputTokens: null,
|
||||
totalTokens: Number(wallet.total_tokens),
|
||||
costCents: Number(deductAll.cost_cents),
|
||||
},
|
||||
last24h: {
|
||||
requestCount: Number(deduct24h.request_count),
|
||||
inputTokens: null,
|
||||
outputTokens: null,
|
||||
totalTokens: Number(deduct24h.total_tokens),
|
||||
costCents: Number(deduct24h.cost_cents),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const listUsageRecords = async ({ userId = null, limit = 50 } = {}) => {
|
||||
const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), 200);
|
||||
const safePageSize = Math.min(Math.max(Number(pageSize) || 50, 1), 200);
|
||||
const safePage = Math.max(Number(page) || 1, 1);
|
||||
const offset = (safePage - 1) * safePageSize;
|
||||
const params = [];
|
||||
let where = '';
|
||||
if (userId) {
|
||||
where = 'WHERE r.user_id = ?';
|
||||
params.push(userId);
|
||||
}
|
||||
const where = userId ? 'WHERE r.user_id = ?' : '';
|
||||
if (userId) params.push(userId);
|
||||
const [[{ total }]] = await pool.query(
|
||||
`SELECT COUNT(*) AS total FROM h5_usage_records r ${where}`,
|
||||
params,
|
||||
);
|
||||
const [rows] = await pool.query(
|
||||
`SELECT r.id, r.user_id, u.username, r.agent_session_id, r.request_id,
|
||||
r.input_tokens, r.output_tokens, r.cost_cents, r.balance_after_cents, r.created_at
|
||||
FROM h5_usage_records r
|
||||
JOIN h5_users u ON u.id = r.user_id
|
||||
FROM h5_usage_records r JOIN h5_users u ON u.id = r.user_id
|
||||
${where}
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT ${safeLimit}`,
|
||||
LIMIT ${safePageSize} OFFSET ${offset}`,
|
||||
params,
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
id: Number(row.id),
|
||||
userId: row.user_id,
|
||||
username: row.username,
|
||||
agentSessionId: row.agent_session_id,
|
||||
requestId: row.request_id,
|
||||
inputTokens: Number(row.input_tokens),
|
||||
outputTokens: Number(row.output_tokens),
|
||||
costCents: Number(row.cost_cents),
|
||||
balanceAfterCents: Number(row.balance_after_cents),
|
||||
createdAt: Number(row.created_at),
|
||||
}));
|
||||
return {
|
||||
records: rows.map((row) => ({ id: Number(row.id), userId: row.user_id, username: row.username, agentSessionId: row.agent_session_id, requestId: row.request_id, inputTokens: Number(row.input_tokens), outputTokens: Number(row.output_tokens), costCents: Number(row.cost_cents), balanceAfterCents: Number(row.balance_after_cents), createdAt: Number(row.created_at) })),
|
||||
total: Number(total),
|
||||
page: safePage,
|
||||
pageSize: safePageSize,
|
||||
};
|
||||
};
|
||||
|
||||
const listBillingLedger = async ({ userId = null, limit = 50, types = null } = {}) => {
|
||||
const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), 200);
|
||||
const params = [];
|
||||
const clauses = [];
|
||||
if (userId) {
|
||||
clauses.push('l.user_id = ?');
|
||||
params.push(userId);
|
||||
const listBillingLedger = async ({ userId = null, page = 1, pageSize = 20, limit = null, types = null } = {}) => {
|
||||
const buildWhere = (params) => {
|
||||
const clauses = [];
|
||||
if (userId) { 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);
|
||||
}
|
||||
return clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
||||
};
|
||||
const mapRow = (row) => ({ id: Number(row.id), userId: row.user_id, username: row.username, type: row.type, amountCents: Number(row.amount_cents), tokens: Number(row.tokens), sessionId: row.session_id, note: row.note, createdAt: Number(row.created_at) });
|
||||
// legacy: if limit is passed (from dashboard summary), skip pagination
|
||||
if (limit !== null) {
|
||||
const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), 200);
|
||||
const params = [];
|
||||
const where = buildWhere(params);
|
||||
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 FROM h5_billing_ledger l JOIN h5_users u ON u.id = l.user_id ${where} ORDER BY l.created_at DESC LIMIT ${safeLimit}`, params);
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
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 safePageSize = Math.min(Math.max(Number(pageSize) || 50, 1), 200);
|
||||
const safePage = Math.max(Number(page) || 1, 1);
|
||||
const offset = (safePage - 1) * safePageSize;
|
||||
const countParams = [];
|
||||
const where = buildWhere(countParams);
|
||||
const [[{ total }]] = await pool.query(
|
||||
`SELECT COUNT(*) AS total FROM h5_billing_ledger l ${where}`,
|
||||
countParams,
|
||||
);
|
||||
const dataParams = [];
|
||||
const dataWhere = buildWhere(dataParams);
|
||||
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
|
||||
FROM h5_billing_ledger l
|
||||
JOIN h5_users u ON u.id = l.user_id
|
||||
${where}
|
||||
FROM h5_billing_ledger l JOIN h5_users u ON u.id = l.user_id
|
||||
${dataWhere}
|
||||
ORDER BY l.created_at DESC
|
||||
LIMIT ${safeLimit}`,
|
||||
params,
|
||||
LIMIT ${safePageSize} OFFSET ${offset}`,
|
||||
dataParams,
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
id: Number(row.id),
|
||||
userId: row.user_id,
|
||||
username: row.username,
|
||||
type: row.type,
|
||||
amountCents: Number(row.amount_cents),
|
||||
tokens: Number(row.tokens),
|
||||
sessionId: row.session_id,
|
||||
note: row.note,
|
||||
createdAt: Number(row.created_at),
|
||||
}));
|
||||
return { entries: rows.map(mapRow), total: Number(total), page: safePage, pageSize: safePageSize };
|
||||
};
|
||||
|
||||
const getAdminSummary = async () => {
|
||||
@@ -1277,7 +1210,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
);
|
||||
if (rows.length === 0) return;
|
||||
|
||||
const { salt, passwordHash, passwordAlgorithm } = await createPasswordRecord(adminPassword);
|
||||
const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(adminPassword);
|
||||
const now = Date.now();
|
||||
await pool.query(
|
||||
`UPDATE h5_users SET salt = ?, password_hash = ?, password_algorithm = ?, updated_at = ? WHERE id = ?`,
|
||||
@@ -1444,11 +1377,27 @@ 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.
|
||||
let sandboxMcp = null;
|
||||
if (effectiveCapabilities.static_publish) {
|
||||
try {
|
||||
const layout = await publishLayoutFor(user, { migrateLegacy: false });
|
||||
sandboxMcp = {
|
||||
serverPath: path.join(h5Root, 'mindspace-sandbox-mcp.mjs'),
|
||||
sandboxRoot: layout.publishDir,
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[getAgentSessionPolicy] sandbox MCP setup failed, falling back:', err?.message);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...buildAgentExtensionPolicy(effectiveCapabilities, {
|
||||
unrestricted: false,
|
||||
policies: policyState.policies,
|
||||
sandboxMcp,
|
||||
}),
|
||||
capabilities: effectiveCapabilities,
|
||||
policies: policyState.policies,
|
||||
unrestricted: false,
|
||||
};
|
||||
@@ -1937,7 +1886,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
}) => {
|
||||
const normalized = await generateWechatUsername(openid);
|
||||
const randomPassword = crypto.randomBytes(24).toString('base64url');
|
||||
const { salt, passwordHash, passwordAlgorithm } = await createPasswordRecord(randomPassword);
|
||||
const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(randomPassword);
|
||||
const userId = crypto.randomUUID();
|
||||
const layout = await publishLayoutFor({ id: userId, username: normalized });
|
||||
const workspaceRoot = layout.publishDir;
|
||||
@@ -2255,7 +2204,6 @@ export function createUserAuth(pool, options = {}) {
|
||||
recharge,
|
||||
billSessionUsage,
|
||||
listUsageRecords,
|
||||
getUsageSummary,
|
||||
listBillingLedger,
|
||||
getAdminSummary,
|
||||
ensureAdminUser,
|
||||
@@ -2328,6 +2276,9 @@ export function resolveCookieDomain() {
|
||||
const base = String(process.env.H5_PUBLIC_BASE_URL ?? '').trim();
|
||||
if (!base) return null;
|
||||
const hostname = new URL(base).hostname.toLowerCase();
|
||||
if (hostname === 'localhost' || hostname.endsWith('.localhost')) {
|
||||
return '.localhost';
|
||||
}
|
||||
if (hostname === 'tkmind.cn' || hostname.endsWith('.tkmind.cn')) {
|
||||
return '.tkmind.cn';
|
||||
}
|
||||
@@ -2339,8 +2290,33 @@ export function resolveCookieDomain() {
|
||||
|
||||
/** 本地 localhost 开发时不设置 Domain,否则浏览器不会保存跨域 cookie。 */
|
||||
export function resolveCookieDomainForRequest(req) {
|
||||
const hostHeader = req?.get?.('host') ?? req?.hostname ?? '';
|
||||
const hostname = String(hostHeader).split(':')[0].toLowerCase();
|
||||
const explicit = String(process.env.H5_COOKIE_DOMAIN ?? '').trim();
|
||||
if (explicit) return explicit;
|
||||
|
||||
const hostCandidates = [
|
||||
req?.get?.('x-forwarded-host'),
|
||||
req?.get?.('host'),
|
||||
req?.hostname,
|
||||
];
|
||||
const origin = req?.get?.('origin');
|
||||
if (origin) {
|
||||
try {
|
||||
hostCandidates.push(new URL(origin).host);
|
||||
} catch {
|
||||
// ignore invalid origin
|
||||
}
|
||||
}
|
||||
|
||||
for (const raw of hostCandidates) {
|
||||
const hostname = String(raw ?? '').split(':')[0].toLowerCase();
|
||||
if (hostname.endsWith('.localhost')) {
|
||||
return '.localhost';
|
||||
}
|
||||
}
|
||||
|
||||
const hostname = String(req?.get?.('host') ?? req?.hostname ?? '')
|
||||
.split(':')[0]
|
||||
.toLowerCase();
|
||||
if (!hostname || hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user