@things-factory/board-ai 10.0.0-beta.108
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/PLAN-styling-tools.md +1253 -0
- package/client/components/board-ai-chat.test.ts +149 -0
- package/client/components/board-ai-chat.ts +2574 -0
- package/client/components/chat-echo-dedup.test.ts +116 -0
- package/client/components/chat-echo-dedup.ts +89 -0
- package/client/components/chat-input-builder.ts +47 -0
- package/client/components/default-catalog-entries.test.ts +59 -0
- package/client/components/default-catalog-entries.ts +100 -0
- package/client/components/default-slash-templates.test.ts +55 -0
- package/client/components/default-slash-templates.ts +129 -0
- package/client/components/markdown.test.ts +518 -0
- package/client/components/markdown.ts +508 -0
- package/client/components/mention-popup-helpers.ts +433 -0
- package/client/components/mention-popup.test.ts +588 -0
- package/client/components/mention-popup.ts +238 -0
- package/client/index.ts +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/board-edit-patch.ts +374 -0
- package/config/config.development.js +43 -0
- package/config/config.production.js +15 -0
- package/dist-client/client/components/board-ai-chat.d.ts +277 -0
- package/dist-client/client/components/board-ai-chat.js +2444 -0
- package/dist-client/client/components/board-ai-chat.js.map +1 -0
- package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
- package/dist-client/client/components/board-ai-chat.test.js +138 -0
- package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
- package/dist-client/client/components/chat-echo-dedup.js +58 -0
- package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
- package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
- package/dist-client/client/components/chat-input-builder.d.ts +39 -0
- package/dist-client/client/components/chat-input-builder.js +26 -0
- package/dist-client/client/components/chat-input-builder.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
- package/dist-client/client/components/default-catalog-entries.js +86 -0
- package/dist-client/client/components/default-catalog-entries.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
- package/dist-client/client/components/default-catalog-entries.test.js +52 -0
- package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.d.ts +17 -0
- package/dist-client/client/components/default-slash-templates.js +113 -0
- package/dist-client/client/components/default-slash-templates.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
- package/dist-client/client/components/default-slash-templates.test.js +51 -0
- package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
- package/dist-client/client/components/markdown.d.ts +60 -0
- package/dist-client/client/components/markdown.js +422 -0
- package/dist-client/client/components/markdown.js.map +1 -0
- package/dist-client/client/components/markdown.test.d.ts +1 -0
- package/dist-client/client/components/markdown.test.js +418 -0
- package/dist-client/client/components/markdown.test.js.map +1 -0
- package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
- package/dist-client/client/components/mention-popup-helpers.js +330 -0
- package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
- package/dist-client/client/components/mention-popup.d.ts +35 -0
- package/dist-client/client/components/mention-popup.js +223 -0
- package/dist-client/client/components/mention-popup.js.map +1 -0
- package/dist-client/client/components/mention-popup.test.d.ts +1 -0
- package/dist-client/client/components/mention-popup.test.js +474 -0
- package/dist-client/client/components/mention-popup.test.js.map +1 -0
- package/dist-client/client/index.d.ts +11 -0
- package/dist-client/client/index.js +12 -0
- package/dist-client/client/index.js.map +1 -0
- package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
- package/dist-client/client/utils/board-edit-patch.js +284 -0
- package/dist-client/client/utils/board-edit-patch.js.map +1 -0
- package/dist-client/server/service/agentic-loop.d.ts +87 -0
- package/dist-client/server/service/agentic-loop.js +276 -0
- package/dist-client/server/service/agentic-loop.js.map +1 -0
- package/dist-client/server/service/assistant.d.ts +84 -0
- package/dist-client/server/service/assistant.js +2220 -0
- package/dist-client/server/service/assistant.js.map +1 -0
- package/dist-client/server/service/catalog-resolver.d.ts +18 -0
- package/dist-client/server/service/catalog-resolver.js +93 -0
- package/dist-client/server/service/catalog-resolver.js.map +1 -0
- package/dist-client/server/service/component-tree.d.ts +68 -0
- package/dist-client/server/service/component-tree.js +68 -0
- package/dist-client/server/service/component-tree.js.map +1 -0
- package/dist-client/server/service/mention-resolver.d.ts +96 -0
- package/dist-client/server/service/mention-resolver.js +172 -0
- package/dist-client/server/service/mention-resolver.js.map +1 -0
- package/dist-client/server/service/prefab-registry.d.ts +73 -0
- package/dist-client/server/service/prefab-registry.js +252 -0
- package/dist-client/server/service/prefab-registry.js.map +1 -0
- package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
- package/dist-client/server/service/styling/clear-tools.js +50 -0
- package/dist-client/server/service/styling/clear-tools.js.map +1 -0
- package/dist-client/server/service/styling/color-utils.d.ts +43 -0
- package/dist-client/server/service/styling/color-utils.js +205 -0
- package/dist-client/server/service/styling/color-utils.js.map +1 -0
- package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
- package/dist-client/server/service/styling/copy-tools.js +78 -0
- package/dist-client/server/service/styling/copy-tools.js.map +1 -0
- package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
- package/dist-client/server/service/styling/effect-tools.js +85 -0
- package/dist-client/server/service/styling/effect-tools.js.map +1 -0
- package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
- package/dist-client/server/service/styling/fill-tools.js +226 -0
- package/dist-client/server/service/styling/fill-tools.js.map +1 -0
- package/dist-client/server/service/styling/material-tools.d.ts +32 -0
- package/dist-client/server/service/styling/material-tools.js +88 -0
- package/dist-client/server/service/styling/material-tools.js.map +1 -0
- package/dist-client/server/service/styling/read-tools.d.ts +34 -0
- package/dist-client/server/service/styling/read-tools.js +42 -0
- package/dist-client/server/service/styling/read-tools.js.map +1 -0
- package/dist-client/server/service/styling/registry.d.ts +91 -0
- package/dist-client/server/service/styling/registry.js +345 -0
- package/dist-client/server/service/styling/registry.js.map +1 -0
- package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-client/server/service/styling/stroke-tools.js +94 -0
- package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
- package/dist-client/server/service/styling/text-tools.d.ts +32 -0
- package/dist-client/server/service/styling/text-tools.js +101 -0
- package/dist-client/server/service/styling/text-tools.js.map +1 -0
- package/dist-client/server/service/types.d.ts +265 -0
- package/dist-client/server/service/types.js +2 -0
- package/dist-client/server/service/types.js.map +1 -0
- package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-client/server/service/validation/board-model-schema.js +402 -0
- package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
- package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
- package/dist-client/server/service/validation/tool-validation.js +193 -0
- package/dist-client/server/service/validation/tool-validation.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +23 -0
- package/dist-server/index.js +27 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/agentic-loop.d.ts +87 -0
- package/dist-server/service/agentic-loop.js +280 -0
- package/dist-server/service/agentic-loop.js.map +1 -0
- package/dist-server/service/apply-patch.d.ts +47 -0
- package/dist-server/service/apply-patch.js +369 -0
- package/dist-server/service/apply-patch.js.map +1 -0
- package/dist-server/service/assistant.d.ts +84 -0
- package/dist-server/service/assistant.js +2237 -0
- package/dist-server/service/assistant.js.map +1 -0
- package/dist-server/service/board-ai-resolver.d.ts +54 -0
- package/dist-server/service/board-ai-resolver.js +409 -0
- package/dist-server/service/board-ai-resolver.js.map +1 -0
- package/dist-server/service/catalog-resolver.d.ts +18 -0
- package/dist-server/service/catalog-resolver.js +98 -0
- package/dist-server/service/catalog-resolver.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
- package/dist-server/service/chat-message/chat-message-publish.js +27 -0
- package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
- package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
- package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-message.d.ts +56 -0
- package/dist-server/service/chat-message/chat-message.js +149 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -0
- package/dist-server/service/chat-message/index.d.ts +5 -0
- package/dist-server/service/chat-message/index.js +12 -0
- package/dist-server/service/chat-message/index.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +31 -0
- package/dist-server/service/chat-session/chat-session.js +131 -0
- package/dist-server/service/chat-session/chat-session.js.map +1 -0
- package/dist-server/service/chat-session/index.d.ts +3 -0
- package/dist-server/service/chat-session/index.js +7 -0
- package/dist-server/service/chat-session/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
- package/dist-server/service/chat-session-participant/index.d.ts +4 -0
- package/dist-server/service/chat-session-participant/index.js +10 -0
- package/dist-server/service/chat-session-participant/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
- package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
- package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
- package/dist-server/service/chat-session-participant/presence.js +22 -0
- package/dist-server/service/chat-session-participant/presence.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +20 -0
- package/dist-server/service/chat-session-resolver.js +356 -0
- package/dist-server/service/chat-session-resolver.js.map +1 -0
- package/dist-server/service/component-tree.d.ts +68 -0
- package/dist-server/service/component-tree.js +73 -0
- package/dist-server/service/component-tree.js.map +1 -0
- package/dist-server/service/index.d.ts +20 -0
- package/dist-server/service/index.js +34 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/mention-marker.d.ts +78 -0
- package/dist-server/service/mention-marker.js +162 -0
- package/dist-server/service/mention-marker.js.map +1 -0
- package/dist-server/service/mention-resolver.d.ts +96 -0
- package/dist-server/service/mention-resolver.js +177 -0
- package/dist-server/service/mention-resolver.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
- package/dist-server/service/patch-entry/index.d.ts +4 -0
- package/dist-server/service/patch-entry/index.js +12 -0
- package/dist-server/service/patch-entry/index.js.map +1 -0
- package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
- package/dist-server/service/patch-entry/patch-entry.js +96 -0
- package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
- package/dist-server/service/prefab-registry.d.ts +73 -0
- package/dist-server/service/prefab-registry.js +258 -0
- package/dist-server/service/prefab-registry.js.map +1 -0
- package/dist-server/service/styling/clear-tools.d.ts +15 -0
- package/dist-server/service/styling/clear-tools.js +53 -0
- package/dist-server/service/styling/clear-tools.js.map +1 -0
- package/dist-server/service/styling/color-utils.d.ts +43 -0
- package/dist-server/service/styling/color-utils.js +210 -0
- package/dist-server/service/styling/color-utils.js.map +1 -0
- package/dist-server/service/styling/copy-tools.d.ts +27 -0
- package/dist-server/service/styling/copy-tools.js +81 -0
- package/dist-server/service/styling/copy-tools.js.map +1 -0
- package/dist-server/service/styling/effect-tools.d.ts +45 -0
- package/dist-server/service/styling/effect-tools.js +90 -0
- package/dist-server/service/styling/effect-tools.js.map +1 -0
- package/dist-server/service/styling/fill-tools.d.ts +60 -0
- package/dist-server/service/styling/fill-tools.js +229 -0
- package/dist-server/service/styling/fill-tools.js.map +1 -0
- package/dist-server/service/styling/material-tools.d.ts +32 -0
- package/dist-server/service/styling/material-tools.js +92 -0
- package/dist-server/service/styling/material-tools.js.map +1 -0
- package/dist-server/service/styling/read-tools.d.ts +34 -0
- package/dist-server/service/styling/read-tools.js +45 -0
- package/dist-server/service/styling/read-tools.js.map +1 -0
- package/dist-server/service/styling/registry.d.ts +91 -0
- package/dist-server/service/styling/registry.js +352 -0
- package/dist-server/service/styling/registry.js.map +1 -0
- package/dist-server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-server/service/styling/stroke-tools.js +97 -0
- package/dist-server/service/styling/stroke-tools.js.map +1 -0
- package/dist-server/service/styling/text-tools.d.ts +32 -0
- package/dist-server/service/styling/text-tools.js +105 -0
- package/dist-server/service/styling/text-tools.js.map +1 -0
- package/dist-server/service/subscribe-auth.d.ts +14 -0
- package/dist-server/service/subscribe-auth.js +33 -0
- package/dist-server/service/subscribe-auth.js.map +1 -0
- package/dist-server/service/types.d.ts +265 -0
- package/dist-server/service/types.js +3 -0
- package/dist-server/service/types.js.map +1 -0
- package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-server/service/validation/board-model-schema.js +409 -0
- package/dist-server/service/validation/board-model-schema.js.map +1 -0
- package/dist-server/service/validation/tool-validation.d.ts +31 -0
- package/dist-server/service/validation/tool-validation.js +196 -0
- package/dist-server/service/validation/tool-validation.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/server/index.ts +23 -0
- package/server/service/agentic-loop.test.ts +705 -0
- package/server/service/agentic-loop.ts +431 -0
- package/server/service/apply-patch-drift.test.ts +829 -0
- package/server/service/apply-patch.test.ts +757 -0
- package/server/service/apply-patch.ts +411 -0
- package/server/service/assistant-integration.test.ts +922 -0
- package/server/service/assistant-llm-smoke.test.ts +296 -0
- package/server/service/assistant.test.ts +2034 -0
- package/server/service/assistant.ts +2378 -0
- package/server/service/board-ai-resolver.ts +407 -0
- package/server/service/catalog-resolver.test.ts +137 -0
- package/server/service/catalog-resolver.ts +102 -0
- package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
- package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
- package/server/service/chat-message/chat-message-broadcast.ts +27 -0
- package/server/service/chat-message/chat-message-publish.ts +22 -0
- package/server/service/chat-message/chat-message-subscription.ts +41 -0
- package/server/service/chat-message/chat-message.ts +169 -0
- package/server/service/chat-message/index.ts +12 -0
- package/server/service/chat-session/chat-session.ts +130 -0
- package/server/service/chat-session/index.ts +5 -0
- package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
- package/server/service/chat-session-participant/index.ts +6 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
- package/server/service/chat-session-participant/invite-notification.ts +48 -0
- package/server/service/chat-session-participant/presence.test.ts +39 -0
- package/server/service/chat-session-participant/presence.ts +21 -0
- package/server/service/chat-session-resolver.ts +312 -0
- package/server/service/component-tree.test.ts +248 -0
- package/server/service/component-tree.ts +135 -0
- package/server/service/index.ts +32 -0
- package/server/service/mention-marker.extract.test.ts +61 -0
- package/server/service/mention-marker.test.ts +228 -0
- package/server/service/mention-marker.ts +184 -0
- package/server/service/mention-resolver.test.ts +273 -0
- package/server/service/mention-resolver.ts +264 -0
- package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
- package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
- package/server/service/patch-entry/board-patch-subscription.ts +50 -0
- package/server/service/patch-entry/index.ts +12 -0
- package/server/service/patch-entry/patch-entry.ts +89 -0
- package/server/service/prefab-registry.test.ts +172 -0
- package/server/service/prefab-registry.ts +332 -0
- package/server/service/styling/clear-tools.test.ts +164 -0
- package/server/service/styling/clear-tools.ts +69 -0
- package/server/service/styling/color-utils.test.ts +130 -0
- package/server/service/styling/color-utils.ts +226 -0
- package/server/service/styling/copy-tools.test.ts +152 -0
- package/server/service/styling/copy-tools.ts +115 -0
- package/server/service/styling/effect-tools.test.ts +277 -0
- package/server/service/styling/effect-tools.ts +124 -0
- package/server/service/styling/fill-tools.test.ts +331 -0
- package/server/service/styling/fill-tools.ts +293 -0
- package/server/service/styling/material-tools.test.ts +294 -0
- package/server/service/styling/material-tools.ts +114 -0
- package/server/service/styling/read-tools.test.ts +78 -0
- package/server/service/styling/read-tools.ts +67 -0
- package/server/service/styling/registry.test.ts +203 -0
- package/server/service/styling/registry.ts +423 -0
- package/server/service/styling/stroke-tools.test.ts +245 -0
- package/server/service/styling/stroke-tools.ts +145 -0
- package/server/service/styling/text-tools.test.ts +253 -0
- package/server/service/styling/text-tools.ts +131 -0
- package/server/service/styling/tier1-tools.test.ts +300 -0
- package/server/service/subscribe-auth.test.ts +57 -0
- package/server/service/subscribe-auth.ts +34 -0
- package/server/service/types.ts +235 -0
- package/server/service/validation/board-model-schema.test.ts +778 -0
- package/server/service/validation/board-model-schema.ts +446 -0
- package/server/service/validation/tool-validation.ts +228 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +49 -0
- package/translations/ja.json +48 -0
- package/translations/ko.json +49 -0
- package/translations/ms.json +48 -0
- package/translations/zh.json +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chat-message 방송 spine — 독립 통합 테스트 (실서버·WS 없이 자족적).
|
|
3
|
+
*
|
|
4
|
+
* 순수 필터(chat-message-broadcast.test.ts)와 달리, 실제 서버가 쓰는 동일한 경로를 구동:
|
|
5
|
+
* publishChatMessage → shell pubsub(in-memory) → 토픽 → chatMessageFilter → async iterator.
|
|
6
|
+
* = resolver 의 subscribe 합성과 동일. WS/Apollo 전송 계층은 patch spine(C2)에서 검증된
|
|
7
|
+
* generic 인프라라 본 테스트 범위 밖 (수동 스모크로 1회 확인).
|
|
8
|
+
*/
|
|
9
|
+
import { filter, pipe } from 'graphql-yoga'
|
|
10
|
+
import { pubsub } from '@things-factory/shell'
|
|
11
|
+
import { publishChatMessage } from './chat-message-publish'
|
|
12
|
+
import { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast'
|
|
13
|
+
|
|
14
|
+
const msg = (id: string, role = 'user', content = 'hi') => ({ id, role, content } as any)
|
|
15
|
+
|
|
16
|
+
/** resolver 의 subscribe 와 동일한 합성으로 구독 이터레이터를 만든다. */
|
|
17
|
+
function subscribe(domainId: string, sessionId: string) {
|
|
18
|
+
return pipe(pubsub.subscribe(CHAT_MESSAGE_TOPIC), filter(chatMessageFilter(domainId, sessionId)))[
|
|
19
|
+
Symbol.asyncIterator
|
|
20
|
+
]()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** it.next() 를 timeout 과 race — 안 오면 'TIMEOUT'. */
|
|
24
|
+
function nextOrTimeout(it: AsyncIterator<any>, ms = 400): Promise<any> {
|
|
25
|
+
return Promise.race([
|
|
26
|
+
it.next().then(r => r.value),
|
|
27
|
+
new Promise(resolve => setTimeout(() => resolve('TIMEOUT'), ms))
|
|
28
|
+
])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('chat-message 방송 통합 (publish → 필터 → 구독)', () => {
|
|
32
|
+
it('같은 domain+session 구독자가 방송 메시지를 수신', async () => {
|
|
33
|
+
const it = subscribe('d1', 's1')
|
|
34
|
+
const p = nextOrTimeout(it)
|
|
35
|
+
publishChatMessage({ domainId: 'd1', sessionId: 's1', message: msg('m1', 'assistant', 'done') })
|
|
36
|
+
const value = await p
|
|
37
|
+
expect(value).not.toBe('TIMEOUT')
|
|
38
|
+
expect(value.message.id).toBe('m1')
|
|
39
|
+
expect(value.message.role).toBe('assistant')
|
|
40
|
+
await it.return?.()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('다른 세션 메시지는 격리 — 내 세션 것만 수신', async () => {
|
|
44
|
+
const it = subscribe('d1', 's1')
|
|
45
|
+
const p = nextOrTimeout(it)
|
|
46
|
+
publishChatMessage({ domainId: 'd1', sessionId: 's2', message: msg('other') }) // 다른 세션 — 필터됨
|
|
47
|
+
publishChatMessage({ domainId: 'd1', sessionId: 's1', message: msg('mine') }) // 내 세션
|
|
48
|
+
const value = await p
|
|
49
|
+
expect(value).not.toBe('TIMEOUT')
|
|
50
|
+
expect(value.message.id).toBe('mine')
|
|
51
|
+
await it.return?.()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('다른 domain 메시지는 격리 (도메인 보안)', async () => {
|
|
55
|
+
const it = subscribe('d1', 's1')
|
|
56
|
+
const p = nextOrTimeout(it)
|
|
57
|
+
publishChatMessage({ domainId: 'd2', sessionId: 's1', message: msg('foreign') }) // 다른 도메인 — 필터됨
|
|
58
|
+
publishChatMessage({ domainId: 'd1', sessionId: 's1', message: msg('local') })
|
|
59
|
+
const value = await p
|
|
60
|
+
expect(value).not.toBe('TIMEOUT')
|
|
61
|
+
expect(value.message.id).toBe('local')
|
|
62
|
+
await it.return?.()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('sessionId 없는 publish 는 방송 안 함 (구독자 미수신)', async () => {
|
|
66
|
+
const it = subscribe('d1', 's1')
|
|
67
|
+
const p = nextOrTimeout(it, 250)
|
|
68
|
+
publishChatMessage({ domainId: 'd1', sessionId: undefined, message: msg('nope') })
|
|
69
|
+
const value = await p
|
|
70
|
+
expect(value).toBe('TIMEOUT') // 아무것도 안 옴
|
|
71
|
+
await it.return?.()
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chat-message 브로드캐스트 — 단위 테스트 (순수 로직).
|
|
3
|
+
* - chatMessageFilter: domain+session 격리(보안 핵심) — 일치만 통과, 불일치/널/session없음 차단.
|
|
4
|
+
* (publishChatMessage / 구독 resolver 는 pubsub·graphql-ws 통합 영역 — 별도 검증.)
|
|
5
|
+
*/
|
|
6
|
+
import { chatMessageFilter, CHAT_MESSAGE_TOPIC } from './chat-message-broadcast'
|
|
7
|
+
import type { ChatMessagePayload } from './chat-message-broadcast'
|
|
8
|
+
|
|
9
|
+
const payload = (domainId: string, sessionId?: string): ChatMessagePayload => ({
|
|
10
|
+
domainId,
|
|
11
|
+
sessionId,
|
|
12
|
+
message: { id: 'm1' } as any
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('chatMessageFilter (domain+session 격리)', () => {
|
|
16
|
+
it('domain+session 모두 일치 → 통과', () => {
|
|
17
|
+
expect(chatMessageFilter('d1', 's1')(payload('d1', 's1'))).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('domain 불일치 → 차단 (도메인 격리)', () => {
|
|
21
|
+
expect(chatMessageFilter('d1', 's1')(payload('d2', 's1'))).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('session 불일치 → 차단 (다른 세션 메시지 누수 방지)', () => {
|
|
25
|
+
expect(chatMessageFilter('d1', 's1')(payload('d1', 's2'))).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('sessionId 없는 페이로드 → 차단', () => {
|
|
29
|
+
expect(chatMessageFilter('d1', 's1')(payload('d1', undefined))).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('null/undefined 페이로드 안전 → 차단', () => {
|
|
33
|
+
expect(chatMessageFilter('d1', 's1')(null as any)).toBe(false)
|
|
34
|
+
expect(chatMessageFilter('d1', 's1')(undefined as any)).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('토픽 상수', () => {
|
|
38
|
+
expect(CHAT_MESSAGE_TOPIC).toBe('board-ai-chat-message')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* chat-message 브로드캐스트 — 순수 로직 (토픽/페이로드/필터). 데코레이터·pubsub import 없음
|
|
3
|
+
* → 단위테스트 가능. 데코레이터 resolver(chat-message-subscription.ts)와 pubsub 의존
|
|
4
|
+
* publishChatMessage 는 별도.
|
|
5
|
+
*
|
|
6
|
+
* patch spine(board-patch-broadcast.ts)과 동일 컨벤션. 다만 채팅 메시지는 board 가 아니라
|
|
7
|
+
* **ChatSession 단위**로 묶이므로 필터 키가 sessionId (+ domain 격리).
|
|
8
|
+
*/
|
|
9
|
+
import type { ChatMessage } from './chat-message.js' // type-only — 런타임 import 아님
|
|
10
|
+
|
|
11
|
+
/** chat-message 브로드캐스트 토픽. */
|
|
12
|
+
export const CHAT_MESSAGE_TOPIC = 'board-ai-chat-message'
|
|
13
|
+
|
|
14
|
+
/** 방송 페이로드 — 구독 필터(domain+session)용 메타 + ChatMessage 본체. */
|
|
15
|
+
export interface ChatMessagePayload {
|
|
16
|
+
domainId: string
|
|
17
|
+
sessionId?: string
|
|
18
|
+
message: ChatMessage
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 같은 domain + session 구독자에게만 통과시키는 필터 (도메인 격리 = 보안 핵심).
|
|
23
|
+
*/
|
|
24
|
+
export function chatMessageFilter(domainId: string, sessionId: string) {
|
|
25
|
+
return (payload: ChatMessagePayload): boolean =>
|
|
26
|
+
!!payload && payload.domainId === domainId && payload.sessionId === sessionId
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* chat-message 방송 publisher — pubsub 의존, 데코레이터 없음.
|
|
3
|
+
*
|
|
4
|
+
* 데코레이터 resolver(chat-message-subscription.ts)와 분리한 이유: babel-jest 가 파라미터
|
|
5
|
+
* 데코레이터(@Root/@Arg)를 파싱 못 해 그 파일을 테스트에서 import 할 수 없다. 이 모듈은
|
|
6
|
+
* 데코레이터가 없어 통합 테스트가 실제 publish→방송 경로를 직접 구동할 수 있다.
|
|
7
|
+
*/
|
|
8
|
+
import { pubsub } from '@things-factory/shell'
|
|
9
|
+
import { CHAT_MESSAGE_TOPIC, type ChatMessagePayload } from './chat-message-broadcast.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ChatMessage 영속 직후 호출 — 같은 세션 구독자에게 방송.
|
|
13
|
+
* sessionId 없으면 skip. pubsub 미가용/실패해도 본 흐름(영속 완료)을 막지 않는다.
|
|
14
|
+
*/
|
|
15
|
+
export function publishChatMessage(payload: ChatMessagePayload): void {
|
|
16
|
+
if (!payload.sessionId) return
|
|
17
|
+
try {
|
|
18
|
+
pubsub.publish(CHAT_MESSAGE_TOPIC, payload)
|
|
19
|
+
} catch {
|
|
20
|
+
/* 방송 생략 — 영속은 이미 끝남 */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* BoardChatMessageSubscription (Phase Board-AI C4 — 그룹챗 메시지 실시간 방송).
|
|
3
|
+
*
|
|
4
|
+
* ChatMessage(사용자 / AI / system) 가 영속될 때마다 같은 세션 구독자에게 방송 →
|
|
5
|
+
* 그룹챗의 척추. 클라이언트(board-ai-chat)가 구독해 원격 메시지를 transcript 에 append.
|
|
6
|
+
* 자기 발신 에코는 클라이언트가 message.id 로 dedup (mutation 응답으로 이미 렌더).
|
|
7
|
+
*
|
|
8
|
+
* patch spine(board-patch-subscription.ts)과 동일 패턴 — PubSub + graphql-ws, domain 격리.
|
|
9
|
+
* 차이: 필터 키가 board 가 아닌 sessionId (채팅은 ChatSession 단위).
|
|
10
|
+
* 순수 로직(토픽/필터)은 chat-message-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.
|
|
11
|
+
*/
|
|
12
|
+
import { Resolver, Subscription, Root, Arg } from 'type-graphql'
|
|
13
|
+
import { filter, pipe } from 'graphql-yoga'
|
|
14
|
+
import { pubsub } from '@things-factory/shell'
|
|
15
|
+
import { ChatMessage } from './chat-message.js'
|
|
16
|
+
import { CHAT_MESSAGE_TOPIC, chatMessageFilter, type ChatMessagePayload } from './chat-message-broadcast.js'
|
|
17
|
+
import { assertDomainSubscribeAllowed } from '../subscribe-auth.js'
|
|
18
|
+
|
|
19
|
+
// publishChatMessage 는 데코레이터 없는 chat-message-publish.ts 에 분리 (테스트 import 가능).
|
|
20
|
+
export { publishChatMessage } from './chat-message-publish.js'
|
|
21
|
+
export { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast.js'
|
|
22
|
+
export type { ChatMessagePayload } from './chat-message-broadcast.js'
|
|
23
|
+
|
|
24
|
+
@Resolver(ChatMessage)
|
|
25
|
+
export class BoardChatMessageSubscription {
|
|
26
|
+
@Subscription({
|
|
27
|
+
description:
|
|
28
|
+
'Subscribes to chat messages (user / assistant / system) of a session for real-time group chat.',
|
|
29
|
+
subscribe: async ({ args, context }) => {
|
|
30
|
+
const { domain, user } = context.state
|
|
31
|
+
const { sessionId } = args
|
|
32
|
+
|
|
33
|
+
await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))
|
|
34
|
+
|
|
35
|
+
return pipe(pubsub.subscribe(CHAT_MESSAGE_TOPIC), filter(chatMessageFilter(domain.id, sessionId)))
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
chatMessageActivity(@Root() payload: ChatMessagePayload, @Arg('sessionId') sessionId: string): ChatMessage {
|
|
39
|
+
return payload.message
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatMessage — ChatSession 의 한 메시지.
|
|
3
|
+
*
|
|
4
|
+
* role: 'user' | 'assistant' | 'system'
|
|
5
|
+
* 'system' 은 사용자 직접 편집/import 진행 등 시스템이 자동 생성하는 컨텍스트 메시지.
|
|
6
|
+
*
|
|
7
|
+
* relatedPatchId / relatedImportSessionId 로 메시지가 트리거한 패치·임포트와 연결.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
Column,
|
|
11
|
+
CreateDateColumn,
|
|
12
|
+
DeleteDateColumn,
|
|
13
|
+
Entity,
|
|
14
|
+
Index,
|
|
15
|
+
ManyToOne,
|
|
16
|
+
PrimaryGeneratedColumn,
|
|
17
|
+
RelationId,
|
|
18
|
+
UpdateDateColumn
|
|
19
|
+
} from 'typeorm'
|
|
20
|
+
import { Field, ID, ObjectType } from 'type-graphql'
|
|
21
|
+
import { GraphQLJSON } from 'graphql-scalars'
|
|
22
|
+
|
|
23
|
+
import { User } from '@things-factory/auth-base'
|
|
24
|
+
import { config } from '@things-factory/env'
|
|
25
|
+
|
|
26
|
+
import { ChatSession } from '../chat-session/chat-session.js'
|
|
27
|
+
|
|
28
|
+
const ORMCONFIG = config.get('ormconfig', {})
|
|
29
|
+
const DATABASE_TYPE = ORMCONFIG.type
|
|
30
|
+
|
|
31
|
+
@Entity()
|
|
32
|
+
@Index('ix_chat_message_1', (message: ChatMessage) => [message.session, message.createdAt])
|
|
33
|
+
@ObjectType({ description: 'A single chat message in a ChatSession.' })
|
|
34
|
+
export class ChatMessage {
|
|
35
|
+
@PrimaryGeneratedColumn('uuid')
|
|
36
|
+
@Field(type => ID, { nullable: true })
|
|
37
|
+
readonly id?: string
|
|
38
|
+
|
|
39
|
+
@ManyToOne(type => ChatSession, session => session.messages, { onDelete: 'CASCADE' })
|
|
40
|
+
session?: ChatSession
|
|
41
|
+
|
|
42
|
+
@RelationId((message: ChatMessage) => message.session)
|
|
43
|
+
sessionId?: string
|
|
44
|
+
|
|
45
|
+
@Column({ type: 'varchar', length: 16 })
|
|
46
|
+
@Field({ description: "'user' | 'assistant' | 'system'" })
|
|
47
|
+
role!: string
|
|
48
|
+
|
|
49
|
+
@Column({
|
|
50
|
+
type:
|
|
51
|
+
DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
|
|
52
|
+
? 'longtext'
|
|
53
|
+
: DATABASE_TYPE == 'oracle'
|
|
54
|
+
? 'clob'
|
|
55
|
+
: DATABASE_TYPE == 'mssql'
|
|
56
|
+
? 'nvarchar'
|
|
57
|
+
: 'text',
|
|
58
|
+
length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
|
|
59
|
+
})
|
|
60
|
+
@Field({ description: 'Message content' })
|
|
61
|
+
content!: string
|
|
62
|
+
|
|
63
|
+
@Column({ nullable: true })
|
|
64
|
+
@Field({ nullable: true, description: 'PatchEntry.id this message triggered (if any).' })
|
|
65
|
+
relatedPatchId?: string
|
|
66
|
+
|
|
67
|
+
@Column({ nullable: true })
|
|
68
|
+
@Field({ nullable: true, description: 'ImportSession.id this message triggered (if any).' })
|
|
69
|
+
relatedImportSessionId?: string
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* AI 가 응답을 만드는 동안 호출한 도구 trace — JSON-stringified.
|
|
73
|
+
*
|
|
74
|
+
* 컬럼은 string (PatchEntry.ops 와 동일 패턴), GraphQL 노출은 toolUsagesJson getter
|
|
75
|
+
* 로 parse 된 객체. UI 의 fold-able 박스 ("AI 가 이런 도구를 사용했습니다") 가
|
|
76
|
+
* 페이지 reload / loadHistory 후에도 살아있도록 영속.
|
|
77
|
+
*
|
|
78
|
+
* 형태: ToolUsage[] (assistant.ts 의 시간순 trace, summarizeToolResult 로 압축).
|
|
79
|
+
* null 이면 도구 호출 없었던 메시지 (text-only 응답).
|
|
80
|
+
*/
|
|
81
|
+
@Column({
|
|
82
|
+
type:
|
|
83
|
+
DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
|
|
84
|
+
? 'longtext'
|
|
85
|
+
: DATABASE_TYPE == 'oracle'
|
|
86
|
+
? 'clob'
|
|
87
|
+
: DATABASE_TYPE == 'mssql'
|
|
88
|
+
? 'nvarchar'
|
|
89
|
+
: 'text',
|
|
90
|
+
length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined,
|
|
91
|
+
nullable: true
|
|
92
|
+
})
|
|
93
|
+
toolUsages?: string
|
|
94
|
+
|
|
95
|
+
/** GraphQL 노출용 — JSON 배열로 parse. */
|
|
96
|
+
@Field(() => GraphQLJSON, {
|
|
97
|
+
nullable: true,
|
|
98
|
+
description:
|
|
99
|
+
'Tool usage trace — array of {name, arguments, result, kind}. Only on assistant messages.'
|
|
100
|
+
})
|
|
101
|
+
get toolUsagesJson(): any {
|
|
102
|
+
if (!this.toolUsages) return null
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(this.toolUsages)
|
|
105
|
+
} catch {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 메시지를 일으킨 사용자.
|
|
112
|
+
* - role='user' → 보낸 사용자
|
|
113
|
+
* - role='assistant' → 그 응답을 트리거한 사용자 (직전 user 메시지의 작성자)
|
|
114
|
+
* - role='system' → 시스템 액션을 일으킨 사용자 (예: 직접 편집한 사용자)
|
|
115
|
+
*
|
|
116
|
+
* nullable — legacy 메시지 (이 필드 추가 이전에 작성) 는 null. 또한 ad-hoc 모드 / 시스템
|
|
117
|
+
* 자동 메시지 중 사용자 컨텍스트가 없는 경우도 null 가능.
|
|
118
|
+
*/
|
|
119
|
+
@ManyToOne(type => User, { nullable: true })
|
|
120
|
+
@Field(type => User, { nullable: true, description: 'User who triggered this message.' })
|
|
121
|
+
creator?: User
|
|
122
|
+
|
|
123
|
+
@RelationId((message: ChatMessage) => message.creator)
|
|
124
|
+
creatorId?: string
|
|
125
|
+
|
|
126
|
+
@ManyToOne(type => User, { nullable: true })
|
|
127
|
+
@Field(type => User, { nullable: true, description: 'Last user to update this message (for future edit support).' })
|
|
128
|
+
updater?: User
|
|
129
|
+
|
|
130
|
+
@RelationId((message: ChatMessage) => message.updater)
|
|
131
|
+
updaterId?: string
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 부모 메시지 — 이 메시지가 "응답하는 대상" / "이어가는 메시지" 의 id.
|
|
135
|
+
*
|
|
136
|
+
* 의미 (role 별):
|
|
137
|
+
* - 'user' → 직전 in-session 메시지 (보통 assistant 또는 system). 첫 turn 은 null.
|
|
138
|
+
* - 'assistant' → 이 응답이 답하는 user 메시지. (regenerate 로 같은 user 메시지를 부모로
|
|
139
|
+
* 하는 형제 assistant 메시지가 생기면 자연스럽게 branching tree 가 됨)
|
|
140
|
+
* - 'system' → 직전 in-session 메시지 (timeline 연속성 유지).
|
|
141
|
+
*
|
|
142
|
+
* 이 한 컬럼으로 향후 어떤 흐름이든 표현 가능 — linear chain / branching (regenerate) /
|
|
143
|
+
* tree (replies-to-specific-message) 등. UI/쿼리는 점진 도입.
|
|
144
|
+
*
|
|
145
|
+
* onDelete: 'SET NULL' — 부모 메시지가 삭제돼도 child 는 남되 dangling 마킹.
|
|
146
|
+
*/
|
|
147
|
+
@ManyToOne(type => ChatMessage, { nullable: true, onDelete: 'SET NULL' })
|
|
148
|
+
parentMessage?: ChatMessage
|
|
149
|
+
|
|
150
|
+
@RelationId((message: ChatMessage) => message.parentMessage)
|
|
151
|
+
@Field({
|
|
152
|
+
nullable: true,
|
|
153
|
+
description:
|
|
154
|
+
'Id of the message this one is in-response-to / continues. Enables linear / branching / tree structures.'
|
|
155
|
+
})
|
|
156
|
+
parentMessageId?: string
|
|
157
|
+
|
|
158
|
+
@CreateDateColumn()
|
|
159
|
+
@Field({ nullable: true, description: 'Creation timestamp.' })
|
|
160
|
+
createdAt?: Date
|
|
161
|
+
|
|
162
|
+
@UpdateDateColumn()
|
|
163
|
+
@Field({ nullable: true, description: 'Last update timestamp (relevant once message editing is supported).' })
|
|
164
|
+
updatedAt?: Date
|
|
165
|
+
|
|
166
|
+
@DeleteDateColumn()
|
|
167
|
+
@Field({ nullable: true, description: 'Soft delete timestamp.' })
|
|
168
|
+
deletedAt?: Date
|
|
169
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ChatMessage } from './chat-message.js'
|
|
2
|
+
|
|
3
|
+
export { ChatMessage }
|
|
4
|
+
export {
|
|
5
|
+
BoardChatMessageSubscription,
|
|
6
|
+
publishChatMessage,
|
|
7
|
+
CHAT_MESSAGE_TOPIC,
|
|
8
|
+
chatMessageFilter
|
|
9
|
+
} from './chat-message-subscription.js'
|
|
10
|
+
export type { ChatMessagePayload } from './chat-message-subscription.js'
|
|
11
|
+
|
|
12
|
+
export const entities = [ChatMessage]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatSession — Board 와 결합되는 AI 협력 세션.
|
|
3
|
+
*
|
|
4
|
+
* 한 보드에 여러 세션 가능 — DB 레벨 unique 제약 없음.
|
|
5
|
+
*
|
|
6
|
+
* 향후 폭증할 세션 분화 차원 (예: thread / 사용자별 / 컨텍스트별 design vs ops /
|
|
7
|
+
* archived / forked from clone / agent별) 을 미리 차단하지 않기 위해 구조 자체는 1:N
|
|
8
|
+
* 으로 열어둠. 단일 세션 정책 (현재 UX) 은 app 레벨 idempotent 로직 — `startBoardAISession`
|
|
9
|
+
* 이 boardId 매칭 시 기존 세션 반환, `chatSessionByBoard` 가 첫 매칭 반환.
|
|
10
|
+
*
|
|
11
|
+
* 미래에 다중 세션 활성화 시 discriminator 컬럼 (kind / name / threadId / creatorId 등)
|
|
12
|
+
* 을 그때 추가. 마이그레이션 부담 최소화 — 현재는 unique 만 풀어 자리 마련.
|
|
13
|
+
*
|
|
14
|
+
* board-service 의 Board 가 owning side 가 될 예정 (ChatSession FK).
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
Column,
|
|
18
|
+
CreateDateColumn,
|
|
19
|
+
DeleteDateColumn,
|
|
20
|
+
Entity,
|
|
21
|
+
Index,
|
|
22
|
+
ManyToOne,
|
|
23
|
+
OneToMany,
|
|
24
|
+
PrimaryGeneratedColumn,
|
|
25
|
+
RelationId,
|
|
26
|
+
UpdateDateColumn
|
|
27
|
+
} from 'typeorm'
|
|
28
|
+
import { Field, ID, ObjectType } from 'type-graphql'
|
|
29
|
+
|
|
30
|
+
import { Domain } from '@things-factory/shell'
|
|
31
|
+
import { User } from '@things-factory/auth-base'
|
|
32
|
+
import { config } from '@things-factory/env'
|
|
33
|
+
|
|
34
|
+
import { ChatMessage } from '../chat-message/chat-message.js'
|
|
35
|
+
import { PatchEntry } from '../patch-entry/patch-entry.js'
|
|
36
|
+
|
|
37
|
+
const ORMCONFIG = config.get('ormconfig', {})
|
|
38
|
+
const DATABASE_TYPE = ORMCONFIG.type
|
|
39
|
+
|
|
40
|
+
@Entity()
|
|
41
|
+
// 인덱스는 조회 성능용으로 유지 (chatSessionByBoard 가 [domain, boardId] 로 자주 쿼리).
|
|
42
|
+
// unique 제약은 의도적으로 제거 — 한 보드에 여러 세션을 미래에 허용하기 위함.
|
|
43
|
+
@Index('ix_chat_session_1', (session: ChatSession) => [session.domain, session.boardId])
|
|
44
|
+
@ObjectType({
|
|
45
|
+
description: 'AI 협력 세션 — Board 와 결합. 한 보드에 여러 세션 가능 (thread / 사용자별 / 컨텍스트별 등 미래 확장).'
|
|
46
|
+
})
|
|
47
|
+
export class ChatSession {
|
|
48
|
+
@PrimaryGeneratedColumn('uuid')
|
|
49
|
+
@Field(type => ID, { nullable: true })
|
|
50
|
+
readonly id?: string
|
|
51
|
+
|
|
52
|
+
@ManyToOne(type => Domain)
|
|
53
|
+
@Field(type => Domain, { nullable: true })
|
|
54
|
+
domain?: Domain
|
|
55
|
+
|
|
56
|
+
@RelationId((session: ChatSession) => session.domain)
|
|
57
|
+
domainId?: string
|
|
58
|
+
|
|
59
|
+
/** Board.id 와 연결. Board 가 owning side 가 될 예정. 1:N — 한 보드에 여러 세션 가능. */
|
|
60
|
+
@Column({ nullable: true })
|
|
61
|
+
@Field({
|
|
62
|
+
nullable: true,
|
|
63
|
+
description:
|
|
64
|
+
'Connected Board id. Multiple sessions per board allowed (future: threads / per-user / contexts).'
|
|
65
|
+
})
|
|
66
|
+
boardId?: string
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 세션의 사용자 정의 이름 (탭 레이블 / 세션 식별 표시용).
|
|
70
|
+
*
|
|
71
|
+
* nullable — 미설정 시 UI 가 fallback 라벨 ("세션 N" 또는 createdAt 기반) 사용. 사용자가
|
|
72
|
+
* 다중 세션 운영 시 의미 부여 (예: "초기 설계", "운영 시뮬레이션", "사용자A 의 사적 세션").
|
|
73
|
+
*/
|
|
74
|
+
@Column({ nullable: true, length: 200 })
|
|
75
|
+
@Field({ nullable: true, description: 'User-given name of this session (tab label / identification).' })
|
|
76
|
+
name?: string
|
|
77
|
+
|
|
78
|
+
@ManyToOne(type => User)
|
|
79
|
+
@Field(type => User, { nullable: true, description: 'User who created this session.' })
|
|
80
|
+
creator?: User
|
|
81
|
+
|
|
82
|
+
@RelationId((session: ChatSession) => session.creator)
|
|
83
|
+
creatorId?: string
|
|
84
|
+
|
|
85
|
+
@ManyToOne(type => User)
|
|
86
|
+
@Field(type => User, { nullable: true, description: 'User who last updated this session.' })
|
|
87
|
+
updater?: User
|
|
88
|
+
|
|
89
|
+
@RelationId((session: ChatSession) => session.updater)
|
|
90
|
+
updaterId?: string
|
|
91
|
+
|
|
92
|
+
/** 토큰 절감용 — 오래된 메시지를 LLM 으로 압축한 요약. Phase 2 에서 작성. */
|
|
93
|
+
@Column({
|
|
94
|
+
nullable: true,
|
|
95
|
+
type:
|
|
96
|
+
DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
|
|
97
|
+
? 'longtext'
|
|
98
|
+
: DATABASE_TYPE == 'oracle'
|
|
99
|
+
? 'clob'
|
|
100
|
+
: DATABASE_TYPE == 'mssql'
|
|
101
|
+
? 'nvarchar'
|
|
102
|
+
: 'text',
|
|
103
|
+
length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
|
|
104
|
+
})
|
|
105
|
+
@Field({ nullable: true, description: 'Compressed summary of older messages (for token saving).' })
|
|
106
|
+
lastSummary?: string
|
|
107
|
+
|
|
108
|
+
/** 사용 모델 식별 — 'anthropic:claude-sonnet-4-6' 등 */
|
|
109
|
+
@Column({ nullable: true })
|
|
110
|
+
@Field({ nullable: true })
|
|
111
|
+
aiClientId?: string
|
|
112
|
+
|
|
113
|
+
@CreateDateColumn()
|
|
114
|
+
@Field({ nullable: true })
|
|
115
|
+
createdAt?: Date
|
|
116
|
+
|
|
117
|
+
@UpdateDateColumn()
|
|
118
|
+
@Field({ nullable: true })
|
|
119
|
+
updatedAt?: Date
|
|
120
|
+
|
|
121
|
+
@DeleteDateColumn()
|
|
122
|
+
deletedAt?: Date
|
|
123
|
+
|
|
124
|
+
// ── 관계 (TypeORM 만, GraphQL 노출은 별도 query 통해 페이징) ──
|
|
125
|
+
@OneToMany(type => ChatMessage, message => message.session)
|
|
126
|
+
messages?: ChatMessage[]
|
|
127
|
+
|
|
128
|
+
@OneToMany(type => PatchEntry, patch => patch.session)
|
|
129
|
+
patches?: PatchEntry[]
|
|
130
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatSessionParticipant — 채팅 세션의 참여 사용자.
|
|
3
|
+
*
|
|
4
|
+
* 한 ChatSession 에 여러 사용자가 참여할 수 있고, 각자 role (owner / member / viewer 등)
|
|
5
|
+
* 을 가짐. 다자간 채팅 / 권한 분화 / "내 세션 목록" / 멤버 표시 등의 기반.
|
|
6
|
+
*
|
|
7
|
+
* 현재 visibility 정책 (도메인 내 모두 공개) 은 그대로 유지 — 본 엔티티는 영속/추적
|
|
8
|
+
* 데이터 만들고, "참여자만 접근" 같은 enforcement 는 정책 굳을 때 별도 phase 에서 도입.
|
|
9
|
+
*
|
|
10
|
+
* 자동 참여 vs 명시적 초대 정책도 미정 — 일단 세션 creator 만 자동 'owner' 로 등록.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
Column,
|
|
14
|
+
CreateDateColumn,
|
|
15
|
+
DeleteDateColumn,
|
|
16
|
+
Entity,
|
|
17
|
+
Index,
|
|
18
|
+
ManyToOne,
|
|
19
|
+
PrimaryGeneratedColumn,
|
|
20
|
+
RelationId,
|
|
21
|
+
UpdateDateColumn
|
|
22
|
+
} from 'typeorm'
|
|
23
|
+
import { Field, ID, ObjectType } from 'type-graphql'
|
|
24
|
+
|
|
25
|
+
import { Domain } from '@things-factory/shell'
|
|
26
|
+
import { User } from '@things-factory/auth-base'
|
|
27
|
+
|
|
28
|
+
import { ChatSession } from '../chat-session/chat-session.js'
|
|
29
|
+
|
|
30
|
+
@Entity('chat_session_participants')
|
|
31
|
+
@Index('ix_chat_session_participant_0', ['domain', 'session', 'user'], {
|
|
32
|
+
unique: true,
|
|
33
|
+
where: '"deleted_at" IS NULL'
|
|
34
|
+
})
|
|
35
|
+
@Index('ix_chat_session_participant_1', ['domain', 'user'], {
|
|
36
|
+
where: '"deleted_at" IS NULL'
|
|
37
|
+
})
|
|
38
|
+
@ObjectType({
|
|
39
|
+
description:
|
|
40
|
+
'Participant of a ChatSession — links a User to a session with a role. Foundation for multi-user chat / per-user filtering / member display.'
|
|
41
|
+
})
|
|
42
|
+
export class ChatSessionParticipant {
|
|
43
|
+
@PrimaryGeneratedColumn('uuid')
|
|
44
|
+
@Field(type => ID, { description: 'Unique participant id.' })
|
|
45
|
+
readonly id?: string
|
|
46
|
+
|
|
47
|
+
@ManyToOne(type => Domain)
|
|
48
|
+
@Field(type => Domain, { nullable: true, description: 'Domain to which this participant belongs.' })
|
|
49
|
+
domain?: Domain
|
|
50
|
+
|
|
51
|
+
@RelationId((p: ChatSessionParticipant) => p.domain)
|
|
52
|
+
domainId?: string
|
|
53
|
+
|
|
54
|
+
@ManyToOne(type => ChatSession, { onDelete: 'CASCADE' })
|
|
55
|
+
@Field(type => ChatSession, { nullable: true, description: 'The ChatSession this participation belongs to.' })
|
|
56
|
+
session?: ChatSession
|
|
57
|
+
|
|
58
|
+
@RelationId((p: ChatSessionParticipant) => p.session)
|
|
59
|
+
sessionId?: string
|
|
60
|
+
|
|
61
|
+
@ManyToOne(type => User)
|
|
62
|
+
@Field(type => User, { nullable: true, description: 'The user participating in the session.' })
|
|
63
|
+
user?: User
|
|
64
|
+
|
|
65
|
+
@RelationId((p: ChatSessionParticipant) => p.user)
|
|
66
|
+
userId?: string
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 참여자의 역할.
|
|
70
|
+
* - 'owner': 세션 만든 사람. rename / delete 권한.
|
|
71
|
+
* - 'member': 일반 참여자. 메시지 송수신.
|
|
72
|
+
* - 'viewer': 읽기 전용 (미래 권한 정책 활성화 시).
|
|
73
|
+
*
|
|
74
|
+
* 시작은 'owner' / 'member' 둘로 충분. 더 세분화 필요해지면 enum 확장.
|
|
75
|
+
*/
|
|
76
|
+
@Column({ type: 'varchar', length: 16, default: 'member' })
|
|
77
|
+
@Field({ description: 'Role of this participant: owner | member | viewer.' })
|
|
78
|
+
role!: string
|
|
79
|
+
|
|
80
|
+
/** 마지막 활동 시각 — presence indicator. nullable. */
|
|
81
|
+
@Column({ nullable: true })
|
|
82
|
+
@Field({ nullable: true, description: 'Last seen / activity timestamp (presence indicator).' })
|
|
83
|
+
lastSeenAt?: Date
|
|
84
|
+
|
|
85
|
+
@ManyToOne(type => User)
|
|
86
|
+
@Field(type => User, { nullable: true, description: 'User who created this participant record (typically session owner who invited).' })
|
|
87
|
+
creator?: User
|
|
88
|
+
|
|
89
|
+
@RelationId((p: ChatSessionParticipant) => p.creator)
|
|
90
|
+
creatorId?: string
|
|
91
|
+
|
|
92
|
+
@ManyToOne(type => User)
|
|
93
|
+
@Field(type => User, { nullable: true, description: 'Last user to update this participant record (e.g., role change).' })
|
|
94
|
+
updater?: User
|
|
95
|
+
|
|
96
|
+
@RelationId((p: ChatSessionParticipant) => p.updater)
|
|
97
|
+
updaterId?: string
|
|
98
|
+
|
|
99
|
+
@CreateDateColumn()
|
|
100
|
+
@Field({ nullable: true, description: 'Joined-at timestamp.' })
|
|
101
|
+
createdAt?: Date
|
|
102
|
+
|
|
103
|
+
@UpdateDateColumn()
|
|
104
|
+
@Field({ nullable: true, description: 'Last update timestamp (role change / lastSeenAt bump).' })
|
|
105
|
+
updatedAt?: Date
|
|
106
|
+
|
|
107
|
+
@DeleteDateColumn()
|
|
108
|
+
@Field({ nullable: true, description: 'Soft delete (= left session) timestamp.' })
|
|
109
|
+
deletedAt?: Date
|
|
110
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { buildInviteNotification } from './invite-notification'
|
|
2
|
+
|
|
3
|
+
describe('buildInviteNotification — 협업 초대 알림 payload', () => {
|
|
4
|
+
test('받는 사람 없으면 null', () => {
|
|
5
|
+
expect(buildInviteNotification([], 'board-1', { name: 'Kim' })).toBeNull()
|
|
6
|
+
expect(buildInviteNotification(undefined as any, 'board-1', { name: 'Kim' })).toBeNull()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('정상 — receivers / type / mode 고정', () => {
|
|
10
|
+
const p = buildInviteNotification(['u1', 'u2'], 'board-9', { name: 'Kim' })!
|
|
11
|
+
expect(p).not.toBeNull()
|
|
12
|
+
expect(p.receivers).toEqual(['u1', 'u2'])
|
|
13
|
+
expect(p.type).toBe('board-collab-invite')
|
|
14
|
+
expect(p.subject).toBe('info')
|
|
15
|
+
expect(p.mode).toBe('background')
|
|
16
|
+
expect(p.title).toBeTruthy()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('inviter 이름 우선, 없으면 email, 둘 다 없으면 someone', () => {
|
|
20
|
+
expect(buildInviteNotification(['u1'], 'b', { name: 'Kim', email: 'k@x.io' })!.body).toContain('Kim')
|
|
21
|
+
expect(buildInviteNotification(['u1'], 'b', { email: 'k@x.io' })!.body).toContain('k@x.io')
|
|
22
|
+
expect(buildInviteNotification(['u1'], 'b', {})!.body).toContain('someone')
|
|
23
|
+
expect(buildInviteNotification(['u1'], 'b', null)!.body).toContain('someone')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('boardId 있으면 모델러 링크, 없으면 url 생략', () => {
|
|
27
|
+
expect(buildInviteNotification(['u1'], 'board-42', { name: 'Kim' })!.url).toBe('board-modeller/board-42')
|
|
28
|
+
expect(buildInviteNotification(['u1'], undefined, { name: 'Kim' })!.url).toBeUndefined()
|
|
29
|
+
})
|
|
30
|
+
})
|