diff --git a/packages/http-routes/src/register.ts b/packages/http-routes/src/register.ts index fff58d4f..5020f5d3 100644 --- a/packages/http-routes/src/register.ts +++ b/packages/http-routes/src/register.ts @@ -7,6 +7,7 @@ import type { FastifyInstance } from 'fastify' import type { HttpRouteContext } from './context' +import { PreferencesManager } from '@openchatlab/node-runtime' import { registerSystemRoutes } from './routes/system' import { registerRestSessionRoutes } from './routes/sessions' import { registerSessionRoutes } from './routes/web/sessions' @@ -38,47 +39,53 @@ export function registerSharedRoutes( ctx: HttpRouteContext, options?: SharedRouteOptions ): void { + // Ensure all routes share one PreferencesManager instance to avoid stale-cache + // overwrites between the session owner routes and the preferences routes. + const resolvedCtx: HttpRouteContext = ctx.preferencesManager + ? ctx + : { ...ctx, preferencesManager: new PreferencesManager(ctx.pathProvider.getSystemDir()) } + // REST API (/api/v1/*) - registerSystemRoutes(server, ctx) - registerRestSessionRoutes(server, ctx) + registerSystemRoutes(server, resolvedCtx) + registerRestSessionRoutes(server, resolvedCtx) // Web UI API (/_web/*) - registerSessionRoutes(server, ctx) - registerMemberRoutes(server, ctx) - registerPreferencesRoutes(server, ctx) - registerAnalyticsRoutes(server, ctx) - registerSqlRoutes(server, ctx) - registerSessionIndexRoutes(server, ctx) - registerExportRoutes(server, ctx) - registerNlpRoutes(server, ctx) + registerSessionRoutes(server, resolvedCtx) + registerMemberRoutes(server, resolvedCtx) + registerPreferencesRoutes(server, resolvedCtx) + registerAnalyticsRoutes(server, resolvedCtx) + registerSqlRoutes(server, resolvedCtx) + registerSessionIndexRoutes(server, resolvedCtx) + registerExportRoutes(server, resolvedCtx) + registerNlpRoutes(server, resolvedCtx) if (options?.requireAi) { const missing: string[] = [] - if (!ctx.aiDataDir) missing.push('aiDataDir') - if (!ctx.aiChatManager) missing.push('aiChatManager') - if (!ctx.assistantManager) missing.push('assistantManager') - if (!ctx.skillManagerCore) missing.push('skillManagerCore') - if (!ctx.llmConfigStore) missing.push('llmConfigStore') - if (!ctx.customProviderStore) missing.push('customProviderStore') - if (!ctx.customModelStore) missing.push('customModelStore') - if (!ctx.runAgentStream) missing.push('runAgentStream') + if (!resolvedCtx.aiDataDir) missing.push('aiDataDir') + if (!resolvedCtx.aiChatManager) missing.push('aiChatManager') + if (!resolvedCtx.assistantManager) missing.push('assistantManager') + if (!resolvedCtx.skillManagerCore) missing.push('skillManagerCore') + if (!resolvedCtx.llmConfigStore) missing.push('llmConfigStore') + if (!resolvedCtx.customProviderStore) missing.push('customProviderStore') + if (!resolvedCtx.customModelStore) missing.push('customModelStore') + if (!resolvedCtx.runAgentStream) missing.push('runAgentStream') if (missing.length > 0) { throw new Error(`[http-routes] requireAi is set but missing AI dependencies: ${missing.join(', ')}`) } } - registerAiAssistantRoutes(server, ctx) - registerAiSkillRoutes(server, ctx) - registerAiLlmRoutes(server, ctx) - registerAiLlmStreamRoutes(server, ctx) - registerAiAgentStreamRoutes(server, ctx) - registerAiToolRoutes(server, ctx) - registerAiChatRoutes(server, ctx) - registerAiSummaryRoutes(server, ctx) + registerAiAssistantRoutes(server, resolvedCtx) + registerAiSkillRoutes(server, resolvedCtx) + registerAiLlmRoutes(server, resolvedCtx) + registerAiLlmStreamRoutes(server, resolvedCtx) + registerAiAgentStreamRoutes(server, resolvedCtx) + registerAiToolRoutes(server, resolvedCtx) + registerAiChatRoutes(server, resolvedCtx) + registerAiSummaryRoutes(server, resolvedCtx) // Merge routes (graceful skip when mergeSessionCache is absent) - registerMergeRoutes(server, ctx) + registerMergeRoutes(server, resolvedCtx) // Cache/storage routes - registerCacheRoutes(server, ctx) + registerCacheRoutes(server, resolvedCtx) } diff --git a/packages/node-runtime/src/services/owner-profile-service.test.ts b/packages/node-runtime/src/services/owner-profile-service.test.ts index a75b4cd0..8f557e65 100644 --- a/packages/node-runtime/src/services/owner-profile-service.test.ts +++ b/packages/node-runtime/src/services/owner-profile-service.test.ts @@ -138,6 +138,7 @@ test('setOwnerAndApplyProfile writes owner, saves platform profile and merges na assert.equal(result.ownerId, 'Alice') assert.equal(result.platform, 'whatsapp') assert.deepEqual(result.updatedSessionIds, []) + assert.deepEqual(result.updatedSessionOwnerIds, {}) assert.equal(env.ownerOf('s1'), 'Alice') env.preferences.invalidateCache() @@ -170,6 +171,8 @@ test('setOwnerAndApplyProfile batch-applies to unowned same-platform sessions on const result = setOwnerAndApplyProfile(env.adapter, env.preferences, 'current', 'Alice') assert.deepEqual(result.updatedSessionIds, ['name-match']) + // name-match session has platformId 'alice' (lowercase) — must differ from source ownerId 'Alice' + assert.deepEqual(result.updatedSessionOwnerIds, { 'name-match': 'alice' }) assert.equal(env.ownerOf('name-match'), 'alice') assert.equal(env.ownerOf('owned'), 'Bob') assert.equal(env.ownerOf('other-platform'), null) @@ -303,6 +306,7 @@ test('exact platformId match works on platforms without name fallback', (t) => { const result = setOwnerAndApplyProfile(env.adapter, env.preferences, 'source', 'wx_me') assert.equal(result.platform, 'weixin') assert.deepEqual(result.updatedSessionIds, ['other']) + assert.deepEqual(result.updatedSessionOwnerIds, { other: 'wx_me' }) assert.equal(env.ownerOf('other'), 'wx_me') env.preferences.invalidateCache() diff --git a/packages/node-runtime/src/services/owner-profile-service.ts b/packages/node-runtime/src/services/owner-profile-service.ts index 96292fec..79243355 100644 --- a/packages/node-runtime/src/services/owner-profile-service.ts +++ b/packages/node-runtime/src/services/owner-profile-service.ts @@ -113,7 +113,12 @@ export function setOwnerAndApplyProfile( updatedAt: Date.now(), } - const updatedSessionIds = applyProfileToOtherSessions(adapter, meta.platform, profile, sessionId) + const updatedSessions = applyProfileToOtherSessions(adapter, meta.platform, profile, sessionId) + const updatedSessionIds = updatedSessions.map((s) => s.id) + const updatedSessionOwnerIds: Record = {} + for (const s of updatedSessions) { + updatedSessionOwnerIds[s.id] = s.ownerId + } const noLongerDismissed = new Set([sessionId, ...updatedSessionIds]) preferences.save({ @@ -121,20 +126,22 @@ export function setOwnerAndApplyProfile( ownerPromptDismissedSessionIds: prefs.ownerPromptDismissedSessionIds.filter((id) => !noLongerDismissed.has(id)), }) - return { sessionId, platform: meta.platform, ownerId: ownerPlatformId, updatedSessionIds } + return { sessionId, platform: meta.platform, ownerId: ownerPlatformId, updatedSessionIds, updatedSessionOwnerIds } } /** * Apply a profile to all other unowned sessions of the same platform. * Unique match only; sessions with an existing owner are never touched. + * Returns the id and the actual platformId written for each updated session + * (may differ from profile.platformId on name-match platforms). */ function applyProfileToOtherSessions( adapter: SessionRuntimeAdapter, platform: string, profile: OwnerProfile, excludeSessionId: string -): string[] { - const updated: string[] = [] +): Array<{ id: string; ownerId: string }> { + const updated: Array<{ id: string; ownerId: string }> = [] for (const id of adapter.listSessionIds()) { if (id === excludeSessionId) continue try { @@ -146,7 +153,7 @@ function applyProfileToOtherSessions( const result = matchOwnerProfile(platform, profile, getMembersWithAliases(db)) if (result.type === 'exact' || result.type === 'name') { coreUpdateSessionOwnerId(adapter.ensureWritable(id), result.platformId) - updated.push(id) + updated.push({ id, ownerId: result.platformId }) } } catch (err) { console.warn(`[OwnerProfile] Failed to apply profile to session ${id}:`, err) diff --git a/packages/shared-types/index.ts b/packages/shared-types/index.ts index 2335984a..d9cf517f 100644 --- a/packages/shared-types/index.ts +++ b/packages/shared-types/index.ts @@ -216,6 +216,13 @@ export interface SetOwnerAndApplyProfileResult { ownerId: string /** Other same-platform sessions auto-filled by the updated profile. */ updatedSessionIds: string[] + /** + * The actual owner_id written to each updated session. + * On name-match platforms the matched member's platformId can differ from + * ownerId (the source session's platformId), so callers must use this map + * rather than ownerId when caching the result for updated sessions. + */ + updatedSessionOwnerIds: Record } export interface Preferences { diff --git a/src/stores/session.ts b/src/stores/session.ts index c2875797..5e616a91 100644 --- a/src/stores/session.ts +++ b/src/stores/session.ts @@ -697,7 +697,7 @@ export const useSessionStore = defineStore( for (const sessionId of [id, ...result.updatedSessionIds]) { const session = sessions.value.find((s) => s.id === sessionId) if (session) { - session.ownerId = result.ownerId + session.ownerId = result.updatedSessionOwnerIds[sessionId] ?? result.ownerId } } return result