feat(agent): 聊天提交统一走 POST /agent/runs 异步网关

引入 Agent Run 网关替代直连 /sessions/:id/reply,并在 api_lockdown 白名单中放行新入口,避免策略拦截导致聊天不可用。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-29 22:50:00 +08:00
parent 16828a58f7
commit 8f620fa714
14 changed files with 784 additions and 89 deletions
+92 -36
View File
@@ -13,6 +13,7 @@ import {
sessionCookie,
} from './auth.mjs';
import { createDbPool, initSchema, isDatabaseConfigured } from './db.mjs';
import { createAgentRunGateway } from './agent-run-gateway.mjs';
import { createTkmindProxy } from './tkmind-proxy.mjs';
import {
clearUserSessionCookie,
@@ -216,6 +217,7 @@ if (ACCESS_PASSWORD) {
let userAuth = null;
let tkmindProxy = null;
let agentRunGateway = null;
let sessionSnapshotService = null;
let conversationMemoryService = null;
let mindSpace = null;
@@ -540,6 +542,11 @@ async function bootstrapUserAuth() {
}
: null,
});
agentRunGateway = createAgentRunGateway({
pool,
userAuth,
tkmindProxy,
});
wechatMpService = createWechatMpService({
config: WECHAT_MP_CONFIG,
userAuth,
@@ -3668,6 +3675,85 @@ api.post('/agent/start', async (req, res, next) => {
return runHandlerChain(tkmindProxy.handlers['POST /agent/start'], req, res, next);
});
api.post('/agent/runs', async (req, res, next) => {
await userAuthReady;
if (!userAuth || !tkmindProxy || !agentRunGateway) return next();
return runHandlerChain(
[
tkmindProxy.requireUser,
tkmindProxy.ensureChatAllowed,
async (request, response) => {
try {
const sessionId = String(request.body?.session_id ?? '').trim() || null;
const requestId = String(request.body?.request_id ?? '').trim();
const userMessage = request.body?.user_message ?? null;
if (!requestId) {
response.status(400).json({ message: '缺少 request_id' });
return;
}
if (!userMessage) {
response.status(400).json({ message: '缺少 user_message' });
return;
}
if (sessionId) {
const owns = await userAuth.ownsSession(request.currentUser.id, sessionId);
if (!owns) {
response.status(403).json({ message: '无权访问该会话' });
return;
}
}
const run = await agentRunGateway.createRun(request.currentUser.id, {
sessionId,
requestId,
userMessage,
});
response.status(202).json({ run });
} catch (err) {
response.status(500).json({
message: err instanceof Error ? err.message : '创建任务失败',
});
}
},
],
req,
res,
next,
);
});
api.get('/agent/runs/:runId', async (req, res, next) => {
await userAuthReady;
if (!userAuth || !tkmindProxy || !agentRunGateway) return next();
return runHandlerChain(
[
tkmindProxy.requireUser,
async (request, response) => {
try {
const run = await agentRunGateway.getRunForUser(
request.currentUser.id,
request.params.runId,
);
if (!run) {
response.status(404).json({ message: '任务不存在' });
return;
}
if (run.status !== 'succeeded' && run.status !== 'failed') {
agentRunGateway.dispatchRun(run.id);
}
response.json({ run });
} catch (err) {
response.status(500).json({
message: err instanceof Error ? err.message : '读取任务失败',
});
}
},
],
req,
res,
next,
);
});
api.post('/agent/resume', async (req, res, next) => {
await userAuthReady;
if (!userAuth || !tkmindProxy) return next();
@@ -3802,46 +3888,16 @@ api.use(async (req, res, next) => {
if (req.path.endsWith('/events') && req.method === 'GET') {
return next();
}
if (req.path.endsWith('/reply') && req.method === 'POST') {
return res.status(410).json({
message: '聊天提交入口已统一为 POST /agent/runs',
code: 'AGENT_RUNS_REQUIRED',
});
}
const owns = await userAuth.ownsSession(req.currentUser.id, sessionId);
if (!owns) {
return res.status(403).json({ message: '无权访问该会话' });
}
if (req.path.endsWith('/reply') && req.method === 'POST') {
const gate = await userAuth.canUseChat(req.currentUser.id);
if (!gate.ok) {
return res.status(402).json({
message: gate.message,
code: gate.code,
balanceCents: gate.balanceCents,
minRechargeCents: gate.minRechargeCents,
suggestedTiers: gate.suggestedTiers,
});
}
try {
await tkmindProxy.reconcileSessionPolicyForUser(req.currentUser.id, sessionId);
} catch (err) {
console.warn(
'Session policy sync before reply failed:',
err instanceof Error ? err.message : err,
);
return res.status(500).json({
message:
err instanceof Error
? `会话策略同步失败:${err.message}`
: '会话策略同步失败',
});
}
if (llmProviderService) {
try {
await tkmindProxy.applySessionLlmProvider(sessionId);
} catch (err) {
console.warn(
'LLM provider sync before reply skipped:',
err instanceof Error ? err.message : err,
);
}
}
}
}
if (req.method === 'POST' && req.path === '/agent/resume' && req.body?.session_id) {