@things-factory/board-ai 10.1.5 → 10.1.7
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/client/components/default-catalog-entries.ts +1 -1
- package/client/components/default-slash-templates.ts +1 -1
- package/client/index.ts +39 -6
- package/dist-client/client/components/default-catalog-entries.d.ts +1 -1
- package/dist-client/client/components/default-catalog-entries.js.map +1 -1
- package/dist-client/client/components/default-slash-templates.d.ts +1 -1
- package/dist-client/client/components/default-slash-templates.js.map +1 -1
- package/dist-client/client/index.d.ts +3 -11
- package/dist-client/client/index.js +37 -6
- package/dist-client/client/index.js.map +1 -1
- package/dist-client/server/service/assistant.d.ts +22 -22
- package/dist-client/server/service/assistant.js +32 -70
- package/dist-client/server/service/assistant.js.map +1 -1
- package/dist-client/server/service/grounding.d.ts +2 -12
- package/dist-client/server/service/grounding.js +15 -23
- package/dist-client/server/service/grounding.js.map +1 -1
- package/dist-client/server/service/types.d.ts +2 -4
- package/dist-client/server/service/types.js.map +1 -1
- package/dist-client/server/service/validation/board-model-schema.d.ts +270 -270
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/assistant.d.ts +22 -22
- package/dist-server/service/assistant.js +25 -76
- package/dist-server/service/assistant.js.map +1 -1
- package/dist-server/service/board-ai-resolver.js +41 -30
- package/dist-server/service/board-ai-resolver.js.map +1 -1
- package/dist-server/service/board-session-resolver.d.ts +12 -0
- package/dist-server/service/board-session-resolver.js +203 -0
- package/dist-server/service/board-session-resolver.js.map +1 -0
- package/dist-server/service/board-tool-rights.d.ts +7 -0
- package/dist-server/service/board-tool-rights.js +62 -0
- package/dist-server/service/board-tool-rights.js.map +1 -0
- package/dist-server/service/grounding.d.ts +2 -12
- package/dist-server/service/grounding.js +16 -24
- package/dist-server/service/grounding.js.map +1 -1
- package/dist-server/service/index.d.ts +4 -10
- package/dist-server/service/index.js +16 -23
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/patch-entry/board-patch-subscription.js +2 -2
- package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -1
- package/dist-server/service/patch-entry/patch-entry.d.ts +1 -1
- package/dist-server/service/patch-entry/patch-entry.js +3 -3
- package/dist-server/service/patch-entry/patch-entry.js.map +1 -1
- package/dist-server/service/types.d.ts +2 -4
- package/dist-server/service/types.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/server/service/apply-patch-drift.test.ts +3 -1
- package/server/service/assistant.ts +47 -73
- package/server/service/board-ai-resolver.ts +22 -12
- package/server/service/board-session-resolver.ts +183 -0
- package/server/service/board-tool-rights.test.ts +96 -0
- package/server/service/board-tool-rights.ts +73 -0
- package/server/service/dock-contract.test.ts +3 -0
- package/server/service/grounding.test.ts +3 -3
- package/server/service/grounding.ts +15 -34
- package/server/service/index.ts +16 -22
- package/server/service/patch-entry/board-patch-subscription.ts +1 -1
- package/server/service/patch-entry/patch-entry.ts +3 -2
- package/server/service/registry-tool-loop.test.ts +1 -0
- package/server/service/tool-exposure.test.ts +2 -0
- package/server/service/types.ts +3 -4
- package/translations/en.json +1 -62
- package/translations/ja.json +1 -61
- package/translations/ko.json +1 -62
- package/translations/ms.json +1 -61
- package/translations/zh.json +1 -61
- package/client/components/board-ai-chat.test.ts +0 -149
- package/client/components/board-ai-chat.ts +0 -3184
- package/client/components/chat-echo-dedup.test.ts +0 -230
- package/client/components/chat-echo-dedup.ts +0 -155
- package/client/components/chat-input-builder.ts +0 -69
- package/client/components/markdown.test.ts +0 -518
- package/client/components/markdown.ts +0 -508
- package/client/components/mention-popup-helpers.ts +0 -433
- package/client/components/mention-popup.test.ts +0 -588
- package/client/components/mention-popup.ts +0 -238
- package/client/utils/board-edit-patch.ts +0 -374
- package/dist-client/client/components/board-ai-chat.d.ts +0 -384
- package/dist-client/client/components/board-ai-chat.js +0 -3008
- package/dist-client/client/components/board-ai-chat.js.map +0 -1
- package/dist-client/client/components/board-ai-chat.test.d.ts +0 -1
- package/dist-client/client/components/board-ai-chat.test.js +0 -138
- package/dist-client/client/components/board-ai-chat.test.js.map +0 -1
- package/dist-client/client/components/chat-echo-dedup.d.ts +0 -54
- package/dist-client/client/components/chat-echo-dedup.js +0 -115
- package/dist-client/client/components/chat-echo-dedup.js.map +0 -1
- package/dist-client/client/components/chat-echo-dedup.test.d.ts +0 -1
- package/dist-client/client/components/chat-echo-dedup.test.js +0 -203
- package/dist-client/client/components/chat-echo-dedup.test.js.map +0 -1
- package/dist-client/client/components/chat-input-builder.d.ts +0 -55
- package/dist-client/client/components/chat-input-builder.js +0 -32
- package/dist-client/client/components/chat-input-builder.js.map +0 -1
- package/dist-client/client/components/markdown.d.ts +0 -60
- package/dist-client/client/components/markdown.js +0 -422
- package/dist-client/client/components/markdown.js.map +0 -1
- package/dist-client/client/components/markdown.test.d.ts +0 -1
- package/dist-client/client/components/markdown.test.js +0 -418
- package/dist-client/client/components/markdown.test.js.map +0 -1
- package/dist-client/client/components/mention-popup-helpers.d.ts +0 -165
- package/dist-client/client/components/mention-popup-helpers.js +0 -330
- package/dist-client/client/components/mention-popup-helpers.js.map +0 -1
- package/dist-client/client/components/mention-popup.d.ts +0 -35
- package/dist-client/client/components/mention-popup.js +0 -223
- package/dist-client/client/components/mention-popup.js.map +0 -1
- package/dist-client/client/components/mention-popup.test.d.ts +0 -1
- package/dist-client/client/components/mention-popup.test.js +0 -474
- package/dist-client/client/components/mention-popup.test.js.map +0 -1
- package/dist-client/client/utils/board-edit-patch.d.ts +0 -157
- package/dist-client/client/utils/board-edit-patch.js +0 -284
- package/dist-client/client/utils/board-edit-patch.js.map +0 -1
- package/dist-server/service/chat-message/chat-message-broadcast.d.ts +0 -13
- package/dist-server/service/chat-message/chat-message-broadcast.js +0 -13
- package/dist-server/service/chat-message/chat-message-broadcast.js.map +0 -1
- package/dist-server/service/chat-message/chat-message-publish.d.ts +0 -6
- package/dist-server/service/chat-message/chat-message-publish.js +0 -27
- package/dist-server/service/chat-message/chat-message-publish.js.map +0 -1
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +0 -8
- package/dist-server/service/chat-message/chat-message-subscription.js +0 -53
- package/dist-server/service/chat-message/chat-message-subscription.js.map +0 -1
- package/dist-server/service/chat-message/chat-message.d.ts +0 -68
- package/dist-server/service/chat-message/chat-message.js +0 -172
- package/dist-server/service/chat-message/chat-message.js.map +0 -1
- package/dist-server/service/chat-message/fold-history.d.ts +0 -43
- package/dist-server/service/chat-message/fold-history.js +0 -33
- package/dist-server/service/chat-message/fold-history.js.map +0 -1
- package/dist-server/service/chat-message/history-summary.d.ts +0 -43
- package/dist-server/service/chat-message/history-summary.js +0 -77
- package/dist-server/service/chat-message/history-summary.js.map +0 -1
- package/dist-server/service/chat-message/index.d.ts +0 -5
- package/dist-server/service/chat-message/index.js +0 -12
- package/dist-server/service/chat-message/index.js.map +0 -1
- package/dist-server/service/chat-message/llm-history.d.ts +0 -46
- package/dist-server/service/chat-message/llm-history.js +0 -87
- package/dist-server/service/chat-message/llm-history.js.map +0 -1
- package/dist-server/service/chat-session/chat-session.d.ts +0 -62
- package/dist-server/service/chat-session/chat-session.js +0 -164
- package/dist-server/service/chat-session/chat-session.js.map +0 -1
- package/dist-server/service/chat-session/index.d.ts +0 -3
- package/dist-server/service/chat-session/index.js +0 -7
- package/dist-server/service/chat-session/index.js.map +0 -1
- package/dist-server/service/chat-session/session-activity-broadcast.d.ts +0 -30
- package/dist-server/service/chat-session/session-activity-broadcast.js +0 -34
- package/dist-server/service/chat-session/session-activity-broadcast.js.map +0 -1
- package/dist-server/service/chat-session/session-activity-publish.d.ts +0 -12
- package/dist-server/service/chat-session/session-activity-publish.js +0 -37
- package/dist-server/service/chat-session/session-activity-publish.js.map +0 -1
- package/dist-server/service/chat-session/session-activity-subscription.d.ts +0 -14
- package/dist-server/service/chat-session/session-activity-subscription.js +0 -87
- package/dist-server/service/chat-session/session-activity-subscription.js.map +0 -1
- package/dist-server/service/chat-session/session-anchor.d.ts +0 -12
- package/dist-server/service/chat-session/session-anchor.js +0 -20
- package/dist-server/service/chat-session/session-anchor.js.map +0 -1
- package/dist-server/service/chat-session/session-inbox.d.ts +0 -80
- package/dist-server/service/chat-session/session-inbox.js +0 -133
- package/dist-server/service/chat-session/session-inbox.js.map +0 -1
- package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +0 -49
- package/dist-server/service/chat-session-participant/chat-session-participant.js +0 -141
- package/dist-server/service/chat-session-participant/chat-session-participant.js.map +0 -1
- package/dist-server/service/chat-session-participant/index.d.ts +0 -4
- package/dist-server/service/chat-session-participant/index.js +0 -10
- package/dist-server/service/chat-session-participant/index.js.map +0 -1
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +0 -27
- package/dist-server/service/chat-session-participant/invite-notification.js +0 -30
- package/dist-server/service/chat-session-participant/invite-notification.js.map +0 -1
- package/dist-server/service/chat-session-participant/presence.d.ts +0 -4
- package/dist-server/service/chat-session-participant/presence.js +0 -22
- package/dist-server/service/chat-session-participant/presence.js.map +0 -1
- package/dist-server/service/chat-session-resolver.d.ts +0 -102
- package/dist-server/service/chat-session-resolver.js +0 -856
- package/dist-server/service/chat-session-resolver.js.map +0 -1
- package/dist-server/service/subscribe-auth.d.ts +0 -14
- package/dist-server/service/subscribe-auth.js +0 -33
- package/dist-server/service/subscribe-auth.js.map +0 -1
- package/server/service/chat-message/chat-message-broadcast.integration.test.ts +0 -73
- package/server/service/chat-message/chat-message-broadcast.test.ts +0 -40
- package/server/service/chat-message/chat-message-broadcast.ts +0 -27
- package/server/service/chat-message/chat-message-publish.ts +0 -22
- package/server/service/chat-message/chat-message-subscription.ts +0 -41
- package/server/service/chat-message/chat-message.ts +0 -195
- package/server/service/chat-message/fold-history.test.ts +0 -98
- package/server/service/chat-message/fold-history.ts +0 -77
- package/server/service/chat-message/history-summary.test.ts +0 -127
- package/server/service/chat-message/history-summary.ts +0 -100
- package/server/service/chat-message/index.ts +0 -12
- package/server/service/chat-message/llm-history.test.ts +0 -194
- package/server/service/chat-message/llm-history.ts +0 -163
- package/server/service/chat-session/chat-session.ts +0 -182
- package/server/service/chat-session/index.ts +0 -5
- package/server/service/chat-session/session-activity-broadcast.ts +0 -57
- package/server/service/chat-session/session-activity-publish.ts +0 -44
- package/server/service/chat-session/session-activity-subscription.ts +0 -67
- package/server/service/chat-session/session-anchor.ts +0 -35
- package/server/service/chat-session/session-inbox.test.ts +0 -185
- package/server/service/chat-session/session-inbox.ts +0 -161
- package/server/service/chat-session-participant/chat-session-participant.ts +0 -135
- package/server/service/chat-session-participant/index.ts +0 -6
- package/server/service/chat-session-participant/invite-notification.test.ts +0 -30
- package/server/service/chat-session-participant/invite-notification.ts +0 -48
- package/server/service/chat-session-participant/presence.test.ts +0 -39
- package/server/service/chat-session-participant/presence.ts +0 -21
- package/server/service/chat-session-resolver.ts +0 -779
- package/server/service/session-anchor.test.ts +0 -65
- package/server/service/subscribe-auth.test.ts +0 -57
- package/server/service/subscribe-auth.ts +0 -34
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { receiveRemoteMessage, reconcileSentLine, remoteMessageToLine, type DedupLine } from './chat-echo-dedup'
|
|
2
|
-
|
|
3
|
-
const msg = (id: string, role = 'user', content = 'hi') => ({ id, role, content })
|
|
4
|
-
|
|
5
|
-
describe('remoteMessageToLine', () => {
|
|
6
|
-
it('필드 매핑 + relatedPatchId/toolUsagesJson 변환', () => {
|
|
7
|
-
const line = remoteMessageToLine({
|
|
8
|
-
id: 'm1',
|
|
9
|
-
role: 'assistant',
|
|
10
|
-
content: 'done',
|
|
11
|
-
relatedPatchId: 'p1',
|
|
12
|
-
toolUsagesJson: [{ name: 'x' }]
|
|
13
|
-
})
|
|
14
|
-
expect(line).toMatchObject({ id: 'm1', role: 'assistant', content: 'done', patchId: 'p1' })
|
|
15
|
-
expect(line.toolUsages).toEqual([{ name: 'x' }])
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('toolUsagesJson 이 배열 아니면 undefined', () => {
|
|
19
|
-
expect(remoteMessageToLine({ id: 'm', role: 'user', content: 'x', toolUsagesJson: null }).toolUsages).toBeUndefined()
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('creator → senderEmail/senderName (그룹챗 발신자 표시·자기 판정)', () => {
|
|
23
|
-
const line = remoteMessageToLine({ id: 'm', role: 'user', content: 'x', creator: { name: 'Kim', email: 'u9@example.com' } })
|
|
24
|
-
expect(line.senderEmail).toBe('u9@example.com')
|
|
25
|
-
expect(line.senderName).toBe('Kim')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('creator 없으면 sender 필드 undefined', () => {
|
|
29
|
-
const line = remoteMessageToLine({ id: 'm', role: 'user', content: 'x', creator: null })
|
|
30
|
-
expect(line.senderEmail).toBeUndefined()
|
|
31
|
-
expect(line.senderName).toBeUndefined()
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
describe('receiveRemoteMessage — 방송 수신/에코 차단', () => {
|
|
36
|
-
it('신규 id → append + seen 등록', () => {
|
|
37
|
-
const res = receiveRemoteMessage([], new Set(), msg('m1'))!
|
|
38
|
-
expect(res).not.toBeNull()
|
|
39
|
-
expect(res.lines).toHaveLength(1)
|
|
40
|
-
expect(res.lines[0].id).toBe('m1')
|
|
41
|
-
expect(res.seen.has('m1')).toBe(true)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('이미 seen 인 id → null (에코·중복 차단)', () => {
|
|
45
|
-
expect(receiveRemoteMessage([], new Set(['m1']), msg('m1'))).toBeNull()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('id 없는 메시지 → null', () => {
|
|
49
|
-
expect(receiveRemoteMessage([], new Set(), { role: 'user', content: 'x' } as any)).toBeNull()
|
|
50
|
-
expect(receiveRemoteMessage([], new Set(), null)).toBeNull()
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('불변 — 원본 배열/셋 미변경', () => {
|
|
54
|
-
const lines: DedupLine[] = []
|
|
55
|
-
const seen = new Set<string>()
|
|
56
|
-
receiveRemoteMessage(lines, seen, msg('m1'))
|
|
57
|
-
expect(lines).toHaveLength(0)
|
|
58
|
-
expect(seen.size).toBe(0)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('다른 참여자 메시지 두 개 순차 append', () => {
|
|
62
|
-
let lines: DedupLine[] = []
|
|
63
|
-
let seen = new Set<string>()
|
|
64
|
-
;({ lines, seen } = receiveRemoteMessage(lines, seen, msg('a'))!)
|
|
65
|
-
;({ lines, seen } = receiveRemoteMessage(lines, seen, msg('b'))!)
|
|
66
|
-
expect(lines.map(l => l.id)).toEqual(['a', 'b'])
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe('reconcileSentLine — optimistic 라인 조정', () => {
|
|
71
|
-
const optimistic = (): DedupLine[] => [{ _localId: 'L1', role: 'user', content: 'typed' }]
|
|
72
|
-
|
|
73
|
-
it('정상(에코 전): serverId 부여 + seen 등록 + _localId 해제', () => {
|
|
74
|
-
const res = reconcileSentLine(optimistic(), new Set(), 'L1', 'srv1', {})
|
|
75
|
-
expect(res.lines).toHaveLength(1)
|
|
76
|
-
expect(res.lines[0].id).toBe('srv1')
|
|
77
|
-
expect(res.lines[0]._localId).toBeUndefined()
|
|
78
|
-
expect(res.seen.has('srv1')).toBe(true)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('레이스(에코 먼저): serverId 가 이미 seen → optimistic 중복 제거', () => {
|
|
82
|
-
// 에코가 먼저 도착해 같은 메시지가 이미 append + seen 됨
|
|
83
|
-
const lines: DedupLine[] = [
|
|
84
|
-
{ id: 'srv1', role: 'user', content: 'typed#enriched' }, // 방송 에코로 들어온 것
|
|
85
|
-
{ _localId: 'L1', role: 'user', content: 'typed' } // optimistic 중복
|
|
86
|
-
]
|
|
87
|
-
const res = reconcileSentLine(lines, new Set(['srv1']), 'L1', 'srv1', {})
|
|
88
|
-
expect(res.lines).toHaveLength(1)
|
|
89
|
-
expect(res.lines[0].id).toBe('srv1')
|
|
90
|
-
expect(res.lines.some(l => l._localId === 'L1')).toBe(false)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('patch 병합 (AI 라인: pending → 실제 응답)', () => {
|
|
94
|
-
const lines: DedupLine[] = [{ _localId: 'A1', role: 'assistant', content: '', pending: true }]
|
|
95
|
-
const res = reconcileSentLine(lines, new Set(), 'A1', 'srv2', {
|
|
96
|
-
content: 'reply',
|
|
97
|
-
pending: false,
|
|
98
|
-
patchId: 'p9'
|
|
99
|
-
})
|
|
100
|
-
expect(res.lines[0]).toMatchObject({ id: 'srv2', content: 'reply', pending: false, patchId: 'p9' })
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('serverId 없음(ad-hoc 세션): 라인 유지 + _localId 만 해제, seen 불변', () => {
|
|
104
|
-
const seen = new Set<string>()
|
|
105
|
-
const res = reconcileSentLine(optimistic(), seen, 'L1', undefined, {})
|
|
106
|
-
expect(res.lines[0]._localId).toBeUndefined()
|
|
107
|
-
expect(res.lines[0].id).toBeUndefined()
|
|
108
|
-
expect(res.seen.size).toBe(0)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('해당 _localId 없으면 변화 없음', () => {
|
|
112
|
-
const lines = optimistic()
|
|
113
|
-
const res = reconcileSentLine(lines, new Set(), 'NOPE', 'srv1', {})
|
|
114
|
-
expect(res.lines).toBe(lines)
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
describe('내 발신 에코 흡수 (대기 중 중복 렌더 방지)', () => {
|
|
119
|
-
/* 방송은 mutation 응답보다 먼저 올 수 있고, 그 응답은 LLM 이 끝난 뒤에 온다.
|
|
120
|
-
* 붙였다 나중에 지우면 사용자는 **대기 시간 내내** 자기 질문을 두 줄로 본다. */
|
|
121
|
-
const pending = (): DedupLine[] => [
|
|
122
|
-
{ _localId: 'L1', role: 'user', content: '질문', senderEmail: 'me@x.com' },
|
|
123
|
-
{ _localId: 'L2', role: 'assistant', content: '', pending: true }
|
|
124
|
-
]
|
|
125
|
-
|
|
126
|
-
it('내 에코는 대기 중인 내 줄에 id 를 입힌다 — 줄 수가 늘지 않는다', () => {
|
|
127
|
-
const r = receiveRemoteMessage(pending(), new Set(), {
|
|
128
|
-
id: 'S1',
|
|
129
|
-
role: 'user',
|
|
130
|
-
content: '질문',
|
|
131
|
-
creator: { name: 'Me', email: 'me@x.com' }
|
|
132
|
-
})!
|
|
133
|
-
expect(r.lines).toHaveLength(2) // append 되지 않음
|
|
134
|
-
expect(r.lines[0].id).toBe('S1')
|
|
135
|
-
expect(r.lines[0]._localId).toBe('L1') // 이후 patch 병합을 위해 유지
|
|
136
|
-
expect(r.seen.has('S1')).toBe(true)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('발신자 email 이 없는 방송(단일 사용자·구형)도 내 줄에 흡수된다', () => {
|
|
140
|
-
const r = receiveRemoteMessage(pending(), new Set(), { id: 'S1', role: 'user', content: '질문' })!
|
|
141
|
-
expect(r.lines).toHaveLength(2)
|
|
142
|
-
expect(r.lines[0].id).toBe('S1')
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('다른 사용자 메시지는 흡수하지 않고 append 한다 — 내 대기 줄을 삼키면 안 된다', () => {
|
|
146
|
-
const r = receiveRemoteMessage(pending(), new Set(), {
|
|
147
|
-
id: 'S9',
|
|
148
|
-
role: 'user',
|
|
149
|
-
content: '남의 말',
|
|
150
|
-
creator: { name: 'Other', email: 'other@x.com' }
|
|
151
|
-
})!
|
|
152
|
-
expect(r.lines).toHaveLength(3)
|
|
153
|
-
expect(r.lines[0].id).toBeUndefined() // 내 대기 줄은 그대로
|
|
154
|
-
expect(r.lines[2]).toMatchObject({ id: 'S9', senderEmail: 'other@x.com' })
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
/* 이 테스트는 예전에 "assistant 방송은 흡수하지 않고 붙인다" 를 규칙으로 굳혀 두고 있었다.
|
|
158
|
-
* 그것이 실제 결함이었다 — 붙이면 뒤이은 mutation 응답이 대기 줄을 지우고, 그 줄에만 있던
|
|
159
|
-
* **영속되지 않는 응답 데이터**(조치 제안 카드·접지 경고)가 함께 사라진다. 규칙을 바로잡는다. */
|
|
160
|
-
it('assistant 방송도 대기 중인 AI 줄에 흡수한다 — 붙이면 제안 카드가 사라진다', () => {
|
|
161
|
-
const lines = [
|
|
162
|
-
{ _localId: 'L1', role: 'user', content: '질문', senderEmail: 'me@x.com' },
|
|
163
|
-
{ _localId: 'L2', role: 'assistant', content: '', pending: true }
|
|
164
|
-
]
|
|
165
|
-
const r = receiveRemoteMessage(lines as any, new Set(), { id: 'S2', role: 'assistant', content: '답' })!
|
|
166
|
-
expect(r.lines).toHaveLength(2)
|
|
167
|
-
expect(r.lines[1]).toMatchObject({ id: 'S2', content: '답', pending: false })
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('대기 중인 AI 줄이 없으면 붙인다 — 다른 참여자가 부른 답변', () => {
|
|
171
|
-
const settled = [{ id: 'S0', role: 'user', content: '질문' }]
|
|
172
|
-
const r = receiveRemoteMessage(settled as any, new Set(), { id: 'S2', role: 'assistant', content: '답' })!
|
|
173
|
-
expect(r.lines).toHaveLength(2)
|
|
174
|
-
expect(r.lines[1].id).toBe('S2')
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('흡수한 뒤 mutation 응답 reconcile 은 그 줄을 지우지 않고 patch 를 병합한다', () => {
|
|
178
|
-
const echo = receiveRemoteMessage(pending(), new Set(), {
|
|
179
|
-
id: 'S1',
|
|
180
|
-
role: 'user',
|
|
181
|
-
content: '질문',
|
|
182
|
-
creator: { email: 'me@x.com' }
|
|
183
|
-
})!
|
|
184
|
-
const after = reconcileSentLine(echo.lines, echo.seen, 'L1', 'S1', { content: '질문(보강)' })
|
|
185
|
-
expect(after.lines).toHaveLength(2) // 자기 줄을 지우지 않음
|
|
186
|
-
expect(after.lines[0]).toMatchObject({ id: 'S1', content: '질문(보강)' })
|
|
187
|
-
expect(after.lines[0]._localId).toBeUndefined()
|
|
188
|
-
})
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
describe('AI 답변 에코 흡수 — 영속되지 않는 응답 데이터를 잃지 않기', () => {
|
|
192
|
-
/* 실제 사고: 방송이 mutation 응답보다 먼저 와서 assistant 줄이 새로 붙고, 뒤이어 응답이 대기 줄을
|
|
193
|
-
* 지웠다. 대기 줄에만 있던 **조치 제안 카드**가 그때 사라졌다(도구는 분명히 호출됐는데도). */
|
|
194
|
-
const pendingAi = () => [
|
|
195
|
-
{ id: 'u1', role: 'user', content: '지게차 한 대 추가해줘' },
|
|
196
|
-
{ _localId: 'L2', role: 'assistant', content: '', pending: true, proposals: [{ command: 'resource.add' }] }
|
|
197
|
-
]
|
|
198
|
-
|
|
199
|
-
it('대기 중인 AI 줄에 흡수한다 — 새 줄로 붙이지 않는다', () => {
|
|
200
|
-
const r = receiveRemoteMessage(pendingAi() as any, new Set(), {
|
|
201
|
-
id: 'a1',
|
|
202
|
-
role: 'assistant',
|
|
203
|
-
content: '제안해 두었습니다.'
|
|
204
|
-
})!
|
|
205
|
-
expect(r.lines).toHaveLength(2)
|
|
206
|
-
expect(r.lines[1].id).toBe('a1')
|
|
207
|
-
expect(r.lines[1].content).toBe('제안해 두었습니다.')
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('제안 카드를 유지한다 — 영속되지 않는 값이라 잃으면 화면에서 버튼이 사라진다', () => {
|
|
211
|
-
const r = receiveRemoteMessage(pendingAi() as any, new Set(), {
|
|
212
|
-
id: 'a1',
|
|
213
|
-
role: 'assistant',
|
|
214
|
-
content: '제안해 두었습니다.'
|
|
215
|
-
})!
|
|
216
|
-
expect(r.lines[1].proposals).toEqual([{ command: 'resource.add' }])
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('대기 표시를 푼다 — 방송이 먼저 오면 답이 더 빨리 보인다', () => {
|
|
220
|
-
const r = receiveRemoteMessage(pendingAi() as any, new Set(), { id: 'a1', role: 'assistant', content: '답' })!
|
|
221
|
-
expect(r.lines[1].pending).toBe(false)
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('대기 줄이 없으면(다른 참여자가 부른 AI 답변) 새 줄로 붙인다', () => {
|
|
225
|
-
const lines = [{ id: 'u1', role: 'user', content: '질문' }]
|
|
226
|
-
const r = receiveRemoteMessage(lines as any, new Set(), { id: 'a9', role: 'assistant', content: '답' })!
|
|
227
|
-
expect(r.lines).toHaveLength(2)
|
|
228
|
-
expect(r.lines[1].id).toBe('a9')
|
|
229
|
-
})
|
|
230
|
-
})
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* 그룹챗 메시지 에코 dedup — 순수 로직.
|
|
3
|
-
*
|
|
4
|
-
* 실시간 방송은 자기 발신 메시지도 되돌려준다(에코). 또 pubsub publish 가 mutation 응답보다
|
|
5
|
-
* 먼저 도착할 수 있어(레이스) optimistic 라인과 방송 라인이 겹칠 수 있다. 이 모듈은 그 조정을
|
|
6
|
-
* **id 기반**으로 결정한다 (서버가 멘션 마커로 content 를 enrich 하므로 content 매칭은 불가).
|
|
7
|
-
*
|
|
8
|
-
* 컴포넌트 상태(this.lines / this._seenMessageIds)에서 분리 → 단위테스트 가능.
|
|
9
|
-
* (C2 의 patch-apply-gate.ts 와 동일한 "순수 게이트" 컨벤션.)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** dedup 에 필요한 최소 라인 형태 (board-ai-chat 의 ChatLine 부분집합). */
|
|
13
|
-
export interface DedupLine {
|
|
14
|
-
id?: string
|
|
15
|
-
_localId?: string
|
|
16
|
-
role: string
|
|
17
|
-
content: string
|
|
18
|
-
senderEmail?: string
|
|
19
|
-
senderName?: string
|
|
20
|
-
[k: string]: any
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** 서버 방송 메시지(필요 필드만). */
|
|
24
|
-
export interface RemoteMessage {
|
|
25
|
-
id?: string
|
|
26
|
-
role: string
|
|
27
|
-
content: string
|
|
28
|
-
relatedPatchId?: string
|
|
29
|
-
toolUsagesJson?: any
|
|
30
|
-
/** 접지 경고(영속) — 답이 언급했으나 근거에 없던 식별자. 나중에 읽는 사람도 같은 주의를 봐야 한다. */
|
|
31
|
-
groundingWarningsJson?: any
|
|
32
|
-
/** 발신자 — 표시용 name + **자기 여부 판정용 email**(내부 관리키 id 는 쓰지 않는다). */
|
|
33
|
-
creator?: { name?: string; email?: string } | null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** 방송 메시지 → 라인. */
|
|
37
|
-
export function remoteMessageToLine(m: RemoteMessage): DedupLine {
|
|
38
|
-
return {
|
|
39
|
-
id: m.id,
|
|
40
|
-
role: m.role,
|
|
41
|
-
content: m.content,
|
|
42
|
-
patchId: m.relatedPatchId ?? undefined,
|
|
43
|
-
toolUsages: Array.isArray(m.toolUsagesJson) ? m.toolUsagesJson : undefined,
|
|
44
|
-
groundingWarnings: Array.isArray(m.groundingWarningsJson) ? m.groundingWarningsJson : undefined,
|
|
45
|
-
senderEmail: m.creator?.email ?? undefined,
|
|
46
|
-
senderName: m.creator?.name ?? undefined
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 원격(방송) 메시지 수신 결과를 계산.
|
|
52
|
-
* - id 없음 / 이미 seen → 변화 없음(null).
|
|
53
|
-
* - **내 발신 에코**이고 아직 서버 id 를 못 받은 optimistic 라인이 있으면 → 그 라인에 id 를 입힌다.
|
|
54
|
-
* - 그 밖 → seen 에 추가 + 라인 append.
|
|
55
|
-
*
|
|
56
|
-
* 왜 '입히기'인가: 방송은 mutation 응답보다 먼저 도착할 수 있다. 예전에는 에코를 새 줄로 붙이고
|
|
57
|
-
* 중복 제거를 mutation 응답 시점(reconcileSentLine)에 했는데, 그 응답은 **LLM 이 끝난 뒤** 오므로
|
|
58
|
-
* 사용자는 대기 시간 내내 자기 질문을 두 줄로 본다. 그래서 붙이지 않고 대기 중인 줄에 흡수시킨다.
|
|
59
|
-
*
|
|
60
|
-
* `_localId` 는 유지한다 — 이후 mutation 응답이 patch(요약·patchId 등)를 그 줄에 병합할 수 있어야 한다.
|
|
61
|
-
*/
|
|
62
|
-
export function receiveRemoteMessage(
|
|
63
|
-
lines: DedupLine[],
|
|
64
|
-
seen: Set<string>,
|
|
65
|
-
m: RemoteMessage | undefined | null
|
|
66
|
-
): { lines: DedupLine[]; seen: Set<string> } | null {
|
|
67
|
-
if (!m || !m.id) return null
|
|
68
|
-
if (seen.has(m.id)) return null
|
|
69
|
-
const nextSeen = new Set(seen)
|
|
70
|
-
nextSeen.add(m.id)
|
|
71
|
-
|
|
72
|
-
/* 내 발신 에코 흡수 — `_localId` 가 있는 줄은 이 클라이언트가 만든 것뿐이다.
|
|
73
|
-
* 발신자 판정: 양쪽 email 이 있으면 같아야 하고, 방송에 발신자 email 이 없으면(단일 사용자·구형)
|
|
74
|
-
* 내 것으로 본다. 다른 사용자의 메시지가 내 대기 줄을 삼키지 않도록 email 이 다르면 흡수하지 않는다. */
|
|
75
|
-
/* AI 답변 에코 흡수 — 사용자 발화와 **같은 이유**로 붙이지 않고 대기 줄에 흡수한다.
|
|
76
|
-
*
|
|
77
|
-
* 왜 필요한가(실제 사고): 방송은 mutation 응답보다 먼저 도착할 수 있다. 예전에는 assistant 방송을
|
|
78
|
-
* 새 줄로 붙였고, 뒤이어 mutation 응답이 "같은 서버 id 를 가진 다른 줄이 있다" 며 **대기 줄을 지웠다**.
|
|
79
|
-
* 그런데 대기 줄에만 있는 것이 있다 — **영속되지 않는 응답 데이터**(조치 제안 카드·접지 경고).
|
|
80
|
-
* 그래서 화면에서 실행 버튼이 사라졌다(제안 도구는 분명히 호출됐는데도).
|
|
81
|
-
*
|
|
82
|
-
* 흡수하면 서버가 보낸 본문(멘션 마커 포함)을 그 줄에 입히면서 대기 표시를 풀 수 있어, 답이 더
|
|
83
|
-
* 빨리 보이는 효과도 있다. */
|
|
84
|
-
if (m.role === 'assistant') {
|
|
85
|
-
const idx = lines.findIndex(l => l.role === 'assistant' && !l.id && !!l._localId)
|
|
86
|
-
if (idx >= 0) {
|
|
87
|
-
const nextLines = [...lines]
|
|
88
|
-
nextLines[idx] = {
|
|
89
|
-
...nextLines[idx],
|
|
90
|
-
id: m.id,
|
|
91
|
-
/* 본문·영속 데이터는 서버 것으로 갱신하고, 이 줄이 들고 있던 값(제안 등)은 유지한다. */
|
|
92
|
-
content: m.content,
|
|
93
|
-
patchId: m.relatedPatchId ?? nextLines[idx].patchId,
|
|
94
|
-
toolUsages: Array.isArray(m.toolUsagesJson) ? m.toolUsagesJson : nextLines[idx].toolUsages,
|
|
95
|
-
groundingWarnings: Array.isArray(m.groundingWarningsJson)
|
|
96
|
-
? m.groundingWarningsJson
|
|
97
|
-
: nextLines[idx].groundingWarnings,
|
|
98
|
-
pending: false
|
|
99
|
-
}
|
|
100
|
-
return { lines: nextLines, seen: nextSeen }
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (m.role === 'user') {
|
|
105
|
-
const echoEmail = m.creator?.email
|
|
106
|
-
const idx = lines.findIndex(
|
|
107
|
-
l =>
|
|
108
|
-
l.role === 'user' &&
|
|
109
|
-
!l.id &&
|
|
110
|
-
!!l._localId &&
|
|
111
|
-
(!echoEmail || l.senderEmail == null || l.senderEmail === echoEmail)
|
|
112
|
-
)
|
|
113
|
-
if (idx >= 0) {
|
|
114
|
-
const nextLines = [...lines]
|
|
115
|
-
nextLines[idx] = {
|
|
116
|
-
...nextLines[idx],
|
|
117
|
-
id: m.id,
|
|
118
|
-
senderEmail: echoEmail ?? nextLines[idx].senderEmail,
|
|
119
|
-
senderName: m.creator?.name ?? nextLines[idx].senderName
|
|
120
|
-
}
|
|
121
|
-
return { lines: nextLines, seen: nextSeen }
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return { lines: [...lines, remoteMessageToLine(m)], seen: nextSeen }
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* 전송한 optimistic 라인(_localId)을 서버 id 로 reconcile.
|
|
130
|
-
* - **다른 줄**이 이미 그 serverId 를 갖고 있으면(옛 append 경로) optimistic 중복 라인 제거.
|
|
131
|
-
* - 아니면 serverId 부여(+ seen 등록) + patch 병합 + _localId 해제.
|
|
132
|
-
* - 해당 _localId 라인이 없으면 변화 없음.
|
|
133
|
-
* 반환: 새 { lines, seen } (불변).
|
|
134
|
-
*/
|
|
135
|
-
export function reconcileSentLine(
|
|
136
|
-
lines: DedupLine[],
|
|
137
|
-
seen: Set<string>,
|
|
138
|
-
localId: string,
|
|
139
|
-
serverId: string | undefined,
|
|
140
|
-
patch: Partial<DedupLine>
|
|
141
|
-
): { lines: DedupLine[]; seen: Set<string> } {
|
|
142
|
-
const idx = lines.findIndex(l => l._localId === localId)
|
|
143
|
-
if (idx < 0) return { lines, seen }
|
|
144
|
-
|
|
145
|
-
const nextLines = [...lines]
|
|
146
|
-
/* seen 만 보고 지우면, 에코를 이 줄에 **흡수**한 경우(id 가 이미 이 줄에 있다) 자기 줄을 지운다.
|
|
147
|
-
* 실제 중복은 "다른 줄이 같은 id 를 가진" 상태일 때뿐이다. */
|
|
148
|
-
if (serverId && nextLines.some((l, i) => i !== idx && l.id === serverId)) {
|
|
149
|
-
nextLines.splice(idx, 1)
|
|
150
|
-
return { lines: nextLines, seen }
|
|
151
|
-
}
|
|
152
|
-
const nextSeen = serverId ? new Set(seen).add(serverId) : seen
|
|
153
|
-
nextLines[idx] = { ...nextLines[idx], ...patch, id: serverId ?? nextLines[idx].id, _localId: undefined }
|
|
154
|
-
return { lines: nextLines, seen: nextSeen }
|
|
155
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* boardAIChat GraphQL mutation 의 input 객체 빌더.
|
|
3
|
-
*
|
|
4
|
-
* board-ai-chat.ts (Lit / Material Web 의존) 와 분리 — pure 함수만 두면 jest 의 node
|
|
5
|
-
* 환경에서도 import 가능. 회귀 테스트 (selectedRefids 누락 등) 가 단위 테스트로 잠긴다.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface ChatMutationInputArgs {
|
|
9
|
-
sessionId?: string
|
|
10
|
-
history: Array<{ role: string; content: string }>
|
|
11
|
-
liveBoard: any
|
|
12
|
-
scopes?: string[]
|
|
13
|
-
/** 편집으로 접은 지점(마지막으로 남긴 서버 메시지 id) — 서버가 그 뒤 이력을 에이전트 문맥에서 뺀다. */
|
|
14
|
-
truncateAfterMessageId?: string
|
|
15
|
-
/** 이 대화면이 무엇을 보고 있는지(예: { spaceId }) — 도구가 대상을 알 수 있게. 없으면 모델이 식별자를 되묻는다. */
|
|
16
|
-
hostContext?: any
|
|
17
|
-
/**
|
|
18
|
-
* 이 대화면이 쓸 수 있는 외부 도구 카테고리(registerToolCategory 이름).
|
|
19
|
-
* 미지정 = 등록된 전부(현행 동작). 지정하면 정의와 **실행** 모두 그 범위로 제한된다.
|
|
20
|
-
*/
|
|
21
|
-
toolCategories?: string[]
|
|
22
|
-
/** 코어 보드 편집 도구(addComponent·스타일링·씬 조작) 사용 여부. 기본 true. */
|
|
23
|
-
boardTools?: boolean
|
|
24
|
-
/**
|
|
25
|
-
* 첫 턴에 도구 호출을 강제할지 — 라이브 상태를 다루는 대화면에서 켠다.
|
|
26
|
-
* 근거 없이 상태를 단언하는 답(도구를 아예 부르지 않은 답)을 원천에서 막는다.
|
|
27
|
-
*/
|
|
28
|
-
requireGroundingTools?: boolean
|
|
29
|
-
knownTypes?: string[]
|
|
30
|
-
categories?: string[]
|
|
31
|
-
componentSchemas?: any
|
|
32
|
-
/**
|
|
33
|
-
* 선택된 컴포넌트의 `refid` 배열 — things-scene universal numeric handle.
|
|
34
|
-
* id (데이터 바인딩 이름) 와는 다른 개념.
|
|
35
|
-
*/
|
|
36
|
-
selectedRefids: number[]
|
|
37
|
-
/**
|
|
38
|
-
* popup 에서 명시 선택한 #mention 매핑 — {token, refid}.
|
|
39
|
-
* 사용자 textarea 에는 #token 만 보이고, 정확한 refid 는 이 별도 채널로 server 에 전달.
|
|
40
|
-
* server 가 mention map 으로 LLM 에 정확한 refid 정보 첨부 (사용자 노출 0).
|
|
41
|
-
*/
|
|
42
|
-
mentions?: Array<{ token: string; refid: number }>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* GraphQL mutation 의 input 객체를 빌드.
|
|
47
|
-
*
|
|
48
|
-
* 핵심 보호 — `selectedRefids` 가 항상 포함되어야 한다. 누락이 사용자에게 보고된
|
|
49
|
-
* "AI 가 선택된 컴포넌트를 모른다" 의 root cause 였다.
|
|
50
|
-
*/
|
|
51
|
-
export function buildChatMutationInput(args: ChatMutationInputArgs): Record<string, any> {
|
|
52
|
-
return {
|
|
53
|
-
sessionId: args.sessionId ?? null,
|
|
54
|
-
messages: args.history,
|
|
55
|
-
currentBoard: args.liveBoard ?? null,
|
|
56
|
-
scopes: args.scopes,
|
|
57
|
-
...(args.truncateAfterMessageId ? { truncateAfterMessageId: args.truncateAfterMessageId } : {}),
|
|
58
|
-
...(args.hostContext ? { hostContext: args.hostContext } : {}),
|
|
59
|
-
/* 미지정은 서버 기본(전부 허용)에 맡긴다 — null 을 보내 굳이 덮어쓰지 않는다. */
|
|
60
|
-
...(args.toolCategories ? { toolCategories: args.toolCategories } : {}),
|
|
61
|
-
...(args.boardTools === false ? { boardTools: false } : {}),
|
|
62
|
-
...(args.requireGroundingTools ? { requireGroundingTools: true } : {}),
|
|
63
|
-
knownTypes: args.knownTypes,
|
|
64
|
-
categories: args.categories,
|
|
65
|
-
componentSchemas: args.componentSchemas,
|
|
66
|
-
selectedRefids: args.selectedRefids,
|
|
67
|
-
...(args.mentions && args.mentions.length > 0 && { mentions: args.mentions })
|
|
68
|
-
}
|
|
69
|
-
}
|