@open-mercato/ai-assistant 0.6.4-develop.3949.1.adc3d0b3b1 → 0.6.4-develop.3968.1.9d87f5fa16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js +8 -2
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +16 -15
- package/dist/modules/ai_assistant/i18n/en.json +1 -0
- package/dist/modules/ai_assistant/i18n/es.json +16 -15
- package/dist/modules/ai_assistant/i18n/pl.json +16 -15
- package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js +19 -0
- package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js.map +2 -2
- package/package.json +6 -6
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.ts +8 -2
- package/src/modules/ai_assistant/i18n/__tests__/conversation-share-translations.test.ts +59 -0
- package/src/modules/ai_assistant/i18n/de.json +16 -15
- package/src/modules/ai_assistant/i18n/en.json +1 -0
- package/src/modules/ai_assistant/i18n/es.json +16 -15
- package/src/modules/ai_assistant/i18n/pl.json +16 -15
- package/src/modules/ai_assistant/subscribers/__tests__/conversation-shared-notify.test.ts +116 -0
- package/src/modules/ai_assistant/subscribers/conversation-shared-notify.ts +28 -0
package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js
CHANGED
|
@@ -124,13 +124,19 @@ async function GET(req, context) {
|
|
|
124
124
|
try {
|
|
125
125
|
const container = await createRequestContainer();
|
|
126
126
|
const repo = createConversationStorage(container);
|
|
127
|
-
const
|
|
127
|
+
const repoCtx = {
|
|
128
128
|
tenantId: callerCtx.tenantId,
|
|
129
129
|
organizationId: callerCtx.organizationId,
|
|
130
130
|
userId: callerCtx.userId,
|
|
131
131
|
canManageConversations: callerCtx.canManageConversations
|
|
132
|
-
}
|
|
132
|
+
};
|
|
133
|
+
const conversation = await repo.getById(callerCtx.conversationId, repoCtx);
|
|
134
|
+
if (!conversation) {
|
|
135
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
136
|
+
}
|
|
137
|
+
const participants = await repo.listParticipants(callerCtx.conversationId, repoCtx);
|
|
133
138
|
return NextResponse.json({
|
|
139
|
+
ownerUserId: conversation.ownerUserId,
|
|
134
140
|
participants: participants.map((p) => ({
|
|
135
141
|
userId: p.userId,
|
|
136
142
|
role: p.role,
|
package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/ai_assistant/api/ai/conversations/%5BconversationId%5D/participants/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport {\n createConversationStorage,\n AiChatConversationAccessError,\n AiChatConversationDuplicateParticipantError,\n} from '../../../../../lib/conversation-storage'\nimport { emitAiAssistantEvent } from '../../../../../events'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\nconst SHARE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.share'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nconst addParticipantBodySchema = z.object({\n userId: z.string().uuid('userId must be a valid UUID'),\n role: z.enum(['viewer']).default('viewer'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Manage conversation participants',\n methods: {\n GET: {\n operationId: 'aiAssistantListConversationParticipants',\n summary: 'List active participants of a conversation.',\n description:\n 'Returns the list of active (non-revoked) participants for the conversation. ' +\n 'Only the conversation owner or a caller with `ai_assistant.conversations.manage` can call this endpoint.',\n responses: [\n {\n status: 200,\n description: 'List of active participants.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required features.' },\n { status: 404, description: 'Conversation not found or not accessible.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantAddConversationParticipant',\n summary: 'Add a participant to a conversation.',\n description:\n 'Grants a named user read access to the conversation. Requires `ai_assistant.conversations.share`. ' +\n 'Only the conversation owner may add participants. If the user was previously revoked, the soft-deleted row is restored.',\n responses: [\n {\n status: 201,\n description: 'Participant added; conversation visibility updated to \"shared\".',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required feature or is not the owner.' },\n { status: 404, description: 'Conversation not found.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n canShare: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\n }\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n if (!auth.tenantId) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations: hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n canShare: hasRequiredFeatures(\n [SHARE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const participants = await repo.listParticipants(callerCtx.conversationId, {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n })\n return NextResponse.json({\n participants: participants.map((p) => ({\n userId: p.userId,\n role: p.role,\n lastReadAt: p.lastReadAt ? p.lastReadAt.toISOString() : null,\n addedAt: p.createdAt.toISOString(),\n })),\n })\n } catch (err) {\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(403, 'Access denied.', 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n\nexport async function POST(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (!callerCtx.canShare) {\n return jsonError(\n 403,\n `Caller lacks required feature \"${SHARE_CONVERSATIONS_FEATURE}\".`,\n 'forbidden',\n )\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return jsonError(400, 'Invalid JSON body.', 'invalid_body')\n }\n const parseResult = addParticipantBodySchema.safeParse(body)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n const targetUserId = parseResult.data.userId\n if (targetUserId === callerCtx.userId) {\n return jsonError(400, 'Cannot share a conversation with yourself.', 'self_share_not_allowed')\n }\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const targetUserFilter: FilterQuery<User> = {\n id: targetUserId,\n tenantId: callerCtx.tenantId,\n deletedAt: null,\n ...(callerCtx.organizationId ? { organizationId: callerCtx.organizationId } : {}),\n }\n const targetUser = await findOneWithDecryption<User>(\n em,\n User,\n targetUserFilter,\n {},\n { tenantId: callerCtx.tenantId, organizationId: callerCtx.organizationId },\n )\n if (!targetUser) {\n return jsonError(\n 400,\n 'Target user must be a staff user in the same tenant and organization.',\n 'user_not_found',\n )\n }\n\n const repo = createConversationStorage(container)\n const participant = await repo.addParticipant(\n callerCtx.conversationId,\n targetUserId,\n parseResult.data.role,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n try {\n await emitAiAssistantEvent(\n 'ai_assistant.conversation.shared',\n {\n conversationId: callerCtx.conversationId,\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n ownerUserId: callerCtx.userId,\n participantUserId: participant.userId,\n role: participant.role,\n },\n { persistent: false },\n )\n } catch {\n // non-fatal\n }\n return NextResponse.json(\n {\n participant: {\n userId: participant.userId,\n role: participant.role,\n lastReadAt: participant.lastReadAt ? participant.lastReadAt.toISOString() : null,\n addedAt: participant.createdAt.toISOString(),\n },\n },\n { status: 201 },\n )\n } catch (err) {\n if (err instanceof AiChatConversationDuplicateParticipantError) {\n return jsonError(409, err.message, 'duplicate_participant')\n }\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (err instanceof Error && err.message.toLowerCase().includes('owner')) {\n return jsonError(403, err.message, 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport {\n createConversationStorage,\n AiChatConversationAccessError,\n AiChatConversationDuplicateParticipantError,\n} from '../../../../../lib/conversation-storage'\nimport { emitAiAssistantEvent } from '../../../../../events'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\nconst SHARE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.share'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nconst addParticipantBodySchema = z.object({\n userId: z.string().uuid('userId must be a valid UUID'),\n role: z.enum(['viewer']).default('viewer'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Manage conversation participants',\n methods: {\n GET: {\n operationId: 'aiAssistantListConversationParticipants',\n summary: 'List active participants of a conversation.',\n description:\n 'Returns the list of active (non-revoked) participants for the conversation. ' +\n 'Only the conversation owner or a caller with `ai_assistant.conversations.manage` can call this endpoint.',\n responses: [\n {\n status: 200,\n description: 'List of active participants.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required features.' },\n { status: 404, description: 'Conversation not found or not accessible.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantAddConversationParticipant',\n summary: 'Add a participant to a conversation.',\n description:\n 'Grants a named user read access to the conversation. Requires `ai_assistant.conversations.share`. ' +\n 'Only the conversation owner may add participants. If the user was previously revoked, the soft-deleted row is restored.',\n responses: [\n {\n status: 201,\n description: 'Participant added; conversation visibility updated to \"shared\".',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required feature or is not the owner.' },\n { status: 404, description: 'Conversation not found.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n canShare: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\n }\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n if (!auth.tenantId) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations: hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n canShare: hasRequiredFeatures(\n [SHARE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const repoCtx = {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n }\n const conversation = await repo.getById(callerCtx.conversationId, repoCtx)\n if (!conversation) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n const participants = await repo.listParticipants(callerCtx.conversationId, repoCtx)\n return NextResponse.json({\n ownerUserId: conversation.ownerUserId,\n participants: participants.map((p) => ({\n userId: p.userId,\n role: p.role,\n lastReadAt: p.lastReadAt ? p.lastReadAt.toISOString() : null,\n addedAt: p.createdAt.toISOString(),\n })),\n })\n } catch (err) {\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(403, 'Access denied.', 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n\nexport async function POST(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (!callerCtx.canShare) {\n return jsonError(\n 403,\n `Caller lacks required feature \"${SHARE_CONVERSATIONS_FEATURE}\".`,\n 'forbidden',\n )\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return jsonError(400, 'Invalid JSON body.', 'invalid_body')\n }\n const parseResult = addParticipantBodySchema.safeParse(body)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n const targetUserId = parseResult.data.userId\n if (targetUserId === callerCtx.userId) {\n return jsonError(400, 'Cannot share a conversation with yourself.', 'self_share_not_allowed')\n }\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const targetUserFilter: FilterQuery<User> = {\n id: targetUserId,\n tenantId: callerCtx.tenantId,\n deletedAt: null,\n ...(callerCtx.organizationId ? { organizationId: callerCtx.organizationId } : {}),\n }\n const targetUser = await findOneWithDecryption<User>(\n em,\n User,\n targetUserFilter,\n {},\n { tenantId: callerCtx.tenantId, organizationId: callerCtx.organizationId },\n )\n if (!targetUser) {\n return jsonError(\n 400,\n 'Target user must be a staff user in the same tenant and organization.',\n 'user_not_found',\n )\n }\n\n const repo = createConversationStorage(container)\n const participant = await repo.addParticipant(\n callerCtx.conversationId,\n targetUserId,\n parseResult.data.role,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n try {\n await emitAiAssistantEvent(\n 'ai_assistant.conversation.shared',\n {\n conversationId: callerCtx.conversationId,\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n ownerUserId: callerCtx.userId,\n participantUserId: participant.userId,\n role: participant.role,\n },\n { persistent: false },\n )\n } catch {\n // non-fatal\n }\n return NextResponse.json(\n {\n participant: {\n userId: participant.userId,\n role: participant.role,\n lastReadAt: participant.lastReadAt ? participant.lastReadAt.toISOString() : null,\n addedAt: participant.createdAt.toISOString(),\n },\n },\n { status: 201 },\n )\n } catch (err) {\n if (err instanceof AiChatConversationDuplicateParticipantError) {\n return jsonError(409, err.message, 'duplicate_participant')\n }\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (err instanceof Error && err.message.toLowerCase().includes('owner')) {\n return jsonError(403, err.message, 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
"ai_assistant.chat.pending_phase3.body": "Diese interaktive Karte kommt in Phase 3 des vereinheitlichten KI-Frameworks.",
|
|
221
221
|
"ai_assistant.chat.pending_phase3.title": "Mutations-Freigabe-Karte ausstehend",
|
|
222
222
|
"ai_assistant.chat.placeholder": "Fragen Sie mich etwas...",
|
|
223
|
-
"ai_assistant.chat.readOnlyNotice": "
|
|
223
|
+
"ai_assistant.chat.readOnlyNotice": "Dies ist eine geteilte Konversation. Sie können lesen, aber nicht antworten.",
|
|
224
224
|
"ai_assistant.chat.reasoning": "Reasoning",
|
|
225
225
|
"ai_assistant.chat.records.fields.amount": "Betrag",
|
|
226
226
|
"ai_assistant.chat.records.fields.category": "Kategorie",
|
|
@@ -284,6 +284,7 @@
|
|
|
284
284
|
"ai_assistant.dock.left": "Links andocken",
|
|
285
285
|
"ai_assistant.dock.minimize": "Minimieren",
|
|
286
286
|
"ai_assistant.dock.right": "Rechts andocken",
|
|
287
|
+
"ai_assistant.launcher.composerPlaceholder": "Fragen Sie mich etwas…",
|
|
287
288
|
"ai_assistant.launcher.dialogTitle": "AI assistants",
|
|
288
289
|
"ai_assistant.launcher.dock.subtitle": "AI assistant",
|
|
289
290
|
"ai_assistant.launcher.empty": "No assistants match your search.",
|
|
@@ -325,9 +326,9 @@
|
|
|
325
326
|
"ai_assistant.modelPicker.triggerAriaLabel": "KI-Modell auswählen",
|
|
326
327
|
"ai_assistant.modelPicker.useDefault": "Standard des Agenten verwenden",
|
|
327
328
|
"ai_assistant.modelPicker.useDefaultWithModel": "Standard des Agenten verwenden: {{model}}",
|
|
328
|
-
"ai_assistant.notifications.conversation_shared.body": "
|
|
329
|
-
"ai_assistant.notifications.conversation_shared.title": "
|
|
330
|
-
"ai_assistant.notifications.conversation_shared.view_button": "
|
|
329
|
+
"ai_assistant.notifications.conversation_shared.body": "Eine KI-Konversation wurde mit Ihnen geteilt.",
|
|
330
|
+
"ai_assistant.notifications.conversation_shared.title": "Konversation mit Ihnen geteilt",
|
|
331
|
+
"ai_assistant.notifications.conversation_shared.view_button": "Konversation öffnen",
|
|
331
332
|
"ai_assistant.playground.agentPickerLabel": "Agent",
|
|
332
333
|
"ai_assistant.playground.chat.notSupportedBody": "Wählen Sie einen Agenten, dessen Ausführungsmodus \"chat\" ist, oder wechseln Sie zum Objekt-Modus-Tab.",
|
|
333
334
|
"ai_assistant.playground.chat.notSupportedTitle": "Chat-Modus ist für diesen Agenten nicht verfügbar.",
|
|
@@ -433,17 +434,17 @@
|
|
|
433
434
|
"ai_assistant.settings.visibilityEnabled": "Im Header sichtbar mit aktiviertem Cmd+J-Shortcut.",
|
|
434
435
|
"ai_assistant.settings.visibilityTitle": "KI-Assistent",
|
|
435
436
|
"ai_assistant.settings.visibilityToggleLabel": "Sichtbarkeit",
|
|
436
|
-
"ai_assistant.share.addParticipant": "
|
|
437
|
-
"ai_assistant.share.allUsersAdded": "
|
|
438
|
-
"ai_assistant.share.dialogDescription": "
|
|
439
|
-
"ai_assistant.share.dialogTitle": "
|
|
440
|
-
"ai_assistant.share.noParticipants": "
|
|
441
|
-
"ai_assistant.share.participantPlaceholder": "
|
|
442
|
-
"ai_assistant.share.removeParticipant": "
|
|
443
|
-
"ai_assistant.share.saved": "
|
|
444
|
-
"ai_assistant.share.saving": "
|
|
445
|
-
"ai_assistant.share.selectUser": "
|
|
446
|
-
"ai_assistant.share.shareButton": "
|
|
437
|
+
"ai_assistant.share.addParticipant": "Teilnehmer hinzufügen",
|
|
438
|
+
"ai_assistant.share.allUsersAdded": "Alle Benutzer bereits hinzugefügt",
|
|
439
|
+
"ai_assistant.share.dialogDescription": "Teilen Sie diese Konversation mit anderen Benutzern. Sie erhalten nur Lesezugriff.",
|
|
440
|
+
"ai_assistant.share.dialogTitle": "Konversation teilen",
|
|
441
|
+
"ai_assistant.share.noParticipants": "Noch keine Teilnehmer. Fügen Sie jemanden hinzu, um diese Konversation zu teilen.",
|
|
442
|
+
"ai_assistant.share.participantPlaceholder": "Nach Benutzer suchen...",
|
|
443
|
+
"ai_assistant.share.removeParticipant": "Entfernen",
|
|
444
|
+
"ai_assistant.share.saved": "Gespeichert",
|
|
445
|
+
"ai_assistant.share.saving": "Wird gespeichert...",
|
|
446
|
+
"ai_assistant.share.selectUser": "Benutzer auswählen...",
|
|
447
|
+
"ai_assistant.share.shareButton": "Teilen",
|
|
447
448
|
"ai_assistant.status.analyzing": "Anfrage wird analysiert...",
|
|
448
449
|
"ai_assistant.status.executing": "Tools werden ausgeführt...",
|
|
449
450
|
"ai_assistant.status.responding": "Antwort wird erstellt...",
|
|
@@ -284,6 +284,7 @@
|
|
|
284
284
|
"ai_assistant.dock.left": "Dock Left",
|
|
285
285
|
"ai_assistant.dock.minimize": "Minimize",
|
|
286
286
|
"ai_assistant.dock.right": "Dock Right",
|
|
287
|
+
"ai_assistant.launcher.composerPlaceholder": "Ask anything…",
|
|
287
288
|
"ai_assistant.launcher.dialogTitle": "AI assistants",
|
|
288
289
|
"ai_assistant.launcher.dock.subtitle": "AI assistant",
|
|
289
290
|
"ai_assistant.launcher.empty": "No assistants match your search.",
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
"ai_assistant.chat.pending_phase3.body": "Esta tarjeta interactiva se integrará en la Fase 3 del marco unificado de IA.",
|
|
221
221
|
"ai_assistant.chat.pending_phase3.title": "Tarjeta de aprobación de mutación pendiente",
|
|
222
222
|
"ai_assistant.chat.placeholder": "Pregunte lo que necesite...",
|
|
223
|
-
"ai_assistant.chat.readOnlyNotice": "
|
|
223
|
+
"ai_assistant.chat.readOnlyNotice": "Esta es una conversación compartida. Puede leerla, pero no puede responder.",
|
|
224
224
|
"ai_assistant.chat.reasoning": "Reasoning",
|
|
225
225
|
"ai_assistant.chat.records.fields.amount": "Importe",
|
|
226
226
|
"ai_assistant.chat.records.fields.category": "Categoría",
|
|
@@ -284,6 +284,7 @@
|
|
|
284
284
|
"ai_assistant.dock.left": "Anclar a la izquierda",
|
|
285
285
|
"ai_assistant.dock.minimize": "Minimizar",
|
|
286
286
|
"ai_assistant.dock.right": "Anclar a la derecha",
|
|
287
|
+
"ai_assistant.launcher.composerPlaceholder": "Pregunte lo que necesite…",
|
|
287
288
|
"ai_assistant.launcher.dialogTitle": "AI assistants",
|
|
288
289
|
"ai_assistant.launcher.dock.subtitle": "AI assistant",
|
|
289
290
|
"ai_assistant.launcher.empty": "No assistants match your search.",
|
|
@@ -325,9 +326,9 @@
|
|
|
325
326
|
"ai_assistant.modelPicker.triggerAriaLabel": "Selector de modelo de IA",
|
|
326
327
|
"ai_assistant.modelPicker.useDefault": "Usar predeterminado",
|
|
327
328
|
"ai_assistant.modelPicker.useDefaultWithModel": "Usar predeterminado del agente: {{model}}",
|
|
328
|
-
"ai_assistant.notifications.conversation_shared.body": "
|
|
329
|
-
"ai_assistant.notifications.conversation_shared.title": "
|
|
330
|
-
"ai_assistant.notifications.conversation_shared.view_button": "
|
|
329
|
+
"ai_assistant.notifications.conversation_shared.body": "Se ha compartido una conversación de IA con usted.",
|
|
330
|
+
"ai_assistant.notifications.conversation_shared.title": "Conversación compartida con usted",
|
|
331
|
+
"ai_assistant.notifications.conversation_shared.view_button": "Ver conversación",
|
|
331
332
|
"ai_assistant.playground.agentPickerLabel": "Agente",
|
|
332
333
|
"ai_assistant.playground.chat.notSupportedBody": "Elija un agente cuyo modo de ejecución sea \"chat\" o cambie a la pestaña de modo objeto.",
|
|
333
334
|
"ai_assistant.playground.chat.notSupportedTitle": "El modo chat no está disponible para este agente.",
|
|
@@ -433,17 +434,17 @@
|
|
|
433
434
|
"ai_assistant.settings.visibilityEnabled": "Habilitado",
|
|
434
435
|
"ai_assistant.settings.visibilityTitle": "Visibilidad del asistente de IA",
|
|
435
436
|
"ai_assistant.settings.visibilityToggleLabel": "Mostrar el asistente de IA a los usuarios",
|
|
436
|
-
"ai_assistant.share.addParticipant": "
|
|
437
|
-
"ai_assistant.share.allUsersAdded": "
|
|
438
|
-
"ai_assistant.share.dialogDescription": "
|
|
439
|
-
"ai_assistant.share.dialogTitle": "
|
|
440
|
-
"ai_assistant.share.noParticipants": "
|
|
441
|
-
"ai_assistant.share.participantPlaceholder": "
|
|
442
|
-
"ai_assistant.share.removeParticipant": "
|
|
443
|
-
"ai_assistant.share.saved": "
|
|
444
|
-
"ai_assistant.share.saving": "
|
|
445
|
-
"ai_assistant.share.selectUser": "
|
|
446
|
-
"ai_assistant.share.shareButton": "
|
|
437
|
+
"ai_assistant.share.addParticipant": "Añadir participante",
|
|
438
|
+
"ai_assistant.share.allUsersAdded": "Todos los usuarios ya añadidos",
|
|
439
|
+
"ai_assistant.share.dialogDescription": "Comparta esta conversación con otros usuarios. Obtendrán acceso de solo lectura.",
|
|
440
|
+
"ai_assistant.share.dialogTitle": "Compartir conversación",
|
|
441
|
+
"ai_assistant.share.noParticipants": "Aún no hay participantes. Añada a alguien para compartir esta conversación.",
|
|
442
|
+
"ai_assistant.share.participantPlaceholder": "Buscar usuario...",
|
|
443
|
+
"ai_assistant.share.removeParticipant": "Quitar",
|
|
444
|
+
"ai_assistant.share.saved": "Guardado",
|
|
445
|
+
"ai_assistant.share.saving": "Guardando...",
|
|
446
|
+
"ai_assistant.share.selectUser": "Seleccione un usuario...",
|
|
447
|
+
"ai_assistant.share.shareButton": "Compartir",
|
|
447
448
|
"ai_assistant.status.analyzing": "Analizando solicitud...",
|
|
448
449
|
"ai_assistant.status.executing": "Ejecutando herramientas...",
|
|
449
450
|
"ai_assistant.status.responding": "Respondiendo...",
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
"ai_assistant.chat.pending_phase3.body": "Ta interaktywna karta pojawi się w Fazie 3 zunifikowanego frameworku AI.",
|
|
221
221
|
"ai_assistant.chat.pending_phase3.title": "Karta zatwierdzania mutacji oczekuje",
|
|
222
222
|
"ai_assistant.chat.placeholder": "Zapytaj mnie o cokolwiek...",
|
|
223
|
-
"ai_assistant.chat.readOnlyNotice": "
|
|
223
|
+
"ai_assistant.chat.readOnlyNotice": "To jest udostępniona konwersacja. Możesz ją czytać, ale nie możesz odpowiadać.",
|
|
224
224
|
"ai_assistant.chat.reasoning": "Reasoning",
|
|
225
225
|
"ai_assistant.chat.records.fields.amount": "Kwota",
|
|
226
226
|
"ai_assistant.chat.records.fields.category": "Kategoria",
|
|
@@ -284,6 +284,7 @@
|
|
|
284
284
|
"ai_assistant.dock.left": "Dokuj po lewej",
|
|
285
285
|
"ai_assistant.dock.minimize": "Minimalizuj",
|
|
286
286
|
"ai_assistant.dock.right": "Dokuj po prawej",
|
|
287
|
+
"ai_assistant.launcher.composerPlaceholder": "Zapytaj o cokolwiek…",
|
|
287
288
|
"ai_assistant.launcher.dialogTitle": "AI assistants",
|
|
288
289
|
"ai_assistant.launcher.dock.subtitle": "AI assistant",
|
|
289
290
|
"ai_assistant.launcher.empty": "No assistants match your search.",
|
|
@@ -325,9 +326,9 @@
|
|
|
325
326
|
"ai_assistant.modelPicker.triggerAriaLabel": "Wybierz model AI",
|
|
326
327
|
"ai_assistant.modelPicker.useDefault": "Użyj domyślnego agenta",
|
|
327
328
|
"ai_assistant.modelPicker.useDefaultWithModel": "Użyj domyślnego agenta: {{model}}",
|
|
328
|
-
"ai_assistant.notifications.conversation_shared.body": "
|
|
329
|
-
"ai_assistant.notifications.conversation_shared.title": "
|
|
330
|
-
"ai_assistant.notifications.conversation_shared.view_button": "
|
|
329
|
+
"ai_assistant.notifications.conversation_shared.body": "Udostępniono Ci konwersację z asystentem AI.",
|
|
330
|
+
"ai_assistant.notifications.conversation_shared.title": "Udostępniono Ci konwersację",
|
|
331
|
+
"ai_assistant.notifications.conversation_shared.view_button": "Otwórz konwersację",
|
|
331
332
|
"ai_assistant.playground.agentPickerLabel": "Agent",
|
|
332
333
|
"ai_assistant.playground.chat.notSupportedBody": "Wybierz agenta, którego tryb wykonywania to \"chat\", lub przełącz się na zakładkę trybu obiektowego.",
|
|
333
334
|
"ai_assistant.playground.chat.notSupportedTitle": "Tryb czatu nie jest dostępny dla tego agenta.",
|
|
@@ -433,17 +434,17 @@
|
|
|
433
434
|
"ai_assistant.settings.visibilityEnabled": "Widoczny w nagłówku ze skrótem Cmd+J.",
|
|
434
435
|
"ai_assistant.settings.visibilityTitle": "Asystent AI",
|
|
435
436
|
"ai_assistant.settings.visibilityToggleLabel": "Widoczność",
|
|
436
|
-
"ai_assistant.share.addParticipant": "
|
|
437
|
-
"ai_assistant.share.allUsersAdded": "
|
|
438
|
-
"ai_assistant.share.dialogDescription": "
|
|
439
|
-
"ai_assistant.share.dialogTitle": "
|
|
440
|
-
"ai_assistant.share.noParticipants": "
|
|
441
|
-
"ai_assistant.share.participantPlaceholder": "
|
|
442
|
-
"ai_assistant.share.removeParticipant": "
|
|
443
|
-
"ai_assistant.share.saved": "
|
|
444
|
-
"ai_assistant.share.saving": "
|
|
445
|
-
"ai_assistant.share.selectUser": "
|
|
446
|
-
"ai_assistant.share.shareButton": "
|
|
437
|
+
"ai_assistant.share.addParticipant": "Dodaj uczestnika",
|
|
438
|
+
"ai_assistant.share.allUsersAdded": "Dodano już wszystkich użytkowników",
|
|
439
|
+
"ai_assistant.share.dialogDescription": "Udostępnij tę konwersację innym użytkownikom. Otrzymają dostęp tylko do odczytu.",
|
|
440
|
+
"ai_assistant.share.dialogTitle": "Udostępnij konwersację",
|
|
441
|
+
"ai_assistant.share.noParticipants": "Brak uczestników. Dodaj kogoś, aby udostępnić tę konwersację.",
|
|
442
|
+
"ai_assistant.share.participantPlaceholder": "Szukaj użytkownika...",
|
|
443
|
+
"ai_assistant.share.removeParticipant": "Usuń",
|
|
444
|
+
"ai_assistant.share.saved": "Zapisano",
|
|
445
|
+
"ai_assistant.share.saving": "Zapisywanie...",
|
|
446
|
+
"ai_assistant.share.selectUser": "Wybierz użytkownika...",
|
|
447
|
+
"ai_assistant.share.shareButton": "Udostępnij",
|
|
447
448
|
"ai_assistant.status.analyzing": "Analizowanie żądania...",
|
|
448
449
|
"ai_assistant.status.executing": "Wykonywanie narzędzi...",
|
|
449
450
|
"ai_assistant.status.responding": "Odpowiadanie...",
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import { resolveNotificationService } from "@open-mercato/core/modules/notifications/lib/notificationService";
|
|
2
2
|
import { buildNotificationFromType } from "@open-mercato/core/modules/notifications/lib/notificationBuilder";
|
|
3
|
+
import { defaultLocale } from "@open-mercato/shared/lib/i18n/config";
|
|
4
|
+
import { loadDictionary } from "@open-mercato/shared/lib/i18n/server";
|
|
5
|
+
import { createTranslator } from "@open-mercato/shared/lib/i18n/translate";
|
|
3
6
|
import { notificationTypes } from "../notifications.js";
|
|
4
7
|
const metadata = {
|
|
5
8
|
event: "ai_assistant.conversation.shared",
|
|
6
9
|
persistent: true,
|
|
7
10
|
id: "ai_assistant:conversation-shared-notify"
|
|
8
11
|
};
|
|
12
|
+
async function resolveDefaultLocaleStrings(titleKey, bodyKey) {
|
|
13
|
+
if (!titleKey && !bodyKey) return { title: titleKey, body: bodyKey };
|
|
14
|
+
try {
|
|
15
|
+
const dict = await loadDictionary(defaultLocale);
|
|
16
|
+
const t = createTranslator(dict);
|
|
17
|
+
return {
|
|
18
|
+
title: titleKey ? t(titleKey) : titleKey,
|
|
19
|
+
body: bodyKey ? t(bodyKey) : bodyKey
|
|
20
|
+
};
|
|
21
|
+
} catch {
|
|
22
|
+
return { title: titleKey, body: bodyKey };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
9
25
|
async function handleConversationShared(payload, ctx) {
|
|
10
26
|
if (!payload?.participantUserId || !payload.tenantId) return;
|
|
11
27
|
const typeDef = notificationTypes.find((t) => t.type === "ai_assistant.conversation_shared");
|
|
@@ -24,6 +40,9 @@ async function handleConversationShared(payload, ctx) {
|
|
|
24
40
|
sourceEntityId: payload.conversationId,
|
|
25
41
|
linkHref: `/backend?openAiConversation=${payload.conversationId}`
|
|
26
42
|
});
|
|
43
|
+
const resolved = await resolveDefaultLocaleStrings(typeDef.titleKey, typeDef.bodyKey);
|
|
44
|
+
if (resolved.title !== void 0) notificationInput.title = resolved.title;
|
|
45
|
+
if (resolved.body !== void 0) notificationInput.body = resolved.body;
|
|
27
46
|
try {
|
|
28
47
|
await notificationService.create(notificationInput, {
|
|
29
48
|
tenantId: payload.tenantId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/subscribers/conversation-shared-notify.ts"],
|
|
4
|
-
"sourcesContent": ["import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { notificationTypes } from '../notifications'\nimport type { AiConversationSharedPayload } from '../events'\n\nexport const metadata = {\n event: 'ai_assistant.conversation.shared',\n persistent: true,\n id: 'ai_assistant:conversation-shared-notify',\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n container?: { resolve<T = unknown>(name: string): T }\n}\n\nexport default async function handleConversationShared(\n payload: AiConversationSharedPayload,\n ctx: ResolverContext,\n): Promise<void> {\n if (!payload?.participantUserId || !payload.tenantId) return\n\n const typeDef = notificationTypes.find((t) => t.type === 'ai_assistant.conversation_shared')\n if (!typeDef) return\n\n const container = ctx.container ?? { resolve: ctx.resolve }\n let notificationService: ReturnType<typeof resolveNotificationService> | null\n try {\n notificationService = resolveNotificationService(container)\n } catch {\n return\n }\n\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: payload.participantUserId,\n bodyVariables: {},\n sourceEntityType: 'ai_assistant:ai_chat_conversation',\n sourceEntityId: payload.conversationId,\n linkHref: `/backend?openAiConversation=${payload.conversationId}`,\n })\n\n try {\n await notificationService.create(notificationInput, {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n } catch (err) {\n console.warn('[ai_assistant.conversationSharedNotify] create failed', err)\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,kCAAkC;AAC3C,SAAS,iCAAiC;AAC1C,SAAS,yBAAyB;AAG3B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAOA,eAAO,yBACL,SACA,KACe;AACf,MAAI,CAAC,SAAS,qBAAqB,CAAC,QAAQ,SAAU;AAEtD,QAAM,UAAU,kBAAkB,KAAK,CAAC,MAAM,EAAE,SAAS,kCAAkC;AAC3F,MAAI,CAAC,QAAS;AAEd,QAAM,YAAY,IAAI,aAAa,EAAE,SAAS,IAAI,QAAQ;AAC1D,MAAI;AACJ,MAAI;AACF,0BAAsB,2BAA2B,SAAS;AAAA,EAC5D,QAAQ;AACN;AAAA,EACF;AAEA,QAAM,oBAAoB,0BAA0B,SAAS;AAAA,IAC3D,iBAAiB,QAAQ;AAAA,IACzB,eAAe,CAAC;AAAA,IAChB,kBAAkB;AAAA,IAClB,gBAAgB,QAAQ;AAAA,IACxB,UAAU,+BAA+B,QAAQ,cAAc;AAAA,EACjE,CAAC;
|
|
4
|
+
"sourcesContent": ["import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { defaultLocale } from '@open-mercato/shared/lib/i18n/config'\nimport { loadDictionary } from '@open-mercato/shared/lib/i18n/server'\nimport { createTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport { notificationTypes } from '../notifications'\nimport type { AiConversationSharedPayload } from '../events'\n\nexport const metadata = {\n event: 'ai_assistant.conversation.shared',\n persistent: true,\n id: 'ai_assistant:conversation-shared-notify',\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n container?: { resolve<T = unknown>(name: string): T }\n}\n\nasync function resolveDefaultLocaleStrings(\n titleKey: string | undefined,\n bodyKey: string | undefined,\n): Promise<{ title: string | undefined; body: string | undefined }> {\n if (!titleKey && !bodyKey) return { title: titleKey, body: bodyKey }\n try {\n const dict = await loadDictionary(defaultLocale)\n const t = createTranslator(dict)\n return {\n title: titleKey ? t(titleKey) : titleKey,\n body: bodyKey ? t(bodyKey) : bodyKey,\n }\n } catch {\n return { title: titleKey, body: bodyKey }\n }\n}\n\nexport default async function handleConversationShared(\n payload: AiConversationSharedPayload,\n ctx: ResolverContext,\n): Promise<void> {\n if (!payload?.participantUserId || !payload.tenantId) return\n\n const typeDef = notificationTypes.find((t) => t.type === 'ai_assistant.conversation_shared')\n if (!typeDef) return\n\n const container = ctx.container ?? { resolve: ctx.resolve }\n let notificationService: ReturnType<typeof resolveNotificationService> | null\n try {\n notificationService = resolveNotificationService(container)\n } catch {\n return\n }\n\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: payload.participantUserId,\n bodyVariables: {},\n sourceEntityType: 'ai_assistant:ai_chat_conversation',\n sourceEntityId: payload.conversationId,\n linkHref: `/backend?openAiConversation=${payload.conversationId}`,\n })\n\n // Persist a human-readable fallback alongside the i18n keys so consumers\n // that do not run the client renderer (email digests, exports) display a\n // resolved string instead of the raw key. The client renderer continues\n // to re-translate via titleKey/bodyKey for the viewer's locale.\n const resolved = await resolveDefaultLocaleStrings(typeDef.titleKey, typeDef.bodyKey)\n if (resolved.title !== undefined) notificationInput.title = resolved.title\n if (resolved.body !== undefined) notificationInput.body = resolved.body\n\n try {\n await notificationService.create(notificationInput, {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n } catch (err) {\n console.warn('[ai_assistant.conversationSharedNotify] create failed', err)\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kCAAkC;AAC3C,SAAS,iCAAiC;AAC1C,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC,SAAS,yBAAyB;AAG3B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAOA,eAAe,4BACb,UACA,SACkE;AAClE,MAAI,CAAC,YAAY,CAAC,QAAS,QAAO,EAAE,OAAO,UAAU,MAAM,QAAQ;AACnE,MAAI;AACF,UAAM,OAAO,MAAM,eAAe,aAAa;AAC/C,UAAM,IAAI,iBAAiB,IAAI;AAC/B,WAAO;AAAA,MACL,OAAO,WAAW,EAAE,QAAQ,IAAI;AAAA,MAChC,MAAM,UAAU,EAAE,OAAO,IAAI;AAAA,IAC/B;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,OAAO,UAAU,MAAM,QAAQ;AAAA,EAC1C;AACF;AAEA,eAAO,yBACL,SACA,KACe;AACf,MAAI,CAAC,SAAS,qBAAqB,CAAC,QAAQ,SAAU;AAEtD,QAAM,UAAU,kBAAkB,KAAK,CAAC,MAAM,EAAE,SAAS,kCAAkC;AAC3F,MAAI,CAAC,QAAS;AAEd,QAAM,YAAY,IAAI,aAAa,EAAE,SAAS,IAAI,QAAQ;AAC1D,MAAI;AACJ,MAAI;AACF,0BAAsB,2BAA2B,SAAS;AAAA,EAC5D,QAAQ;AACN;AAAA,EACF;AAEA,QAAM,oBAAoB,0BAA0B,SAAS;AAAA,IAC3D,iBAAiB,QAAQ;AAAA,IACzB,eAAe,CAAC;AAAA,IAChB,kBAAkB;AAAA,IAClB,gBAAgB,QAAQ;AAAA,IACxB,UAAU,+BAA+B,QAAQ,cAAc;AAAA,EACjE,CAAC;AAMD,QAAM,WAAW,MAAM,4BAA4B,QAAQ,UAAU,QAAQ,OAAO;AACpF,MAAI,SAAS,UAAU,OAAW,mBAAkB,QAAQ,SAAS;AACrE,MAAI,SAAS,SAAS,OAAW,mBAAkB,OAAO,SAAS;AAEnE,MAAI;AACF,UAAM,oBAAoB,OAAO,mBAAmB;AAAA,MAClD,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ,kBAAkB;AAAA,IAC5C,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,YAAQ,KAAK,yDAAyD,GAAG;AAAA,EAC3E;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ai-assistant",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.3968.1.9d87f5fa16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -98,16 +98,16 @@
|
|
|
98
98
|
"zod-to-json-schema": "^3.25.2"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
102
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
101
|
+
"@open-mercato/shared": "0.6.4-develop.3968.1.9d87f5fa16",
|
|
102
|
+
"@open-mercato/ui": "0.6.4-develop.3968.1.9d87f5fa16",
|
|
103
103
|
"react": "^19.0.0",
|
|
104
104
|
"react-dom": "^19.0.0",
|
|
105
105
|
"zod": ">=3.23.0"
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
|
-
"@open-mercato/cli": "0.6.4-develop.
|
|
109
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
110
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
108
|
+
"@open-mercato/cli": "0.6.4-develop.3968.1.9d87f5fa16",
|
|
109
|
+
"@open-mercato/shared": "0.6.4-develop.3968.1.9d87f5fa16",
|
|
110
|
+
"@open-mercato/ui": "0.6.4-develop.3968.1.9d87f5fa16",
|
|
111
111
|
"@types/react": "^19.2.15",
|
|
112
112
|
"@types/react-dom": "^19.2.3",
|
|
113
113
|
"react": "19.2.6",
|
package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.ts
CHANGED
|
@@ -168,13 +168,19 @@ export async function GET(req: NextRequest, context: RouteContext): Promise<Resp
|
|
|
168
168
|
try {
|
|
169
169
|
const container = await createRequestContainer()
|
|
170
170
|
const repo = createConversationStorage(container)
|
|
171
|
-
const
|
|
171
|
+
const repoCtx = {
|
|
172
172
|
tenantId: callerCtx.tenantId,
|
|
173
173
|
organizationId: callerCtx.organizationId,
|
|
174
174
|
userId: callerCtx.userId,
|
|
175
175
|
canManageConversations: callerCtx.canManageConversations,
|
|
176
|
-
}
|
|
176
|
+
}
|
|
177
|
+
const conversation = await repo.getById(callerCtx.conversationId, repoCtx)
|
|
178
|
+
if (!conversation) {
|
|
179
|
+
return jsonError(404, 'Conversation not found.', 'conversation_not_found')
|
|
180
|
+
}
|
|
181
|
+
const participants = await repo.listParticipants(callerCtx.conversationId, repoCtx)
|
|
177
182
|
return NextResponse.json({
|
|
183
|
+
ownerUserId: conversation.ownerUserId,
|
|
178
184
|
participants: participants.map((p) => ({
|
|
179
185
|
userId: p.userId,
|
|
180
186
|
role: p.role,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression coverage for issue #2097 (BUG-004): the conversation-sharing
|
|
3
|
+
* UI and notification strings MUST ship localized values in every supported
|
|
4
|
+
* locale, not the English copy. Catches drift when keys are renamed/added
|
|
5
|
+
* but their PL/DE/ES values are forgotten.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import enDict from '../en.json'
|
|
9
|
+
import plDict from '../pl.json'
|
|
10
|
+
import deDict from '../de.json'
|
|
11
|
+
import esDict from '../es.json'
|
|
12
|
+
|
|
13
|
+
type Dict = Record<string, string>
|
|
14
|
+
|
|
15
|
+
const KEYS_TO_LOCALIZE = [
|
|
16
|
+
'ai_assistant.chat.readOnlyNotice',
|
|
17
|
+
'ai_assistant.launcher.composerPlaceholder',
|
|
18
|
+
'ai_assistant.notifications.conversation_shared.title',
|
|
19
|
+
'ai_assistant.notifications.conversation_shared.body',
|
|
20
|
+
'ai_assistant.notifications.conversation_shared.view_button',
|
|
21
|
+
'ai_assistant.share.addParticipant',
|
|
22
|
+
'ai_assistant.share.allUsersAdded',
|
|
23
|
+
'ai_assistant.share.dialogDescription',
|
|
24
|
+
'ai_assistant.share.dialogTitle',
|
|
25
|
+
'ai_assistant.share.noParticipants',
|
|
26
|
+
'ai_assistant.share.participantPlaceholder',
|
|
27
|
+
'ai_assistant.share.removeParticipant',
|
|
28
|
+
'ai_assistant.share.saved',
|
|
29
|
+
'ai_assistant.share.saving',
|
|
30
|
+
'ai_assistant.share.selectUser',
|
|
31
|
+
'ai_assistant.share.shareButton',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const LOCALE_DICTS: Array<[string, Dict]> = [
|
|
35
|
+
['pl', plDict as Dict],
|
|
36
|
+
['de', deDict as Dict],
|
|
37
|
+
['es', esDict as Dict],
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
describe('ai_assistant conversation-share i18n keys', () => {
|
|
41
|
+
it.each(KEYS_TO_LOCALIZE)('en.json defines %s', (key) => {
|
|
42
|
+
expect((enDict as Dict)[key]).toBeTruthy()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
for (const [locale, dict] of LOCALE_DICTS) {
|
|
46
|
+
describe(`${locale}.json`, () => {
|
|
47
|
+
it.each(KEYS_TO_LOCALIZE)('defines %s', (key) => {
|
|
48
|
+
expect(dict[key]).toBeTruthy()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it.each(KEYS_TO_LOCALIZE)('localizes %s (value differs from en)', (key) => {
|
|
52
|
+
const enValue = (enDict as Dict)[key]
|
|
53
|
+
const localizedValue = dict[key]
|
|
54
|
+
expect(localizedValue).toBeTruthy()
|
|
55
|
+
expect(localizedValue).not.toBe(enValue)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
})
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
"ai_assistant.chat.pending_phase3.body": "Diese interaktive Karte kommt in Phase 3 des vereinheitlichten KI-Frameworks.",
|
|
221
221
|
"ai_assistant.chat.pending_phase3.title": "Mutations-Freigabe-Karte ausstehend",
|
|
222
222
|
"ai_assistant.chat.placeholder": "Fragen Sie mich etwas...",
|
|
223
|
-
"ai_assistant.chat.readOnlyNotice": "
|
|
223
|
+
"ai_assistant.chat.readOnlyNotice": "Dies ist eine geteilte Konversation. Sie können lesen, aber nicht antworten.",
|
|
224
224
|
"ai_assistant.chat.reasoning": "Reasoning",
|
|
225
225
|
"ai_assistant.chat.records.fields.amount": "Betrag",
|
|
226
226
|
"ai_assistant.chat.records.fields.category": "Kategorie",
|
|
@@ -284,6 +284,7 @@
|
|
|
284
284
|
"ai_assistant.dock.left": "Links andocken",
|
|
285
285
|
"ai_assistant.dock.minimize": "Minimieren",
|
|
286
286
|
"ai_assistant.dock.right": "Rechts andocken",
|
|
287
|
+
"ai_assistant.launcher.composerPlaceholder": "Fragen Sie mich etwas…",
|
|
287
288
|
"ai_assistant.launcher.dialogTitle": "AI assistants",
|
|
288
289
|
"ai_assistant.launcher.dock.subtitle": "AI assistant",
|
|
289
290
|
"ai_assistant.launcher.empty": "No assistants match your search.",
|
|
@@ -325,9 +326,9 @@
|
|
|
325
326
|
"ai_assistant.modelPicker.triggerAriaLabel": "KI-Modell auswählen",
|
|
326
327
|
"ai_assistant.modelPicker.useDefault": "Standard des Agenten verwenden",
|
|
327
328
|
"ai_assistant.modelPicker.useDefaultWithModel": "Standard des Agenten verwenden: {{model}}",
|
|
328
|
-
"ai_assistant.notifications.conversation_shared.body": "
|
|
329
|
-
"ai_assistant.notifications.conversation_shared.title": "
|
|
330
|
-
"ai_assistant.notifications.conversation_shared.view_button": "
|
|
329
|
+
"ai_assistant.notifications.conversation_shared.body": "Eine KI-Konversation wurde mit Ihnen geteilt.",
|
|
330
|
+
"ai_assistant.notifications.conversation_shared.title": "Konversation mit Ihnen geteilt",
|
|
331
|
+
"ai_assistant.notifications.conversation_shared.view_button": "Konversation öffnen",
|
|
331
332
|
"ai_assistant.playground.agentPickerLabel": "Agent",
|
|
332
333
|
"ai_assistant.playground.chat.notSupportedBody": "Wählen Sie einen Agenten, dessen Ausführungsmodus \"chat\" ist, oder wechseln Sie zum Objekt-Modus-Tab.",
|
|
333
334
|
"ai_assistant.playground.chat.notSupportedTitle": "Chat-Modus ist für diesen Agenten nicht verfügbar.",
|
|
@@ -433,17 +434,17 @@
|
|
|
433
434
|
"ai_assistant.settings.visibilityEnabled": "Im Header sichtbar mit aktiviertem Cmd+J-Shortcut.",
|
|
434
435
|
"ai_assistant.settings.visibilityTitle": "KI-Assistent",
|
|
435
436
|
"ai_assistant.settings.visibilityToggleLabel": "Sichtbarkeit",
|
|
436
|
-
"ai_assistant.share.addParticipant": "
|
|
437
|
-
"ai_assistant.share.allUsersAdded": "
|
|
438
|
-
"ai_assistant.share.dialogDescription": "
|
|
439
|
-
"ai_assistant.share.dialogTitle": "
|
|
440
|
-
"ai_assistant.share.noParticipants": "
|
|
441
|
-
"ai_assistant.share.participantPlaceholder": "
|
|
442
|
-
"ai_assistant.share.removeParticipant": "
|
|
443
|
-
"ai_assistant.share.saved": "
|
|
444
|
-
"ai_assistant.share.saving": "
|
|
445
|
-
"ai_assistant.share.selectUser": "
|
|
446
|
-
"ai_assistant.share.shareButton": "
|
|
437
|
+
"ai_assistant.share.addParticipant": "Teilnehmer hinzufügen",
|
|
438
|
+
"ai_assistant.share.allUsersAdded": "Alle Benutzer bereits hinzugefügt",
|
|
439
|
+
"ai_assistant.share.dialogDescription": "Teilen Sie diese Konversation mit anderen Benutzern. Sie erhalten nur Lesezugriff.",
|
|
440
|
+
"ai_assistant.share.dialogTitle": "Konversation teilen",
|
|
441
|
+
"ai_assistant.share.noParticipants": "Noch keine Teilnehmer. Fügen Sie jemanden hinzu, um diese Konversation zu teilen.",
|
|
442
|
+
"ai_assistant.share.participantPlaceholder": "Nach Benutzer suchen...",
|
|
443
|
+
"ai_assistant.share.removeParticipant": "Entfernen",
|
|
444
|
+
"ai_assistant.share.saved": "Gespeichert",
|
|
445
|
+
"ai_assistant.share.saving": "Wird gespeichert...",
|
|
446
|
+
"ai_assistant.share.selectUser": "Benutzer auswählen...",
|
|
447
|
+
"ai_assistant.share.shareButton": "Teilen",
|
|
447
448
|
"ai_assistant.status.analyzing": "Anfrage wird analysiert...",
|
|
448
449
|
"ai_assistant.status.executing": "Tools werden ausgeführt...",
|
|
449
450
|
"ai_assistant.status.responding": "Antwort wird erstellt...",
|
|
@@ -284,6 +284,7 @@
|
|
|
284
284
|
"ai_assistant.dock.left": "Dock Left",
|
|
285
285
|
"ai_assistant.dock.minimize": "Minimize",
|
|
286
286
|
"ai_assistant.dock.right": "Dock Right",
|
|
287
|
+
"ai_assistant.launcher.composerPlaceholder": "Ask anything…",
|
|
287
288
|
"ai_assistant.launcher.dialogTitle": "AI assistants",
|
|
288
289
|
"ai_assistant.launcher.dock.subtitle": "AI assistant",
|
|
289
290
|
"ai_assistant.launcher.empty": "No assistants match your search.",
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
"ai_assistant.chat.pending_phase3.body": "Esta tarjeta interactiva se integrará en la Fase 3 del marco unificado de IA.",
|
|
221
221
|
"ai_assistant.chat.pending_phase3.title": "Tarjeta de aprobación de mutación pendiente",
|
|
222
222
|
"ai_assistant.chat.placeholder": "Pregunte lo que necesite...",
|
|
223
|
-
"ai_assistant.chat.readOnlyNotice": "
|
|
223
|
+
"ai_assistant.chat.readOnlyNotice": "Esta es una conversación compartida. Puede leerla, pero no puede responder.",
|
|
224
224
|
"ai_assistant.chat.reasoning": "Reasoning",
|
|
225
225
|
"ai_assistant.chat.records.fields.amount": "Importe",
|
|
226
226
|
"ai_assistant.chat.records.fields.category": "Categoría",
|
|
@@ -284,6 +284,7 @@
|
|
|
284
284
|
"ai_assistant.dock.left": "Anclar a la izquierda",
|
|
285
285
|
"ai_assistant.dock.minimize": "Minimizar",
|
|
286
286
|
"ai_assistant.dock.right": "Anclar a la derecha",
|
|
287
|
+
"ai_assistant.launcher.composerPlaceholder": "Pregunte lo que necesite…",
|
|
287
288
|
"ai_assistant.launcher.dialogTitle": "AI assistants",
|
|
288
289
|
"ai_assistant.launcher.dock.subtitle": "AI assistant",
|
|
289
290
|
"ai_assistant.launcher.empty": "No assistants match your search.",
|
|
@@ -325,9 +326,9 @@
|
|
|
325
326
|
"ai_assistant.modelPicker.triggerAriaLabel": "Selector de modelo de IA",
|
|
326
327
|
"ai_assistant.modelPicker.useDefault": "Usar predeterminado",
|
|
327
328
|
"ai_assistant.modelPicker.useDefaultWithModel": "Usar predeterminado del agente: {{model}}",
|
|
328
|
-
"ai_assistant.notifications.conversation_shared.body": "
|
|
329
|
-
"ai_assistant.notifications.conversation_shared.title": "
|
|
330
|
-
"ai_assistant.notifications.conversation_shared.view_button": "
|
|
329
|
+
"ai_assistant.notifications.conversation_shared.body": "Se ha compartido una conversación de IA con usted.",
|
|
330
|
+
"ai_assistant.notifications.conversation_shared.title": "Conversación compartida con usted",
|
|
331
|
+
"ai_assistant.notifications.conversation_shared.view_button": "Ver conversación",
|
|
331
332
|
"ai_assistant.playground.agentPickerLabel": "Agente",
|
|
332
333
|
"ai_assistant.playground.chat.notSupportedBody": "Elija un agente cuyo modo de ejecución sea \"chat\" o cambie a la pestaña de modo objeto.",
|
|
333
334
|
"ai_assistant.playground.chat.notSupportedTitle": "El modo chat no está disponible para este agente.",
|
|
@@ -433,17 +434,17 @@
|
|
|
433
434
|
"ai_assistant.settings.visibilityEnabled": "Habilitado",
|
|
434
435
|
"ai_assistant.settings.visibilityTitle": "Visibilidad del asistente de IA",
|
|
435
436
|
"ai_assistant.settings.visibilityToggleLabel": "Mostrar el asistente de IA a los usuarios",
|
|
436
|
-
"ai_assistant.share.addParticipant": "
|
|
437
|
-
"ai_assistant.share.allUsersAdded": "
|
|
438
|
-
"ai_assistant.share.dialogDescription": "
|
|
439
|
-
"ai_assistant.share.dialogTitle": "
|
|
440
|
-
"ai_assistant.share.noParticipants": "
|
|
441
|
-
"ai_assistant.share.participantPlaceholder": "
|
|
442
|
-
"ai_assistant.share.removeParticipant": "
|
|
443
|
-
"ai_assistant.share.saved": "
|
|
444
|
-
"ai_assistant.share.saving": "
|
|
445
|
-
"ai_assistant.share.selectUser": "
|
|
446
|
-
"ai_assistant.share.shareButton": "
|
|
437
|
+
"ai_assistant.share.addParticipant": "Añadir participante",
|
|
438
|
+
"ai_assistant.share.allUsersAdded": "Todos los usuarios ya añadidos",
|
|
439
|
+
"ai_assistant.share.dialogDescription": "Comparta esta conversación con otros usuarios. Obtendrán acceso de solo lectura.",
|
|
440
|
+
"ai_assistant.share.dialogTitle": "Compartir conversación",
|
|
441
|
+
"ai_assistant.share.noParticipants": "Aún no hay participantes. Añada a alguien para compartir esta conversación.",
|
|
442
|
+
"ai_assistant.share.participantPlaceholder": "Buscar usuario...",
|
|
443
|
+
"ai_assistant.share.removeParticipant": "Quitar",
|
|
444
|
+
"ai_assistant.share.saved": "Guardado",
|
|
445
|
+
"ai_assistant.share.saving": "Guardando...",
|
|
446
|
+
"ai_assistant.share.selectUser": "Seleccione un usuario...",
|
|
447
|
+
"ai_assistant.share.shareButton": "Compartir",
|
|
447
448
|
"ai_assistant.status.analyzing": "Analizando solicitud...",
|
|
448
449
|
"ai_assistant.status.executing": "Ejecutando herramientas...",
|
|
449
450
|
"ai_assistant.status.responding": "Respondiendo...",
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
"ai_assistant.chat.pending_phase3.body": "Ta interaktywna karta pojawi się w Fazie 3 zunifikowanego frameworku AI.",
|
|
221
221
|
"ai_assistant.chat.pending_phase3.title": "Karta zatwierdzania mutacji oczekuje",
|
|
222
222
|
"ai_assistant.chat.placeholder": "Zapytaj mnie o cokolwiek...",
|
|
223
|
-
"ai_assistant.chat.readOnlyNotice": "
|
|
223
|
+
"ai_assistant.chat.readOnlyNotice": "To jest udostępniona konwersacja. Możesz ją czytać, ale nie możesz odpowiadać.",
|
|
224
224
|
"ai_assistant.chat.reasoning": "Reasoning",
|
|
225
225
|
"ai_assistant.chat.records.fields.amount": "Kwota",
|
|
226
226
|
"ai_assistant.chat.records.fields.category": "Kategoria",
|
|
@@ -284,6 +284,7 @@
|
|
|
284
284
|
"ai_assistant.dock.left": "Dokuj po lewej",
|
|
285
285
|
"ai_assistant.dock.minimize": "Minimalizuj",
|
|
286
286
|
"ai_assistant.dock.right": "Dokuj po prawej",
|
|
287
|
+
"ai_assistant.launcher.composerPlaceholder": "Zapytaj o cokolwiek…",
|
|
287
288
|
"ai_assistant.launcher.dialogTitle": "AI assistants",
|
|
288
289
|
"ai_assistant.launcher.dock.subtitle": "AI assistant",
|
|
289
290
|
"ai_assistant.launcher.empty": "No assistants match your search.",
|
|
@@ -325,9 +326,9 @@
|
|
|
325
326
|
"ai_assistant.modelPicker.triggerAriaLabel": "Wybierz model AI",
|
|
326
327
|
"ai_assistant.modelPicker.useDefault": "Użyj domyślnego agenta",
|
|
327
328
|
"ai_assistant.modelPicker.useDefaultWithModel": "Użyj domyślnego agenta: {{model}}",
|
|
328
|
-
"ai_assistant.notifications.conversation_shared.body": "
|
|
329
|
-
"ai_assistant.notifications.conversation_shared.title": "
|
|
330
|
-
"ai_assistant.notifications.conversation_shared.view_button": "
|
|
329
|
+
"ai_assistant.notifications.conversation_shared.body": "Udostępniono Ci konwersację z asystentem AI.",
|
|
330
|
+
"ai_assistant.notifications.conversation_shared.title": "Udostępniono Ci konwersację",
|
|
331
|
+
"ai_assistant.notifications.conversation_shared.view_button": "Otwórz konwersację",
|
|
331
332
|
"ai_assistant.playground.agentPickerLabel": "Agent",
|
|
332
333
|
"ai_assistant.playground.chat.notSupportedBody": "Wybierz agenta, którego tryb wykonywania to \"chat\", lub przełącz się na zakładkę trybu obiektowego.",
|
|
333
334
|
"ai_assistant.playground.chat.notSupportedTitle": "Tryb czatu nie jest dostępny dla tego agenta.",
|
|
@@ -433,17 +434,17 @@
|
|
|
433
434
|
"ai_assistant.settings.visibilityEnabled": "Widoczny w nagłówku ze skrótem Cmd+J.",
|
|
434
435
|
"ai_assistant.settings.visibilityTitle": "Asystent AI",
|
|
435
436
|
"ai_assistant.settings.visibilityToggleLabel": "Widoczność",
|
|
436
|
-
"ai_assistant.share.addParticipant": "
|
|
437
|
-
"ai_assistant.share.allUsersAdded": "
|
|
438
|
-
"ai_assistant.share.dialogDescription": "
|
|
439
|
-
"ai_assistant.share.dialogTitle": "
|
|
440
|
-
"ai_assistant.share.noParticipants": "
|
|
441
|
-
"ai_assistant.share.participantPlaceholder": "
|
|
442
|
-
"ai_assistant.share.removeParticipant": "
|
|
443
|
-
"ai_assistant.share.saved": "
|
|
444
|
-
"ai_assistant.share.saving": "
|
|
445
|
-
"ai_assistant.share.selectUser": "
|
|
446
|
-
"ai_assistant.share.shareButton": "
|
|
437
|
+
"ai_assistant.share.addParticipant": "Dodaj uczestnika",
|
|
438
|
+
"ai_assistant.share.allUsersAdded": "Dodano już wszystkich użytkowników",
|
|
439
|
+
"ai_assistant.share.dialogDescription": "Udostępnij tę konwersację innym użytkownikom. Otrzymają dostęp tylko do odczytu.",
|
|
440
|
+
"ai_assistant.share.dialogTitle": "Udostępnij konwersację",
|
|
441
|
+
"ai_assistant.share.noParticipants": "Brak uczestników. Dodaj kogoś, aby udostępnić tę konwersację.",
|
|
442
|
+
"ai_assistant.share.participantPlaceholder": "Szukaj użytkownika...",
|
|
443
|
+
"ai_assistant.share.removeParticipant": "Usuń",
|
|
444
|
+
"ai_assistant.share.saved": "Zapisano",
|
|
445
|
+
"ai_assistant.share.saving": "Zapisywanie...",
|
|
446
|
+
"ai_assistant.share.selectUser": "Wybierz użytkownika...",
|
|
447
|
+
"ai_assistant.share.shareButton": "Udostępnij",
|
|
447
448
|
"ai_assistant.status.analyzing": "Analizowanie żądania...",
|
|
448
449
|
"ai_assistant.status.executing": "Wykonywanie narzędzi...",
|
|
449
450
|
"ai_assistant.status.responding": "Odpowiadanie...",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression coverage for issue #2097 (BUG-004).
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the conversation-shared notification subscriber persists a
|
|
5
|
+
* resolved title/body string rather than the raw i18n key, so that
|
|
6
|
+
* consumers that do not run the client renderer (email, export, digest)
|
|
7
|
+
* see human-readable text instead of `ai_assistant.notifications.…`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
jest.mock('@open-mercato/shared/lib/i18n/server', () => ({
|
|
11
|
+
loadDictionary: jest.fn(async () => ({
|
|
12
|
+
'ai_assistant.notifications.conversation_shared.title': 'Conversation shared with you',
|
|
13
|
+
'ai_assistant.notifications.conversation_shared.body':
|
|
14
|
+
'An AI conversation has been shared with you.',
|
|
15
|
+
})),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
jest.mock('@open-mercato/core/modules/notifications/lib/notificationService', () => ({
|
|
19
|
+
resolveNotificationService: jest.fn(),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
import handleConversationShared from '../conversation-shared-notify'
|
|
23
|
+
import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'
|
|
24
|
+
import type { AiConversationSharedPayload } from '../../events'
|
|
25
|
+
|
|
26
|
+
const RESOLVE_NOTIFICATION_SERVICE = resolveNotificationService as jest.MockedFunction<
|
|
27
|
+
typeof resolveNotificationService
|
|
28
|
+
>
|
|
29
|
+
|
|
30
|
+
function makePayload(
|
|
31
|
+
overrides: Partial<AiConversationSharedPayload> = {},
|
|
32
|
+
): AiConversationSharedPayload {
|
|
33
|
+
return {
|
|
34
|
+
conversationId: 'conv-1',
|
|
35
|
+
tenantId: 'tenant-1',
|
|
36
|
+
organizationId: 'org-1',
|
|
37
|
+
ownerUserId: 'user-owner',
|
|
38
|
+
participantUserId: 'user-recipient',
|
|
39
|
+
role: 'viewer',
|
|
40
|
+
...overrides,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeCtx(create: jest.Mock) {
|
|
45
|
+
const container = { resolve: jest.fn() }
|
|
46
|
+
RESOLVE_NOTIFICATION_SERVICE.mockReturnValue({ create } as never)
|
|
47
|
+
return { resolve: (name: string) => container.resolve(name), container }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('conversation-shared-notify subscriber', () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
jest.clearAllMocks()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('persists a resolved default-locale title and body, not the raw i18n key', async () => {
|
|
56
|
+
const create = jest.fn(async () => ({ ok: true }))
|
|
57
|
+
const ctx = makeCtx(create)
|
|
58
|
+
|
|
59
|
+
await handleConversationShared(makePayload(), ctx)
|
|
60
|
+
|
|
61
|
+
expect(create).toHaveBeenCalledTimes(1)
|
|
62
|
+
const [input] = create.mock.calls[0]
|
|
63
|
+
expect(input.title).toBe('Conversation shared with you')
|
|
64
|
+
expect(input.body).toBe('An AI conversation has been shared with you.')
|
|
65
|
+
expect(input.titleKey).toBe('ai_assistant.notifications.conversation_shared.title')
|
|
66
|
+
expect(input.bodyKey).toBe('ai_assistant.notifications.conversation_shared.body')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('still calls the notification service with the recipient tenant scope', async () => {
|
|
70
|
+
const create = jest.fn(async () => ({ ok: true }))
|
|
71
|
+
const ctx = makeCtx(create)
|
|
72
|
+
|
|
73
|
+
await handleConversationShared(makePayload(), ctx)
|
|
74
|
+
|
|
75
|
+
const [, scope] = create.mock.calls[0]
|
|
76
|
+
expect(scope).toEqual({ tenantId: 'tenant-1', organizationId: 'org-1' })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('returns early without calling create when participantUserId is missing', async () => {
|
|
80
|
+
const create = jest.fn(async () => ({ ok: true }))
|
|
81
|
+
const ctx = makeCtx(create)
|
|
82
|
+
|
|
83
|
+
await handleConversationShared(
|
|
84
|
+
makePayload({ participantUserId: '' as unknown as string }),
|
|
85
|
+
ctx,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
expect(create).not.toHaveBeenCalled()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('returns early without calling create when tenantId is missing', async () => {
|
|
92
|
+
const create = jest.fn(async () => ({ ok: true }))
|
|
93
|
+
const ctx = makeCtx(create)
|
|
94
|
+
|
|
95
|
+
await handleConversationShared(
|
|
96
|
+
makePayload({ tenantId: '' as unknown as string }),
|
|
97
|
+
ctx,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
expect(create).not.toHaveBeenCalled()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('falls back to the i18n key when the dictionary lookup throws', async () => {
|
|
104
|
+
const i18nServer = jest.requireMock('@open-mercato/shared/lib/i18n/server')
|
|
105
|
+
i18nServer.loadDictionary.mockRejectedValueOnce(new Error('dictionary unavailable'))
|
|
106
|
+
const create = jest.fn(async () => ({ ok: true }))
|
|
107
|
+
const ctx = makeCtx(create)
|
|
108
|
+
|
|
109
|
+
await handleConversationShared(makePayload(), ctx)
|
|
110
|
+
|
|
111
|
+
expect(create).toHaveBeenCalledTimes(1)
|
|
112
|
+
const [input] = create.mock.calls[0]
|
|
113
|
+
expect(input.title).toBe('ai_assistant.notifications.conversation_shared.title')
|
|
114
|
+
expect(input.body).toBe('ai_assistant.notifications.conversation_shared.body')
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'
|
|
2
2
|
import { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'
|
|
3
|
+
import { defaultLocale } from '@open-mercato/shared/lib/i18n/config'
|
|
4
|
+
import { loadDictionary } from '@open-mercato/shared/lib/i18n/server'
|
|
5
|
+
import { createTranslator } from '@open-mercato/shared/lib/i18n/translate'
|
|
3
6
|
import { notificationTypes } from '../notifications'
|
|
4
7
|
import type { AiConversationSharedPayload } from '../events'
|
|
5
8
|
|
|
@@ -14,6 +17,23 @@ type ResolverContext = {
|
|
|
14
17
|
container?: { resolve<T = unknown>(name: string): T }
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
async function resolveDefaultLocaleStrings(
|
|
21
|
+
titleKey: string | undefined,
|
|
22
|
+
bodyKey: string | undefined,
|
|
23
|
+
): Promise<{ title: string | undefined; body: string | undefined }> {
|
|
24
|
+
if (!titleKey && !bodyKey) return { title: titleKey, body: bodyKey }
|
|
25
|
+
try {
|
|
26
|
+
const dict = await loadDictionary(defaultLocale)
|
|
27
|
+
const t = createTranslator(dict)
|
|
28
|
+
return {
|
|
29
|
+
title: titleKey ? t(titleKey) : titleKey,
|
|
30
|
+
body: bodyKey ? t(bodyKey) : bodyKey,
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
return { title: titleKey, body: bodyKey }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
export default async function handleConversationShared(
|
|
18
38
|
payload: AiConversationSharedPayload,
|
|
19
39
|
ctx: ResolverContext,
|
|
@@ -39,6 +59,14 @@ export default async function handleConversationShared(
|
|
|
39
59
|
linkHref: `/backend?openAiConversation=${payload.conversationId}`,
|
|
40
60
|
})
|
|
41
61
|
|
|
62
|
+
// Persist a human-readable fallback alongside the i18n keys so consumers
|
|
63
|
+
// that do not run the client renderer (email digests, exports) display a
|
|
64
|
+
// resolved string instead of the raw key. The client renderer continues
|
|
65
|
+
// to re-translate via titleKey/bodyKey for the viewer's locale.
|
|
66
|
+
const resolved = await resolveDefaultLocaleStrings(typeDef.titleKey, typeDef.bodyKey)
|
|
67
|
+
if (resolved.title !== undefined) notificationInput.title = resolved.title
|
|
68
|
+
if (resolved.body !== undefined) notificationInput.body = resolved.body
|
|
69
|
+
|
|
42
70
|
try {
|
|
43
71
|
await notificationService.create(notificationInput, {
|
|
44
72
|
tenantId: payload.tenantId,
|