@things-factory/board-ai 10.1.207 → 10.1.210
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-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/board-ai-resolver.d.ts +5 -5
- package/dist-server/service/board-ai-resolver.js +15 -48
- package/dist-server/service/board-ai-resolver.js.map +1 -1
- package/dist-server/service/board-session-resolver.js +3 -8
- package/dist-server/service/board-session-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/server/service/board-ai-resolver.ts +21 -57
- package/server/service/board-session-resolver.ts +3 -9
- package/test/mention-seat-one-place.test.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/board-ai",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.210",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"@operato/i18n": "^10.0.0",
|
|
33
33
|
"@operato/scene-ops": "^10.14.1",
|
|
34
34
|
"@operato/styles": "^10.0.0",
|
|
35
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
36
|
-
"@things-factory/ai-client-base": "^10.1.
|
|
37
|
-
"@things-factory/auth-base": "^10.1.
|
|
38
|
-
"@things-factory/board-import": "^10.1.
|
|
35
|
+
"@things-factory/ai-assistant": "^10.1.210",
|
|
36
|
+
"@things-factory/ai-client-base": "^10.1.208",
|
|
37
|
+
"@things-factory/auth-base": "^10.1.208",
|
|
38
|
+
"@things-factory/board-import": "^10.1.208",
|
|
39
39
|
"@things-factory/env": "^10.1.149",
|
|
40
|
-
"@things-factory/shell": "^10.1.
|
|
40
|
+
"@things-factory/shell": "^10.1.208",
|
|
41
41
|
"dompurify": "^3.0.0",
|
|
42
42
|
"graphql-tag": "^2.12.6",
|
|
43
43
|
"lit": "^3.1.2",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"copyfiles": "^2.4.1",
|
|
48
48
|
"rimraf": "^5.0.0"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "89fbda12d43d8532ba4e95ba6d6d7e05e2da2502"
|
|
51
51
|
}
|
|
@@ -45,13 +45,11 @@ import { publishChatMessage } from '@things-factory/ai-assistant'
|
|
|
45
45
|
import { PatchEntry } from './patch-entry/patch-entry.js'
|
|
46
46
|
import { publishBoardPatch } from './patch-entry/board-patch-subscription.js'
|
|
47
47
|
import { ChatSessionParticipant } from '@things-factory/ai-assistant'
|
|
48
|
-
import {
|
|
48
|
+
import { seatTurn, sendInviteNotification } from '@things-factory/ai-assistant'
|
|
49
49
|
import {
|
|
50
50
|
injectMentionRefids,
|
|
51
51
|
injectAiMentionRefids,
|
|
52
|
-
stripMentionRefids
|
|
53
|
-
extractMentionUserIds,
|
|
54
|
-
newParticipantUserIds
|
|
52
|
+
stripMentionRefids
|
|
55
53
|
} from './mention-marker.js'
|
|
56
54
|
|
|
57
55
|
@InputType()
|
|
@@ -392,7 +390,9 @@ export class BoardAIChatResolver {
|
|
|
392
390
|
// C4: @user 멘션 → 세션 참여자(member) 자동 등록 (초대). best-effort — 실패해도 chat 흐름 무영향.
|
|
393
391
|
// enriched content 의 {userId:UUID} 마커에서 추출 → 기존 참여자/자기 제외한 신규만 member 로 등록.
|
|
394
392
|
try {
|
|
395
|
-
await this._inviteMentionedUsers(enriched, session
|
|
393
|
+
await this._inviteMentionedUsers(enriched, session as any, domain.id, user, (context.state as any).notify, (key, options) =>
|
|
394
|
+
typeof (context as any).t === 'function' ? (context as any).t(key, options) : String(options?.defaultValue ?? key)
|
|
395
|
+
)
|
|
396
396
|
} catch {
|
|
397
397
|
/* 초대 실패는 대화를 막지 않는다 */
|
|
398
398
|
}
|
|
@@ -691,66 +691,30 @@ export class BoardAIChatResolver {
|
|
|
691
691
|
}
|
|
692
692
|
|
|
693
693
|
/**
|
|
694
|
-
* C4: 메시지에 @멘션된 사용자를 세션 참여자(member)로
|
|
695
|
-
*
|
|
696
|
-
*
|
|
694
|
+
* C4: 메시지에 @멘션된 사용자를 세션 참여자(member)로 등록 — 초대.
|
|
695
|
+
*
|
|
696
|
+
* 자리는 ai-assistant 의 한 곳(`seatTurn` — 공개 방의 글쓴이, 그리고 멘션 → `seatParticipants`)이 준다. 여기에 따로 있던 사본은 공개
|
|
697
|
+
* 범위를 보지 않아, 그룹 · 비공개 보드 대화에서도 누구의 멘션이든 사람을 앉혔다(2026-10-24, ADR-0073 결정 3).
|
|
698
|
+
* 알림도 같은 한 곳(`sendInviteNotification`) — 보드 문구와 모델러 링크.
|
|
697
699
|
*/
|
|
698
700
|
private async _inviteMentionedUsers(
|
|
699
701
|
content: string,
|
|
700
|
-
|
|
702
|
+
session: { id?: string; boardId?: string; name?: string; visibility?: string },
|
|
701
703
|
domainId: string,
|
|
702
704
|
inviter: any,
|
|
703
|
-
|
|
704
|
-
|
|
705
|
+
notify?: (arg: any) => Promise<void>,
|
|
706
|
+
t?: (key: string, options: Record<string, unknown>) => string
|
|
705
707
|
): Promise<void> {
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
708
|
+
const { invited } = await seatTurn({
|
|
709
|
+
content,
|
|
710
|
+
sessionId: session.id!,
|
|
711
|
+
domainId,
|
|
712
|
+
inviter,
|
|
713
|
+
participants: getRepository(ChatSessionParticipant),
|
|
714
|
+
visibility: session.visibility
|
|
713
715
|
})
|
|
714
|
-
|
|
715
|
-
const fresh = newParticipantUserIds(mentioned, activeIds, inviter?.id)
|
|
716
|
-
if (fresh.length === 0) return
|
|
717
|
-
|
|
718
|
-
for (const userId of fresh) {
|
|
719
|
-
// 과거에 나갔던(soft-deleted) 참여자면 되살리고, 아니면 새로 등록.
|
|
720
|
-
const prior = existing.find(p => p.userId === userId && p.deletedAt)
|
|
721
|
-
if (prior) {
|
|
722
|
-
await repo.update(prior.id!, { deletedAt: null as any, role: 'member', updater: inviter } as any)
|
|
723
|
-
} else {
|
|
724
|
-
await repo.save({
|
|
725
|
-
domain: { id: domainId } as any,
|
|
726
|
-
session: { id: sessionId } as any,
|
|
727
|
-
user: { id: userId } as any,
|
|
728
|
-
role: 'member',
|
|
729
|
-
creator: inviter,
|
|
730
|
-
updater: inviter
|
|
731
|
-
} as any)
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// C4b-2: 초대 알림 — notification 모듈의 기존 발송 메커니즘 재사용(중복 구현 금지).
|
|
736
|
-
// context.state.notify 는 notification-middleware 가 전역 주입(FCM/pubsub). duck-typed →
|
|
737
|
-
// board-ai 에 의존성 추가 없이 런타임 호출. 미로드 환경이면 undefined → graceful skip.
|
|
738
|
-
this._notifyInvited(notify, fresh, boardId, inviter)
|
|
716
|
+
await sendInviteNotification(notify, invited, inviter, session.name, session.boardId, t)
|
|
739
717
|
}
|
|
740
718
|
|
|
741
|
-
/** 초대된 사용자들에게 협업 초대 알림 발송 (best-effort, capability 있을 때만). */
|
|
742
|
-
private _notifyInvited(
|
|
743
|
-
notify: ((arg: any) => Promise<void>) | undefined,
|
|
744
|
-
userIds: string[],
|
|
745
|
-
boardId: string | undefined,
|
|
746
|
-
inviter: any
|
|
747
|
-
): void {
|
|
748
|
-
if (typeof notify !== 'function') return
|
|
749
|
-
const payload = buildInviteNotification(userIds, boardId, inviter)
|
|
750
|
-
if (!payload) return
|
|
751
|
-
Promise.resolve(notify(payload)).catch(() => {
|
|
752
|
-
/* 알림 실패는 초대·대화를 막지 않는다 */
|
|
753
|
-
})
|
|
754
|
-
}
|
|
755
719
|
}
|
|
756
720
|
|
|
@@ -30,8 +30,7 @@ import { publishSessionActivity } from '@things-factory/ai-assistant'
|
|
|
30
30
|
import { ChatMessage } from '@things-factory/ai-assistant'
|
|
31
31
|
import { publishChatMessage } from '@things-factory/ai-assistant'
|
|
32
32
|
import { ChatSessionResolver } from '@things-factory/ai-assistant'
|
|
33
|
-
import {
|
|
34
|
-
import { mayReadSession } from '@things-factory/ai-assistant'
|
|
33
|
+
import { isSessionParticipant, mayReadSession } from '@things-factory/ai-assistant'
|
|
35
34
|
import { PatchEntry } from './patch-entry/patch-entry.js'
|
|
36
35
|
import { mayRevertPatch } from './patch-entry/revert-permission.js'
|
|
37
36
|
import { publishBoardPatch } from './patch-entry/board-patch-subscription.js'
|
|
@@ -187,13 +186,8 @@ export class BoardSessionResolver {
|
|
|
187
186
|
relations: ['session']
|
|
188
187
|
})
|
|
189
188
|
const session: any = patch?.session
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
? !!(await getRepository(ChatSessionParticipant, tx).findOneBy({
|
|
193
|
-
session: { id: session.id },
|
|
194
|
-
user: { id: user.id }
|
|
195
|
-
} as any))
|
|
196
|
-
: false
|
|
189
|
+
/* The room's own lookup - a hide-only `viewer` row is not a seat, and a group or private room asks it (ADR-0073). */
|
|
190
|
+
const isParticipant = !!session && (await isSessionParticipant(domain.id, session.id, user?.id, tx))
|
|
197
191
|
|
|
198
192
|
if (!mayRevertPatch(patch ? { sessionVisibility: session?.visibility } : null, isParticipant, mayReadSession)) {
|
|
199
193
|
throw new Error(`PatchEntry ${patchId} not found`)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A board conversation seats people the way every conversation does (ADR-0073 decision 3).
|
|
3
|
+
*
|
|
4
|
+
* board-ai had its own copy of "a mention seats the person": it never asked the room's visibility, so in a group
|
|
5
|
+
* or private board conversation anyone's mention seated anyone. The seat now comes from ai-assistant's one place,
|
|
6
|
+
* which asks `maySeat`. This holds the resolver to it - a second copy fails here.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it } from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
|
|
13
|
+
const resolver = readFileSync(join(import.meta.dirname, '../server/service/board-ai-resolver.ts'), 'utf-8')
|
|
14
|
+
|
|
15
|
+
describe('a board conversation seats people through the one place', () => {
|
|
16
|
+
it('★ the turn\'s seats (writer, then mentions) go through seatTurn, with the room visibility', () => {
|
|
17
|
+
assert.match(resolver, /await seatTurn\(\{[\s\S]*?visibility: session\.visibility[\s\S]*?\}\)/)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('★ no seat is written here - no member row, no own mention parser', () => {
|
|
21
|
+
assert.doesNotMatch(resolver, /role: 'member'/)
|
|
22
|
+
assert.doesNotMatch(resolver, /newParticipantUserIds|extractMentionUserIds/)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('the invite is told the shared way, with the board link', () => {
|
|
26
|
+
assert.match(resolver, /sendInviteNotification\(notify, invited, inviter, session\.name, session\.boardId, t\)/)
|
|
27
|
+
})
|
|
28
|
+
})
|