feat(runtime): wire personal memory observation and skill runtime config
Hook shadow pipeline observation into session finish and agent runs, and expose skill runtime settings through auth and runtime status endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+62
-21
@@ -192,6 +192,7 @@ import { createSessionSnapshotService } from './session-snapshot.mjs';
|
||||
import { createConversationMemoryService } from './conversation-memory.mjs';
|
||||
import { createManagedMemoryV2Runtime } from './memory-v2-runtime.mjs';
|
||||
import { createMemoryV2AdminConfigService } from './memory-v2-admin-config.mjs';
|
||||
import { createSkillRuntimeAdminConfigService } from './skill-runtime-admin-config.mjs';
|
||||
import { createWechatScheduleLlmConfigService } from './wechat-schedule-llm-config.mjs';
|
||||
import { createExperienceService } from './experience-service.mjs';
|
||||
import { attachAsrRoutes } from './asr-proxy.mjs';
|
||||
@@ -202,24 +203,16 @@ import { syncPageDataPolicyAccessMode } from './page-data-publish-sync.mjs';
|
||||
import { upsertPageDataPolicyIndex } from './page-data-policy-index.mjs';
|
||||
import { attachShenmeiOpinionFormRoutes } from './shenmei-opinion-form-routes.mjs';
|
||||
import { isNativeH5ApiPath } from './policies.mjs';
|
||||
import {
|
||||
applyMemindRuntimeProfile,
|
||||
describeMemindRuntimeProfile,
|
||||
loadMemindEnvFiles,
|
||||
} from './scripts/memind-runtime-profile.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
const value = trimmed.slice(eq + 1).trim();
|
||||
if (!process.env[key]) process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFile(path.join(__dirname, '../../.env.local'));
|
||||
loadEnvFile(path.join(__dirname, '.env'));
|
||||
loadMemindEnvFiles(__dirname);
|
||||
applyMemindRuntimeProfile({ rootDir: __dirname });
|
||||
|
||||
function parseApiTargets() {
|
||||
const csvTargets = (process.env.TKMIND_API_TARGETS ?? '')
|
||||
@@ -349,6 +342,15 @@ async function unregisterAgentSessionForUser(userId, sessionId) {
|
||||
await access.unregisterSession({ userId, sessionId });
|
||||
}
|
||||
let agentRunGateway = null;
|
||||
const sessionPageDeliveryLocks = new Map();
|
||||
function beginSessionPageDelivery(sessionId) {
|
||||
sessionPageDeliveryLocks.set(sessionId, Number(sessionPageDeliveryLocks.get(sessionId) ?? 0) + 1);
|
||||
}
|
||||
function endSessionPageDelivery(sessionId) {
|
||||
const remaining = Number(sessionPageDeliveryLocks.get(sessionId) ?? 0) - 1;
|
||||
if (remaining > 0) sessionPageDeliveryLocks.set(sessionId, remaining);
|
||||
else sessionPageDeliveryLocks.delete(sessionId);
|
||||
}
|
||||
let chatIntentRouter = null;
|
||||
let toolGateway = null;
|
||||
let directChatService = null;
|
||||
@@ -356,6 +358,7 @@ let sessionSnapshotService = null;
|
||||
let conversationMemoryService = null;
|
||||
let memoryV2 = null;
|
||||
let memoryV2ConfigService = null;
|
||||
let skillRuntimeConfigService = null;
|
||||
let wechatScheduleLlmConfigService = null;
|
||||
let mindSpace = null;
|
||||
let mindSpaceAssets = null;
|
||||
@@ -639,6 +642,7 @@ async function bootstrapUserAuth() {
|
||||
console.warn('LLM provider boot sync skipped:', err instanceof Error ? err.message : err);
|
||||
});
|
||||
memoryV2ConfigService = createMemoryV2AdminConfigService(pool);
|
||||
skillRuntimeConfigService = createSkillRuntimeAdminConfigService(pool, { h5Root: __dirname });
|
||||
wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool);
|
||||
conversationMemoryService = createConversationMemoryService(pool, {
|
||||
llmProviderService,
|
||||
@@ -705,9 +709,16 @@ async function bootstrapUserAuth() {
|
||||
chatIntentRouter,
|
||||
sessionSnapshotService,
|
||||
conversationMemoryService,
|
||||
syncUserPagesOnSuccess: async ({ userId }) => {
|
||||
await syncUserGeneratedPages(userId);
|
||||
observePersonalMemoryOnSuccess: async ({ userId, sessionId, userMessage }) => {
|
||||
if (!memoryV2?.observePersonalMemory) return;
|
||||
await memoryV2.observePersonalMemory({
|
||||
userId,
|
||||
sessionId,
|
||||
messages: [userMessage],
|
||||
});
|
||||
},
|
||||
syncUserPagesOnSuccess: async ({ userId }) => syncUserGeneratedPages(userId),
|
||||
isSessionExternallyBusy: ({ sessionId }) => Number(sessionPageDeliveryLocks.get(sessionId) ?? 0) > 0,
|
||||
autoDispatch: ['1', 'true', 'yes', 'on'].includes(
|
||||
String(process.env.MEMIND_AGENT_RUN_AUTODISPATCH ?? '1').trim().toLowerCase(),
|
||||
),
|
||||
@@ -851,6 +862,16 @@ app.use(attachUserSession);
|
||||
|
||||
// ============ Legacy password auth ============
|
||||
|
||||
async function resolveSkillRuntimeForClient() {
|
||||
if (!skillRuntimeConfigService?.getPublicRuntimeConfig) return null;
|
||||
try {
|
||||
return await skillRuntimeConfigService.getPublicRuntimeConfig();
|
||||
} catch (err) {
|
||||
console.warn('[SkillRuntime] public config unavailable:', err instanceof Error ? err.message : err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/auth/status', async (req, res) => {
|
||||
await userAuthReady;
|
||||
if (userAuth) {
|
||||
@@ -859,6 +880,7 @@ app.get('/auth/status', async (req, res) => {
|
||||
if (!me) return res.json({ authenticated: false, mode: 'user' });
|
||||
const row = await userAuth.getUserById(me.id);
|
||||
const capabilityState = await userAuth.resolveUserCapabilities(row);
|
||||
const skillRuntime = await resolveSkillRuntimeForClient();
|
||||
return res.json({
|
||||
authenticated: true,
|
||||
user: me,
|
||||
@@ -866,6 +888,7 @@ app.get('/auth/status', async (req, res) => {
|
||||
capabilities: capabilityState.capabilities,
|
||||
grantedSkills: capabilityState.grantedSkills ?? [],
|
||||
unrestricted: capabilityState.unrestricted,
|
||||
skillRuntime,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Auth] status failed:', err instanceof Error ? err.message : err);
|
||||
@@ -1309,10 +1332,11 @@ app.get('/auth/me', async (req, res) => {
|
||||
if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
|
||||
const me = await userAuth.getMe(userToken(req));
|
||||
if (!me) return res.status(401).json({ message: '未登录' });
|
||||
const [paths, capabilityState, subscription] = await Promise.all([
|
||||
const [paths, capabilityState, subscription, skillRuntime] = await Promise.all([
|
||||
userAuth.listPathGrants(me.id),
|
||||
userAuth.resolveUserCapabilities(await userAuth.getUserById(me.id)),
|
||||
subscriptionService ? subscriptionService.getActiveSubscription(me.id) : null,
|
||||
resolveSkillRuntimeForClient(),
|
||||
]);
|
||||
return res.json({
|
||||
user: { ...me, subscription },
|
||||
@@ -1320,6 +1344,7 @@ app.get('/auth/me', async (req, res) => {
|
||||
capabilities: capabilityState.capabilities,
|
||||
grantedSkills: capabilityState.grantedSkills ?? [],
|
||||
unrestricted: capabilityState.unrestricted,
|
||||
skillRuntime,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3209,11 +3234,10 @@ const SAVE_TARGET_CATEGORIES = new Set(['draft', 'oa', 'public']);
|
||||
async function syncUserGeneratedPages(userId) {
|
||||
if (!userId) return;
|
||||
if (workspacePageDeliver?.syncAndDeliver) {
|
||||
await workspacePageDeliver.syncAndDeliver(userId);
|
||||
return;
|
||||
return await workspacePageDeliver.syncAndDeliver(userId);
|
||||
}
|
||||
if (!mindSpacePageSync) return;
|
||||
await mindSpacePageSync.syncUserGeneratedPages(userId);
|
||||
return await mindSpacePageSync.syncUserGeneratedPages(userId);
|
||||
}
|
||||
|
||||
async function resolveChatSaveBundle(user, h5Root, input = {}) {
|
||||
@@ -5004,6 +5028,8 @@ api.get('/sessions/:sessionId/events', async (req, res, next) => {
|
||||
// workspace HTML into the asset store before a later restart rebuilds the
|
||||
// workspace from DB-backed assets only.
|
||||
const onAfterFinish = async (sid, uid) => {
|
||||
beginSessionPageDelivery(sid);
|
||||
try {
|
||||
const apiFetchFn = async (pathname, init) => {
|
||||
const target = await tkmindProxy.resolveTarget(sid);
|
||||
return tkmindProxy.apiFetchTo(target, pathname, init);
|
||||
@@ -5085,6 +5111,20 @@ api.get('/sessions/:sessionId/events', async (req, res, next) => {
|
||||
tkmindProxy,
|
||||
userText: lastUserText,
|
||||
});
|
||||
if (lastUserMessage && memoryV2?.observePersonalMemory) {
|
||||
await memoryV2.observePersonalMemory({
|
||||
userId: uid,
|
||||
sessionId: sid,
|
||||
messages: [lastUserMessage],
|
||||
}).catch((err) => {
|
||||
console.warn(
|
||||
`[memory-v2] finish shadow observation skipped for session ${sid}: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
endSessionPageDelivery(sid);
|
||||
}
|
||||
};
|
||||
return tkmindProxy.proxySessionEvents(req, res, sessionId, {
|
||||
onAfterFinish,
|
||||
@@ -6233,6 +6273,7 @@ app.get('*', (_req, res) => {
|
||||
process.exit(1);
|
||||
}
|
||||
const server = app.listen(PORT, HOST, () => {
|
||||
console.log(`[Portal] Runtime profile: ${describeMemindRuntimeProfile()}`);
|
||||
console.log(`TKMind H5 @ http://${HOST}:${PORT}`);
|
||||
console.log(`Proxy -> ${API_TARGETS.join(', ')}`);
|
||||
console.log(`Auth -> ${enabled ? 'multi-user (MySQL)' : legacyAuth ? 'legacy password' : 'disabled'}`);
|
||||
|
||||
Reference in New Issue
Block a user