Improve WeChat MP replies and ship MindSpace/H5 production updates.
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+107
-21
@@ -64,11 +64,51 @@ function extractSessionId(req, body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderService }) {
|
||||
export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth, llmProviderService }) {
|
||||
const targets = apiTargets?.length ? apiTargets : apiTarget ? [apiTarget] : [];
|
||||
const primaryTarget = targets[0] ?? apiTarget ?? '';
|
||||
let rrIdx = 0;
|
||||
|
||||
async function targetHealthy(target) {
|
||||
try {
|
||||
const upstream = await apiFetch(target, apiSecret, '/status', {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(1500),
|
||||
});
|
||||
return upstream.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function pickTarget() {
|
||||
if (targets.length <= 1) return primaryTarget;
|
||||
for (let i = 0; i < targets.length; i += 1) {
|
||||
const target = targets[rrIdx];
|
||||
rrIdx = (rrIdx + 1) % targets.length;
|
||||
if (await targetHealthy(target)) return target;
|
||||
}
|
||||
return primaryTarget;
|
||||
}
|
||||
|
||||
async function resolveTarget(sessionId) {
|
||||
if (targets.length <= 1 || !sessionId) return primaryTarget;
|
||||
try {
|
||||
const node = await userAuth.getSessionNode(sessionId);
|
||||
return targets[node] ?? primaryTarget;
|
||||
} catch {
|
||||
return primaryTarget;
|
||||
}
|
||||
}
|
||||
|
||||
async function applySessionLlmProvider(sessionId) {
|
||||
if (!llmProviderService || !sessionId) return null;
|
||||
try {
|
||||
return await llmProviderService.applyBestProviderForSession(sessionId);
|
||||
const target = await resolveTarget(sessionId);
|
||||
return await llmProviderService.applyBestProviderForSession(
|
||||
sessionId,
|
||||
(url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init),
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'LLM provider apply skipped:',
|
||||
@@ -78,6 +118,15 @@ export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderS
|
||||
}
|
||||
}
|
||||
|
||||
async function applyLocalFallbackForSession(sessionId) {
|
||||
if (!llmProviderService || !sessionId) return null;
|
||||
const target = await resolveTarget(sessionId);
|
||||
return llmProviderService.applyLocalFallbackForSession(
|
||||
sessionId,
|
||||
(url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init),
|
||||
);
|
||||
}
|
||||
|
||||
const requireUser = async (req, res, next) => {
|
||||
try {
|
||||
const session = req.userSession;
|
||||
@@ -120,7 +169,8 @@ export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderS
|
||||
try {
|
||||
const workingDir = await userAuth.resolveWorkingDir(req.currentUser.id);
|
||||
const sessionPolicy = await userAuth.getAgentSessionPolicy(req.currentUser.id);
|
||||
const upstream = await apiFetch(apiTarget, apiSecret, '/agent/start', {
|
||||
const startTarget = await pickTarget();
|
||||
const upstream = await apiFetch(startTarget, apiSecret, '/agent/start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
working_dir: workingDir,
|
||||
@@ -138,9 +188,13 @@ export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderS
|
||||
}
|
||||
const session = JSON.parse(text);
|
||||
if (session?.id) {
|
||||
await userAuth.registerAgentSession(req.currentUser.id, session.id);
|
||||
await userAuth.registerAgentSession(
|
||||
req.currentUser.id,
|
||||
session.id,
|
||||
Math.max(0, targets.indexOf(startTarget)),
|
||||
);
|
||||
if (sessionPolicy.gooseMode) {
|
||||
const modeRes = await apiFetch(apiTarget, apiSecret, '/agent/update_session', {
|
||||
const modeRes = await apiFetch(startTarget, apiSecret, '/agent/update_session', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id: session.id,
|
||||
@@ -154,7 +208,7 @@ export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderS
|
||||
}
|
||||
}
|
||||
const publishLayout = await userAuth.getUserPublishLayout(req.currentUser.id);
|
||||
const api = (pathname, init) => apiFetch(apiTarget, apiSecret, pathname, init);
|
||||
const api = (pathname, init) => apiFetch(startTarget, apiSecret, pathname, init);
|
||||
try {
|
||||
await reconcileAgentSession(api, session.id, {
|
||||
workingDir,
|
||||
@@ -203,7 +257,8 @@ export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderS
|
||||
return;
|
||||
}
|
||||
|
||||
const upstream = await apiFetch(apiTarget, apiSecret, '/agent/resume', {
|
||||
const resumeTarget = await resolveTarget(sessionId);
|
||||
const upstream = await apiFetch(resumeTarget, apiSecret, '/agent/resume', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req.body ?? {}),
|
||||
});
|
||||
@@ -221,12 +276,13 @@ export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderS
|
||||
const publishLayout = await userAuth.getUserPublishLayout(req.currentUser.id);
|
||||
try {
|
||||
await reconcileAgentSession(
|
||||
(pathname, init) => apiFetch(apiTarget, apiSecret, pathname, init),
|
||||
(pathname, init) => apiFetch(resumeTarget, apiSecret, pathname, init),
|
||||
sessionId,
|
||||
{
|
||||
workingDir,
|
||||
sessionPolicy,
|
||||
sandboxConstraints: publishLayout?.constraints ?? null,
|
||||
tolerateInvalidWorkingDir: true,
|
||||
userContext: publishLayout
|
||||
? {
|
||||
userId: req.currentUser.id,
|
||||
@@ -261,18 +317,40 @@ export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderS
|
||||
requireUser,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const upstream = await apiFetch(apiTarget, apiSecret, '/sessions', {
|
||||
method: 'GET',
|
||||
});
|
||||
const text = await upstream.text();
|
||||
if (!upstream.ok) {
|
||||
res.status(upstream.status).send(text);
|
||||
const owned = await userAuth.listOwnedSessionIds(req.currentUser.id);
|
||||
const sessionsById = new Map();
|
||||
let healthyTargets = 0;
|
||||
let lastFailure = null;
|
||||
|
||||
for (const target of targets) {
|
||||
try {
|
||||
const upstream = await apiFetch(target, apiSecret, '/sessions', {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(3000),
|
||||
});
|
||||
const text = await upstream.text();
|
||||
if (!upstream.ok) {
|
||||
lastFailure = text || `upstream ${upstream.status}`;
|
||||
continue;
|
||||
}
|
||||
healthyTargets += 1;
|
||||
const payload = JSON.parse(text);
|
||||
for (const item of payload.sessions ?? []) {
|
||||
if (owned.has(item.id)) sessionsById.set(item.id, item);
|
||||
}
|
||||
} catch (err) {
|
||||
lastFailure = err instanceof Error ? err.message : '读取会话失败';
|
||||
}
|
||||
}
|
||||
|
||||
if (healthyTargets === 0) {
|
||||
res.status(502).json({ message: lastFailure ?? '所有 goose 服务不可用' });
|
||||
return;
|
||||
}
|
||||
const payload = JSON.parse(text);
|
||||
const owned = await userAuth.listOwnedSessionIds(req.currentUser.id);
|
||||
const sessions = (payload.sessions ?? []).filter((item) => owned.has(item.id));
|
||||
res.json({ sessions });
|
||||
if (healthyTargets < targets.length) {
|
||||
res.setHeader('X-TKMind-Degraded', '1');
|
||||
}
|
||||
res.json({ sessions: [...sessionsById.values()] });
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: err instanceof Error ? err.message : '读取会话失败' });
|
||||
}
|
||||
@@ -307,7 +385,8 @@ export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderS
|
||||
const proxySessionEvents = async (req, res, sessionId) => {
|
||||
try {
|
||||
const pathname = `/sessions/${sessionId}/events`;
|
||||
const upstream = await apiFetch(apiTarget, apiSecret, pathname, {
|
||||
const sessionTarget = await resolveTarget(sessionId);
|
||||
const upstream = await apiFetch(sessionTarget, apiSecret, pathname, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'text/event-stream',
|
||||
@@ -400,7 +479,11 @@ export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderS
|
||||
? JSON.stringify(req.body)
|
||||
: undefined;
|
||||
|
||||
const upstream = await apiFetch(apiTarget, apiSecret, pathname, {
|
||||
const sessionMatch = pathname.match(/^\/sessions\/([^/]+)/);
|
||||
const fallbackTarget =
|
||||
req.goosedTarget ??
|
||||
(sessionMatch ? await resolveTarget(sessionMatch[1]) : primaryTarget);
|
||||
const upstream = await apiFetch(fallbackTarget, apiSecret, pathname, {
|
||||
method: req.method,
|
||||
body,
|
||||
headers: {
|
||||
@@ -419,11 +502,14 @@ export function createTkmindProxy({ apiTarget, apiSecret, userAuth, llmProviderS
|
||||
requireUser,
|
||||
ensureChatAllowed,
|
||||
applySessionLlmProvider,
|
||||
applyLocalFallbackForSession,
|
||||
handlers,
|
||||
sessionScoped,
|
||||
proxyFallback,
|
||||
proxySessionEvents,
|
||||
apiFetch: (pathname, init) => apiFetch(apiTarget, apiSecret, pathname, init),
|
||||
resolveTarget,
|
||||
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