feat(agent): 聊天提交统一走 POST /agent/runs 异步网关
引入 Agent Run 网关替代直连 /sessions/:id/reply,并在 api_lockdown 白名单中放行新入口,避免策略拦截导致聊天不可用。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+92
-36
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user