@things-factory/board-ai 10.0.0 → 10.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/client/components/board-ai-chat.ts +107 -2
  2. package/client/components/chat-echo-dedup.ts +3 -0
  3. package/client/components/chat-input-builder.ts +6 -0
  4. package/dist-client/client/components/board-ai-chat.d.ts +17 -0
  5. package/dist-client/client/components/board-ai-chat.js +102 -2
  6. package/dist-client/client/components/board-ai-chat.js.map +1 -1
  7. package/dist-client/client/components/chat-echo-dedup.d.ts +2 -0
  8. package/dist-client/client/components/chat-echo-dedup.js +1 -0
  9. package/dist-client/client/components/chat-echo-dedup.js.map +1 -1
  10. package/dist-client/client/components/chat-input-builder.d.ts +5 -0
  11. package/dist-client/client/components/chat-input-builder.js +1 -0
  12. package/dist-client/client/components/chat-input-builder.js.map +1 -1
  13. package/dist-client/server/service/agentic-loop.d.ts +18 -0
  14. package/dist-client/server/service/agentic-loop.js +10 -1
  15. package/dist-client/server/service/agentic-loop.js.map +1 -1
  16. package/dist-client/server/service/assistant.js +14 -2
  17. package/dist-client/server/service/assistant.js.map +1 -1
  18. package/dist-client/server/service/grounding.d.ts +17 -0
  19. package/dist-client/server/service/grounding.js +42 -0
  20. package/dist-client/server/service/grounding.js.map +1 -0
  21. package/dist-client/server/service/types.d.ts +16 -0
  22. package/dist-client/server/service/types.js.map +1 -1
  23. package/dist-client/tsconfig.tsbuildinfo +1 -1
  24. package/dist-server/service/agentic-loop.d.ts +18 -0
  25. package/dist-server/service/agentic-loop.js +10 -1
  26. package/dist-server/service/agentic-loop.js.map +1 -1
  27. package/dist-server/service/assistant.js +14 -2
  28. package/dist-server/service/assistant.js.map +1 -1
  29. package/dist-server/service/board-ai-resolver.d.ts +2 -0
  30. package/dist-server/service/board-ai-resolver.js +22 -1
  31. package/dist-server/service/board-ai-resolver.js.map +1 -1
  32. package/dist-server/service/chat-message/chat-message.d.ts +12 -0
  33. package/dist-server/service/chat-message/chat-message.js +23 -0
  34. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  35. package/dist-server/service/chat-session/session-inbox.d.ts +26 -0
  36. package/dist-server/service/chat-session/session-inbox.js +41 -0
  37. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  38. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +11 -0
  39. package/dist-server/service/chat-session-participant/chat-session-participant.js +17 -1
  40. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -1
  41. package/dist-server/service/chat-session-resolver.d.ts +44 -1
  42. package/dist-server/service/chat-session-resolver.js +306 -6
  43. package/dist-server/service/chat-session-resolver.js.map +1 -1
  44. package/dist-server/service/grounding.d.ts +17 -0
  45. package/dist-server/service/grounding.js +46 -0
  46. package/dist-server/service/grounding.js.map +1 -0
  47. package/dist-server/service/types.d.ts +16 -0
  48. package/dist-server/service/types.js.map +1 -1
  49. package/dist-server/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +5 -5
  51. package/server/service/agentic-loop.test.ts +63 -0
  52. package/server/service/agentic-loop.ts +28 -1
  53. package/server/service/assistant.ts +15 -2
  54. package/server/service/board-ai-resolver.ts +23 -1
  55. package/server/service/chat-message/chat-message.ts +26 -0
  56. package/server/service/chat-session/session-inbox.test.ts +69 -1
  57. package/server/service/chat-session/session-inbox.ts +45 -0
  58. package/server/service/chat-session-participant/chat-session-participant.ts +14 -0
  59. package/server/service/chat-session-resolver.ts +297 -5
  60. package/server/service/grounding.test.ts +55 -0
  61. package/server/service/grounding.ts +53 -0
  62. package/server/service/types.ts +16 -0
  63. package/translations/en.json +3 -1
  64. package/translations/ja.json +3 -1
  65. package/translations/ko.json +2 -0
  66. package/translations/ms.json +3 -1
  67. package/translations/zh.json +3 -1
@@ -16,6 +16,7 @@ declare class BoardAIChatInput {
16
16
  truncateAfterMessageId?: string;
17
17
  toolCategories?: string[];
18
18
  boardTools?: boolean;
19
+ requireGroundingTools?: boolean;
19
20
  knownTypes?: string[];
20
21
  categories?: string[];
21
22
  componentSchemas?: any;
@@ -36,6 +37,7 @@ declare class BoardAIChatOutput {
36
37
  patchId?: string;
37
38
  toolUsages?: any;
38
39
  actions?: any;
40
+ groundingWarnings?: any;
39
41
  }
40
42
  export declare class BoardAIChatResolver {
41
43
  /**
@@ -108,6 +108,13 @@ tslib_1.__decorate([
108
108
  }),
109
109
  tslib_1.__metadata("design:type", Boolean)
110
110
  ], BoardAIChatInput.prototype, "boardTools", void 0);
111
+ tslib_1.__decorate([
112
+ (0, type_graphql_1.Field)({
113
+ nullable: true,
114
+ description: 'Force a tool call on the first turn (toolChoice: required). Turn this on for surfaces that discuss live state: the grounding guard catches invented identifiers but not an invented situation stated without any, which only happens when the model answers without querying at all. Concept / how-to questions are unaffected — they are answered through a documentation tool.'
115
+ }),
116
+ tslib_1.__metadata("design:type", Boolean)
117
+ ], BoardAIChatInput.prototype, "requireGroundingTools", void 0);
111
118
  tslib_1.__decorate([
112
119
  (0, type_graphql_1.Field)(() => [String], {
113
120
  nullable: true,
@@ -215,6 +222,13 @@ tslib_1.__decorate([
215
222
  }),
216
223
  tslib_1.__metadata("design:type", Object)
217
224
  ], BoardAIChatOutput.prototype, "actions", void 0);
225
+ tslib_1.__decorate([
226
+ (0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, {
227
+ nullable: true,
228
+ description: 'Ungrounded identifiers mentioned by the reply — present in the answer but absent from everything the model received (prompt, board context, history, tool results). Hallucination candidates surfaced to the user; the reply itself is not altered. Null when the answer is grounded.'
229
+ }),
230
+ tslib_1.__metadata("design:type", Object)
231
+ ], BoardAIChatOutput.prototype, "groundingWarnings", void 0);
218
232
  BoardAIChatOutput = tslib_1.__decorate([
219
233
  (0, type_graphql_1.ObjectType)()
220
234
  ], BoardAIChatOutput);
@@ -344,6 +358,7 @@ let BoardAIChatResolver = class BoardAIChatResolver {
344
358
  /* 대화면별 도구 노출 — 미지정이면 현행(전부 허용) 그대로. */
345
359
  toolCategories: input.toolCategories ?? undefined,
346
360
  boardTools: input.boardTools ?? undefined,
361
+ requireGroundingTools: input.requireGroundingTools ?? undefined,
347
362
  knownTypes: input.knownTypes,
348
363
  categories: input.categories,
349
364
  selectedRefids: input.selectedRefids,
@@ -385,6 +400,11 @@ let BoardAIChatResolver = class BoardAIChatResolver {
385
400
  content: enrichedReply,
386
401
  relatedPatchId: patchEntry?.id,
387
402
  toolUsages: r.toolUsages && r.toolUsages.length > 0 ? JSON.stringify(r.toolUsages) : undefined,
403
+ /* 접지 경고를 함께 영속 — 나중에 읽는 참여자도 같은 주의를 봐야 한다(재계산 불가: 근거 원문이 남지 않는다).
404
+ * 컬럼 길이(1024) 안에 들도록 앞 20개까지만 — 그 이상이면 목록이 아니라 답 자체가 문제다. */
405
+ groundingWarnings: r.groundingWarnings && r.groundingWarnings.length > 0
406
+ ? JSON.stringify(r.groundingWarnings.slice(0, 20))
407
+ : undefined,
388
408
  // assistant 메시지의 creator = 이 응답을 트리거한 user (= 직전 user 메시지의 작성자).
389
409
  // 같은 turn 으로 묶임 → 향후 사용자별 필터에서 user/assistant 가 함께 따라옴.
390
410
  creator: user,
@@ -423,7 +443,8 @@ let BoardAIChatResolver = class BoardAIChatResolver {
423
443
  assistantMessageId,
424
444
  patchId,
425
445
  toolUsages: r.toolUsages ?? null,
426
- actions: r.actions && r.actions.length > 0 ? r.actions : null
446
+ actions: r.actions && r.actions.length > 0 ? r.actions : null,
447
+ groundingWarnings: r.groundingWarnings && r.groundingWarnings.length > 0 ? r.groundingWarnings : null
427
448
  };
428
449
  }
429
450
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"board-ai-resolver.js","sourceRoot":"","sources":["../../server/service/board-ai-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GraphQL resolver — AI 주도 보드 모델링 (chat).\n *\n * Mutation: boardAIChat — 자연어 채팅. sessionId 가 있으면 메시지/패치 영속.\n * sessionId 가 없으면 ad-hoc 모드로 단발 호출 (영속 없음).\n */\nimport { Resolver, Mutation, Arg, Ctx, Directive, Field, InputType, Int, ObjectType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport '@things-factory/auth-base'\n\nimport { config } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\nimport { createAIClient, getDefaultAIClient, type AIClient } from '@things-factory/ai-client-base'\nimport type { ComponentCategory } from '@things-factory/board-import'\n\nimport { DefaultBoardAIAssistant } from './assistant.js'\nimport type { LLMMessage } from './types.js'\nimport { ChatSession } from './chat-session/chat-session.js'\nimport { ChatMessage } from './chat-message/chat-message.js'\nimport { buildLlmHistory } from './chat-message/llm-history.js'\nimport { activityPatch, autoSessionName } from './chat-session/session-inbox.js'\nimport { publishSessionActivity } from './chat-session/session-activity-publish.js'\nimport { publishChatMessage } from './chat-message/chat-message-publish.js'\nimport { PatchEntry } from './patch-entry/patch-entry.js'\nimport { publishBoardPatch } from './patch-entry/board-patch-subscription.js'\nimport { ChatSessionParticipant } from './chat-session-participant/chat-session-participant.js'\nimport { buildInviteNotification } from './chat-session-participant/invite-notification.js'\nimport {\n injectMentionRefids,\n injectAiMentionRefids,\n stripMentionRefids,\n extractMentionUserIds,\n newParticipantUserIds\n} from './mention-marker.js'\n\n@InputType()\nclass BoardAILLMMessageInput {\n @Field({ description: \"Role: 'user' or 'assistant'\" })\n role: string\n\n @Field({ description: 'Message content' })\n content: string\n}\n\n@InputType()\nclass BoardAIMentionPickInput {\n @Field({ description: 'Token used by user (e.g. \"경광등1\" — the part after #).' })\n token: string\n\n @Field(() => Int, {\n description:\n 'Refid explicitly picked from popup. Server uses this for deterministic resolution — bypasses fallback name/id/type matching for this token.'\n })\n refid: number\n}\n\n@InputType()\nclass BoardAIChatInput {\n @Field({\n nullable: true,\n description: 'ChatSession id. Omit for ad-hoc (no persistence). When given, messages/patches are persisted.'\n })\n sessionId?: string\n\n @Field(() => [BoardAILLMMessageInput], {\n description: 'Conversation history (latest user message at the end). For persisted sessions, only the last user message is appended; older are loaded from DB.'\n })\n messages: BoardAILLMMessageInput[]\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Current BoardModel JSON.' })\n currentBoard?: any\n\n @Field(() => [String], {\n nullable: true,\n description:\n \"Capability scopes the AI is allowed to use (e.g. 'create', 'edit', 'style'). Filters which tools are exposed to the LLM.\"\n })\n scopes?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Host context describing what this surface is looking at (e.g. { spaceId } for a space deliberation, { boardId } for board authoring). Tools read it as ctx.state.host so the model never has to ask the user for identifiers. Client-supplied — never used for authorization; tools verify the scope belongs to the caller tenant.'\n })\n hostContext?: any\n\n @Field({\n nullable: true,\n description:\n 'Id of the last message to keep from the stored history. The client keeps its own truncation when a user edits a message (the server history is intentionally preserved), so pass the last kept message id to hide the retracted span from the agent. Omit to use the whole stored history.'\n })\n truncateAfterMessageId?: string\n\n @Field(() => [String], {\n nullable: true,\n description:\n 'Registered external tool categories this conversation may use (registerToolCategory names, e.g. [\"twin\", \"board-import\"]). Omit to allow every registered category. Applies to tool definitions AND execution, so a surface cannot call a category it was not granted.'\n })\n toolCategories?: string[]\n\n @Field({\n nullable: true,\n description:\n 'Whether core board editing tools (addComponent / styling / scene actions) are available. Defaults to true. Set false for surfaces that must not modify the board — e.g. an operations chat where irreversible commands live, so reversible edits and irreversible actions never share one conversation.'\n })\n boardTools?: boolean\n\n @Field(() => [String], {\n nullable: true,\n description: 'Component types the LLM is allowed to emit (e.g. [\"rect\", \"label\"]). Constrains generation to the current solution.'\n })\n knownTypes?: string[]\n\n @Field(() => [String], {\n nullable: true,\n description: 'Component categories to surface (e.g. [\"3D\", \"form\"]). Subset filter for groupings exposed in component-schemas.'\n })\n categories?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Component schemas — array of { type, description?, group?, properties? } so the LLM knows valid props per type.'\n })\n componentSchemas?: any\n\n @Field(() => [Int], {\n nullable: true,\n description:\n 'Refids of components currently selected in the modeller (universal numeric handle, things-scene auto-assigned). For \"selected\" / \"선택한\" intent. Note: distinct from `id` which is a data-binding name.'\n })\n selectedRefids?: number[]\n\n @Field(() => [BoardAIMentionPickInput], {\n nullable: true,\n description:\n 'Explicit #mention picks from popup — {token, refid}. User text stays clean (#token only); these picks let server resolve deterministically without exposing refid in the user-visible message. Tokens not in this list fall back to free name/id/type matching.'\n })\n mentions?: BoardAIMentionPickInput[]\n\n @Field({\n nullable: true,\n description: 'AI model identifier override (e.g. \"claude-opus-4-7\"). Falls back to the configured default when omitted.'\n })\n model?: string\n\n @Field({\n nullable: true,\n description: 'Sampling temperature override. Falls back to assistant default when omitted.'\n })\n temperature?: number\n\n @Field({\n nullable: true,\n description: 'Max output tokens override per LLM call. Falls back to assistant default when omitted.'\n })\n maxTokens?: number\n}\n\n@ObjectType()\nclass BoardAIChatOutput {\n @Field({ description: 'Conversational reply.' })\n reply: string\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'BoardEditPatch.' })\n patch?: any\n\n @Field({ nullable: true, description: 'Clarifying question when input is ambiguous.' })\n followUp?: string\n\n @Field({ description: 'AI client identifier (provider:model).' })\n clientId: string\n\n @Field({ nullable: true, description: 'Echo of session id (when persisted).' })\n sessionId?: string\n\n @Field({ nullable: true, description: 'Persisted ChatMessage id of the user input.' })\n userMessageId?: string\n\n @Field({ nullable: true, description: 'Persisted ChatMessage id of the AI reply.' })\n assistantMessageId?: string\n\n @Field({ nullable: true, description: 'Persisted PatchEntry id (when patch was generated).' })\n patchId?: string\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Tool usages collected during agentic loop — sequence of {name, arguments, result, kind}. UI fold-able box for transparency / debug.'\n })\n toolUsages?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Ephemeral scene actions (selection/view/mode) — sequence of BoardActionOp. Applied via board-action-execute event on the host. Distinct from `patch` which carries persistent model changes.'\n })\n actions?: any\n}\n\n@Resolver()\nexport class BoardAIChatResolver {\n /**\n * NOTE: 일부러 @Directive('@transaction') 사용하지 않는다.\n * LLM 호출은 수 초~수십 초 — 트랜잭션 안에서 호출하면 SQLite write lock 을\n * 그 시간 동안 보유 → 다른 작업 (cache 정리 등) 과 SQLITE_BUSY 충돌.\n * 따라서 DB 쓰기를 LLM 호출 전후의 짧은 단위로 나누고 각각 auto-commit.\n * 데이터 일관성: 채팅 이력은 부분 누락보다 부분 보존이 안전 (대화 컨텍스트).\n */\n @Mutation(() => BoardAIChatOutput, {\n description: 'AI 주도 보드 모델링 — 자연어 채팅으로 보드 생성·구조편집·스타일링. sessionId 로 영속 컨텍스트.'\n })\n @Directive('@privilege(category: \"board-ai\", privilege: \"mutation\")')\n async boardAIChat(\n @Arg('input') input: BoardAIChatInput,\n @Ctx() context: ResolverContext\n ): Promise<BoardAIChatOutput> {\n const base = resolveAIClient()\n if (!base) {\n throw new Error(\n 'No AI client configured. Set config.aiClient = { provider: \"anthropic\" | \"gemini\", model, apiKey } or call setDefaultAIClient(...).'\n )\n }\n\n const { domain, user } = context.state\n const sessionRepo = getRepository(ChatSession)\n const messageRepo = getRepository(ChatMessage)\n const patchRepo = getRepository(PatchEntry)\n\n // ── Phase A: LLM 호출 전 짧은 DB 쓰기 ─────────────────────────\n let session: ChatSession | undefined\n if (input.sessionId) {\n const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } })\n if (!found) throw new Error(`ChatSession ${input.sessionId} not found`)\n session = found\n }\n\n const lastUserMsg = input.messages[input.messages.length - 1]\n let userMessageId: string | undefined\n if (session && lastUserMsg && lastUserMsg.role === 'user') {\n // popup pick 의 refid 를 #token{refid:N} inline marker 로 content 에 박아 저장.\n // 단일 source-of-truth — 별도 컬럼 없이 content 안에서 token-refid 매핑 보존.\n const enriched = injectMentionRefids(lastUserMsg.content, input.mentions)\n // parentMessage = 이 세션의 직전 메시지 (보통 assistant / system / 또는 첫 turn 이면 null).\n // tree 구조 인프라 — branching (regenerate / 사용자별 분기) 는 같은 부모 child 로.\n const previousLast = await messageRepo.findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const saved = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'user',\n content: enriched,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n })\n userMessageId = saved.id\n\n /* 세션 활동 갱신(비정규화) — 목록 최근활동순 정렬과 다른 참여자의 안 읽음 판정 기준.\n * 이름이 아직 없으면 **첫 발화로 자동 작명** 한다 — `세션 2` 보다 \"A-DC 포화 원인\" 이 목록에서\n * 훨씬 유용하다. 사용자가 직접 바꾼 이름은 여기서 건드리지 않는다(이름이 있으면 그대로).\n * best-effort: 실패해도 대화를 막지 않는다(목록 정렬이 한 칸 늦을 뿐). */\n try {\n const patch: any = activityPatch(enriched)\n if (!session.name) patch.name = autoSessionName(enriched)\n await sessionRepo.update(session.id!, patch)\n } catch {\n /* noop */\n }\n /* 내가 참여한 세션 활동 방송 — 창을 닫아 둔 다른 참여자의 배지·목록을 위해. */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: enriched,\n actorId: user?.id\n })\n\n // C4: 사용자 메시지를 같은 세션 구독자에게 방송 (그룹챗 실시간). best-effort.\n // 자기 발신 에코는 클라이언트가 message.id 로 dedup.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: saved })\n\n // C4: @user 멘션 → 세션 참여자(member) 자동 등록 (초대). best-effort — 실패해도 chat 흐름 무영향.\n // enriched content 의 {userId:UUID} 마커에서 추출 → 기존 참여자/자기 제외한 신규만 member 로 등록.\n try {\n await this._inviteMentionedUsers(enriched, session.id!, domain.id, user, session.boardId, (context.state as any).notify)\n } catch {\n /* 초대 실패는 대화를 막지 않는다 */\n }\n }\n\n // ── Phase B: LLM 호출 (DB 잠금 없음, 오래 걸려도 OK) ───────────\n const assistant = new DefaultBoardAIAssistant(base, {\n scopes: input.scopes,\n knownTypes: input.knownTypes,\n categories: input.categories as ComponentCategory[] | undefined\n })\n\n /* 에이전트가 볼 이력 — **세션이 있으면 서버 정본(ChatMessage)** 을 쓴다.\n * 클라이언트가 보낸 줄을 쓰면 협의에서 참여자마다 화면이 달라 에이전트가 사람마다 다른 대화를 본다.\n * 발신자 표기·system 기록 포함·접은 구간 제외 규칙은 buildLlmHistory 가 소유(그 파일 머리말 참조).\n * 즉석 대화(sessionId 없음)는 영속 이력이 없으므로 클라이언트 이력을 그대로 쓴다.\n *\n * 정렬은 createdAt ASC + id ASC — 같은 시각 삽입의 순서까지 결정적으로 만든다(조립 쪽에서\n * 다시 정렬하지 않으므로 여기서 확정해야 한다). 멘션 마커는 여기서 제거해 넘긴다. */\n let llmMessages: LLMMessage[]\n if (session) {\n const rows = await messageRepo.find({\n where: { session: { id: session.id } as any },\n relations: { creator: true },\n order: { createdAt: 'ASC', id: 'ASC' }\n })\n llmMessages = buildLlmHistory(\n rows.map(row => ({\n id: row.id,\n role: row.role,\n content: stripMentionRefids(row.content ?? ''),\n senderId: (row as any).creator?.id,\n /* 표시 이름만 — 이메일 등 개인정보는 프롬프트에 넣지 않는다. */\n senderName: (row as any).creator?.name || (row as any).creator?.username\n })),\n {\n truncateAfterMessageId: input.truncateAfterMessageId ?? undefined,\n /* 방금 저장한 사용자 메시지는 접은 구간보다 뒤에 있어도 반드시 남긴다. */\n keepMessageIds: userMessageId ? [userMessageId] : []\n }\n )\n } else {\n llmMessages = input.messages.map(m => ({\n role: m.role === 'assistant' ? 'assistant' : 'user',\n content: stripMentionRefids(m.content)\n }))\n }\n\n const r = await assistant.chat(llmMessages, input.currentBoard ?? undefined, {\n scopes: input.scopes,\n hostContext: input.hostContext ?? undefined,\n /* 대화면별 도구 노출 — 미지정이면 현행(전부 허용) 그대로. */\n toolCategories: input.toolCategories ?? undefined,\n boardTools: input.boardTools ?? undefined,\n knownTypes: input.knownTypes,\n categories: input.categories as ComponentCategory[] | undefined,\n selectedRefids: input.selectedRefids,\n mentionPicks: input.mentions,\n componentSchemas: Array.isArray(input.componentSchemas) ? input.componentSchemas : undefined,\n model: input.model,\n temperature: input.temperature ?? undefined,\n maxTokens: input.maxTokens ?? undefined,\n // 외부 도메인 (board-import 등) 의 read/external tool builder 가 server-side\n // 의존성 (도메인 / 유저 / 트랜잭션) 을 사용할 수 있도록 ResolverContext.state 전달.\n // board-ai 코어 자체는 이 값을 알지 않고 dispatch 단계에서 closure 로만 흘림.\n toolCallContext: context.state\n })\n\n // ── Phase C: LLM 호출 후 짧은 DB 쓰기 ─────────────────────────\n let assistantMessageId: string | undefined\n let patchId: string | undefined\n if (session) {\n let patchEntry: PatchEntry | undefined\n if (r.patch) {\n patchEntry = await patchRepo.save({\n session: { id: session.id } as any,\n source: 'ai',\n ops: JSON.stringify(r.patch.ops),\n summary: r.patch.summary,\n confidence: r.patch.confidence,\n reverted: false\n })\n patchId = patchEntry.id\n // C1: 같은 board 구독자에게 방송 (실시간 공동편집 spine). best-effort.\n publishBoardPatch({ domainId: domain.id, boardId: session.boardId, patch: patchEntry })\n }\n\n // AI 응답의 #token 들을 currentBoard 에 대해 resolve 한 뒤 inline marker 로 박음.\n // patch 로 새로 만든 컴포넌트는 input.currentBoard 에 없으므로 marker 없이 저장 →\n // 클릭 시 runtime fallback 으로 해석 (그때는 patch 적용 후라 scene 에 존재).\n const enrichedReply = injectAiMentionRefids(r.reply, input.currentBoard ?? null)\n\n const aiMsg = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'assistant',\n content: enrichedReply,\n relatedPatchId: patchEntry?.id,\n toolUsages: r.toolUsages && r.toolUsages.length > 0 ? JSON.stringify(r.toolUsages) : undefined,\n // assistant 메시지의 creator = 이 응답을 트리거한 user (= 직전 user 메시지의 작성자).\n // 같은 turn 으로 묶임 → 향후 사용자별 필터에서 user/assistant 가 함께 따라옴.\n creator: user,\n updater: user,\n // parentMessage = 이 응답이 답하는 user 메시지. regenerate 시 같은 user 메시지를\n // 부모로 하는 형제 assistant 들이 생기면 자연스럽게 branching tree.\n parentMessage: userMessageId ? ({ id: userMessageId } as any) : undefined\n })\n assistantMessageId = aiMsg.id\n\n // C4: AI 응답도 같은 세션 구독자에게 방송 (트리거한 본인 포함 — id dedup). best-effort.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: aiMsg })\n /* AI 응답도 세션 활동이다 — 이걸 빼면 다른 참여자에게 \"새 소식 없음\" 으로 보인다. */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: enrichedReply,\n actorId: user?.id\n })\n\n await sessionRepo.update(session.id, {\n aiClientId: base.id,\n updater: user,\n /* AI 응답도 세션 활동이다 — 이걸 빼면 다른 참여자에게 \"새 소식 없음\"으로 보인다. */\n ...activityPatch(enrichedReply)\n })\n }\n\n return {\n reply: r.reply,\n patch: r.patch ?? null,\n followUp: r.followUp ?? null,\n clientId: base.id,\n sessionId: session?.id,\n userMessageId,\n assistantMessageId,\n patchId,\n toolUsages: r.toolUsages ?? null,\n actions: r.actions && r.actions.length > 0 ? r.actions : null\n }\n }\n\n /**\n * C4: 메시지에 @멘션된 사용자를 세션 참여자(member)로 자동 등록 — 초대.\n * enriched content 의 {userId:UUID} 마커에서 추출 → 기존 참여자·자기 제외한 신규만 등록.\n * 소프트삭제(deletedAt)된 과거 참여자는 다시 살린다(재초대). 신규 등록 후 협업 초대 알림 발송(C4b-2, best-effort).\n */\n private async _inviteMentionedUsers(\n content: string,\n sessionId: string,\n domainId: string,\n inviter: any,\n boardId?: string,\n notify?: (arg: any) => Promise<void>\n ): Promise<void> {\n const mentioned = extractMentionUserIds(content)\n if (mentioned.length === 0) return\n\n const repo = getRepository(ChatSessionParticipant)\n const existing = await repo.find({\n where: { session: { id: sessionId } as any, domain: { id: domainId } as any },\n withDeleted: true\n })\n const activeIds = existing.filter(p => !p.deletedAt).map(p => p.userId!).filter(Boolean)\n const fresh = newParticipantUserIds(mentioned, activeIds, inviter?.id)\n if (fresh.length === 0) return\n\n for (const userId of fresh) {\n // 과거에 나갔던(soft-deleted) 참여자면 되살리고, 아니면 새로 등록.\n const prior = existing.find(p => p.userId === userId && p.deletedAt)\n if (prior) {\n await repo.update(prior.id!, { deletedAt: null as any, role: 'member', updater: inviter } as any)\n } else {\n await repo.save({\n domain: { id: domainId } as any,\n session: { id: sessionId } as any,\n user: { id: userId } as any,\n role: 'member',\n creator: inviter,\n updater: inviter\n } as any)\n }\n }\n\n // C4b-2: 초대 알림 — notification 모듈의 기존 발송 메커니즘 재사용(중복 구현 금지).\n // context.state.notify 는 notification-middleware 가 전역 주입(FCM/pubsub). duck-typed →\n // board-ai 에 의존성 추가 없이 런타임 호출. 미로드 환경이면 undefined → graceful skip.\n this._notifyInvited(notify, fresh, boardId, inviter)\n }\n\n /** 초대된 사용자들에게 협업 초대 알림 발송 (best-effort, capability 있을 때만). */\n private _notifyInvited(\n notify: ((arg: any) => Promise<void>) | undefined,\n userIds: string[],\n boardId: string | undefined,\n inviter: any\n ): void {\n if (typeof notify !== 'function') return\n const payload = buildInviteNotification(userIds, boardId, inviter)\n if (!payload) return\n Promise.resolve(notify(payload)).catch(() => {\n /* 알림 실패는 초대·대화를 막지 않는다 */\n })\n }\n}\n\nfunction resolveAIClient(): AIClient | undefined {\n const existing = getDefaultAIClient()\n if (existing) return existing\n const cfg = config.get('aiClient', null) as any\n if (cfg && cfg.provider) {\n return createAIClient(cfg)\n }\n return undefined\n}\n"]}
1
+ {"version":3,"file":"board-ai-resolver.js","sourceRoot":"","sources":["../../server/service/board-ai-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GraphQL resolver — AI 주도 보드 모델링 (chat).\n *\n * Mutation: boardAIChat — 자연어 채팅. sessionId 가 있으면 메시지/패치 영속.\n * sessionId 가 없으면 ad-hoc 모드로 단발 호출 (영속 없음).\n */\nimport { Resolver, Mutation, Arg, Ctx, Directive, Field, InputType, Int, ObjectType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport '@things-factory/auth-base'\n\nimport { config } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\nimport { createAIClient, getDefaultAIClient, type AIClient } from '@things-factory/ai-client-base'\nimport type { ComponentCategory } from '@things-factory/board-import'\n\nimport { DefaultBoardAIAssistant } from './assistant.js'\nimport type { LLMMessage } from './types.js'\nimport { ChatSession } from './chat-session/chat-session.js'\nimport { ChatMessage } from './chat-message/chat-message.js'\nimport { buildLlmHistory } from './chat-message/llm-history.js'\nimport { activityPatch, autoSessionName } from './chat-session/session-inbox.js'\nimport { publishSessionActivity } from './chat-session/session-activity-publish.js'\nimport { publishChatMessage } from './chat-message/chat-message-publish.js'\nimport { PatchEntry } from './patch-entry/patch-entry.js'\nimport { publishBoardPatch } from './patch-entry/board-patch-subscription.js'\nimport { ChatSessionParticipant } from './chat-session-participant/chat-session-participant.js'\nimport { buildInviteNotification } from './chat-session-participant/invite-notification.js'\nimport {\n injectMentionRefids,\n injectAiMentionRefids,\n stripMentionRefids,\n extractMentionUserIds,\n newParticipantUserIds\n} from './mention-marker.js'\n\n@InputType()\nclass BoardAILLMMessageInput {\n @Field({ description: \"Role: 'user' or 'assistant'\" })\n role: string\n\n @Field({ description: 'Message content' })\n content: string\n}\n\n@InputType()\nclass BoardAIMentionPickInput {\n @Field({ description: 'Token used by user (e.g. \"경광등1\" — the part after #).' })\n token: string\n\n @Field(() => Int, {\n description:\n 'Refid explicitly picked from popup. Server uses this for deterministic resolution — bypasses fallback name/id/type matching for this token.'\n })\n refid: number\n}\n\n@InputType()\nclass BoardAIChatInput {\n @Field({\n nullable: true,\n description: 'ChatSession id. Omit for ad-hoc (no persistence). When given, messages/patches are persisted.'\n })\n sessionId?: string\n\n @Field(() => [BoardAILLMMessageInput], {\n description: 'Conversation history (latest user message at the end). For persisted sessions, only the last user message is appended; older are loaded from DB.'\n })\n messages: BoardAILLMMessageInput[]\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Current BoardModel JSON.' })\n currentBoard?: any\n\n @Field(() => [String], {\n nullable: true,\n description:\n \"Capability scopes the AI is allowed to use (e.g. 'create', 'edit', 'style'). Filters which tools are exposed to the LLM.\"\n })\n scopes?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Host context describing what this surface is looking at (e.g. { spaceId } for a space deliberation, { boardId } for board authoring). Tools read it as ctx.state.host so the model never has to ask the user for identifiers. Client-supplied — never used for authorization; tools verify the scope belongs to the caller tenant.'\n })\n hostContext?: any\n\n @Field({\n nullable: true,\n description:\n 'Id of the last message to keep from the stored history. The client keeps its own truncation when a user edits a message (the server history is intentionally preserved), so pass the last kept message id to hide the retracted span from the agent. Omit to use the whole stored history.'\n })\n truncateAfterMessageId?: string\n\n @Field(() => [String], {\n nullable: true,\n description:\n 'Registered external tool categories this conversation may use (registerToolCategory names, e.g. [\"twin\", \"board-import\"]). Omit to allow every registered category. Applies to tool definitions AND execution, so a surface cannot call a category it was not granted.'\n })\n toolCategories?: string[]\n\n @Field({\n nullable: true,\n description:\n 'Whether core board editing tools (addComponent / styling / scene actions) are available. Defaults to true. Set false for surfaces that must not modify the board — e.g. an operations chat where irreversible commands live, so reversible edits and irreversible actions never share one conversation.'\n })\n boardTools?: boolean\n\n @Field({\n nullable: true,\n description:\n 'Force a tool call on the first turn (toolChoice: required). Turn this on for surfaces that discuss live state: the grounding guard catches invented identifiers but not an invented situation stated without any, which only happens when the model answers without querying at all. Concept / how-to questions are unaffected — they are answered through a documentation tool.'\n })\n requireGroundingTools?: boolean\n\n @Field(() => [String], {\n nullable: true,\n description: 'Component types the LLM is allowed to emit (e.g. [\"rect\", \"label\"]). Constrains generation to the current solution.'\n })\n knownTypes?: string[]\n\n @Field(() => [String], {\n nullable: true,\n description: 'Component categories to surface (e.g. [\"3D\", \"form\"]). Subset filter for groupings exposed in component-schemas.'\n })\n categories?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Component schemas — array of { type, description?, group?, properties? } so the LLM knows valid props per type.'\n })\n componentSchemas?: any\n\n @Field(() => [Int], {\n nullable: true,\n description:\n 'Refids of components currently selected in the modeller (universal numeric handle, things-scene auto-assigned). For \"selected\" / \"선택한\" intent. Note: distinct from `id` which is a data-binding name.'\n })\n selectedRefids?: number[]\n\n @Field(() => [BoardAIMentionPickInput], {\n nullable: true,\n description:\n 'Explicit #mention picks from popup — {token, refid}. User text stays clean (#token only); these picks let server resolve deterministically without exposing refid in the user-visible message. Tokens not in this list fall back to free name/id/type matching.'\n })\n mentions?: BoardAIMentionPickInput[]\n\n @Field({\n nullable: true,\n description: 'AI model identifier override (e.g. \"claude-opus-4-7\"). Falls back to the configured default when omitted.'\n })\n model?: string\n\n @Field({\n nullable: true,\n description: 'Sampling temperature override. Falls back to assistant default when omitted.'\n })\n temperature?: number\n\n @Field({\n nullable: true,\n description: 'Max output tokens override per LLM call. Falls back to assistant default when omitted.'\n })\n maxTokens?: number\n}\n\n@ObjectType()\nclass BoardAIChatOutput {\n @Field({ description: 'Conversational reply.' })\n reply: string\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'BoardEditPatch.' })\n patch?: any\n\n @Field({ nullable: true, description: 'Clarifying question when input is ambiguous.' })\n followUp?: string\n\n @Field({ description: 'AI client identifier (provider:model).' })\n clientId: string\n\n @Field({ nullable: true, description: 'Echo of session id (when persisted).' })\n sessionId?: string\n\n @Field({ nullable: true, description: 'Persisted ChatMessage id of the user input.' })\n userMessageId?: string\n\n @Field({ nullable: true, description: 'Persisted ChatMessage id of the AI reply.' })\n assistantMessageId?: string\n\n @Field({ nullable: true, description: 'Persisted PatchEntry id (when patch was generated).' })\n patchId?: string\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Tool usages collected during agentic loop — sequence of {name, arguments, result, kind}. UI fold-able box for transparency / debug.'\n })\n toolUsages?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Ephemeral scene actions (selection/view/mode) — sequence of BoardActionOp. Applied via board-action-execute event on the host. Distinct from `patch` which carries persistent model changes.'\n })\n actions?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Ungrounded identifiers mentioned by the reply — present in the answer but absent from everything the model received (prompt, board context, history, tool results). Hallucination candidates surfaced to the user; the reply itself is not altered. Null when the answer is grounded.'\n })\n groundingWarnings?: any\n}\n\n@Resolver()\nexport class BoardAIChatResolver {\n /**\n * NOTE: 일부러 @Directive('@transaction') 사용하지 않는다.\n * LLM 호출은 수 초~수십 초 — 트랜잭션 안에서 호출하면 SQLite write lock 을\n * 그 시간 동안 보유 → 다른 작업 (cache 정리 등) 과 SQLITE_BUSY 충돌.\n * 따라서 DB 쓰기를 LLM 호출 전후의 짧은 단위로 나누고 각각 auto-commit.\n * 데이터 일관성: 채팅 이력은 부분 누락보다 부분 보존이 안전 (대화 컨텍스트).\n */\n @Mutation(() => BoardAIChatOutput, {\n description: 'AI 주도 보드 모델링 — 자연어 채팅으로 보드 생성·구조편집·스타일링. sessionId 로 영속 컨텍스트.'\n })\n @Directive('@privilege(category: \"board-ai\", privilege: \"mutation\")')\n async boardAIChat(\n @Arg('input') input: BoardAIChatInput,\n @Ctx() context: ResolverContext\n ): Promise<BoardAIChatOutput> {\n const base = resolveAIClient()\n if (!base) {\n throw new Error(\n 'No AI client configured. Set config.aiClient = { provider: \"anthropic\" | \"gemini\", model, apiKey } or call setDefaultAIClient(...).'\n )\n }\n\n const { domain, user } = context.state\n const sessionRepo = getRepository(ChatSession)\n const messageRepo = getRepository(ChatMessage)\n const patchRepo = getRepository(PatchEntry)\n\n // ── Phase A: LLM 호출 전 짧은 DB 쓰기 ─────────────────────────\n let session: ChatSession | undefined\n if (input.sessionId) {\n const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } })\n if (!found) throw new Error(`ChatSession ${input.sessionId} not found`)\n session = found\n }\n\n const lastUserMsg = input.messages[input.messages.length - 1]\n let userMessageId: string | undefined\n if (session && lastUserMsg && lastUserMsg.role === 'user') {\n // popup pick 의 refid 를 #token{refid:N} inline marker 로 content 에 박아 저장.\n // 단일 source-of-truth — 별도 컬럼 없이 content 안에서 token-refid 매핑 보존.\n const enriched = injectMentionRefids(lastUserMsg.content, input.mentions)\n // parentMessage = 이 세션의 직전 메시지 (보통 assistant / system / 또는 첫 turn 이면 null).\n // tree 구조 인프라 — branching (regenerate / 사용자별 분기) 는 같은 부모 child 로.\n const previousLast = await messageRepo.findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const saved = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'user',\n content: enriched,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n })\n userMessageId = saved.id\n\n /* 세션 활동 갱신(비정규화) — 목록 최근활동순 정렬과 다른 참여자의 안 읽음 판정 기준.\n * 이름이 아직 없으면 **첫 발화로 자동 작명** 한다 — `세션 2` 보다 \"A-DC 포화 원인\" 이 목록에서\n * 훨씬 유용하다. 사용자가 직접 바꾼 이름은 여기서 건드리지 않는다(이름이 있으면 그대로).\n * best-effort: 실패해도 대화를 막지 않는다(목록 정렬이 한 칸 늦을 뿐). */\n try {\n const patch: any = activityPatch(enriched)\n if (!session.name) patch.name = autoSessionName(enriched)\n await sessionRepo.update(session.id!, patch)\n } catch {\n /* noop */\n }\n /* 내가 참여한 세션 활동 방송 — 창을 닫아 둔 다른 참여자의 배지·목록을 위해. */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: enriched,\n actorId: user?.id\n })\n\n // C4: 사용자 메시지를 같은 세션 구독자에게 방송 (그룹챗 실시간). best-effort.\n // 자기 발신 에코는 클라이언트가 message.id 로 dedup.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: saved })\n\n // C4: @user 멘션 → 세션 참여자(member) 자동 등록 (초대). best-effort — 실패해도 chat 흐름 무영향.\n // enriched content 의 {userId:UUID} 마커에서 추출 → 기존 참여자/자기 제외한 신규만 member 로 등록.\n try {\n await this._inviteMentionedUsers(enriched, session.id!, domain.id, user, session.boardId, (context.state as any).notify)\n } catch {\n /* 초대 실패는 대화를 막지 않는다 */\n }\n }\n\n // ── Phase B: LLM 호출 (DB 잠금 없음, 오래 걸려도 OK) ───────────\n const assistant = new DefaultBoardAIAssistant(base, {\n scopes: input.scopes,\n knownTypes: input.knownTypes,\n categories: input.categories as ComponentCategory[] | undefined\n })\n\n /* 에이전트가 볼 이력 — **세션이 있으면 서버 정본(ChatMessage)** 을 쓴다.\n * 클라이언트가 보낸 줄을 쓰면 협의에서 참여자마다 화면이 달라 에이전트가 사람마다 다른 대화를 본다.\n * 발신자 표기·system 기록 포함·접은 구간 제외 규칙은 buildLlmHistory 가 소유(그 파일 머리말 참조).\n * 즉석 대화(sessionId 없음)는 영속 이력이 없으므로 클라이언트 이력을 그대로 쓴다.\n *\n * 정렬은 createdAt ASC + id ASC — 같은 시각 삽입의 순서까지 결정적으로 만든다(조립 쪽에서\n * 다시 정렬하지 않으므로 여기서 확정해야 한다). 멘션 마커는 여기서 제거해 넘긴다. */\n let llmMessages: LLMMessage[]\n if (session) {\n const rows = await messageRepo.find({\n where: { session: { id: session.id } as any },\n relations: { creator: true },\n order: { createdAt: 'ASC', id: 'ASC' }\n })\n llmMessages = buildLlmHistory(\n rows.map(row => ({\n id: row.id,\n role: row.role,\n content: stripMentionRefids(row.content ?? ''),\n senderId: (row as any).creator?.id,\n /* 표시 이름만 — 이메일 등 개인정보는 프롬프트에 넣지 않는다. */\n senderName: (row as any).creator?.name || (row as any).creator?.username\n })),\n {\n truncateAfterMessageId: input.truncateAfterMessageId ?? undefined,\n /* 방금 저장한 사용자 메시지는 접은 구간보다 뒤에 있어도 반드시 남긴다. */\n keepMessageIds: userMessageId ? [userMessageId] : []\n }\n )\n } else {\n llmMessages = input.messages.map(m => ({\n role: m.role === 'assistant' ? 'assistant' : 'user',\n content: stripMentionRefids(m.content)\n }))\n }\n\n const r = await assistant.chat(llmMessages, input.currentBoard ?? undefined, {\n scopes: input.scopes,\n hostContext: input.hostContext ?? undefined,\n /* 대화면별 도구 노출 — 미지정이면 현행(전부 허용) 그대로. */\n toolCategories: input.toolCategories ?? undefined,\n boardTools: input.boardTools ?? undefined,\n requireGroundingTools: input.requireGroundingTools ?? undefined,\n knownTypes: input.knownTypes,\n categories: input.categories as ComponentCategory[] | undefined,\n selectedRefids: input.selectedRefids,\n mentionPicks: input.mentions,\n componentSchemas: Array.isArray(input.componentSchemas) ? input.componentSchemas : undefined,\n model: input.model,\n temperature: input.temperature ?? undefined,\n maxTokens: input.maxTokens ?? undefined,\n // 외부 도메인 (board-import 등) 의 read/external tool builder 가 server-side\n // 의존성 (도메인 / 유저 / 트랜잭션) 을 사용할 수 있도록 ResolverContext.state 전달.\n // board-ai 코어 자체는 이 값을 알지 않고 dispatch 단계에서 closure 로만 흘림.\n toolCallContext: context.state\n })\n\n // ── Phase C: LLM 호출 후 짧은 DB 쓰기 ─────────────────────────\n let assistantMessageId: string | undefined\n let patchId: string | undefined\n if (session) {\n let patchEntry: PatchEntry | undefined\n if (r.patch) {\n patchEntry = await patchRepo.save({\n session: { id: session.id } as any,\n source: 'ai',\n ops: JSON.stringify(r.patch.ops),\n summary: r.patch.summary,\n confidence: r.patch.confidence,\n reverted: false\n })\n patchId = patchEntry.id\n // C1: 같은 board 구독자에게 방송 (실시간 공동편집 spine). best-effort.\n publishBoardPatch({ domainId: domain.id, boardId: session.boardId, patch: patchEntry })\n }\n\n // AI 응답의 #token 들을 currentBoard 에 대해 resolve 한 뒤 inline marker 로 박음.\n // patch 로 새로 만든 컴포넌트는 input.currentBoard 에 없으므로 marker 없이 저장 →\n // 클릭 시 runtime fallback 으로 해석 (그때는 patch 적용 후라 scene 에 존재).\n const enrichedReply = injectAiMentionRefids(r.reply, input.currentBoard ?? null)\n\n const aiMsg = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'assistant',\n content: enrichedReply,\n relatedPatchId: patchEntry?.id,\n toolUsages: r.toolUsages && r.toolUsages.length > 0 ? JSON.stringify(r.toolUsages) : undefined,\n /* 접지 경고를 함께 영속 — 나중에 읽는 참여자도 같은 주의를 봐야 한다(재계산 불가: 근거 원문이 남지 않는다).\n * 컬럼 길이(1024) 안에 들도록 앞 20개까지만 — 그 이상이면 목록이 아니라 답 자체가 문제다. */\n groundingWarnings:\n r.groundingWarnings && r.groundingWarnings.length > 0\n ? JSON.stringify(r.groundingWarnings.slice(0, 20))\n : undefined,\n // assistant 메시지의 creator = 이 응답을 트리거한 user (= 직전 user 메시지의 작성자).\n // 같은 turn 으로 묶임 → 향후 사용자별 필터에서 user/assistant 가 함께 따라옴.\n creator: user,\n updater: user,\n // parentMessage = 이 응답이 답하는 user 메시지. regenerate 시 같은 user 메시지를\n // 부모로 하는 형제 assistant 들이 생기면 자연스럽게 branching tree.\n parentMessage: userMessageId ? ({ id: userMessageId } as any) : undefined\n })\n assistantMessageId = aiMsg.id\n\n // C4: AI 응답도 같은 세션 구독자에게 방송 (트리거한 본인 포함 — id dedup). best-effort.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: aiMsg })\n /* AI 응답도 세션 활동이다 — 이걸 빼면 다른 참여자에게 \"새 소식 없음\" 으로 보인다. */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: enrichedReply,\n actorId: user?.id\n })\n\n await sessionRepo.update(session.id, {\n aiClientId: base.id,\n updater: user,\n /* AI 응답도 세션 활동이다 — 이걸 빼면 다른 참여자에게 \"새 소식 없음\"으로 보인다. */\n ...activityPatch(enrichedReply)\n })\n }\n\n return {\n reply: r.reply,\n patch: r.patch ?? null,\n followUp: r.followUp ?? null,\n clientId: base.id,\n sessionId: session?.id,\n userMessageId,\n assistantMessageId,\n patchId,\n toolUsages: r.toolUsages ?? null,\n actions: r.actions && r.actions.length > 0 ? r.actions : null,\n groundingWarnings: r.groundingWarnings && r.groundingWarnings.length > 0 ? r.groundingWarnings : null\n }\n }\n\n /**\n * C4: 메시지에 @멘션된 사용자를 세션 참여자(member)로 자동 등록 — 초대.\n * enriched content 의 {userId:UUID} 마커에서 추출 → 기존 참여자·자기 제외한 신규만 등록.\n * 소프트삭제(deletedAt)된 과거 참여자는 다시 살린다(재초대). 신규 등록 후 협업 초대 알림 발송(C4b-2, best-effort).\n */\n private async _inviteMentionedUsers(\n content: string,\n sessionId: string,\n domainId: string,\n inviter: any,\n boardId?: string,\n notify?: (arg: any) => Promise<void>\n ): Promise<void> {\n const mentioned = extractMentionUserIds(content)\n if (mentioned.length === 0) return\n\n const repo = getRepository(ChatSessionParticipant)\n const existing = await repo.find({\n where: { session: { id: sessionId } as any, domain: { id: domainId } as any },\n withDeleted: true\n })\n const activeIds = existing.filter(p => !p.deletedAt).map(p => p.userId!).filter(Boolean)\n const fresh = newParticipantUserIds(mentioned, activeIds, inviter?.id)\n if (fresh.length === 0) return\n\n for (const userId of fresh) {\n // 과거에 나갔던(soft-deleted) 참여자면 되살리고, 아니면 새로 등록.\n const prior = existing.find(p => p.userId === userId && p.deletedAt)\n if (prior) {\n await repo.update(prior.id!, { deletedAt: null as any, role: 'member', updater: inviter } as any)\n } else {\n await repo.save({\n domain: { id: domainId } as any,\n session: { id: sessionId } as any,\n user: { id: userId } as any,\n role: 'member',\n creator: inviter,\n updater: inviter\n } as any)\n }\n }\n\n // C4b-2: 초대 알림 — notification 모듈의 기존 발송 메커니즘 재사용(중복 구현 금지).\n // context.state.notify 는 notification-middleware 가 전역 주입(FCM/pubsub). duck-typed →\n // board-ai 에 의존성 추가 없이 런타임 호출. 미로드 환경이면 undefined → graceful skip.\n this._notifyInvited(notify, fresh, boardId, inviter)\n }\n\n /** 초대된 사용자들에게 협업 초대 알림 발송 (best-effort, capability 있을 때만). */\n private _notifyInvited(\n notify: ((arg: any) => Promise<void>) | undefined,\n userIds: string[],\n boardId: string | undefined,\n inviter: any\n ): void {\n if (typeof notify !== 'function') return\n const payload = buildInviteNotification(userIds, boardId, inviter)\n if (!payload) return\n Promise.resolve(notify(payload)).catch(() => {\n /* 알림 실패는 초대·대화를 막지 않는다 */\n })\n }\n}\n\nfunction resolveAIClient(): AIClient | undefined {\n const existing = getDefaultAIClient()\n if (existing) return existing\n const cfg = config.get('aiClient', null) as any\n if (cfg && cfg.provider) {\n return createAIClient(cfg)\n }\n return undefined\n}\n"]}
@@ -21,6 +21,18 @@ export declare class ChatMessage {
21
21
  toolUsages?: string;
22
22
  /** GraphQL 노출용 — JSON 배열로 parse. */
23
23
  get toolUsagesJson(): any;
24
+ /**
25
+ * 접지 경고 — 이 답이 언급했으나 근거(프롬프트·보드 문맥·이력·도구 결과)에 없던 식별자 목록.
26
+ *
27
+ * **왜 영속하는가**: 도크의 협의는 여러 사람이 며칠에 걸쳐 읽는다. 경고가 그 자리에서만
28
+ * 보이고 새로 고치면 사라지면, 나중에 읽는 사람은 검증되지 않은 문장을 검증된 것으로 읽는다.
29
+ * 다시 계산할 수도 없다 — 근거였던 도구 결과 원문은 남지 않고 압축본만 남기 때문이다.
30
+ *
31
+ * 형태: string[] 를 JSON-stringify. 비었으면 null(= 접지 정상).
32
+ */
33
+ groundingWarnings?: string;
34
+ /** GraphQL 노출용 — 식별자 배열로 parse. */
35
+ get groundingWarningsJson(): any;
24
36
  /**
25
37
  * 메시지를 일으킨 사용자.
26
38
  * - role='user' → 보낸 사용자
@@ -30,6 +30,17 @@ let ChatMessage = class ChatMessage {
30
30
  return null;
31
31
  }
32
32
  }
33
+ /** GraphQL 노출용 — 식별자 배열로 parse. */
34
+ get groundingWarningsJson() {
35
+ if (!this.groundingWarnings)
36
+ return null;
37
+ try {
38
+ return JSON.parse(this.groundingWarnings);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
33
44
  };
34
45
  exports.ChatMessage = ChatMessage;
35
46
  tslib_1.__decorate([
@@ -96,6 +107,18 @@ tslib_1.__decorate([
96
107
  tslib_1.__metadata("design:type", Object),
97
108
  tslib_1.__metadata("design:paramtypes", [])
98
109
  ], ChatMessage.prototype, "toolUsagesJson", null);
110
+ tslib_1.__decorate([
111
+ (0, typeorm_1.Column)({ type: 'varchar', length: 1024, nullable: true }),
112
+ tslib_1.__metadata("design:type", String)
113
+ ], ChatMessage.prototype, "groundingWarnings", void 0);
114
+ tslib_1.__decorate([
115
+ (0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, {
116
+ nullable: true,
117
+ description: 'Ungrounded identifiers mentioned by this reply (hallucination candidates). Null when grounded.'
118
+ }),
119
+ tslib_1.__metadata("design:type", Object),
120
+ tslib_1.__metadata("design:paramtypes", [])
121
+ ], ChatMessage.prototype, "groundingWarningsJson", null);
99
122
  tslib_1.__decorate([
100
123
  (0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
101
124
  (0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true, description: 'User who triggered this message.' }),
@@ -1 +1 @@
1
- {"version":3,"file":"chat-message.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;GAOG;AACH,qCAUgB;AAChB,+CAAoD;AACpD,qDAA6C;AAE7C,yDAAgD;AAChD,6CAA4C;AAE5C,qEAA6D;AAE7D,MAAM,SAAS,GAAG,YAAM,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AAC7C,MAAM,aAAa,GAAG,SAAS,CAAC,IAAI,CAAA;AAK7B,IAAM,WAAW,GAAjB,MAAM,WAAW;IA6DtB,oCAAoC;IACpC,IAKI,cAAc;QAChB,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO,IAAI,CAAA;QACjC,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;CA6DF,CAAA;AAvIY,kCAAW;AAGb;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;uCAClB;AAGpB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,6BAAW,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;sCAC3E,6BAAW;4CAAA;AAGrB;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;8CACpC;AAIlB;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,iCAAiC,EAAE,CAAC;;yCAC7C;AAcb;IAZC,IAAA,gBAAM,EAAC;QACN,IAAI,EACF,aAAa,IAAI,OAAO,IAAI,aAAa,IAAI,SAAS;YACpD,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,aAAa,IAAI,QAAQ;gBACzB,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,aAAa,IAAI,OAAO;oBACxB,CAAC,CAAC,UAAU;oBACZ,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,aAAa,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;KACrD,CAAC;IACD,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAC;;4CAC1B;AAIhB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC;;mDAClE;AAIvB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;;2DAC7D;AAwB/B;IAZC,IAAA,gBAAM,EAAC;QACN,IAAI,EACF,aAAa,IAAI,OAAO,IAAI,aAAa,IAAI,SAAS;YACpD,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,aAAa,IAAI,QAAQ;gBACzB,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,aAAa,IAAI,OAAO;oBACxB,CAAC,CAAC,UAAU;oBACZ,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,aAAa,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;QACpD,QAAQ,EAAE,IAAI;KACf,CAAC;;+CACiB;AAGnB;IAAC,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE;QACxB,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,0FAA0F;KAC7F,CAAC;;;iDAQD;AAaD;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,kCAAkC,EAAE,CAAC;sCAC/E,gBAAI;4CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;8CACpC;AAIlB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC;sCAC1G,gBAAI;4CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;8CACpC;AAiBlB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;sCACzD,WAAW;kDAAA;AAQ3B;IANC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC;IAC3D,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,yGAAyG;KAC5G,CAAC;;oDACsB;AAIxB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qBAAqB,EAAE,CAAC;sCAClD,IAAI;8CAAA;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qEAAqE,EAAE,CAAC;sCAClG,IAAI;8CAAA;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,wBAAwB,EAAE,CAAC;sCACrD,IAAI;8CAAA;sBAtIL,WAAW;IAHvB,IAAA,gBAAM,GAAE;IACR,IAAA,eAAK,EAAC,mBAAmB,EAAE,CAAC,OAAoB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1F,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;GAC1D,WAAW,CAuIvB","sourcesContent":["/**\n * ChatMessage — ChatSession 의 한 메시지.\n *\n * role: 'user' | 'assistant' | 'system'\n * 'system' 은 사용자 직접 편집/import 진행 등 시스템이 자동 생성하는 컨텍스트 메시지.\n *\n * relatedPatchId / relatedImportSessionId 로 메시지가 트리거한 패치·임포트와 연결.\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\n\nimport { User } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n@Index('ix_chat_message_1', (message: ChatMessage) => [message.session, message.createdAt])\n@ObjectType({ description: 'A single chat message in a ChatSession.' })\nexport class ChatMessage {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => ChatSession, session => session.messages, { onDelete: 'CASCADE' })\n session?: ChatSession\n\n @RelationId((message: ChatMessage) => message.session)\n sessionId?: string\n\n @Column({ type: 'varchar', length: 16 })\n @Field({ description: \"'user' | 'assistant' | 'system'\" })\n role!: string\n\n @Column({\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n @Field({ description: 'Message content' })\n content!: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'PatchEntry.id this message triggered (if any).' })\n relatedPatchId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'ImportSession.id this message triggered (if any).' })\n relatedImportSessionId?: string\n\n /**\n * AI 가 응답을 만드는 동안 호출한 도구 trace — JSON-stringified.\n *\n * 컬럼은 string (PatchEntry.ops 와 동일 패턴), GraphQL 노출은 toolUsagesJson getter\n * 로 parse 된 객체. UI 의 fold-able 박스 (\"AI 가 이런 도구를 사용했습니다\") 가\n * 페이지 reload / loadHistory 후에도 살아있도록 영속.\n *\n * 형태: ToolUsage[] (assistant.ts 의 시간순 trace, summarizeToolResult 로 압축).\n * null 이면 도구 호출 없었던 메시지 (text-only 응답).\n */\n @Column({\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined,\n nullable: true\n })\n toolUsages?: string\n\n /** GraphQL 노출용 — JSON 배열로 parse. */\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Tool usage trace — array of {name, arguments, result, kind}. Only on assistant messages.'\n })\n get toolUsagesJson(): any {\n if (!this.toolUsages) return null\n try {\n return JSON.parse(this.toolUsages)\n } catch {\n return null\n }\n }\n\n /**\n * 메시지를 일으킨 사용자.\n * - role='user' → 보낸 사용자\n * - role='assistant' → 그 응답을 트리거한 사용자 (직전 user 메시지의 작성자)\n * - role='system' → 시스템 액션을 일으킨 사용자 (예: 직접 편집한 사용자)\n *\n * nullable — legacy 메시지 (이 필드 추가 이전에 작성) 는 null. 또한 ad-hoc 모드 / 시스템\n * 자동 메시지 중 사용자 컨텍스트가 없는 경우도 null 가능.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'User who triggered this message.' })\n creator?: User\n\n @RelationId((message: ChatMessage) => message.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'Last user to update this message (for future edit support).' })\n updater?: User\n\n @RelationId((message: ChatMessage) => message.updater)\n updaterId?: string\n\n /**\n * 부모 메시지 — 이 메시지가 \"응답하는 대상\" / \"이어가는 메시지\" 의 id.\n *\n * 의미 (role 별):\n * - 'user' → 직전 in-session 메시지 (보통 assistant 또는 system). 첫 turn 은 null.\n * - 'assistant' → 이 응답이 답하는 user 메시지. (regenerate 로 같은 user 메시지를 부모로\n * 하는 형제 assistant 메시지가 생기면 자연스럽게 branching tree 가 됨)\n * - 'system' → 직전 in-session 메시지 (timeline 연속성 유지).\n *\n * 이 한 컬럼으로 향후 어떤 흐름이든 표현 가능 — linear chain / branching (regenerate) /\n * tree (replies-to-specific-message) 등. UI/쿼리는 점진 도입.\n *\n * onDelete: 'SET NULL' — 부모 메시지가 삭제돼도 child 는 남되 dangling 마킹.\n */\n @ManyToOne(type => ChatMessage, { nullable: true, onDelete: 'SET NULL' })\n parentMessage?: ChatMessage\n\n @RelationId((message: ChatMessage) => message.parentMessage)\n @Field({\n nullable: true,\n description:\n 'Id of the message this one is in-response-to / continues. Enables linear / branching / tree structures.'\n })\n parentMessageId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Creation timestamp.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Last update timestamp (relevant once message editing is supported).' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'Soft delete timestamp.' })\n deletedAt?: Date\n}\n"]}
1
+ {"version":3,"file":"chat-message.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ChatMessage — ChatSession 의 한 메시지.\n *\n * role: 'user' | 'assistant' | 'system'\n * 'system' 은 사용자 직접 편집/import 진행 등 시스템이 자동 생성하는 컨텍스트 메시지.\n *\n * relatedPatchId / relatedImportSessionId 로 메시지가 트리거한 패치·임포트와 연결.\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\n\nimport { User } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n@Index('ix_chat_message_1', (message: ChatMessage) => [message.session, message.createdAt])\n@ObjectType({ description: 'A single chat message in a ChatSession.' })\nexport class ChatMessage {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => ChatSession, session => session.messages, { onDelete: 'CASCADE' })\n session?: ChatSession\n\n @RelationId((message: ChatMessage) => message.session)\n sessionId?: string\n\n @Column({ type: 'varchar', length: 16 })\n @Field({ description: \"'user' | 'assistant' | 'system'\" })\n role!: string\n\n @Column({\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n @Field({ description: 'Message content' })\n content!: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'PatchEntry.id this message triggered (if any).' })\n relatedPatchId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'ImportSession.id this message triggered (if any).' })\n relatedImportSessionId?: string\n\n /**\n * AI 가 응답을 만드는 동안 호출한 도구 trace — JSON-stringified.\n *\n * 컬럼은 string (PatchEntry.ops 와 동일 패턴), GraphQL 노출은 toolUsagesJson getter\n * 로 parse 된 객체. UI 의 fold-able 박스 (\"AI 가 이런 도구를 사용했습니다\") 가\n * 페이지 reload / loadHistory 후에도 살아있도록 영속.\n *\n * 형태: ToolUsage[] (assistant.ts 의 시간순 trace, summarizeToolResult 로 압축).\n * null 이면 도구 호출 없었던 메시지 (text-only 응답).\n */\n @Column({\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined,\n nullable: true\n })\n toolUsages?: string\n\n /** GraphQL 노출용 — JSON 배열로 parse. */\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Tool usage trace — array of {name, arguments, result, kind}. Only on assistant messages.'\n })\n get toolUsagesJson(): any {\n if (!this.toolUsages) return null\n try {\n return JSON.parse(this.toolUsages)\n } catch {\n return null\n }\n }\n\n /**\n * 접지 경고 — 이 답이 언급했으나 근거(프롬프트·보드 문맥·이력·도구 결과)에 없던 식별자 목록.\n *\n * **왜 영속하는가**: 도크의 협의는 여러 사람이 며칠에 걸쳐 읽는다. 경고가 그 자리에서만\n * 보이고 새로 고치면 사라지면, 나중에 읽는 사람은 검증되지 않은 문장을 검증된 것으로 읽는다.\n * 다시 계산할 수도 없다 — 근거였던 도구 결과 원문은 남지 않고 압축본만 남기 때문이다.\n *\n * 형태: string[] 를 JSON-stringify. 비었으면 null(= 접지 정상).\n */\n @Column({ type: 'varchar', length: 1024, nullable: true })\n groundingWarnings?: string\n\n /** GraphQL 노출용 — 식별자 배열로 parse. */\n @Field(() => GraphQLJSON, {\n nullable: true,\n description: 'Ungrounded identifiers mentioned by this reply (hallucination candidates). Null when grounded.'\n })\n get groundingWarningsJson(): any {\n if (!this.groundingWarnings) return null\n try {\n return JSON.parse(this.groundingWarnings)\n } catch {\n return null\n }\n }\n\n /**\n * 메시지를 일으킨 사용자.\n * - role='user' → 보낸 사용자\n * - role='assistant' → 그 응답을 트리거한 사용자 (직전 user 메시지의 작성자)\n * - role='system' → 시스템 액션을 일으킨 사용자 (예: 직접 편집한 사용자)\n *\n * nullable — legacy 메시지 (이 필드 추가 이전에 작성) 는 null. 또한 ad-hoc 모드 / 시스템\n * 자동 메시지 중 사용자 컨텍스트가 없는 경우도 null 가능.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'User who triggered this message.' })\n creator?: User\n\n @RelationId((message: ChatMessage) => message.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'Last user to update this message (for future edit support).' })\n updater?: User\n\n @RelationId((message: ChatMessage) => message.updater)\n updaterId?: string\n\n /**\n * 부모 메시지 — 이 메시지가 \"응답하는 대상\" / \"이어가는 메시지\" 의 id.\n *\n * 의미 (role 별):\n * - 'user' → 직전 in-session 메시지 (보통 assistant 또는 system). 첫 turn 은 null.\n * - 'assistant' → 이 응답이 답하는 user 메시지. (regenerate 로 같은 user 메시지를 부모로\n * 하는 형제 assistant 메시지가 생기면 자연스럽게 branching tree 가 됨)\n * - 'system' → 직전 in-session 메시지 (timeline 연속성 유지).\n *\n * 이 한 컬럼으로 향후 어떤 흐름이든 표현 가능 — linear chain / branching (regenerate) /\n * tree (replies-to-specific-message) 등. UI/쿼리는 점진 도입.\n *\n * onDelete: 'SET NULL' — 부모 메시지가 삭제돼도 child 는 남되 dangling 마킹.\n */\n @ManyToOne(type => ChatMessage, { nullable: true, onDelete: 'SET NULL' })\n parentMessage?: ChatMessage\n\n @RelationId((message: ChatMessage) => message.parentMessage)\n @Field({\n nullable: true,\n description:\n 'Id of the message this one is in-response-to / continues. Enables linear / branching / tree structures.'\n })\n parentMessageId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Creation timestamp.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Last update timestamp (relevant once message editing is supported).' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'Soft delete timestamp.' })\n deletedAt?: Date\n}\n"]}
@@ -13,12 +13,16 @@ export interface InboxSessionRow {
13
13
  export interface MyParticipation {
14
14
  sessionId?: string;
15
15
  lastReadAt?: Date | string | null;
16
+ /** 내 목록에서 숨긴 시각 — 이후 새 활동이 생기면 다시 보인다. */
17
+ hiddenAt?: Date | string | null;
16
18
  }
17
19
  export interface InboxEntry extends InboxSessionRow {
18
20
  /** 안 읽은 활동이 있는가 — 목록의 점 표시. */
19
21
  unread: boolean;
20
22
  /** 내가 참여자인가 — 아니면 열어볼 수는 있어도 읽음 상태를 갖지 않는다. */
21
23
  participant: boolean;
24
+ /** 내가 숨긴 대화인가 — 목록에서 빠지지만 필터로 볼 수 있다(되돌리기 가능). */
25
+ hidden?: boolean;
22
26
  }
23
27
  /**
24
28
  * 세션 + 내 참여 기록 → 목록 항목. **최근 활동순**(활동 없으면 생성순) 내림차순.
@@ -32,6 +36,28 @@ export interface InboxEntry extends InboxSessionRow {
32
36
  * 잡으면 방금 읽은 화면에 배지가 남는다.
33
37
  */
34
38
  export declare function buildInbox(sessions: InboxSessionRow[], mine: MyParticipation[]): InboxEntry[];
39
+ /**
40
+ * 숨김 표시·필터.
41
+ *
42
+ * 숨김은 **나에게만** 적용되고 되돌릴 수 있다. 숨긴 **뒤에 새 활동이 생기면 다시 보인다** —
43
+ * 아직 참여 중인 대화에서 조용히 잘려 나가는 것이 목록 한 줄보다 나쁘다.
44
+ *
45
+ * `showHidden` 이면 숨긴 것도 함께 돌려주되 표시(hidden)를 남긴다 — 필터를 켠 사용자가 무엇이
46
+ * 숨겨진 것인지 알아야 되돌릴 수 있다.
47
+ */
48
+ export declare function applyHidden(entries: InboxEntry[], hiddenAt: Map<string, Date | string | null | undefined>, showHidden?: boolean): InboxEntry[];
49
+ /**
50
+ * 검색 — 이름과 마지막 미리보기에 대한 대소문자 무시 부분일치.
51
+ *
52
+ * **왜 메모리에서 하는가**: 이 프레임워크는 5개 DB 드라이버를 지원한다. 대소문자 무시 검색은
53
+ * 드라이버마다 표기가 갈리고(ILIKE=pg 전용, LOWER()+LIKE 는 되지만 정렬·NULL 취급이 또 갈린다),
54
+ * 목록 경로에 드라이버별 SQL 을 두는 것은 이 프로젝트의 규율에 반한다. 그래서 조회는 앵커로
55
+ * **상한을 두고** 가져오고(아래 INBOX_SCAN_CAP) 걸러내기·정렬·페이지는 여기서 한다.
56
+ * 상한을 넘으면 잘렸다는 사실을 그대로 알린다(조용히 일부만 보여주지 않는다).
57
+ */
58
+ export declare function searchInbox(entries: InboxEntry[], search?: string): InboxEntry[];
59
+ /** 한 번에 훑는 세션 상한 — 이보다 많으면 잘렸다고 알린다(정직). */
60
+ export declare const INBOX_SCAN_CAP = 500;
35
61
  /** 안 읽은 세션 수 — 도크 버튼 배지(개수가 아니라 **세션 수**). */
36
62
  export declare function countUnreadSessions(entries: InboxEntry[]): number;
37
63
  /**
@@ -16,7 +16,10 @@
16
16
  * 숫자가 필요해지면 화면에 보이는 세션에 한해 따로 세면 된다. 처음부터 N번 세지 않는다.
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.INBOX_SCAN_CAP = void 0;
19
20
  exports.buildInbox = buildInbox;
21
+ exports.applyHidden = applyHidden;
22
+ exports.searchInbox = searchInbox;
20
23
  exports.countUnreadSessions = countUnreadSessions;
21
24
  exports.activityPatch = activityPatch;
22
25
  exports.autoSessionName = autoSessionName;
@@ -59,6 +62,44 @@ function buildInbox(sessions, mine) {
59
62
  });
60
63
  return entries.sort((a, b) => (time(b.lastMessageAt) || time(b.createdAt)) - (time(a.lastMessageAt) || time(a.createdAt)));
61
64
  }
65
+ /**
66
+ * 숨김 표시·필터.
67
+ *
68
+ * 숨김은 **나에게만** 적용되고 되돌릴 수 있다. 숨긴 **뒤에 새 활동이 생기면 다시 보인다** —
69
+ * 아직 참여 중인 대화에서 조용히 잘려 나가는 것이 목록 한 줄보다 나쁘다.
70
+ *
71
+ * `showHidden` 이면 숨긴 것도 함께 돌려주되 표시(hidden)를 남긴다 — 필터를 켠 사용자가 무엇이
72
+ * 숨겨진 것인지 알아야 되돌릴 수 있다.
73
+ */
74
+ function applyHidden(entries, hiddenAt, showHidden = false) {
75
+ const out = [];
76
+ for (const entry of entries) {
77
+ const at = hiddenAt.get(entry.id ?? '');
78
+ /* 숨긴 시각보다 **뒤에** 활동이 있으면 숨김은 풀린 것으로 본다. */
79
+ const hidden = !!at && time(entry.lastMessageAt) <= time(at);
80
+ if (hidden && !showHidden)
81
+ continue;
82
+ out.push(hidden ? { ...entry, hidden: true } : entry);
83
+ }
84
+ return out;
85
+ }
86
+ /**
87
+ * 검색 — 이름과 마지막 미리보기에 대한 대소문자 무시 부분일치.
88
+ *
89
+ * **왜 메모리에서 하는가**: 이 프레임워크는 5개 DB 드라이버를 지원한다. 대소문자 무시 검색은
90
+ * 드라이버마다 표기가 갈리고(ILIKE=pg 전용, LOWER()+LIKE 는 되지만 정렬·NULL 취급이 또 갈린다),
91
+ * 목록 경로에 드라이버별 SQL 을 두는 것은 이 프로젝트의 규율에 반한다. 그래서 조회는 앵커로
92
+ * **상한을 두고** 가져오고(아래 INBOX_SCAN_CAP) 걸러내기·정렬·페이지는 여기서 한다.
93
+ * 상한을 넘으면 잘렸다는 사실을 그대로 알린다(조용히 일부만 보여주지 않는다).
94
+ */
95
+ function searchInbox(entries, search) {
96
+ const q = (search ?? '').trim().toLowerCase();
97
+ if (!q)
98
+ return entries;
99
+ return entries.filter(e => `${e.name ?? ''}\n${e.lastMessagePreview ?? ''}`.toLowerCase().includes(q));
100
+ }
101
+ /** 한 번에 훑는 세션 상한 — 이보다 많으면 잘렸다고 알린다(정직). */
102
+ exports.INBOX_SCAN_CAP = 500;
62
103
  /** 안 읽은 세션 수 — 도크 버튼 배지(개수가 아니라 **세션 수**). */
63
104
  function countUnreadSessions(entries) {
64
105
  return entries.filter(e => e.unread).length;
@@ -1 +1 @@
1
- {"version":3,"file":"session-inbox.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-inbox.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 세션 목록(받은편지함) 순수 로직 — 리졸버와 분리해 단위 테스트 가능.\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 대화가 늘면(가벼운 질문을 승격할 때마다 새 세션) \"어디로 갈지\"를 목록에서 골라야 하고, 다른 참여자가\n * 남긴 새 메시지를 알아야 한다. 세션 목록은 **최근 활동순**이어야 하고 **안 읽음**이 보여야 한다.\n *\n * ── 집계 쿼리를 쓰지 않는다 ─────────────────────────────────────────────────\n * 세션마다 메시지를 집계하면(MAX/COUNT + group by) 목록 한 번에 집계가 붙고, 이 프레임워크는 5개 DB\n * 드라이버를 지원해야 한다. 대신 `ChatSession.lastMessageAt` 을 비정규화해 두고(메시지 저장 시 갱신),\n * 참여자의 `lastReadAt` 과 **값 비교**로 안 읽음을 판정한다 — 조회 두 번(세션·내 참여자)으로 끝난다.\n *\n * ── v1 은 개수가 아니라 \"안 읽음 여부\" ──────────────────────────────────────\n * 정확한 미읽음 **개수**는 세션별 count 가 필요하다(목록 N개면 N번). 목록에서는 점 하나로 충분하고,\n * 숫자가 필요해지면 화면에 보이는 세션에 한해 따로 세면 된다. 처음부터 N번 세지 않는다.\n */\n\n/** 목록에 필요한 세션 필드(엔티티·typeorm 비의존). */\nexport interface InboxSessionRow {\n id?: string\n name?: string\n anchorType?: string\n anchorId?: string\n boardId?: string\n lastMessageAt?: Date | string | null\n lastMessagePreview?: string | null\n createdAt?: Date | string | null\n}\n\n/** 내 참여 기록(읽은 지점). */\nexport interface MyParticipation {\n sessionId?: string\n lastReadAt?: Date | string | null\n}\n\nexport interface InboxEntry extends InboxSessionRow {\n /** 안 읽은 활동이 있는가 — 목록의 점 표시. */\n unread: boolean\n /** 내가 참여자인가 — 아니면 열어볼 수는 있어도 읽음 상태를 갖지 않는다. */\n participant: boolean\n}\n\nfunction time(v: Date | string | null | undefined): number {\n if (!v) return 0\n const t = v instanceof Date ? v.getTime() : Date.parse(v)\n return Number.isFinite(t) ? t : 0\n}\n\n/**\n * 세션 + 내 참여 기록 → 목록 항목. **최근 활동순**(활동 없으면 생성순) 내림차순.\n *\n * 안 읽음 판정: 세션의 마지막 활동이 내가 읽은 지점보다 **뒤**면 안 읽음.\n * · 참여자가 아니면 안 읽음으로 표시하지 않는다(내 것이 아닌 대화에 배지를 띄우지 않는다).\n * · 참여자인데 한 번도 읽지 않았고(lastReadAt 없음) 활동이 있으면 안 읽음이다.\n * · 활동이 없는 세션(빈 대화)은 안 읽음이 아니다.\n *\n * 같은 시각 비교는 **엄격 초과**만 안 읽음으로 본다 — 읽은 직후 같은 밀리초에 저장된 활동을 안 읽음으로\n * 잡으면 방금 읽은 화면에 배지가 남는다.\n */\nexport function buildInbox(sessions: InboxSessionRow[], mine: MyParticipation[]): InboxEntry[] {\n const readBy = new Map<string, number>()\n const isParticipant = new Set<string>()\n for (const p of mine) {\n if (!p.sessionId) continue\n isParticipant.add(p.sessionId)\n readBy.set(p.sessionId, time(p.lastReadAt))\n }\n\n const entries = sessions.map(session => {\n const id = session.id ?? ''\n const activity = time(session.lastMessageAt)\n const participant = isParticipant.has(id)\n const read = readBy.get(id) ?? 0\n return {\n ...session,\n participant,\n unread: participant && activity > 0 && activity > read\n }\n })\n\n return entries.sort(\n (a, b) => (time(b.lastMessageAt) || time(b.createdAt)) - (time(a.lastMessageAt) || time(a.createdAt))\n )\n}\n\n/** 안 읽은 세션 수 — 도크 버튼 배지(개수가 아니라 **세션 수**). */\nexport function countUnreadSessions(entries: InboxEntry[]): number {\n return entries.filter(e => e.unread).length\n}\n\n/**\n * 메시지 저장 시 세션에 반영할 활동 값. 미리보기는 짧게 자르고 개행을 없앤다\n * (목록 한 줄에 들어가야 하고, 컬럼 길이 제한이 있다).\n */\nexport function activityPatch(content: string, at: Date = new Date()): { lastMessageAt: Date; lastMessagePreview: string } {\n const flat = (content ?? '').replace(/\\s+/g, ' ').trim()\n return { lastMessageAt: at, lastMessagePreview: flat.slice(0, 200) }\n}\n\n/**\n * 첫 발화로 세션 이름을 만든다.\n *\n * `세션 2` 같은 순번은 목록에서 아무것도 알려주지 않는다 — \"A-DC 포화 원인\" 이어야 돌아갈 대화를 찾는다.\n * 사용자가 직접 지은 이름은 이 함수로 덮지 않는다(호출부가 이름 없을 때만 부른다).\n *\n * 규칙: 한 줄로 눌러 첫 문장 경계(., ?, !, 개행)까지, 최대 40자. 문장 부호로 끝나면 떼어 낸다\n * (목록 제목에 물음표가 붙으면 길이만 먹는다).\n */\nexport function autoSessionName(content: string, max = 40): string {\n const flat = (content ?? '').replace(/\\s+/g, ' ').trim()\n if (!flat) return ''\n const cut = flat.search(/[.?!\\n]/)\n const head = (cut > 0 ? flat.slice(0, cut) : flat).trim()\n const name = head.length > max ? head.slice(0, max).trim() + '…' : head\n return name\n}\n"]}
1
+ {"version":3,"file":"session-inbox.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-inbox.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 세션 목록(받은편지함) 순수 로직 — 리졸버와 분리해 단위 테스트 가능.\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 대화가 늘면(가벼운 질문을 승격할 때마다 새 세션) \"어디로 갈지\"를 목록에서 골라야 하고, 다른 참여자가\n * 남긴 새 메시지를 알아야 한다. 세션 목록은 **최근 활동순**이어야 하고 **안 읽음**이 보여야 한다.\n *\n * ── 집계 쿼리를 쓰지 않는다 ─────────────────────────────────────────────────\n * 세션마다 메시지를 집계하면(MAX/COUNT + group by) 목록 한 번에 집계가 붙고, 이 프레임워크는 5개 DB\n * 드라이버를 지원해야 한다. 대신 `ChatSession.lastMessageAt` 을 비정규화해 두고(메시지 저장 시 갱신),\n * 참여자의 `lastReadAt` 과 **값 비교**로 안 읽음을 판정한다 — 조회 두 번(세션·내 참여자)으로 끝난다.\n *\n * ── v1 은 개수가 아니라 \"안 읽음 여부\" ──────────────────────────────────────\n * 정확한 미읽음 **개수**는 세션별 count 가 필요하다(목록 N개면 N번). 목록에서는 점 하나로 충분하고,\n * 숫자가 필요해지면 화면에 보이는 세션에 한해 따로 세면 된다. 처음부터 N번 세지 않는다.\n */\n\n/** 목록에 필요한 세션 필드(엔티티·typeorm 비의존). */\nexport interface InboxSessionRow {\n id?: string\n name?: string\n anchorType?: string\n anchorId?: string\n boardId?: string\n lastMessageAt?: Date | string | null\n lastMessagePreview?: string | null\n createdAt?: Date | string | null\n}\n\n/** 내 참여 기록(읽은 지점). */\nexport interface MyParticipation {\n sessionId?: string\n lastReadAt?: Date | string | null\n /** 내 목록에서 숨긴 시각 — 이후 새 활동이 생기면 다시 보인다. */\n hiddenAt?: Date | string | null\n}\n\nexport interface InboxEntry extends InboxSessionRow {\n /** 안 읽은 활동이 있는가 — 목록의 점 표시. */\n unread: boolean\n /** 내가 참여자인가 — 아니면 열어볼 수는 있어도 읽음 상태를 갖지 않는다. */\n participant: boolean\n /** 내가 숨긴 대화인가 — 목록에서 빠지지만 필터로 볼 수 있다(되돌리기 가능). */\n hidden?: boolean\n}\n\nfunction time(v: Date | string | null | undefined): number {\n if (!v) return 0\n const t = v instanceof Date ? v.getTime() : Date.parse(v)\n return Number.isFinite(t) ? t : 0\n}\n\n/**\n * 세션 + 내 참여 기록 → 목록 항목. **최근 활동순**(활동 없으면 생성순) 내림차순.\n *\n * 안 읽음 판정: 세션의 마지막 활동이 내가 읽은 지점보다 **뒤**면 안 읽음.\n * · 참여자가 아니면 안 읽음으로 표시하지 않는다(내 것이 아닌 대화에 배지를 띄우지 않는다).\n * · 참여자인데 한 번도 읽지 않았고(lastReadAt 없음) 활동이 있으면 안 읽음이다.\n * · 활동이 없는 세션(빈 대화)은 안 읽음이 아니다.\n *\n * 같은 시각 비교는 **엄격 초과**만 안 읽음으로 본다 — 읽은 직후 같은 밀리초에 저장된 활동을 안 읽음으로\n * 잡으면 방금 읽은 화면에 배지가 남는다.\n */\nexport function buildInbox(sessions: InboxSessionRow[], mine: MyParticipation[]): InboxEntry[] {\n const readBy = new Map<string, number>()\n const isParticipant = new Set<string>()\n for (const p of mine) {\n if (!p.sessionId) continue\n isParticipant.add(p.sessionId)\n readBy.set(p.sessionId, time(p.lastReadAt))\n }\n\n const entries = sessions.map(session => {\n const id = session.id ?? ''\n const activity = time(session.lastMessageAt)\n const participant = isParticipant.has(id)\n const read = readBy.get(id) ?? 0\n return {\n ...session,\n participant,\n unread: participant && activity > 0 && activity > read\n }\n })\n\n return entries.sort(\n (a, b) => (time(b.lastMessageAt) || time(b.createdAt)) - (time(a.lastMessageAt) || time(a.createdAt))\n )\n}\n\n/**\n * 숨김 표시·필터.\n *\n * 숨김은 **나에게만** 적용되고 되돌릴 수 있다. 숨긴 **뒤에 새 활동이 생기면 다시 보인다** —\n * 아직 참여 중인 대화에서 조용히 잘려 나가는 것이 목록 한 줄보다 나쁘다.\n *\n * `showHidden` 이면 숨긴 것도 함께 돌려주되 표시(hidden)를 남긴다 — 필터를 켠 사용자가 무엇이\n * 숨겨진 것인지 알아야 되돌릴 수 있다.\n */\nexport function applyHidden(entries: InboxEntry[], hiddenAt: Map<string, Date | string | null | undefined>, showHidden = false): InboxEntry[] {\n const out: InboxEntry[] = []\n for (const entry of entries) {\n const at = hiddenAt.get(entry.id ?? '')\n /* 숨긴 시각보다 **뒤에** 활동이 있으면 숨김은 풀린 것으로 본다. */\n const hidden = !!at && time(entry.lastMessageAt) <= time(at)\n if (hidden && !showHidden) continue\n out.push(hidden ? { ...entry, hidden: true } : entry)\n }\n return out\n}\n\n/**\n * 검색 — 이름과 마지막 미리보기에 대한 대소문자 무시 부분일치.\n *\n * **왜 메모리에서 하는가**: 이 프레임워크는 5개 DB 드라이버를 지원한다. 대소문자 무시 검색은\n * 드라이버마다 표기가 갈리고(ILIKE=pg 전용, LOWER()+LIKE 는 되지만 정렬·NULL 취급이 또 갈린다),\n * 목록 경로에 드라이버별 SQL 을 두는 것은 이 프로젝트의 규율에 반한다. 그래서 조회는 앵커로\n * **상한을 두고** 가져오고(아래 INBOX_SCAN_CAP) 걸러내기·정렬·페이지는 여기서 한다.\n * 상한을 넘으면 잘렸다는 사실을 그대로 알린다(조용히 일부만 보여주지 않는다).\n */\nexport function searchInbox(entries: InboxEntry[], search?: string): InboxEntry[] {\n const q = (search ?? '').trim().toLowerCase()\n if (!q) return entries\n return entries.filter(e =>\n `${e.name ?? ''}\\n${e.lastMessagePreview ?? ''}`.toLowerCase().includes(q)\n )\n}\n\n/** 한 번에 훑는 세션 상한 — 이보다 많으면 잘렸다고 알린다(정직). */\nexport const INBOX_SCAN_CAP = 500\n\n/** 안 읽은 세션 수 — 도크 버튼 배지(개수가 아니라 **세션 수**). */\nexport function countUnreadSessions(entries: InboxEntry[]): number {\n return entries.filter(e => e.unread).length\n}\n\n/**\n * 메시지 저장 시 세션에 반영할 활동 값. 미리보기는 짧게 자르고 개행을 없앤다\n * (목록 한 줄에 들어가야 하고, 컬럼 길이 제한이 있다).\n */\nexport function activityPatch(content: string, at: Date = new Date()): { lastMessageAt: Date; lastMessagePreview: string } {\n const flat = (content ?? '').replace(/\\s+/g, ' ').trim()\n return { lastMessageAt: at, lastMessagePreview: flat.slice(0, 200) }\n}\n\n/**\n * 첫 발화로 세션 이름을 만든다.\n *\n * `세션 2` 같은 순번은 목록에서 아무것도 알려주지 않는다 — \"A-DC 포화 원인\" 이어야 돌아갈 대화를 찾는다.\n * 사용자가 직접 지은 이름은 이 함수로 덮지 않는다(호출부가 이름 없을 때만 부른다).\n *\n * 규칙: 한 줄로 눌러 첫 문장 경계(., ?, !, 개행)까지, 최대 40자. 문장 부호로 끝나면 떼어 낸다\n * (목록 제목에 물음표가 붙으면 길이만 먹는다).\n */\nexport function autoSessionName(content: string, max = 40): string {\n const flat = (content ?? '').replace(/\\s+/g, ' ').trim()\n if (!flat) return ''\n const cut = flat.search(/[.?!\\n]/)\n const head = (cut > 0 ? flat.slice(0, cut) : flat).trim()\n const name = head.length > max ? head.slice(0, max).trim() + '…' : head\n return name\n}\n"]}
@@ -28,6 +28,17 @@ export declare class ChatSessionParticipant {
28
28
  * nullable — 한 번도 읽지 않은 참여자(초대 직후)는 세션의 모든 활동이 안 읽음이다.
29
29
  */
30
30
  lastReadAt?: Date;
31
+ /**
32
+ * 이 사용자가 **자기 목록에서 숨긴 시각**. 숨김은 **나에게만** 적용된다 — 남의 목록·대화 내용은 그대로다.
33
+ *
34
+ * 왜 삭제와 나누는가: 협의는 여럿의 기록이므로 지우는 것은 **만든 사람**만 할 수 있다(되돌릴 수 없다).
35
+ * 그 권한이 없는 사람에게도 "내 목록을 정리할" 방법은 있어야 하고, 그것이 숨김이다.
36
+ *
37
+ * 숨긴 **뒤에 새 활동이 생기면 다시 나타난다** — 아직 참여자인 대화에서 조용히 잘려 나가는 것이
38
+ * 목록이 한 줄 늘어나는 것보다 나쁘다. 완전히 빠지려면 대화에서 나가야 한다(별개 기능).
39
+ * nullable — 값이 없으면 숨기지 않은 상태.
40
+ */
41
+ hiddenAt?: Date;
31
42
  creator?: User;
32
43
  creatorId?: string;
33
44
  updater?: User;
@@ -74,8 +74,24 @@ tslib_1.__decorate([
74
74
  tslib_1.__decorate([
75
75
  (0, typeorm_1.Column)({ nullable: true }),
76
76
  (0, type_graphql_1.Field)({ nullable: true, description: 'Point up to which this participant has read the session. Distinct from lastSeenAt (presence): a window can be open without being read, and the read point survives disconnection. Null means nothing read yet.' }),
77
- tslib_1.__metadata("design:type", Date)
77
+ tslib_1.__metadata("design:type", Date
78
+ /**
79
+ * 이 사용자가 **자기 목록에서 숨긴 시각**. 숨김은 **나에게만** 적용된다 — 남의 목록·대화 내용은 그대로다.
80
+ *
81
+ * 왜 삭제와 나누는가: 협의는 여럿의 기록이므로 지우는 것은 **만든 사람**만 할 수 있다(되돌릴 수 없다).
82
+ * 그 권한이 없는 사람에게도 "내 목록을 정리할" 방법은 있어야 하고, 그것이 숨김이다.
83
+ *
84
+ * 숨긴 **뒤에 새 활동이 생기면 다시 나타난다** — 아직 참여자인 대화에서 조용히 잘려 나가는 것이
85
+ * 목록이 한 줄 늘어나는 것보다 나쁘다. 완전히 빠지려면 대화에서 나가야 한다(별개 기능).
86
+ * nullable — 값이 없으면 숨기지 않은 상태.
87
+ */
88
+ )
78
89
  ], ChatSessionParticipant.prototype, "lastReadAt", void 0);
90
+ tslib_1.__decorate([
91
+ (0, typeorm_1.Column)({ nullable: true }),
92
+ (0, type_graphql_1.Field)({ nullable: true, description: 'When this user hid the session from their own list. Per-user only; the session and other participants are unaffected. Activity newer than this timestamp makes it reappear.' }),
93
+ tslib_1.__metadata("design:type", Date)
94
+ ], ChatSessionParticipant.prototype, "hiddenAt", void 0);
79
95
  tslib_1.__decorate([
80
96
  (0, typeorm_1.ManyToOne)(type => auth_base_1.User),
81
97
  (0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true, description: 'User who created this participant record (typically session owner who invited).' }),
@@ -1 +1 @@
1
- {"version":3,"file":"chat-session-participant.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/chat-session-participant.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ChatSessionParticipant — 채팅 세션의 참여 사용자.\n *\n * 한 ChatSession 에 여러 사용자가 참여할 수 있고, 각자 role (owner / member / viewer 등)\n * 을 가짐. 다자간 채팅 / 권한 분화 / \"내 세션 목록\" / 멤버 표시 등의 기반.\n *\n * 현재 visibility 정책 (도메인 내 모두 공개) 은 그대로 유지 — 본 엔티티는 영속/추적\n * 데이터 만들고, \"참여자만 접근\" 같은 enforcement 는 정책 굳을 때 별도 phase 에서 도입.\n *\n * 자동 참여 vs 명시적 초대 정책도 미정 — 일단 세션 creator 만 자동 'owner' 로 등록.\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\n\n@Entity('chat_session_participants')\n@Index('ix_chat_session_participant_0', ['domain', 'session', 'user'], {\n unique: true,\n where: '\"deleted_at\" IS NULL'\n})\n@Index('ix_chat_session_participant_1', ['domain', 'user'], {\n where: '\"deleted_at\" IS NULL'\n})\n@ObjectType({\n description:\n 'Participant of a ChatSession — links a User to a session with a role. Foundation for multi-user chat / per-user filtering / member display.'\n})\nexport class ChatSessionParticipant {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique participant id.' })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Domain to which this participant belongs.' })\n domain?: Domain\n\n @RelationId((p: ChatSessionParticipant) => p.domain)\n domainId?: string\n\n @ManyToOne(type => ChatSession, { onDelete: 'CASCADE' })\n @Field(type => ChatSession, { nullable: true, description: 'The ChatSession this participation belongs to.' })\n session?: ChatSession\n\n @RelationId((p: ChatSessionParticipant) => p.session)\n sessionId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'The user participating in the session.' })\n user?: User\n\n @RelationId((p: ChatSessionParticipant) => p.user)\n userId?: string\n\n /**\n * 참여자의 역할.\n * - 'owner': 세션 만든 사람. rename / delete 권한.\n * - 'member': 일반 참여자. 메시지 송수신.\n * - 'viewer': 읽기 전용 (미래 권한 정책 활성화 시).\n *\n * 시작은 'owner' / 'member' 둘로 충분. 더 세분화 필요해지면 enum 확장.\n */\n @Column({ type: 'varchar', length: 16, default: 'member' })\n @Field({ description: 'Role of this participant: owner | member | viewer.' })\n role!: string\n\n /** 마지막 활동 시각 — presence indicator. nullable. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Last seen / activity timestamp (presence indicator).' })\n lastSeenAt?: Date\n\n /**\n * 이 참여자가 **읽은 지점**. 세션의 마지막 활동 시각과 비교해 안 읽음을 판정한다.\n *\n * `lastSeenAt`(접속 표시)과 다른 개념이다 — 창을 열어 두고 화면을 안 볼 수 있고, 접속이 끊긴 뒤에도\n * 읽은 지점은 남는다. 두 값을 섞어 쓰면 \"봤는데 안 읽음\" 또는 그 반대가 된다.\n * nullable — 한 번도 읽지 않은 참여자(초대 직후)는 세션의 모든 활동이 안 읽음이다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Point up to which this participant has read the session. Distinct from lastSeenAt (presence): a window can be open without being read, and the read point survives disconnection. Null means nothing read yet.' })\n lastReadAt?: Date\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this participant record (typically session owner who invited).' })\n creator?: User\n\n @RelationId((p: ChatSessionParticipant) => p.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'Last user to update this participant record (e.g., role change).' })\n updater?: User\n\n @RelationId((p: ChatSessionParticipant) => p.updater)\n updaterId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Joined-at timestamp.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Last update timestamp (role change / lastSeenAt bump).' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'Soft delete (= left session) timestamp.' })\n deletedAt?: Date\n}\n"]}
1
+ {"version":3,"file":"chat-session-participant.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/chat-session-participant.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ChatSessionParticipant — 채팅 세션의 참여 사용자.\n *\n * 한 ChatSession 에 여러 사용자가 참여할 수 있고, 각자 role (owner / member / viewer 등)\n * 을 가짐. 다자간 채팅 / 권한 분화 / \"내 세션 목록\" / 멤버 표시 등의 기반.\n *\n * 현재 visibility 정책 (도메인 내 모두 공개) 은 그대로 유지 — 본 엔티티는 영속/추적\n * 데이터 만들고, \"참여자만 접근\" 같은 enforcement 는 정책 굳을 때 별도 phase 에서 도입.\n *\n * 자동 참여 vs 명시적 초대 정책도 미정 — 일단 세션 creator 만 자동 'owner' 로 등록.\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\n\n@Entity('chat_session_participants')\n@Index('ix_chat_session_participant_0', ['domain', 'session', 'user'], {\n unique: true,\n where: '\"deleted_at\" IS NULL'\n})\n@Index('ix_chat_session_participant_1', ['domain', 'user'], {\n where: '\"deleted_at\" IS NULL'\n})\n@ObjectType({\n description:\n 'Participant of a ChatSession — links a User to a session with a role. Foundation for multi-user chat / per-user filtering / member display.'\n})\nexport class ChatSessionParticipant {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique participant id.' })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Domain to which this participant belongs.' })\n domain?: Domain\n\n @RelationId((p: ChatSessionParticipant) => p.domain)\n domainId?: string\n\n @ManyToOne(type => ChatSession, { onDelete: 'CASCADE' })\n @Field(type => ChatSession, { nullable: true, description: 'The ChatSession this participation belongs to.' })\n session?: ChatSession\n\n @RelationId((p: ChatSessionParticipant) => p.session)\n sessionId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'The user participating in the session.' })\n user?: User\n\n @RelationId((p: ChatSessionParticipant) => p.user)\n userId?: string\n\n /**\n * 참여자의 역할.\n * - 'owner': 세션 만든 사람. rename / delete 권한.\n * - 'member': 일반 참여자. 메시지 송수신.\n * - 'viewer': 읽기 전용 (미래 권한 정책 활성화 시).\n *\n * 시작은 'owner' / 'member' 둘로 충분. 더 세분화 필요해지면 enum 확장.\n */\n @Column({ type: 'varchar', length: 16, default: 'member' })\n @Field({ description: 'Role of this participant: owner | member | viewer.' })\n role!: string\n\n /** 마지막 활동 시각 — presence indicator. nullable. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Last seen / activity timestamp (presence indicator).' })\n lastSeenAt?: Date\n\n /**\n * 이 참여자가 **읽은 지점**. 세션의 마지막 활동 시각과 비교해 안 읽음을 판정한다.\n *\n * `lastSeenAt`(접속 표시)과 다른 개념이다 — 창을 열어 두고 화면을 안 볼 수 있고, 접속이 끊긴 뒤에도\n * 읽은 지점은 남는다. 두 값을 섞어 쓰면 \"봤는데 안 읽음\" 또는 그 반대가 된다.\n * nullable — 한 번도 읽지 않은 참여자(초대 직후)는 세션의 모든 활동이 안 읽음이다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Point up to which this participant has read the session. Distinct from lastSeenAt (presence): a window can be open without being read, and the read point survives disconnection. Null means nothing read yet.' })\n lastReadAt?: Date\n\n /**\n * 이 사용자가 **자기 목록에서 숨긴 시각**. 숨김은 **나에게만** 적용된다 — 남의 목록·대화 내용은 그대로다.\n *\n * 왜 삭제와 나누는가: 협의는 여럿의 기록이므로 지우는 것은 **만든 사람**만 할 수 있다(되돌릴 수 없다).\n * 그 권한이 없는 사람에게도 \"내 목록을 정리할\" 방법은 있어야 하고, 그것이 숨김이다.\n *\n * 숨긴 **뒤에 새 활동이 생기면 다시 나타난다** — 아직 참여자인 대화에서 조용히 잘려 나가는 것이\n * 목록이 한 줄 늘어나는 것보다 나쁘다. 완전히 빠지려면 대화에서 나가야 한다(별개 기능).\n * nullable — 값이 없으면 숨기지 않은 상태.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When this user hid the session from their own list. Per-user only; the session and other participants are unaffected. Activity newer than this timestamp makes it reappear.' })\n hiddenAt?: Date\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this participant record (typically session owner who invited).' })\n creator?: User\n\n @RelationId((p: ChatSessionParticipant) => p.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'Last user to update this participant record (e.g., role change).' })\n updater?: User\n\n @RelationId((p: ChatSessionParticipant) => p.updater)\n updaterId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Joined-at timestamp.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Last update timestamp (role change / lastSeenAt bump).' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'Soft delete (= left session) timestamp.' })\n deletedAt?: Date\n}\n"]}