@things-factory/ai-assistant 10.1.125 → 10.1.129

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 (41) 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 +3 -3
  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 +10 -2
  38. package/translations/ja.json +1 -1
  39. package/translations/ko.json +10 -2
  40. package/translations/ms.json +1 -1
  41. package/translations/zh.json +1 -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.129",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh",
@@ -33,7 +33,7 @@
33
33
  "@operato/popup": "^10.16.0",
34
34
  "@operato/shell": "^10.0.0",
35
35
  "@operato/styles": "^10.0.0",
36
- "@things-factory/ai-client-base": "^10.1.114",
36
+ "@things-factory/ai-client-base": "^10.1.129",
37
37
  "@things-factory/apptool-base": "^10.1.123",
38
38
  "@things-factory/attachment-base": "^10.1.123",
39
39
  "@things-factory/auth-base": "^10.1.123",
@@ -52,5 +52,5 @@
52
52
  "copyfiles": "^2.4.1",
53
53
  "rimraf": "^5.0.0"
54
54
  },
55
- "gitHead": "74e02381ea5f179dca2b7516078eb2709ac28ffe"
55
+ "gitHead": "4d1ef242d9b7e6669a34c27b6d31aed778c07a90"
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',
@@ -91,7 +91,7 @@
91
91
  "ai-assistant.notice.stopped": "the request stopped partway, so only part of it was done.",
92
92
  "ai-assistant.notice.provider-busy": "the ai service did not answer just now. try again in a moment.",
93
93
  "ai-assistant.notice.provider-error": "something went wrong while handling this. tell an administrator if it keeps happening.",
94
- "ai-assistant.notice.too-long": "this ran long and stopped at step {iter}. try asking in smaller pieces.",
94
+ "ai-assistant.notice.too-long": "this stopped after {iter} lookups without an answer.",
95
95
  "ai-assistant.notice.tool-kept-failing": "{tool} failed {count} times in a row, so it stopped. try saying it differently.",
96
96
  "ai-assistant.text.no-match": "no match",
97
97
  "ai-assistant.text.messages-unreadable": "could not read this conversation.",
@@ -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
  }
@@ -91,7 +91,7 @@
91
91
  "ai-assistant.notice.stopped": "リクエストが途中で止まり、一部だけ処理しました。",
92
92
  "ai-assistant.notice.provider-busy": "AI サービスが今応答していません。少し待ってからもう一度お試しください。",
93
93
  "ai-assistant.notice.provider-error": "処理中に問題が発生しました。同じことが続く場合は管理者にお知らせください。",
94
- "ai-assistant.notice.too-long": "処理が長くなり {iter} 段階で止まりました。分けて依頼してみてください。",
94
+ "ai-assistant.notice.too-long": "{iter} 回の照会で答えを出せずに止まりました。",
95
95
  "ai-assistant.notice.tool-kept-failing": "{tool} の呼び出しが {count} 回続けて失敗したため止まりました。言い方を変えてみてください。",
96
96
  "ai-assistant.text.no-match": "一致するものがありません",
97
97
  "ai-assistant.text.messages-unreadable": "この会話を読み取れませんでした。",
@@ -91,7 +91,7 @@
91
91
  "ai-assistant.notice.stopped": "요청이 중간에 멈춰서 일부만 처리했습니다.",
92
92
  "ai-assistant.notice.provider-busy": "AI 서비스가 지금 응답하지 않습니다. 잠시 뒤에 다시 해 보세요.",
93
93
  "ai-assistant.notice.provider-error": "처리 중에 문제가 생겼습니다. 같은 문제가 계속되면 관리자에게 알려 주세요.",
94
- "ai-assistant.notice.too-long": "일이 길어져서 {iter}단계에서 멈췄습니다. 나눠서 요청해 보세요.",
94
+ "ai-assistant.notice.too-long": "조회 {iter}번 안에 답을 만들지 못하고 멈췄습니다.",
95
95
  "ai-assistant.notice.tool-kept-failing": "{tool} 호출이 {count}번 잇따라 실패해서 멈췄습니다. 다르게 말해 보세요.",
96
96
  "ai-assistant.text.no-match": "맞는 것이 없습니다",
97
97
  "ai-assistant.text.messages-unreadable": "이 대화를 읽지 못했습니다.",
@@ -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
  }
@@ -91,7 +91,7 @@
91
91
  "ai-assistant.notice.stopped": "permintaan terhenti separuh jalan, jadi sebahagian sahaja selesai.",
92
92
  "ai-assistant.notice.provider-busy": "perkhidmatan ai tidak menjawab sekarang. cuba lagi sebentar lagi.",
93
93
  "ai-assistant.notice.provider-error": "ada masalah semasa mengendalikan ini. beritahu pentadbir jika ia berulang.",
94
- "ai-assistant.notice.too-long": "ini berjalan lama dan berhenti pada langkah {iter}. cuba minta dalam bahagian yang lebih kecil.",
94
+ "ai-assistant.notice.too-long": "ini berhenti selepas {iter} carian tanpa jawapan.",
95
95
  "ai-assistant.notice.tool-kept-failing": "{tool} gagal {count} kali berturut-turut, jadi ia berhenti. cuba nyatakan dengan cara lain.",
96
96
  "ai-assistant.text.no-match": "tiada padanan",
97
97
  "ai-assistant.text.messages-unreadable": "tidak dapat membaca perbualan ini.",
@@ -91,7 +91,7 @@
91
91
  "ai-assistant.notice.stopped": "请求中途停止,只处理了一部分。",
92
92
  "ai-assistant.notice.provider-busy": "AI 服务暂时没有回应,请稍后再试。",
93
93
  "ai-assistant.notice.provider-error": "处理时出了问题。如果一直这样,请告知管理员。",
94
- "ai-assistant.notice.too-long": "处理过长,在第 {iter} 步停止。请分成小一些的请求。",
94
+ "ai-assistant.notice.too-long": "查询 {iter} 次后仍未得出答案,已停止。",
95
95
  "ai-assistant.notice.tool-kept-failing": "{tool} 连续失败 {count} 次,已停止。请换一种说法。",
96
96
  "ai-assistant.text.no-match": "没有匹配项",
97
97
  "ai-assistant.text.messages-unreadable": "无法读取这个对话。",