@things-factory/ai-assistant 10.1.125 → 10.1.127

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.
Files changed (38) hide show
  1. package/client/components/assistant-chat.ts +122 -68
  2. package/client/components/chat-echo-dedup.ts +2 -1
  3. package/client/components/reply-message.test.ts +15 -0
  4. package/client/components/reply-message.ts +11 -0
  5. package/client/components/roster-identity.test.ts +18 -0
  6. package/client/components/roster-identity.ts +10 -0
  7. package/dist-client/components/assistant-chat.d.ts +7 -0
  8. package/dist-client/components/assistant-chat.js +127 -68
  9. package/dist-client/components/assistant-chat.js.map +1 -1
  10. package/dist-client/components/chat-echo-dedup.d.ts +1 -0
  11. package/dist-client/components/chat-echo-dedup.js +1 -0
  12. package/dist-client/components/chat-echo-dedup.js.map +1 -1
  13. package/dist-client/components/reply-message.d.ts +5 -0
  14. package/dist-client/components/reply-message.js +14 -0
  15. package/dist-client/components/reply-message.js.map +1 -0
  16. package/dist-client/components/reply-message.test.d.ts +1 -0
  17. package/dist-client/components/reply-message.test.js +16 -0
  18. package/dist-client/components/reply-message.test.js.map +1 -0
  19. package/dist-client/components/roster-identity.d.ts +11 -0
  20. package/dist-client/components/roster-identity.js +11 -0
  21. package/dist-client/components/roster-identity.js.map +1 -0
  22. package/dist-client/components/roster-identity.test.d.ts +1 -0
  23. package/dist-client/components/roster-identity.test.js +20 -0
  24. package/dist-client/components/roster-identity.test.js.map +1 -0
  25. package/dist-client/tsconfig.tsbuildinfo +1 -1
  26. package/dist-server/service/chat-session-participant/read-roster.d.ts +9 -0
  27. package/dist-server/service/chat-session-participant/read-roster.js +15 -0
  28. package/dist-server/service/chat-session-participant/read-roster.js.map +1 -0
  29. package/dist-server/service/chat-session-resolver.d.ts +1 -1
  30. package/dist-server/service/chat-session-resolver.js +15 -11
  31. package/dist-server/service/chat-session-resolver.js.map +1 -1
  32. package/dist-server/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +2 -2
  34. package/server/service/chat-session-participant/read-roster.test.ts +18 -0
  35. package/server/service/chat-session-participant/read-roster.ts +14 -0
  36. package/server/service/chat-session-resolver.ts +11 -8
  37. package/translations/en.json +9 -1
  38. package/translations/ko.json +9 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/ai-assistant",
3
- "version": "10.1.125",
3
+ "version": "10.1.127",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh",
@@ -52,5 +52,5 @@
52
52
  "copyfiles": "^2.4.1",
53
53
  "rimraf": "^5.0.0"
54
54
  },
55
- "gitHead": "74e02381ea5f179dca2b7516078eb2709ac28ffe"
55
+ "gitHead": "1a224d9f1cbf56697a739e505b97333dd090d473"
56
56
  }
@@ -0,0 +1,18 @@
1
+ import { readRoster } from './read-roster'
2
+
3
+ describe('participant roster identities', () => {
4
+ it('loads the participating user relation for public conversations', async () => {
5
+ const find = jest.fn(async options => options.relations.includes('user') ? [{id:'p',user:{id:'u',name:'김희태'}}] : [{id:'p'}])
6
+ const isParticipant=jest.fn(async()=>false)
7
+ const rows=await readRoster({id:'room',domainId:'domain',visibility:'open'},{find,isParticipant})
8
+ expect(rows).toEqual([{id:'p',user:{id:'u',name:'김희태'}}])
9
+ expect(find).toHaveBeenCalledWith(expect.objectContaining({where:{session:{id:'room'},domain:{id:'domain'}},relations:['user']}))
10
+ expect(isParticipant).not.toHaveBeenCalled()
11
+ })
12
+ it('does not expose identities from a private room to an outsider', async () => {
13
+ const find=jest.fn(async()=>[{id:'p',user:{name:'Private'}}])
14
+ expect(await readRoster({id:'r',domainId:'d',visibility:'private'},{find,isParticipant:async()=>false})).toEqual([])
15
+ expect(find).not.toHaveBeenCalled()
16
+ expect(await readRoster({id:'r',domainId:'d',visibility:'private'},{find,isParticipant:async()=>true})).toEqual([{id:'p',user:{name:'Private'}}])
17
+ })
18
+ })
@@ -0,0 +1,14 @@
1
+ import { mayReadSession } from '../chat-session/session-visibility.js'
2
+
3
+ /** Read the room roster with user identities hydrated, after applying the room's visibility gate. */
4
+ export async function readRoster<T>(
5
+ room: { id: string; domainId: string; visibility?: string },
6
+ port: { isParticipant(): Promise<boolean>; find(options: any): Promise<T[]> }
7
+ ): Promise<T[]> {
8
+ if (!mayReadSession(room.visibility, room.visibility === 'private' ? await port.isParticipant() : false)) return []
9
+ return port.find({
10
+ where: { session: { id: room.id }, domain: { id: room.domainId } },
11
+ relations: ['user'],
12
+ order: { createdAt: 'ASC' }
13
+ })
14
+ }
@@ -21,6 +21,7 @@ import { readerGate } from './chat-message/reader-gate.js'
21
21
  import { initialVisibility, mayReadSession, SESSION_VISIBILITIES } from './chat-session/session-visibility.js'
22
22
  import { inviteMentionedUsers } from './chat-session-participant/mention-invite.js'
23
23
  import { buildInviteNotification } from './chat-session-participant/invite-notification.js'
24
+ import { readRoster } from './chat-session-participant/read-roster.js'
24
25
  import { isSessionParticipant } from './chat-session-participant/participant-lookup.js'
25
26
  import { assertMayWriteSession } from './chat-session/session-write-gate.js'
26
27
 
@@ -570,15 +571,15 @@ export class ChatSessionResolver {
570
571
  @Arg('sessionId') sessionId: string,
571
572
  @Ctx() context: ResolverContext
572
573
  ): Promise<ChatSessionParticipant[]> {
573
- const { domain } = context.state
574
+ const { domain, user } = context.state
574
575
  const session = await getRepository(ChatSession).findOneBy({
575
576
  id: sessionId,
576
577
  domain: { id: domain.id }
577
578
  })
578
579
  if (!session) return []
579
- return await getRepository(ChatSessionParticipant).find({
580
- where: { session: { id: sessionId } as any, domain: { id: domain.id } },
581
- order: { createdAt: 'ASC' }
580
+ return readRoster({id:sessionId,domainId:domain.id,visibility:(session as any).visibility}, {
581
+ isParticipant: () => isSessionParticipant(domain.id,sessionId,user?.id),
582
+ find: options => getRepository(ChatSessionParticipant).find(options)
582
583
  })
583
584
  }
584
585
 
@@ -872,6 +873,7 @@ export class ChatSessionResolver {
872
873
  async sayToRoom(
873
874
  @Arg('sessionId') sessionId: string,
874
875
  @Arg('content') content: string,
876
+ @Arg('replyToId', { nullable: true }) replyToId: string | undefined,
875
877
  @Ctx() context: ResolverContext
876
878
  ): Promise<string | null> {
877
879
  const { domain, user, tx } = context.state
@@ -882,10 +884,11 @@ export class ChatSessionResolver {
882
884
  const body = roomMessageBody(content)
883
885
  if (!body) return null
884
886
 
885
- const previousLast = await getRepository(ChatMessage, tx).findOne({
886
- where: { session: { id: session.id } as any },
887
- order: { createdAt: 'DESC' }
888
- })
887
+ const messages = getRepository(ChatMessage, tx)
888
+ const previousLast = replyToId
889
+ ? await messages.findOneBy({ id: replyToId, session: { id: session.id } as any })
890
+ : await messages.findOne({ where: { session: { id: session.id } as any }, order: { createdAt: 'DESC' } })
891
+ if (replyToId && !previousLast) throw new Error('The message to reply to is not in this conversation')
889
892
  const saved = await getRepository(ChatMessage, tx).save({
890
893
  session: { id: session.id } as any,
891
894
  role: 'user',
@@ -309,5 +309,13 @@
309
309
  "ai-assistant.text.send-shortcut": "{key} to send",
310
310
  "ai-assistant.text.send-tooltip-wide": "Send ({key})",
311
311
  "ai-assistant.text.table-shape": "{rows} rows × {columns} columns",
312
- "ai-assistant.button.table-to-words": "Put in as words"
312
+ "ai-assistant.button.table-to-words": "Put in as words",
313
+ "ai-assistant.text.invite-checking": "Checking participant identities before showing invitation eligibility.",
314
+ "ai-assistant.text.participant-unavailable": "User unavailable",
315
+ "ai-assistant.text.send-to-people": "To people",
316
+ "ai-assistant.text.send-to-ai": "To AI",
317
+ "ai-assistant.text.room-attachments-unavailable": "Attachments to participants are not supported yet. Remove the attachments before sending.",
318
+ "ai-assistant.button.reply": "Reply",
319
+ "ai-assistant.button.cancel-reply": "Cancel reply",
320
+ "ai-assistant.text.reply-to": "Reply to {name}"
313
321
  }
@@ -309,5 +309,13 @@
309
309
  "ai-assistant.text.send-shortcut": "{key} 보내기",
310
310
  "ai-assistant.text.send-tooltip-wide": "전송 ({key})",
311
311
  "ai-assistant.text.table-shape": "{rows}행 × {columns}열",
312
- "ai-assistant.button.table-to-words": "글로 넣기"
312
+ "ai-assistant.button.table-to-words": "글로 넣기",
313
+ "ai-assistant.text.invite-checking": "참여자 정보를 확인 중입니다. 확인되면 초대 가능 여부를 표시합니다.",
314
+ "ai-assistant.text.participant-unavailable": "사용자 정보 없음",
315
+ "ai-assistant.text.send-to-people": "참여자에게",
316
+ "ai-assistant.text.send-to-ai": "AI에게",
317
+ "ai-assistant.text.room-attachments-unavailable": "참여자에게 첨부파일을 보내는 기능은 아직 지원하지 않습니다. 첨부파일을 제거한 뒤 보내주세요.",
318
+ "ai-assistant.button.reply": "답장",
319
+ "ai-assistant.button.cancel-reply": "답장 취소",
320
+ "ai-assistant.text.reply-to": "{name}에게 답장"
313
321
  }