feat(agent): 聊天提交统一走 POST /agent/runs 异步网关
引入 Agent Run 网关替代直连 /sessions/:id/reply,并在 api_lockdown 白名单中放行新入口,避免策略拦截导致聊天不可用。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+111
-6
@@ -363,14 +363,16 @@ export function createTkmindProxy({
|
||||
recipe = null,
|
||||
} = {},
|
||||
) {
|
||||
const resolvedWorkingDir = workingDir ?? await userAuth.resolveWorkingDir(userId);
|
||||
const resolvedSessionPolicy = sessionPolicy ?? await userAuth.getAgentSessionPolicy(userId);
|
||||
const startTarget = await pickTarget();
|
||||
const upstream = await apiFetch(startTarget, apiSecret, '/agent/start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...(workingDir ? { working_dir: workingDir } : {}),
|
||||
enable_context_memory: sessionPolicy?.enableContextMemory,
|
||||
...(sessionPolicy?.extensionOverrides
|
||||
? { extension_overrides: sessionPolicy.extensionOverrides }
|
||||
...(resolvedWorkingDir ? { working_dir: resolvedWorkingDir } : {}),
|
||||
enable_context_memory: resolvedSessionPolicy?.enableContextMemory,
|
||||
...(resolvedSessionPolicy?.extensionOverrides
|
||||
? { extension_overrides: resolvedSessionPolicy.extensionOverrides }
|
||||
: {}),
|
||||
...(recipe ? { recipe } : {}),
|
||||
}),
|
||||
@@ -384,12 +386,12 @@ export function createTkmindProxy({
|
||||
throw new Error('创建会话失败:缺少 session id');
|
||||
}
|
||||
await userAuth.registerAgentSession(userId, session.id, startTarget);
|
||||
if (sessionPolicy?.gooseMode) {
|
||||
if (resolvedSessionPolicy?.gooseMode) {
|
||||
const modeRes = await apiFetch(startTarget, apiSecret, '/agent/update_session', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id: session.id,
|
||||
goose_mode: sessionPolicy.gooseMode,
|
||||
goose_mode: resolvedSessionPolicy.gooseMode,
|
||||
}),
|
||||
});
|
||||
if (!modeRes.ok) {
|
||||
@@ -397,6 +399,29 @@ export function createTkmindProxy({
|
||||
throw new Error(modeText || `设置会话模式失败 (${modeRes.status})`);
|
||||
}
|
||||
}
|
||||
const publishLayout = await userAuth.getUserPublishLayout(userId);
|
||||
const userMemories = conversationMemoryService?.listMemories
|
||||
? await conversationMemoryService.listMemories(userId, { limit: 40 }).catch(() => [])
|
||||
: [];
|
||||
await reconcileAgentSession(
|
||||
(pathname, init) => apiFetch(startTarget, apiSecret, pathname, init),
|
||||
session.id,
|
||||
{
|
||||
workingDir: resolvedWorkingDir,
|
||||
sessionPolicy: resolvedSessionPolicy,
|
||||
sandboxConstraints: publishLayout?.constraints ?? null,
|
||||
userMemories,
|
||||
userContext: publishLayout
|
||||
? {
|
||||
userId,
|
||||
displayName: publishLayout.displayName,
|
||||
username: publishLayout.username,
|
||||
slug: publishLayout.slug,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
);
|
||||
await applySessionLlmProvider(session.id);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -499,6 +524,68 @@ export function createTkmindProxy({
|
||||
);
|
||||
}
|
||||
|
||||
async function submitSessionReplyForUser(userId, sessionId, requestId, userMessage) {
|
||||
if (!userId || !sessionId) throw new Error('缺少会话信息');
|
||||
const owns = await userAuth.ownsSession(userId, sessionId);
|
||||
if (!owns) throw new Error('无权访问该会话');
|
||||
const gate = await userAuth.canUseChat(userId);
|
||||
if (!gate.ok) {
|
||||
const err = new Error(gate.message ?? '余额不足,请充值后继续使用');
|
||||
err.code = gate.code;
|
||||
err.status = 402;
|
||||
throw err;
|
||||
}
|
||||
|
||||
await reconcileSessionPolicyForUser(userId, sessionId);
|
||||
await applySessionLlmProvider(sessionId);
|
||||
|
||||
const user = await userAuth.getUserById(userId);
|
||||
if (!user) throw new Error('用户不存在');
|
||||
let finalUserMessage = userMessage;
|
||||
if (llmProviderService && messageHasImages(userMessage) && await llmProviderService.hasVisionKey()) {
|
||||
const publishLayout = await userAuth.getUserPublishLayout(userId).catch(() => null);
|
||||
const visionResult = await buildVisionBody(userMessage, userId, publishLayout).catch(() => null);
|
||||
if (visionResult?.userMessage) {
|
||||
finalUserMessage = visionResult.userMessage;
|
||||
}
|
||||
if (visionResult?.billableImageCount > 0 && subscriptionService) {
|
||||
await subscriptionService.consumeImageQuota(userId, visionResult.billableImageCount).catch((err) => {
|
||||
console.warn(
|
||||
'Subscription image quota consume skipped:',
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
const policyState = await userAuth.resolveUserPolicies(user);
|
||||
let body = {
|
||||
request_id: requestId,
|
||||
user_message: finalUserMessage,
|
||||
};
|
||||
if (!policyState.unrestricted) {
|
||||
body = injectTaskRoutingHint(
|
||||
body,
|
||||
await userAuth.getAgentSessionPolicy(userId),
|
||||
);
|
||||
}
|
||||
|
||||
const target = await resolveTarget(sessionId);
|
||||
const upstream = await apiFetch(
|
||||
target,
|
||||
apiSecret,
|
||||
`/sessions/${encodeURIComponent(sessionId)}/reply`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
);
|
||||
if (!upstream.ok) {
|
||||
const text = await upstream.text().catch(() => '');
|
||||
throw new Error(text || `发送失败 (${upstream.status})`);
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const requireUser = async (req, res, next) => {
|
||||
try {
|
||||
const session = req.userSession;
|
||||
@@ -877,6 +964,16 @@ export function createTkmindProxy({
|
||||
});
|
||||
return;
|
||||
}
|
||||
const isAgentRunPath =
|
||||
(req.method === 'POST' && pathname === '/agent/runs')
|
||||
|| (req.method === 'GET' && /^\/agent\/runs\/[^/]+$/.test(pathname));
|
||||
if (isAgentRunPath) {
|
||||
res.status(503).json({
|
||||
message: 'Agent Run 接口需在 Portal 本地处理,请确认 server.mjs 已更新并重启后端',
|
||||
code: 'AGENT_RUNS_NATIVE_REQUIRED',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const policyState = await userAuth.resolveUserPolicies(req.currentUser);
|
||||
const capabilityState = await userAuth.resolveUserCapabilities(req.currentUser);
|
||||
const gate = evaluateProxyRequest(req.method, pathname, policyState.policies, {
|
||||
@@ -896,6 +993,13 @@ export function createTkmindProxy({
|
||||
}
|
||||
const isReplyPath = pathname.match(/^\/sessions\/[^/]+\/reply$/) && req.method === 'POST';
|
||||
const sessionMatch = pathname.match(/^\/sessions\/([^/]+)/);
|
||||
if (isReplyPath) {
|
||||
res.status(410).json({
|
||||
message: '聊天提交入口已统一为 POST /agent/runs',
|
||||
code: 'AGENT_RUNS_REQUIRED',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let baseBody = req.body;
|
||||
if (isReplyPath && !policyState.unrestricted) {
|
||||
@@ -983,6 +1087,7 @@ export function createTkmindProxy({
|
||||
proxySessionEvents,
|
||||
resolveTarget,
|
||||
startSessionForUser,
|
||||
submitSessionReplyForUser,
|
||||
apiFetch: async (pathname, init) => apiFetch(await pickTarget(), apiSecret, pathname, init),
|
||||
apiFetchTo: (target, pathname, init) => apiFetch(target, apiSecret, pathname, init),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user