@things-factory/ai-assistant 10.1.207 → 10.1.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/components/assistant-chat.ts +238 -9
- package/client/components/assistant-dock.ts +9 -4
- package/client/components/assistant-invite.ts +138 -0
- package/client/components/assistant-session-manager.ts +53 -12
- package/client/components/proposal-request.test.ts +10 -0
- package/client/components/roster-identity.test.ts +2 -12
- package/client/components/roster-identity.ts +0 -6
- package/client/setup-assistant-dock.ts +1 -0
- package/client/utils/assistant-session-actions.ts +3 -1
- package/client/utils/assistant-session-admin.ts +9 -2
- package/client/utils/assistant-session-controller.ts +1 -1
- package/client/utils/assistant-typing.test.ts +33 -0
- package/client/utils/assistant-typing.ts +53 -0
- package/dist-client/components/assistant-chat.d.ts +28 -0
- package/dist-client/components/assistant-chat.js +259 -8
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.js +12 -4
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/assistant-invite.d.ts +39 -0
- package/dist-client/components/assistant-invite.js +150 -0
- package/dist-client/components/assistant-invite.js.map +1 -0
- package/dist-client/components/assistant-session-manager.d.ts +5 -1
- package/dist-client/components/assistant-session-manager.js +40 -10
- package/dist-client/components/assistant-session-manager.js.map +1 -1
- package/dist-client/components/proposal-request.test.js +8 -0
- package/dist-client/components/proposal-request.test.js.map +1 -1
- package/dist-client/components/roster-identity.d.ts +0 -2
- package/dist-client/components/roster-identity.js +0 -7
- package/dist-client/components/roster-identity.js.map +1 -1
- package/dist-client/components/roster-identity.test.js +2 -13
- package/dist-client/components/roster-identity.test.js.map +1 -1
- package/dist-client/setup-assistant-dock.js +1 -0
- package/dist-client/setup-assistant-dock.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-session-actions.d.ts +1 -0
- package/dist-client/utils/assistant-session-actions.js +3 -1
- package/dist-client/utils/assistant-session-actions.js.map +1 -1
- package/dist-client/utils/assistant-session-admin.d.ts +5 -2
- package/dist-client/utils/assistant-session-admin.js +3 -0
- package/dist-client/utils/assistant-session-admin.js.map +1 -1
- package/dist-client/utils/assistant-session-controller.d.ts +1 -1
- package/dist-client/utils/assistant-session-controller.js.map +1 -1
- package/dist-client/utils/assistant-typing.d.ts +20 -0
- package/dist-client/utils/assistant-typing.js +48 -0
- package/dist-client/utils/assistant-typing.js.map +1 -0
- package/dist-client/utils/assistant-typing.test.d.ts +1 -0
- package/dist-client/utils/assistant-typing.test.js +30 -0
- package/dist-client/utils/assistant-typing.test.js.map +1 -0
- package/dist-server/service/assistant-chat-resolver.js +5 -12
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/index.d.ts +1 -1
- package/dist-server/service/assistant-mode/index.js +4 -1
- package/dist-server/service/assistant-mode/index.js.map +1 -1
- package/dist-server/service/assistant-mode/mode-categories.js +9 -2
- package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
- package/dist-server/service/assistant-mode/screen-rules.d.ts +19 -0
- package/dist-server/service/assistant-mode/screen-rules.js +40 -6
- package/dist-server/service/assistant-mode/screen-rules.js.map +1 -1
- package/dist-server/service/chat-message/chat-message-room.js +3 -1
- package/dist-server/service/chat-message/chat-message-room.js.map +1 -1
- package/dist-server/service/chat-message/chat-typing-subscription.d.ts +10 -0
- package/dist-server/service/chat-message/chat-typing-subscription.js +68 -0
- package/dist-server/service/chat-message/chat-typing-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-typing.d.ts +28 -0
- package/dist-server/service/chat-message/chat-typing.js +40 -0
- package/dist-server/service/chat-message/chat-typing.js.map +1 -0
- package/dist-server/service/chat-session/attachment-tool-result.d.ts +1 -0
- package/dist-server/service/chat-session/attachment-tool-result.js +6 -0
- package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -1
- package/dist-server/service/chat-session/attachment-tools.js +2 -2
- package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
- package/dist-server/service/chat-session/chat-session.d.ts +2 -1
- package/dist-server/service/chat-session/chat-session.js +1 -1
- package/dist-server/service/chat-session/chat-session.js.map +1 -1
- package/dist-server/service/chat-session/session-activity-broadcast.d.ts +12 -0
- package/dist-server/service/chat-session/session-activity-broadcast.js +15 -0
- package/dist-server/service/chat-session/session-activity-broadcast.js.map +1 -1
- package/dist-server/service/chat-session/session-activity-publish.d.ts +2 -0
- package/dist-server/service/chat-session/session-activity-publish.js +5 -2
- package/dist-server/service/chat-session/session-activity-publish.js.map +1 -1
- package/dist-server/service/chat-session/session-visibility.d.ts +17 -2
- package/dist-server/service/chat-session/session-visibility.js +28 -3
- package/dist-server/service/chat-session/session-visibility.js.map +1 -1
- package/dist-server/service/chat-session/session-write-gate.d.ts +1 -1
- package/dist-server/service/chat-session/session-write-gate.js +1 -1
- package/dist-server/service/chat-session/session-write-gate.js.map +1 -1
- package/dist-server/service/chat-session-participant/index.d.ts +1 -0
- package/dist-server/service/chat-session-participant/index.js +4 -1
- package/dist-server/service/chat-session-participant/index.js.map +1 -1
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +16 -1
- package/dist-server/service/chat-session-participant/invite-notification.js +34 -5
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -1
- package/dist-server/service/chat-session-participant/mention-invite.d.ts +30 -4
- package/dist-server/service/chat-session-participant/mention-invite.js +78 -31
- package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -1
- package/dist-server/service/chat-session-participant/read-roster.js +5 -2
- package/dist-server/service/chat-session-participant/read-roster.js.map +1 -1
- package/dist-server/service/chat-session-resolver.d.ts +27 -0
- package/dist-server/service/chat-session-resolver.js +226 -25
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/service/data-entry/entry-target.d.ts +19 -0
- package/dist-server/service/data-entry/entry-target.js +20 -5
- package/dist-server/service/data-entry/entry-target.js.map +1 -1
- package/dist-server/service/data-entry/entry-tools.js +2 -2
- package/dist-server/service/data-entry/entry-tools.js.map +1 -1
- package/dist-server/service/data-entry/entry-update.d.ts +2 -1
- package/dist-server/service/data-entry/entry-update.js +5 -3
- package/dist-server/service/data-entry/entry-update.js.map +1 -1
- package/dist-server/service/data-read/list-read.d.ts +10 -0
- package/dist-server/service/data-read/list-read.js +12 -0
- package/dist-server/service/data-read/list-read.js.map +1 -1
- package/dist-server/service/data-read/read-target.d.ts +7 -0
- package/dist-server/service/data-read/read-target.js +2 -2
- package/dist-server/service/data-read/read-target.js.map +1 -1
- package/dist-server/service/data-read/read-tools.js +1 -1
- package/dist-server/service/data-read/read-tools.js.map +1 -1
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +9 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/testing/conversation-script.d.ts +1 -1
- package/dist-server/service/testing/conversation-script.js.map +1 -1
- package/dist-server/service/testing/mode-conformance.js +4 -0
- package/dist-server/service/testing/mode-conformance.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/server/explicit-graphql-types.test.ts +1 -1
- package/server/service/assistant-chat-resolver.ts +7 -12
- package/server/service/assistant-mode/index.ts +1 -1
- package/server/service/assistant-mode/mode-categories.ts +11 -3
- package/server/service/assistant-mode/mode-front.test.ts +53 -1
- package/server/service/assistant-mode/resolve.test.ts +2 -1
- package/server/service/assistant-mode/screen-rules.test.ts +41 -3
- package/server/service/assistant-mode/screen-rules.ts +44 -5
- package/server/service/chat-message/chat-message-room.test.ts +7 -2
- package/server/service/chat-message/chat-message-room.ts +3 -1
- package/server/service/chat-message/chat-typing-subscription.ts +52 -0
- package/server/service/chat-message/chat-typing.test.ts +45 -0
- package/server/service/chat-message/chat-typing.ts +48 -0
- package/server/service/chat-message/fold-history-window.test.ts +1 -1
- package/server/service/chat-session/attachment-tool-result.test.ts +9 -1
- package/server/service/chat-session/attachment-tool-result.ts +7 -0
- package/server/service/chat-session/attachment-tools.ts +3 -3
- package/server/service/chat-session/chat-session.ts +3 -2
- package/server/service/chat-session/group-room.test.ts +233 -0
- package/server/service/chat-session/session-activity-broadcast.ts +20 -0
- package/server/service/chat-session/session-activity-publish.ts +6 -1
- package/server/service/chat-session/session-activity-receivers.test.ts +26 -0
- package/server/service/chat-session/session-visibility.test.ts +32 -1
- package/server/service/chat-session/session-visibility.ts +33 -4
- package/server/service/chat-session/session-write-gate.test.ts +7 -1
- package/server/service/chat-session/session-write-gate.ts +2 -2
- package/server/service/chat-session-participant/index.ts +2 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +36 -1
- package/server/service/chat-session-participant/invite-notification.ts +43 -5
- package/server/service/chat-session-participant/mention-invite.test.ts +49 -1
- package/server/service/chat-session-participant/mention-invite.ts +82 -28
- package/server/service/chat-session-participant/read-roster.test.ts +13 -4
- package/server/service/chat-session-participant/read-roster.ts +6 -3
- package/server/service/chat-session-resolver.ts +204 -27
- package/server/service/data-entry/data-entry-update.test.ts +19 -0
- package/server/service/data-entry/data-entry.test.ts +65 -1
- package/server/service/data-entry/entry-target.ts +36 -5
- package/server/service/data-entry/entry-tools.ts +2 -2
- package/server/service/data-entry/entry-update.ts +6 -3
- package/server/service/data-read/list-missing.test.ts +49 -0
- package/server/service/data-read/list-read.ts +18 -0
- package/server/service/data-read/read-target.ts +10 -3
- package/server/service/data-read/read-tools.ts +1 -1
- package/server/service/data-read/record-keys.test.ts +36 -0
- package/server/service/index.ts +6 -1
- package/server/service/testing/conversation-script.ts +1 -1
- package/server/service/testing/mode-conformance.ts +2 -0
- package/test/assistant-dock.test.ts +5 -1
- package/test/assistant-session-manager.test.ts +4 -2
- package/test/open-doors.test.ts +10 -5
- package/translations/en.json +29 -4
- package/translations/es.json +29 -4
- package/translations/ja.json +29 -4
- package/translations/ko.json +30 -5
- package/translations/ms.json +29 -4
- package/translations/zh.json +29 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-typing-subscription.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-typing-subscription.ts"],"names":[],"mappings":";;;;AAAA;;;GAGG;AACH,+CAAmF;AACnF,+CAA2C;AAC3C,iDAA2F;AAE3F,qEAA6D;AAC7D,yGAAgG;AAChG,2EAAyD;AACzD,iEAAuD;AACvD,qDAA8F;AAGvF,IAAM,UAAU,GAAhB,MAAM,UAAU;CAYtB,CAAA;AAZY,gCAAU;AAErB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,uCAAuC,EAAE,CAAC;;6CAC9C;AAGlB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,qBAAqB,EAAE,CAAC;;0CAC/B;AAGf;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,oCAAoC,EAAE,CAAC;;wCAChD;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,iCAAiC,EAAE,CAAC;;sCAC/C;qBAXA,UAAU;IADtB,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,iHAAiH,EAAE,CAAC;GAClI,UAAU,CAYtB;AAGM,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IAkBjC,UAAU,CAAS,OAA0B,EAAoB,SAAiB;QAChF,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAA;IACrG,CAAC;CACF,CAAA;AArBY,wDAAsB;AAkBjC;IAjBC,IAAA,2BAAY,EAAC,GAAG,EAAE,CAAC,UAAU,EAAE;QAC9B,WAAW,EAAE,4GAA4G;QACzH,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;YAC1B,MAAM,IAAA,oCAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAE1F,0FAA0F;YAC1F,MAAM,IAAI,GAAG,IAAA,qCAAc,EACzB,EAAE,QAAQ,EAAE,IAAA,qBAAa,EAAC,6BAAW,CAAC,EAAE,YAAY,EAAE,IAAA,qBAAa,EAAC,oDAAsB,CAAC,EAAE,EAC7F,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CACrD,CAAA;YACD,IAAI,CAAC,CAAC,MAAM,IAAA,uCAAW,EAAC,IAAI,CAAC,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAA;YAEzE,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,kCAAiB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,iCAAgB,EAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;QAClH,CAAC;KACF,CAAC;IACU,mBAAA,IAAA,mBAAI,GAAE,CAAA;IAA8B,mBAAA,IAAA,kBAAG,EAAC,WAAW,CAAC,CAAA;;;4CAAqB,UAAU;wDAE9F;iCApBU,sBAAsB;IADlC,IAAA,uBAAQ,EAAC,UAAU,CAAC;GACR,sBAAsB,CAqBlC","sourcesContent":["/*\n * ChatTypingSubscription - \"someone is typing\" in a conversation, live. The rules are in `chat-typing.ts`;\n * this file holds only pubsub and the decorators, which jest cannot read.\n */\nimport { Resolver, Subscription, Root, Arg, ObjectType, Field } from 'type-graphql'\nimport { filter, pipe } from 'graphql-yoga'\nimport { assertDomainSubscribeAllowed, getRepository, pubsub } from '@things-factory/shell'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\nimport { ChatSessionParticipant } from '../chat-session-participant/chat-session-participant.js'\nimport { mayHearRoom } from './chat-message-broadcast.js'\nimport { chatRoomLookup } from './chat-message-room.js'\nimport { CHAT_TYPING_TOPIC, chatTypingFilter, type ChatTypingPayload } from './chat-typing.js'\n\n@ObjectType({ description: 'Someone in the conversation is typing. Nothing is stored; the screen lets the notice fade without the next one.' })\nexport class ChatTyping {\n @Field({ description: 'Conversation the person is typing in.' })\n sessionId!: string\n\n @Field({ description: 'User who is typing.' })\n userId!: string\n\n @Field({ description: 'Name the room shows for that user.' })\n name!: string\n\n @Field({ description: 'When the notice was sent (ISO).' })\n at!: string\n}\n\n@Resolver(ChatTyping)\nexport class ChatTypingSubscription {\n @Subscription(() => ChatTyping, {\n description: 'Subscribes to \"someone is typing\" notices of a conversation the caller may read - never the caller\\'s own.',\n subscribe: async ({ args, context }) => {\n const { domain, user } = context.state\n const { sessionId } = args\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n\n /* The same room question the messages ask - refused in the words of a missing session. */\n const room = chatRoomLookup(\n { sessions: getRepository(ChatSession), participants: getRepository(ChatSessionParticipant) },\n { domainId: domain.id, sessionId, userId: user?.id }\n )\n if (!(await mayHearRoom(room))) throw new Error('chat session not found')\n\n return pipe(pubsub.subscribe(CHAT_TYPING_TOPIC), filter(chatTypingFilter(domain.id, sessionId, user?.id, room)))\n }\n })\n chatTyping(@Root() payload: ChatTypingPayload, @Arg('sessionId') sessionId: string): ChatTyping {\n return { sessionId: payload.sessionId, userId: payload.userId, name: payload.name, at: payload.at }\n }\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ChatRoomLookup } from './chat-message-broadcast.js';
|
|
2
|
+
export declare const CHAT_TYPING_TOPIC = "ai-assistant-chat-typing";
|
|
3
|
+
/** How long a notice stands on the screen without the next one. The client sends one every few seconds while typing. */
|
|
4
|
+
export declare const TYPING_NOTICE_MS = 6000;
|
|
5
|
+
export interface ChatTypingPayload {
|
|
6
|
+
domainId: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
/** The name the room shows - the same name a message's sender line shows. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** ISO time the notice was sent. */
|
|
12
|
+
at: string;
|
|
13
|
+
}
|
|
14
|
+
/** The notice a typing person sends - a name is cut to one line, as the room's other names are. */
|
|
15
|
+
export declare function typingNotice(input: {
|
|
16
|
+
domainId: string;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
user: {
|
|
19
|
+
id?: string;
|
|
20
|
+
name?: string | null;
|
|
21
|
+
email?: string | null;
|
|
22
|
+
};
|
|
23
|
+
}, now?: Date): ChatTypingPayload | null;
|
|
24
|
+
/**
|
|
25
|
+
* Passes a notice of the same domain and session, not the listener's own, and only while the room is open to
|
|
26
|
+
* the listener.
|
|
27
|
+
*/
|
|
28
|
+
export declare function chatTypingFilter(domainId: string, sessionId: string, listenerId: string | undefined, room: ChatRoomLookup): (payload: ChatTypingPayload) => Promise<boolean>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TYPING_NOTICE_MS = exports.CHAT_TYPING_TOPIC = void 0;
|
|
4
|
+
exports.typingNotice = typingNotice;
|
|
5
|
+
exports.chatTypingFilter = chatTypingFilter;
|
|
6
|
+
/*
|
|
7
|
+
* "Someone is typing" - the room's live line, pure part (topic, payload, filter). No pubsub, no decorators,
|
|
8
|
+
* so the tests run what the subscription runs (`chat-typing.test.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Nothing is stored. A person typing is not a message, and a line that outlives the typing would lie, so a
|
|
11
|
+
* notice says only who and when, and the screen lets it fade after a few seconds without a new one.
|
|
12
|
+
*
|
|
13
|
+
* Who hears it is the room's question, asked as the messages ask it (`mayHearRoom`): an open room anyone in
|
|
14
|
+
* the domain, a group or private room the people seated in it - at subscribe time and again at every notice,
|
|
15
|
+
* because a member can be sent out while listening. Who may say it is the write gate's question: only a
|
|
16
|
+
* person who may write here is typing here.
|
|
17
|
+
*/
|
|
18
|
+
const chat_message_broadcast_js_1 = require("./chat-message-broadcast.js");
|
|
19
|
+
exports.CHAT_TYPING_TOPIC = 'ai-assistant-chat-typing';
|
|
20
|
+
/** How long a notice stands on the screen without the next one. The client sends one every few seconds while typing. */
|
|
21
|
+
exports.TYPING_NOTICE_MS = 6_000;
|
|
22
|
+
/** The notice a typing person sends - a name is cut to one line, as the room's other names are. */
|
|
23
|
+
function typingNotice(input, now = new Date()) {
|
|
24
|
+
if (!input.user?.id || !input.sessionId)
|
|
25
|
+
return null;
|
|
26
|
+
const name = String(input.user.name || input.user.email || '').replace(/[\r\n\t]+/g, ' ').trim().slice(0, 80);
|
|
27
|
+
return { domainId: input.domainId, sessionId: input.sessionId, userId: input.user.id, name, at: now.toISOString() };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Passes a notice of the same domain and session, not the listener's own, and only while the room is open to
|
|
31
|
+
* the listener.
|
|
32
|
+
*/
|
|
33
|
+
function chatTypingFilter(domainId, sessionId, listenerId, room) {
|
|
34
|
+
return async (payload) => !!payload &&
|
|
35
|
+
payload.domainId === domainId &&
|
|
36
|
+
payload.sessionId === sessionId &&
|
|
37
|
+
payload.userId !== listenerId &&
|
|
38
|
+
(await (0, chat_message_broadcast_js_1.mayHearRoom)(room));
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=chat-typing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-typing.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-typing.ts"],"names":[],"mappings":";;;AA8BA,oCAIC;AAMD,4CAOC;AA/CD;;;;;;;;;;;GAWG;AACH,2EAA8E;AAEjE,QAAA,iBAAiB,GAAG,0BAA0B,CAAA;AAE3D,wHAAwH;AAC3G,QAAA,gBAAgB,GAAG,KAAK,CAAA;AAYrC,mGAAmG;AACnG,SAAgB,YAAY,CAAC,KAAkH,EAAE,GAAG,GAAG,IAAI,IAAI,EAAE;IAC/J,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,IAAI,CAAA;IACpD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC7G,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,CAAA;AACrH,CAAC;AAED;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,QAAgB,EAAE,SAAiB,EAAE,UAA8B,EAAE,IAAoB;IACxH,OAAO,KAAK,EAAE,OAA0B,EAAoB,EAAE,CAC5D,CAAC,CAAC,OAAO;QACT,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC7B,OAAO,CAAC,SAAS,KAAK,SAAS;QAC/B,OAAO,CAAC,MAAM,KAAK,UAAU;QAC7B,CAAC,MAAM,IAAA,uCAAW,EAAC,IAAI,CAAC,CAAC,CAAA;AAC7B,CAAC","sourcesContent":["/*\n * \"Someone is typing\" - the room's live line, pure part (topic, payload, filter). No pubsub, no decorators,\n * so the tests run what the subscription runs (`chat-typing.test.ts`).\n *\n * Nothing is stored. A person typing is not a message, and a line that outlives the typing would lie, so a\n * notice says only who and when, and the screen lets it fade after a few seconds without a new one.\n *\n * Who hears it is the room's question, asked as the messages ask it (`mayHearRoom`): an open room anyone in\n * the domain, a group or private room the people seated in it - at subscribe time and again at every notice,\n * because a member can be sent out while listening. Who may say it is the write gate's question: only a\n * person who may write here is typing here.\n */\nimport { mayHearRoom, type ChatRoomLookup } from './chat-message-broadcast.js'\n\nexport const CHAT_TYPING_TOPIC = 'ai-assistant-chat-typing'\n\n/** How long a notice stands on the screen without the next one. The client sends one every few seconds while typing. */\nexport const TYPING_NOTICE_MS = 6_000\n\nexport interface ChatTypingPayload {\n domainId: string\n sessionId: string\n userId: string\n /** The name the room shows - the same name a message's sender line shows. */\n name: string\n /** ISO time the notice was sent. */\n at: string\n}\n\n/** The notice a typing person sends - a name is cut to one line, as the room's other names are. */\nexport function typingNotice(input: { domainId: string; sessionId: string; user: { id?: string; name?: string | null; email?: string | null } }, now = new Date()): ChatTypingPayload | null {\n if (!input.user?.id || !input.sessionId) return null\n const name = String(input.user.name || input.user.email || '').replace(/[\\r\\n\\t]+/g, ' ').trim().slice(0, 80)\n return { domainId: input.domainId, sessionId: input.sessionId, userId: input.user.id, name, at: now.toISOString() }\n}\n\n/**\n * Passes a notice of the same domain and session, not the listener's own, and only while the room is open to\n * the listener.\n */\nexport function chatTypingFilter(domainId: string, sessionId: string, listenerId: string | undefined, room: ChatRoomLookup) {\n return async (payload: ChatTypingPayload): Promise<boolean> =>\n !!payload &&\n payload.domainId === domainId &&\n payload.sessionId === sessionId &&\n payload.userId !== listenerId &&\n (await mayHearRoom(room))\n}\n"]}
|
|
@@ -26,3 +26,4 @@ export declare function attachmentToolResult(attachmentId: string, file: FileFor
|
|
|
26
26
|
text: string;
|
|
27
27
|
images?: undefined;
|
|
28
28
|
};
|
|
29
|
+
export declare const NO_SUCH_ATTACHMENT = "there is no file with this id in a conversation here - it is not an attachment. Do not ask for it again; a record is read with its own read tool.";
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NO_SUCH_ATTACHMENT = void 0;
|
|
3
4
|
exports.attachmentToolResult = attachmentToolResult;
|
|
4
5
|
function attachmentToolResult(attachmentId, file) {
|
|
5
6
|
return file.type === 'image'
|
|
6
7
|
? { attachmentId, name: file.name, type: 'image', images: [{ data: file.data, mediaType: file.mediaType }], note: 'the picture is attached right after this result - look at it.' }
|
|
7
8
|
: { attachmentId, name: file.name, type: 'text', text: file.text, ...(file.truncated ? { note: 'the file is longer; this is its beginning.' } : {}) };
|
|
8
9
|
}
|
|
10
|
+
/*
|
|
11
|
+
* What an id that names no file of the conversation gets (2026-10-24): given a deposit's id, the model asked for it as a
|
|
12
|
+
* file eight times in one turn. The answer says the id is no file and where a record is read instead.
|
|
13
|
+
*/
|
|
14
|
+
exports.NO_SUCH_ATTACHMENT = 'there is no file with this id in a conversation here - it is not an attachment. Do not ask for it again; a record is read with its own read tool.';
|
|
9
15
|
//# sourceMappingURL=attachment-tool-result.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-tool-result.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tool-result.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"attachment-tool-result.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tool-result.ts"],"names":[],"mappings":";;;AAaA,oDAIC;AAJD,SAAgB,oBAAoB,CAAC,YAAoB,EAAE,IAAkB;IAC3E,OAAO,IAAI,CAAC,IAAI,KAAK,OAAO;QAC1B,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,+DAA+D,EAAE;QACnL,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,4CAA4C,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;AACzJ,CAAC;AAED;;;GAGG;AACU,QAAA,kBAAkB,GAC7B,mJAAmJ,CAAA","sourcesContent":["/*\n * What readChatAttachment answers for a file of the conversation (design/ai/shared-dock.md §9-4).\n *\n * A text file comes back as its text. A picture comes back as the picture: the reserved `images` field is taken out\n * of the JSON by the loop (ai-client-base takeToolImages) and sent as an image part of the same turn - so a receipt\n * sent three turns ago can be read again without asking the person to send it again.\n *\n * Pure, so it is tested without storage or the shell.\n */\nexport type FileForModel =\n | { type: 'image'; data: Buffer; mediaType: string; name: string }\n | { type: 'text'; text: string; name: string; truncated: boolean }\n\nexport function attachmentToolResult(attachmentId: string, file: FileForModel) {\n return file.type === 'image'\n ? { attachmentId, name: file.name, type: 'image', images: [{ data: file.data, mediaType: file.mediaType }], note: 'the picture is attached right after this result - look at it.' }\n : { attachmentId, name: file.name, type: 'text', text: file.text, ...(file.truncated ? { note: 'the file is longer; this is its beginning.' } : {}) }\n}\n\n/*\n * What an id that names no file of the conversation gets (2026-10-24): given a deposit's id, the model asked for it as a\n * file eight times in one turn. The answer says the id is no file and where a record is read instead.\n */\nexport const NO_SUCH_ATTACHMENT =\n 'there is no file with this id in a conversation here - it is not an attachment. Do not ask for it again; a record is read with its own read tool.'\n"]}
|
|
@@ -41,11 +41,11 @@ const readChatAttachment = {
|
|
|
41
41
|
const row = await (0, shell_1.getRepository)(attachment_base_1.Attachment).findOne({ where: { id, domain: { id: domain.id }, refType: session_attachments_js_1.CHAT_ATTACHMENT_REF } });
|
|
42
42
|
const session = row && (await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOne({ where: { id: row.refBy, domain: { id: domain.id } } }));
|
|
43
43
|
if (!row || !session)
|
|
44
|
-
return { absent: 'not-found', attachmentId: id, message:
|
|
44
|
+
return { absent: 'not-found', attachmentId: id, message: attachment_tool_result_js_1.NO_SUCH_ATTACHMENT };
|
|
45
45
|
const participant = await (0, participant_lookup_js_1.isSessionParticipant)(domain.id, session.id, user?.id);
|
|
46
46
|
if (!(0, session_visibility_js_1.mayReadSession)(session.visibility, participant)) {
|
|
47
47
|
/* Same answer as absent - saying "it exists but is private" would itself leak. */
|
|
48
|
-
return { absent: 'not-found', attachmentId: id, message:
|
|
48
|
+
return { absent: 'not-found', attachmentId: id, message: attachment_tool_result_js_1.NO_SUCH_ATTACHMENT };
|
|
49
49
|
}
|
|
50
50
|
const file = await (0, session_attachments_js_1.attachmentForModel)(row, (0, attachment_policy_js_1.textCharsOf)((0, registry_js_1.getAssistantMode)(session.kind || '')?.attachments));
|
|
51
51
|
return (0, attachment_tool_result_js_1.attachmentToolResult)(id, file);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-tools.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tools.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,mEAAuG;AACvG,qEAA4D;AAC5D,iDAAqD;AAErD,uDAA+C;AAC/C,mEAAwD;AACxD,6FAAwF;AACxF,qEAAkF;AAClF,iEAAoD;AACpD,+DAAgE;AAChE,
|
|
1
|
+
{"version":3,"file":"attachment-tools.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tools.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,mEAAuG;AACvG,qEAA4D;AAC5D,iDAAqD;AAErD,uDAA+C;AAC/C,mEAAwD;AACxD,6FAAwF;AACxF,qEAAkF;AAClF,iEAAoD;AACpD,+DAAgE;AAChE,2EAAsF;AAEzE,QAAA,wBAAwB,GAAG,kBAAkB,CAAA;AAE1D,MAAM,kBAAkB,GAAa;IACnC,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,oBAAoB;IAC1B,KAAK,EAAE,CAAC,oBAAoB,CAAC;IAC7B,WAAW,EACT,uHAAuH;QACvH,wHAAwH;IAC1H,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gDAAgD,EAAE,EAAE;QAC/G,QAAQ,EAAE,CAAC,cAAc,CAAC;KAC3B;IACD,OAAO,EAAE,KAAK,EAAE,IAAS,EAAE,OAAY,EAAE,EAAE;QACzC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,KAAK,IAAI,EAAE,CAAA;QAC7C,MAAM,EAAE,GAAG,OAAO,IAAI,EAAE,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QACjF,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAA;QAElG,MAAM,GAAG,GAAQ,MAAM,IAAA,qBAAa,EAAC,4BAAU,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,4CAAmB,EAAS,EAAE,CAAC,CAAA;QAC3I,MAAM,OAAO,GAAQ,GAAG,IAAI,CAAC,MAAM,IAAA,qBAAa,EAAC,6BAAW,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAS,EAAE,CAAC,CAAC,CAAA;QACtI,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,EAAE,8CAAkB,EAAE,CAAA;QAEnG,MAAM,WAAW,GAAG,MAAM,IAAA,4CAAoB,EAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAC/E,IAAI,CAAC,IAAA,sCAAc,EAAC,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC;YACrD,kFAAkF;YAClF,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,EAAE,8CAAkB,EAAE,CAAA;QAC/E,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,IAAA,2CAAkB,EAAC,GAAG,EAAE,IAAA,kCAAW,EAAC,IAAA,8BAAgB,EAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAA;QAC1G,OAAO,IAAA,gDAAoB,EAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IACvC,CAAC;CACU,CAAA;AAEA,QAAA,sBAAsB,GAAiB;IAClD,IAAI,EAAE,gCAAwB;IAC9B,QAAQ,EACN,wLAAwL;IAC1L,KAAK,EAAE,CAAC,kBAAkB,CAAC;CAC5B,CAAA;AAED,IAAA,qCAAoB,EAAC,8BAAsB,CAAC,CAAA","sourcesContent":["/*\n * readChatAttachment - the model opens a file sent in an earlier turn (design/ai/shared-dock.md §9-4).\n *\n * A file reaches the model in full only in the turn it was sent; later turns carry one line naming it.\n * When the model needs it again it asks here - a text file comes back as its text, a picture as the picture itself. The caller must be able to read the conversation the file\n * belongs to - the same gate as reading its messages (ADR-0073 decision 1) - so a file id from elsewhere\n * opens nothing.\n *\n * The category is opened for every mode that declares an attachment policy (resolve.ts).\n */\nimport { registerToolCategory, type ToolCategory, type ToolSpec } from '@things-factory/ai-client-base'\nimport { Attachment } from '@things-factory/attachment-base'\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSession } from './chat-session.js'\nimport { mayReadSession } from './session-visibility.js'\nimport { isSessionParticipant } from '../chat-session-participant/participant-lookup.js'\nimport { attachmentForModel, CHAT_ATTACHMENT_REF } from './session-attachments.js'\nimport { textCharsOf } from './attachment-policy.js'\nimport { getAssistantMode } from '../assistant-mode/registry.js'\nimport { attachmentToolResult, NO_SUCH_ATTACHMENT } from './attachment-tool-result.js'\n\nexport const CHAT_ATTACHMENT_CATEGORY = 'chat-attachments'\n\nconst readChatAttachment: ToolSpec = {\n kind: 'read',\n name: 'readChatAttachment',\n doors: ['Query.chatMessages'],\n description:\n 'Open a file that was sent earlier in this conversation. History names such files as \"[attachment <id>: <name> ...]\"; ' +\n 'pass that id. An image comes back as the picture itself, a text file as its text (cut when long - the answer says so).',\n schema: {\n type: 'object',\n properties: { attachmentId: { type: 'string', description: 'The id from the \"[attachment <id>: ...]\" line.' } },\n required: ['attachmentId']\n },\n builder: async (args: any, context: any) => {\n const { domain, user } = context?.state ?? {}\n const id = typeof args?.attachmentId === 'string' ? args.attachmentId.trim() : ''\n if (!domain?.id || !id) return { absent: 'no-attachment', message: 'no attachment id was given.' }\n\n const row: any = await getRepository(Attachment).findOne({ where: { id, domain: { id: domain.id }, refType: CHAT_ATTACHMENT_REF } as any })\n const session: any = row && (await getRepository(ChatSession).findOne({ where: { id: row.refBy, domain: { id: domain.id } } as any }))\n if (!row || !session) return { absent: 'not-found', attachmentId: id, message: NO_SUCH_ATTACHMENT }\n\n const participant = await isSessionParticipant(domain.id, session.id, user?.id)\n if (!mayReadSession(session.visibility, participant)) {\n /* Same answer as absent - saying \"it exists but is private\" would itself leak. */\n return { absent: 'not-found', attachmentId: id, message: NO_SUCH_ATTACHMENT }\n }\n\n const file = await attachmentForModel(row, textCharsOf(getAssistantMode(session.kind || '')?.attachments))\n return attachmentToolResult(id, file)\n }\n} as ToolSpec\n\nexport const chatAttachmentCategory: ToolCategory = {\n name: CHAT_ATTACHMENT_CATEGORY,\n guidance:\n '- A line \"[attachment <id>: <name> ...]\" names a file sent in an earlier turn. Call readChatAttachment with that id before answering about its contents; never guess what a file says.',\n specs: [readChatAttachment]\n}\n\nregisterToolCategory(chatAttachmentCategory)\n"]}
|
|
@@ -56,7 +56,8 @@ export declare class ChatSession {
|
|
|
56
56
|
* 이 대화를 도메인 안 누구나 읽는가 — ADR-0073 판정 2.
|
|
57
57
|
*
|
|
58
58
|
* `open` 도메인 안이면 누구나 들어와 읽고 말한다. 초대 절차는 없다.
|
|
59
|
-
* `
|
|
59
|
+
* `group` owner 가 들인 사람과 owner 만 읽고 말한다(2026-10-24, ADR-0073 결정 3).
|
|
60
|
+
* `private` owner 혼자. 아무도 앉히지 않고, 그룹을 비공개로 바꾸면 들인 사람이 나간다.
|
|
60
61
|
*
|
|
61
62
|
* 새 세션은 **모두** `private` 으로 시작한다(2026-09-30 — `initialVisibility`). 앵커는 무엇에 대한
|
|
62
63
|
* 이야기인지를 말할 뿐 누구에게 보이는지를 정하지 않는다. 칸의 기본값 `open` 은 칸이 서기 전의 옛 행을
|
|
@@ -110,7 +110,7 @@ tslib_1.__decorate([
|
|
|
110
110
|
], ChatSession.prototype, "kind", void 0);
|
|
111
111
|
tslib_1.__decorate([
|
|
112
112
|
(0, typeorm_1.Column)({ type: 'varchar', length: 16, default: 'open' }),
|
|
113
|
-
(0, type_graphql_1.Field)({ description: "Who may read this conversation: 'open' (anyone in the domain) or 'private' (
|
|
113
|
+
(0, type_graphql_1.Field)({ description: "Who may read this conversation: 'open' (anyone in the domain), 'group' (the owner and the people the owner brought in) or 'private' (the owner alone)." }),
|
|
114
114
|
tslib_1.__metadata("design:type", String)
|
|
115
115
|
], ChatSession.prototype, "visibility", void 0);
|
|
116
116
|
tslib_1.__decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-session.js","sourceRoot":"","sources":["../../../server/service/chat-session/chat-session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ChatSession — Board 와 결합되는 AI 협력 세션.\n *\n * 한 보드에 여러 세션 가능 — DB 레벨 unique 제약 없음.\n *\n * 향후 폭증할 세션 분화 차원 (예: thread / 사용자별 / 컨텍스트별 design vs ops /\n * archived / forked from clone / agent별) 을 미리 차단하지 않기 위해 구조 자체는 1:N\n * 으로 열어둠. 단일 세션 정책 (현재 UX) 은 app 레벨 idempotent 로직 — `startBoardAISession`\n * 이 boardId 매칭 시 기존 세션 반환, `chatSessionByBoard` 가 첫 매칭 반환.\n *\n * 미래에 다중 세션 활성화 시 discriminator 컬럼 (kind / name / threadId / creatorId 등)\n * 을 그때 추가. 마이그레이션 부담 최소화 — 현재는 unique 만 풀어 자리 마련.\n *\n * board-service 의 Board 가 owning side 가 될 예정 (ChatSession FK).\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n OneToMany,\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'\nimport { config } from '@things-factory/env'\n\nimport { ChatMessage } from '../chat-message/chat-message.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n// 인덱스는 조회 성능용으로 유지 (chatSessionByBoard 가 [domain, boardId] 로 자주 쿼리).\n// unique 제약은 의도적으로 제거 — 한 보드에 여러 세션을 미래에 허용하기 위함.\n@Index('ix_chat_session_1', (session: ChatSession) => [session.domain, session.boardId])\n// 앵커 조회용 — chatSessionsByAnchor 가 [domain, anchorType, anchorId] 로 질의.\n@Index('ix_chat_session_2', (session: ChatSession) => [session.domain, session.anchorType, session.anchorId])\n@ObjectType({\n description: 'One AI conversation, anchored to what it is about. An anchor may have several — threads, one per person, one per context.'\n})\nexport class ChatSession {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true })\n domain?: Domain\n\n @RelationId((session: ChatSession) => session.domain)\n domainId?: string\n\n /** Board.id 와 연결. Board 가 owning side 가 될 예정. 1:N — 한 보드에 여러 세션 가능.\n *\n * **앵커 일반화 이후에도 유지되는 하위호환 컬럼**이다(제거하지 말 것). 앵커가 board 인 세션은\n * anchorId 와 함께 이 컬럼에도 같은 값을 쓴다(dual-write) — 기존 조회/인덱스/소비자가 그대로 동작한다. */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n 'Connected Board id. Multiple sessions per board allowed (future: threads / per-user / contexts). Kept for backward compatibility — board-anchored sessions dual-write this alongside anchorId.'\n })\n boardId?: string\n\n /**\n * 대화가 매달린 대상의 **종류**. 세션은 보드에만 매달리지 않는다 —\n * 저작 대화는 보드에, 운영 대화는 공간(사이트)에 매달린다. 앵커가 대화의 성격과 이력 경계를 정한다.\n *\n * 'board' | 'space' | 'instance' | 'none'. 미설정(기존 데이터)은 boardId 가 있으면 board 로 읽는다 —\n * 백필 마이그레이션 없이 lazy 하위호환(컬럼 추가만).\n */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n \"What this conversation is anchored to: 'board' | 'figure' (authoring a Figure) | 'application' | 'space' | 'instance' | 'domain' | 'subject' (one subject in a 360, '<axis>:<itemId>') | 'none'. Absent on legacy rows — treated as 'board' when boardId is present.\"\n })\n anchorType?: string\n\n /** 앵커 대상의 id(보드 id · 공간 id · 인스턴스 id). anchorType 과 함께 의미를 갖는다. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the anchored target (board id / space id / instance id).' })\n anchorId?: string\n\n /**\n * 세션의 사용자 정의 이름 (탭 레이블 / 세션 식별 표시용).\n *\n * nullable — 미설정 시 UI 가 fallback 라벨 (\"세션 N\" 또는 createdAt 기반) 사용. 사용자가\n * 다중 세션 운영 시 의미 부여 (예: \"초기 설계\", \"운영 시뮬레이션\", \"사용자A 의 사적 세션\").\n */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'User-given name of this session (tab label / identification).' })\n name?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this session.' })\n creator?: User\n\n @RelationId((session: ChatSession) => session.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who last updated this session.' })\n updater?: User\n\n @RelationId((session: ChatSession) => session.updater)\n updaterId?: string\n\n /** 토큰 절감용 — 오래된 메시지를 LLM 으로 압축한 요약. Phase 2 에서 작성. */\n /**\n * 마지막 메시지 시각 — **비정규화**. 목록 정렬(최근 활동순)과 안 읽음 판정의 기준.\n *\n * 왜 비정규화인가: 세션마다 메시지를 집계하면(MAX/COUNT + group by) 목록 한 번에 집계 쿼리가 붙고,\n * 이 프레임워크는 5개 DB 드라이버를 지원해야 한다. 세션 컬럼 하나로 두면 정렬·비교가 값싸게 끝나고\n * 드라이버 차이가 없다. 메시지를 저장할 때 함께 갱신한다(activityPatch).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Timestamp of the last message in this session (denormalized for list ordering and unread comparison — avoids per-session aggregation across the supported DB drivers).' })\n lastMessageAt?: Date\n\n /**\n * 어느 대화인가 — `twin.board` · `figure.catalog` 같은 모드의 kind (ADR-0072 결정 3).\n *\n * **서버가 적는다.** 클라이언트가 보내는 값이 아니다 — 등록된 모드에서 나온 것만 적힌다(`addressee`\n * 와 같은 결). 한 대상 위에 대화가 둘 설 수 있게 되면서(A17), 세션이 자기가 어느 대화인지 알아야\n * 한다. 몰랐을 때 화면에서 이렇게 나왔다: 모드를 바꿔도 같은 세션이 그대로 남아, 「도형 찾기」로\n * 바꾼 뒤 보낸 말이 앞의 고치기 대화에 붙었다(2026-09-18, Figure 레인 :3300).\n *\n * 값이 없는 세션은 이 칸이 서기 전에 난 것이다. 다음 턴에 요청한 kind 로 한 번 찍고, 찍힌 뒤에는\n * 「요청한 모드와 다르면 거절」 하나만 남는다. 마이그레이션은 만들지 않는다.\n */\n @Column({ type: 'varchar', length: 64, nullable: true })\n @Field({ nullable: true, description: \"Which conversation this session is — the registered mode kind. Written by the server, never sent by the client.\" })\n kind?: string\n\n /**\n * 이 대화를 도메인 안 누구나 읽는가 — ADR-0073 판정 2.\n *\n * `open` 도메인 안이면 누구나 들어와 읽고 말한다. 초대 절차는 없다.\n * `private` 참여자만 읽는다.\n *\n * 새 세션은 **모두** `private` 으로 시작한다(2026-09-30 — `initialVisibility`). 앵커는 무엇에 대한\n * 이야기인지를 말할 뿐 누구에게 보이는지를 정하지 않는다. 칸의 기본값 `open` 은 칸이 서기 전의 옛 행을\n * 그날까지의 규칙대로 읽기 위한 것이고, 새 행은 늘 값을 적는다.\n *\n * owner 가 양방향으로 바꿀 수 있다. 공개로 바꾸기 전에 화면이 한 줄을 먼저 보이고, 공개 대화는 도크가\n * 다르게 그린다.\n *\n * open 이어도 답 행의 문 판정(ADR-0073 판정 1)은 그대로다 — 방에 들어온 것과 값을 읽는 것은 다르다.\n */\n @Column({ type: 'varchar', length: 16, default: 'open' })\n @Field({ description: \"Who may read this conversation: 'open' (anyone in the domain) or 'private' (participants only).\" })\n visibility!: string\n\n /** 목록에 보여줄 마지막 메시지 미리보기 — 목록에서 메시지 테이블을 다시 읽지 않기 위해 짧게 저장. */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'Short preview of the last message, stored so the session list does not need to query messages.' })\n lastMessagePreview?: string\n\n /**\n * 그 미리보기가 지나온 문 — ADR-0073 판정 1.\n *\n * 목록은 답을 **다시 읽는 두 번째 길**이다. 메시지 화면에서만 가리면 같은 문장이 목록에 그대로 뜬다.\n * 문이 둘이고 한쪽이 느슨하면 느슨한 쪽이 이긴다(ADR-0061 결정 1). 미리보기를 저장할 때 그 행의 문을\n * 함께 옮겨 적고, 목록도 같은 관문을 지난다.\n */\n @Column({ type: 'simple-array', nullable: true })\n @Field(() => [String], { nullable: true, description: 'Doors of the message the preview came from — the session list passes the same gate as the conversation.' })\n lastMessageDoors?: string[]\n\n @Column({\n nullable: true,\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({ nullable: true, description: 'Compressed summary of older messages (for token saving).' })\n lastSummary?: string\n\n /**\n * `lastSummary` 가 **어디까지 덮는지** — 이 메시지까지 요약에 반영됐다.\n *\n * 이게 없으면 누적 요약을 할 수 없다: 매 턴 전체를 다시 요약하거나(비용) 이미 요약한 구간을\n * 또 이어 붙인다(중복). 이력에서 이 id 를 못 찾으면(삭제·편집) 덮은 범위를 알 수 없으므로\n * 전부 미반영으로 보고 다시 요약한다 — 빠뜨리는 쪽보다 안전하다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the last message folded into lastSummary — enables incremental summarization instead of re-reading the whole history.' })\n summaryUpToMessageId?: string\n\n /** 사용 모델 식별 — 'anthropic:claude-sonnet-4-6' 등 */\n @Column({ nullable: true })\n @Field({ nullable: true })\n aiClientId?: string\n\n /** The agent that answered the last turn. Every turn's agent is in the AI usage rows. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'The AI agent that answered the last turn.' })\n agentId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @DeleteDateColumn()\n deletedAt?: Date\n\n // ── 관계 (TypeORM 만, GraphQL 노출은 별도 query 통해 페이징) ──\n @OneToMany(type => ChatMessage, message => message.session)\n messages?: ChatMessage[]\n\n /*\n * ── 보드 패치 관계를 끊었다 (2026-09-10) ──────────────────────────────────\n * 여기 `@OneToMany(PatchEntry)` 가 있었다. 그래서 **대화 세션이 보드 편집기를 알아야** 했고,\n * 세션 계열을 중립 패키지로 옮기려는 순간 `ai-assistant → board-ai → ai-assistant` 순환이\n * 된다.\n *\n * 끊어도 잃는 것이 없다 — 이 관계를 **자료로 읽는 자리가 하나도 없었다**(전 저장소에서\n * `session.patches` 를 읽는 코드 0 건). 유일한 쓰임이 `PatchEntry` 쪽의 역방향 선언 한 줄이었고,\n * TypeORM 은 역방향 없이도 `@ManyToOne` 을 만든다. 패치는 `sessionId` 로 찾는다\n * (`chatPatches` 가 이미 그렇게 한다).\n *\n * 방향은 남는다: **보드 패치가 세션을 안다. 세션은 보드 패치를 모른다.** 종속이 한쪽이면\n * 중립 쪽이 옮겨질 수 있다.\n */\n}\n"]}
|
|
1
|
+
{"version":3,"file":"chat-session.js","sourceRoot":"","sources":["../../../server/service/chat-session/chat-session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ChatSession — Board 와 결합되는 AI 협력 세션.\n *\n * 한 보드에 여러 세션 가능 — DB 레벨 unique 제약 없음.\n *\n * 향후 폭증할 세션 분화 차원 (예: thread / 사용자별 / 컨텍스트별 design vs ops /\n * archived / forked from clone / agent별) 을 미리 차단하지 않기 위해 구조 자체는 1:N\n * 으로 열어둠. 단일 세션 정책 (현재 UX) 은 app 레벨 idempotent 로직 — `startBoardAISession`\n * 이 boardId 매칭 시 기존 세션 반환, `chatSessionByBoard` 가 첫 매칭 반환.\n *\n * 미래에 다중 세션 활성화 시 discriminator 컬럼 (kind / name / threadId / creatorId 등)\n * 을 그때 추가. 마이그레이션 부담 최소화 — 현재는 unique 만 풀어 자리 마련.\n *\n * board-service 의 Board 가 owning side 가 될 예정 (ChatSession FK).\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n OneToMany,\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'\nimport { config } from '@things-factory/env'\n\nimport { ChatMessage } from '../chat-message/chat-message.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n// 인덱스는 조회 성능용으로 유지 (chatSessionByBoard 가 [domain, boardId] 로 자주 쿼리).\n// unique 제약은 의도적으로 제거 — 한 보드에 여러 세션을 미래에 허용하기 위함.\n@Index('ix_chat_session_1', (session: ChatSession) => [session.domain, session.boardId])\n// 앵커 조회용 — chatSessionsByAnchor 가 [domain, anchorType, anchorId] 로 질의.\n@Index('ix_chat_session_2', (session: ChatSession) => [session.domain, session.anchorType, session.anchorId])\n@ObjectType({\n description: 'One AI conversation, anchored to what it is about. An anchor may have several — threads, one per person, one per context.'\n})\nexport class ChatSession {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true })\n domain?: Domain\n\n @RelationId((session: ChatSession) => session.domain)\n domainId?: string\n\n /** Board.id 와 연결. Board 가 owning side 가 될 예정. 1:N — 한 보드에 여러 세션 가능.\n *\n * **앵커 일반화 이후에도 유지되는 하위호환 컬럼**이다(제거하지 말 것). 앵커가 board 인 세션은\n * anchorId 와 함께 이 컬럼에도 같은 값을 쓴다(dual-write) — 기존 조회/인덱스/소비자가 그대로 동작한다. */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n 'Connected Board id. Multiple sessions per board allowed (future: threads / per-user / contexts). Kept for backward compatibility — board-anchored sessions dual-write this alongside anchorId.'\n })\n boardId?: string\n\n /**\n * 대화가 매달린 대상의 **종류**. 세션은 보드에만 매달리지 않는다 —\n * 저작 대화는 보드에, 운영 대화는 공간(사이트)에 매달린다. 앵커가 대화의 성격과 이력 경계를 정한다.\n *\n * 'board' | 'space' | 'instance' | 'none'. 미설정(기존 데이터)은 boardId 가 있으면 board 로 읽는다 —\n * 백필 마이그레이션 없이 lazy 하위호환(컬럼 추가만).\n */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n \"What this conversation is anchored to: 'board' | 'figure' (authoring a Figure) | 'application' | 'space' | 'instance' | 'domain' | 'subject' (one subject in a 360, '<axis>:<itemId>') | 'none'. Absent on legacy rows — treated as 'board' when boardId is present.\"\n })\n anchorType?: string\n\n /** 앵커 대상의 id(보드 id · 공간 id · 인스턴스 id). anchorType 과 함께 의미를 갖는다. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the anchored target (board id / space id / instance id).' })\n anchorId?: string\n\n /**\n * 세션의 사용자 정의 이름 (탭 레이블 / 세션 식별 표시용).\n *\n * nullable — 미설정 시 UI 가 fallback 라벨 (\"세션 N\" 또는 createdAt 기반) 사용. 사용자가\n * 다중 세션 운영 시 의미 부여 (예: \"초기 설계\", \"운영 시뮬레이션\", \"사용자A 의 사적 세션\").\n */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'User-given name of this session (tab label / identification).' })\n name?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this session.' })\n creator?: User\n\n @RelationId((session: ChatSession) => session.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who last updated this session.' })\n updater?: User\n\n @RelationId((session: ChatSession) => session.updater)\n updaterId?: string\n\n /** 토큰 절감용 — 오래된 메시지를 LLM 으로 압축한 요약. Phase 2 에서 작성. */\n /**\n * 마지막 메시지 시각 — **비정규화**. 목록 정렬(최근 활동순)과 안 읽음 판정의 기준.\n *\n * 왜 비정규화인가: 세션마다 메시지를 집계하면(MAX/COUNT + group by) 목록 한 번에 집계 쿼리가 붙고,\n * 이 프레임워크는 5개 DB 드라이버를 지원해야 한다. 세션 컬럼 하나로 두면 정렬·비교가 값싸게 끝나고\n * 드라이버 차이가 없다. 메시지를 저장할 때 함께 갱신한다(activityPatch).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Timestamp of the last message in this session (denormalized for list ordering and unread comparison — avoids per-session aggregation across the supported DB drivers).' })\n lastMessageAt?: Date\n\n /**\n * 어느 대화인가 — `twin.board` · `figure.catalog` 같은 모드의 kind (ADR-0072 결정 3).\n *\n * **서버가 적는다.** 클라이언트가 보내는 값이 아니다 — 등록된 모드에서 나온 것만 적힌다(`addressee`\n * 와 같은 결). 한 대상 위에 대화가 둘 설 수 있게 되면서(A17), 세션이 자기가 어느 대화인지 알아야\n * 한다. 몰랐을 때 화면에서 이렇게 나왔다: 모드를 바꿔도 같은 세션이 그대로 남아, 「도형 찾기」로\n * 바꾼 뒤 보낸 말이 앞의 고치기 대화에 붙었다(2026-09-18, Figure 레인 :3300).\n *\n * 값이 없는 세션은 이 칸이 서기 전에 난 것이다. 다음 턴에 요청한 kind 로 한 번 찍고, 찍힌 뒤에는\n * 「요청한 모드와 다르면 거절」 하나만 남는다. 마이그레이션은 만들지 않는다.\n */\n @Column({ type: 'varchar', length: 64, nullable: true })\n @Field({ nullable: true, description: \"Which conversation this session is — the registered mode kind. Written by the server, never sent by the client.\" })\n kind?: string\n\n /**\n * 이 대화를 도메인 안 누구나 읽는가 — ADR-0073 판정 2.\n *\n * `open` 도메인 안이면 누구나 들어와 읽고 말한다. 초대 절차는 없다.\n * `group` owner 가 들인 사람과 owner 만 읽고 말한다(2026-10-24, ADR-0073 결정 3).\n * `private` owner 혼자. 아무도 앉히지 않고, 그룹을 비공개로 바꾸면 들인 사람이 나간다.\n *\n * 새 세션은 **모두** `private` 으로 시작한다(2026-09-30 — `initialVisibility`). 앵커는 무엇에 대한\n * 이야기인지를 말할 뿐 누구에게 보이는지를 정하지 않는다. 칸의 기본값 `open` 은 칸이 서기 전의 옛 행을\n * 그날까지의 규칙대로 읽기 위한 것이고, 새 행은 늘 값을 적는다.\n *\n * owner 가 양방향으로 바꿀 수 있다. 공개로 바꾸기 전에 화면이 한 줄을 먼저 보이고, 공개 대화는 도크가\n * 다르게 그린다.\n *\n * open 이어도 답 행의 문 판정(ADR-0073 판정 1)은 그대로다 — 방에 들어온 것과 값을 읽는 것은 다르다.\n */\n @Column({ type: 'varchar', length: 16, default: 'open' })\n @Field({ description: \"Who may read this conversation: 'open' (anyone in the domain), 'group' (the owner and the people the owner brought in) or 'private' (the owner alone).\" })\n visibility!: string\n\n /** 목록에 보여줄 마지막 메시지 미리보기 — 목록에서 메시지 테이블을 다시 읽지 않기 위해 짧게 저장. */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'Short preview of the last message, stored so the session list does not need to query messages.' })\n lastMessagePreview?: string\n\n /**\n * 그 미리보기가 지나온 문 — ADR-0073 판정 1.\n *\n * 목록은 답을 **다시 읽는 두 번째 길**이다. 메시지 화면에서만 가리면 같은 문장이 목록에 그대로 뜬다.\n * 문이 둘이고 한쪽이 느슨하면 느슨한 쪽이 이긴다(ADR-0061 결정 1). 미리보기를 저장할 때 그 행의 문을\n * 함께 옮겨 적고, 목록도 같은 관문을 지난다.\n */\n @Column({ type: 'simple-array', nullable: true })\n @Field(() => [String], { nullable: true, description: 'Doors of the message the preview came from — the session list passes the same gate as the conversation.' })\n lastMessageDoors?: string[]\n\n @Column({\n nullable: true,\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({ nullable: true, description: 'Compressed summary of older messages (for token saving).' })\n lastSummary?: string\n\n /**\n * `lastSummary` 가 **어디까지 덮는지** — 이 메시지까지 요약에 반영됐다.\n *\n * 이게 없으면 누적 요약을 할 수 없다: 매 턴 전체를 다시 요약하거나(비용) 이미 요약한 구간을\n * 또 이어 붙인다(중복). 이력에서 이 id 를 못 찾으면(삭제·편집) 덮은 범위를 알 수 없으므로\n * 전부 미반영으로 보고 다시 요약한다 — 빠뜨리는 쪽보다 안전하다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the last message folded into lastSummary — enables incremental summarization instead of re-reading the whole history.' })\n summaryUpToMessageId?: string\n\n /** 사용 모델 식별 — 'anthropic:claude-sonnet-4-6' 등 */\n @Column({ nullable: true })\n @Field({ nullable: true })\n aiClientId?: string\n\n /** The agent that answered the last turn. Every turn's agent is in the AI usage rows. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'The AI agent that answered the last turn.' })\n agentId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @DeleteDateColumn()\n deletedAt?: Date\n\n // ── 관계 (TypeORM 만, GraphQL 노출은 별도 query 통해 페이징) ──\n @OneToMany(type => ChatMessage, message => message.session)\n messages?: ChatMessage[]\n\n /*\n * ── 보드 패치 관계를 끊었다 (2026-09-10) ──────────────────────────────────\n * 여기 `@OneToMany(PatchEntry)` 가 있었다. 그래서 **대화 세션이 보드 편집기를 알아야** 했고,\n * 세션 계열을 중립 패키지로 옮기려는 순간 `ai-assistant → board-ai → ai-assistant` 순환이\n * 된다.\n *\n * 끊어도 잃는 것이 없다 — 이 관계를 **자료로 읽는 자리가 하나도 없었다**(전 저장소에서\n * `session.patches` 를 읽는 코드 0 건). 유일한 쓰임이 `PatchEntry` 쪽의 역방향 선언 한 줄이었고,\n * TypeORM 은 역방향 없이도 `@ManyToOne` 을 만든다. 패치는 `sessionId` 로 찾는다\n * (`chatPatches` 가 이미 그렇게 한다).\n *\n * 방향은 남는다: **보드 패치가 세션을 안다. 세션은 보드 패치를 모른다.** 종속이 한쪽이면\n * 중립 쪽이 옮겨질 수 있다.\n */\n}\n"]}
|
|
@@ -23,6 +23,18 @@ export interface SessionActivityPayload {
|
|
|
23
23
|
/** 이 활동을 만든 사용자 — 자기 발신은 배지를 올리지 않도록 클라가 쓴다. */
|
|
24
24
|
actorId?: string;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Who hears a session's activity - pure, so the rule is tested alone.
|
|
28
|
+
*
|
|
29
|
+
* A `viewer` row only remembers a hide. In an open room its person may read anyway, so they hear (a hidden
|
|
30
|
+
* conversation comes back on new activity). In a group or private room they are outside, and the notice
|
|
31
|
+
* carries a preview - they do not hear it. `alsoTo` is the people who just left, told once so their list
|
|
32
|
+
* drops the conversation.
|
|
33
|
+
*/
|
|
34
|
+
export declare function activityReceivers(rows: ReadonlyArray<{
|
|
35
|
+
userId?: string;
|
|
36
|
+
role?: string;
|
|
37
|
+
}>, visibility: string | null | undefined, alsoTo?: ReadonlyArray<string>): string[];
|
|
26
38
|
/**
|
|
27
39
|
* 같은 도메인 + **내가 참여자인 세션**의 활동만 통과. 도메인 격리가 보안 핵심이고,
|
|
28
40
|
* 참여자 검사가 "내 것만" 을 성립시킨다(참여하지 않은 대화의 활동을 받지 않는다).
|
|
@@ -18,9 +18,24 @@
|
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
exports.SESSION_ACTIVITY_TOPIC = void 0;
|
|
21
|
+
exports.activityReceivers = activityReceivers;
|
|
21
22
|
exports.sessionActivityFilter = sessionActivityFilter;
|
|
23
|
+
const session_visibility_js_1 = require("./session-visibility.js");
|
|
24
|
+
const participant_lookup_roles_js_1 = require("../chat-session-participant/participant-lookup-roles.js");
|
|
22
25
|
/** 세션 활동 방송 토픽. */
|
|
23
26
|
exports.SESSION_ACTIVITY_TOPIC = 'board-ai-session-activity';
|
|
27
|
+
/**
|
|
28
|
+
* Who hears a session's activity - pure, so the rule is tested alone.
|
|
29
|
+
*
|
|
30
|
+
* A `viewer` row only remembers a hide. In an open room its person may read anyway, so they hear (a hidden
|
|
31
|
+
* conversation comes back on new activity). In a group or private room they are outside, and the notice
|
|
32
|
+
* carries a preview - they do not hear it. `alsoTo` is the people who just left, told once so their list
|
|
33
|
+
* drops the conversation.
|
|
34
|
+
*/
|
|
35
|
+
function activityReceivers(rows, visibility, alsoTo = []) {
|
|
36
|
+
const heard = rows.filter(row => (0, session_visibility_js_1.isOpenRoom)(visibility) || (0, participant_lookup_roles_js_1.isRoomRole)(row.role)).map(row => row.userId);
|
|
37
|
+
return [...new Set([...heard, ...alsoTo])].filter((id) => !!id);
|
|
38
|
+
}
|
|
24
39
|
/**
|
|
25
40
|
* 같은 도메인 + **내가 참여자인 세션**의 활동만 통과. 도메인 격리가 보안 핵심이고,
|
|
26
41
|
* 참여자 검사가 "내 것만" 을 성립시킨다(참여하지 않은 대화의 활동을 받지 않는다).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-activity-broadcast.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-activity-broadcast.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;
|
|
1
|
+
{"version":3,"file":"session-activity-broadcast.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-activity-broadcast.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;AAyCH,8CAOC;AAMD,sDAMC;AA1DD,mEAAoD;AACpD,yGAAoF;AAEpF,mBAAmB;AACN,QAAA,sBAAsB,GAAG,2BAA2B,CAAA;AA2BjE;;;;;;;GAOG;AACH,SAAgB,iBAAiB,CAC/B,IAAuD,EACvD,UAAqC,EACrC,SAAgC,EAAE;IAElC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,kCAAU,EAAC,UAAU,CAAC,IAAI,IAAA,wCAAU,EAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACvG,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,KAAK,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;AAC/E,CAAC;AAED;;;GAGG;AACH,SAAgB,qBAAqB,CAAC,QAAgB,EAAE,MAAc;IACpE,OAAO,CAAC,OAA+B,EAAW,EAAE,CAClD,CAAC,CAAC,OAAO;QACT,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC7B,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC;QACrC,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;AAC3C,CAAC","sourcesContent":["/*\n * 세션 활동 방송 — 순수 로직(토픽·페이로드·필터). 데코레이터·pubsub import 없음 → 단위 테스트 가능.\n *\n * ── 왜 메시지 방송(chat-message)과 따로 두는가 ──────────────────────────────\n * 기존 `chatMessageActivity` 는 **그 세션을 열어 둔** 구독자에게 메시지 본문을 보낸다. 그래서 창을 닫아\n * 두면 새 메시지를 알 수 없다 — 초대 알림 한 번 받고 나면 이후 대화는 사용자가 스스로 들어와 봐야 안다.\n *\n * 이 토픽은 **내가 참여한 모든 세션**의 활동을 알린다. 목적이 배지·목록 갱신이므로 본문을 싣지 않고\n * 최소 정보(세션·앵커·시각·미리보기)만 보낸다:\n * · 열지도 않은 세션들의 본문이 한 채널로 쏟아지는 것을 피한다(대역·주의 낭비).\n * · 본문이 필요한 시점은 그 세션을 열 때이고, 그때는 기존 메시지 구독이 담당한다.\n *\n * ── 구독자 판정을 필터에서 DB 로 하지 않는다 ────────────────────────────────\n * 필터는 **구독자 × 메시지마다** 돈다. 여기서 DB 를 읽으면 방송 한 번이 구독자 수만큼의 질의가 된다.\n * 그래서 발행 시점에 참여자 id 목록을 페이로드에 실어 보내고, 필터는 배열 포함 검사만 한다.\n * 참여자 조회는 발행당 1회 — 사람 대화 빈도에서 감당 가능한 비용이다.\n */\n\nimport { isOpenRoom } from './session-visibility.js'\nimport { isRoomRole } from '../chat-session-participant/participant-lookup-roles.js'\n\n/** 세션 활동 방송 토픽. */\nexport const SESSION_ACTIVITY_TOPIC = 'board-ai-session-activity'\n\n/** 방송 페이로드 — 필터용 메타(도메인·참여자) + 목록 갱신용 최소 정보. 본문은 싣지 않는다. */\n/**\n * 활동 종류.\n * message — 새 메시지. **안 읽음 배지를 올린다.**\n * meta — 이름 변경·참여자 변동 등 세션 자체의 변화. 목록은 갱신하지만 배지는 올리지 않는다.\n * (이름이 바뀐 것을 \"새 소식\" 으로 알리면 소음이 된다.)\n */\nexport type SessionActivityKind = 'message' | 'meta'\n\nexport interface SessionActivityPayload {\n domainId: string\n sessionId: string\n kind: SessionActivityKind\n /** 이 세션 참여자 user id — 필터가 이 배열로 수신자를 정한다(필터에서 DB 를 읽지 않기 위함). */\n participantIds: string[]\n anchorType?: string\n anchorId?: string\n /** 활동 시각(ISO) — 클라가 목록 정렬·안 읽음 판정에 쓴다. */\n at: string\n /** 한 줄 미리보기(짧게). 본문 전체가 아니다. */\n preview?: string\n /** 이 활동을 만든 사용자 — 자기 발신은 배지를 올리지 않도록 클라가 쓴다. */\n actorId?: string\n}\n\n/**\n * Who hears a session's activity - pure, so the rule is tested alone.\n *\n * A `viewer` row only remembers a hide. In an open room its person may read anyway, so they hear (a hidden\n * conversation comes back on new activity). In a group or private room they are outside, and the notice\n * carries a preview - they do not hear it. `alsoTo` is the people who just left, told once so their list\n * drops the conversation.\n */\nexport function activityReceivers(\n rows: ReadonlyArray<{ userId?: string; role?: string }>,\n visibility: string | null | undefined,\n alsoTo: ReadonlyArray<string> = []\n): string[] {\n const heard = rows.filter(row => isOpenRoom(visibility) || isRoomRole(row.role)).map(row => row.userId)\n return [...new Set([...heard, ...alsoTo])].filter((id): id is string => !!id)\n}\n\n/**\n * 같은 도메인 + **내가 참여자인 세션**의 활동만 통과. 도메인 격리가 보안 핵심이고,\n * 참여자 검사가 \"내 것만\" 을 성립시킨다(참여하지 않은 대화의 활동을 받지 않는다).\n */\nexport function sessionActivityFilter(domainId: string, userId: string) {\n return (payload: SessionActivityPayload): boolean =>\n !!payload &&\n payload.domainId === domainId &&\n Array.isArray(payload.participantIds) &&\n payload.participantIds.includes(userId)\n}\n"]}
|
|
@@ -11,24 +11,27 @@ exports.publishSessionActivity = publishSessionActivity;
|
|
|
11
11
|
*/
|
|
12
12
|
const shell_1 = require("@things-factory/shell");
|
|
13
13
|
const chat_session_participant_js_1 = require("../chat-session-participant/chat-session-participant.js");
|
|
14
|
+
const chat_session_js_1 = require("./chat-session.js");
|
|
14
15
|
const session_activity_broadcast_js_1 = require("./session-activity-broadcast.js");
|
|
16
|
+
const session_activity_broadcast_js_2 = require("./session-activity-broadcast.js");
|
|
15
17
|
async function publishSessionActivity(input) {
|
|
16
18
|
try {
|
|
17
19
|
const participants = await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).find({
|
|
18
20
|
where: { domain: { id: input.domainId }, session: { id: input.sessionId } }
|
|
19
21
|
});
|
|
22
|
+
const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOne({ where: { id: input.sessionId, domain: { id: input.domainId } } });
|
|
20
23
|
const payload = {
|
|
21
24
|
domainId: input.domainId,
|
|
22
25
|
sessionId: input.sessionId,
|
|
23
26
|
kind: input.kind ?? 'message',
|
|
24
|
-
participantIds:
|
|
27
|
+
participantIds: (0, session_activity_broadcast_js_1.activityReceivers)(participants, session?.visibility, input.alsoTo),
|
|
25
28
|
anchorType: input.anchorType,
|
|
26
29
|
anchorId: input.anchorId,
|
|
27
30
|
at: (input.at ?? new Date()).toISOString(),
|
|
28
31
|
preview: input.preview,
|
|
29
32
|
actorId: input.actorId
|
|
30
33
|
};
|
|
31
|
-
shell_1.pubsub.publish(
|
|
34
|
+
shell_1.pubsub.publish(session_activity_broadcast_js_2.SESSION_ACTIVITY_TOPIC, payload);
|
|
32
35
|
}
|
|
33
36
|
catch {
|
|
34
37
|
/* 방송 실패는 대화를 막지 않는다 */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-activity-publish.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-activity-publish.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"session-activity-publish.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-activity-publish.ts"],"names":[],"mappings":";;AAeA,wDAiCC;AAhDD;;;;;;;GAOG;AACH,iDAA6D;AAE7D,yGAAgG;AAChG,uDAA+C;AAC/C,mFAAmE;AACnE,mFAA+H;AAExH,KAAK,UAAU,sBAAsB,CAAC,KAY5C;IACC,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,MAAM,IAAA,qBAAa,EAAC,oDAAsB,CAAC,CAAC,IAAI,CAAC;YACpE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAS,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,SAAS,EAAS,EAAE;SAC1F,CAAC,CAAA;QACF,MAAM,OAAO,GAAG,MAAM,IAAA,qBAAa,EAAC,6BAAW,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAS,EAAE,CAAC,CAAA;QACnI,MAAM,OAAO,GAA2B;YACtC,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,SAAS;YAC7B,cAAc,EAAE,IAAA,iDAAiB,EAAC,YAAmB,EAAG,OAAe,EAAE,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC;YAClG,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE;YAC1C,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB,CAAA;QACD,cAAM,CAAC,OAAO,CAAC,sDAAsB,EAAE,OAAO,CAAC,CAAA;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,uBAAuB;IACzB,CAAC;AACH,CAAC","sourcesContent":["/*\n * 세션 활동 발행 — pubsub 의존만(데코레이터 없음). 순수 로직은 session-activity-broadcast.ts.\n *\n * 참여자 목록을 여기서 읽어 페이로드에 싣는다 — 필터는 구독자 × 메시지마다 돌기 때문에 거기서 DB 를\n * 읽으면 방송 한 번이 구독자 수만큼의 질의가 된다. 발행당 1회로 끝낸다.\n *\n * best-effort: 실패해도 대화·저장을 막지 않는다(배지가 한 번 늦을 뿐).\n */\nimport { pubsub, getRepository } from '@things-factory/shell'\n\nimport { ChatSessionParticipant } from '../chat-session-participant/chat-session-participant.js'\nimport { ChatSession } from './chat-session.js'\nimport { activityReceivers } from './session-activity-broadcast.js'\nimport { SESSION_ACTIVITY_TOPIC, type SessionActivityPayload, type SessionActivityKind } from './session-activity-broadcast.js'\n\nexport async function publishSessionActivity(input: {\n domainId: string\n sessionId: string\n /** message = 새 메시지(배지 올림) · meta = 이름 변경 등(목록만 갱신). 기본 message. */\n kind?: SessionActivityKind\n anchorType?: string\n anchorId?: string\n at?: Date\n preview?: string\n actorId?: string\n /** People who just left the room - told once more, so their list drops it. They are no longer seated. */\n alsoTo?: string[]\n}): Promise<void> {\n try {\n const participants = await getRepository(ChatSessionParticipant).find({\n where: { domain: { id: input.domainId } as any, session: { id: input.sessionId } as any }\n })\n const session = await getRepository(ChatSession).findOne({ where: { id: input.sessionId, domain: { id: input.domainId } } as any })\n const payload: SessionActivityPayload = {\n domainId: input.domainId,\n sessionId: input.sessionId,\n kind: input.kind ?? 'message',\n participantIds: activityReceivers(participants as any, (session as any)?.visibility, input.alsoTo),\n anchorType: input.anchorType,\n anchorId: input.anchorId,\n at: (input.at ?? new Date()).toISOString(),\n preview: input.preview,\n actorId: input.actorId\n }\n pubsub.publish(SESSION_ACTIVITY_TOPIC, payload)\n } catch {\n /* 방송 실패는 대화를 막지 않는다 */\n }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SessionVisibility = 'open' | 'private';
|
|
1
|
+
export type SessionVisibility = 'open' | 'group' | 'private';
|
|
2
2
|
export declare const SESSION_VISIBILITIES: readonly SessionVisibility[];
|
|
3
3
|
/**
|
|
4
4
|
* 이렇게 만들어진 세션은 무엇으로 시작하나 — **언제나 비공개.**
|
|
@@ -14,8 +14,23 @@ export declare function initialVisibility(_session?: {
|
|
|
14
14
|
anchorType?: string | null;
|
|
15
15
|
copyOf?: string | null;
|
|
16
16
|
}): SessionVisibility;
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Is the room open to everyone in the domain - so no seat needs looking up?
|
|
19
|
+
*
|
|
20
|
+
* Doors skip the participant lookup for an open room. They asked `visibility === 'private'` for it, and a
|
|
21
|
+
* group room is neither: asked that way, a group conversation read like an open one. They ask here.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isOpenRoom(visibility: string | null | undefined): boolean;
|
|
24
|
+
/** 이 사람이 이 대화를 읽을 수 있나. open 이면 도메인 안 누구나, group · private 이면 앉은 사람만. */
|
|
18
25
|
export declare function mayReadSession(visibility: string | null | undefined, isParticipant: boolean): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* May this person seat someone in this conversation - by a mention or by inviting them?
|
|
28
|
+
*
|
|
29
|
+
* In an open room a seat grants nothing (anyone walks in), so anyone may point a colleague at it. In a group
|
|
30
|
+
* the seat is the key, and only the owner hands it out - a member's mention tells, it does not seat. A private
|
|
31
|
+
* room seats nobody: it is the owner's alone. Both seating doors ask this one line.
|
|
32
|
+
*/
|
|
33
|
+
export declare function maySeat(visibility: string | null | undefined, seatRole: string | null | undefined): boolean;
|
|
19
34
|
/**
|
|
20
35
|
* 이 사람이 이 대화에 **쓸** 수 있나 — 읽을 수 있는 사람만 쓴다.
|
|
21
36
|
*
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.SESSION_VISIBILITIES = void 0;
|
|
9
9
|
exports.initialVisibility = initialVisibility;
|
|
10
|
+
exports.isOpenRoom = isOpenRoom;
|
|
10
11
|
exports.mayReadSession = mayReadSession;
|
|
12
|
+
exports.maySeat = maySeat;
|
|
11
13
|
exports.mayWriteSession = mayWriteSession;
|
|
12
|
-
exports.SESSION_VISIBILITIES = ['open', 'private'];
|
|
14
|
+
exports.SESSION_VISIBILITIES = ['open', 'group', 'private'];
|
|
13
15
|
/**
|
|
14
16
|
* 이렇게 만들어진 세션은 무엇으로 시작하나 — **언제나 비공개.**
|
|
15
17
|
*
|
|
@@ -23,9 +25,32 @@ exports.SESSION_VISIBILITIES = ['open', 'private'];
|
|
|
23
25
|
function initialVisibility(_session = {}) {
|
|
24
26
|
return 'private';
|
|
25
27
|
}
|
|
26
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Is the room open to everyone in the domain - so no seat needs looking up?
|
|
30
|
+
*
|
|
31
|
+
* Doors skip the participant lookup for an open room. They asked `visibility === 'private'` for it, and a
|
|
32
|
+
* group room is neither: asked that way, a group conversation read like an open one. They ask here.
|
|
33
|
+
*/
|
|
34
|
+
function isOpenRoom(visibility) {
|
|
35
|
+
return (visibility ?? 'open') === 'open';
|
|
36
|
+
}
|
|
37
|
+
/** 이 사람이 이 대화를 읽을 수 있나. open 이면 도메인 안 누구나, group · private 이면 앉은 사람만. */
|
|
27
38
|
function mayReadSession(visibility, isParticipant) {
|
|
28
|
-
return (visibility
|
|
39
|
+
return isOpenRoom(visibility) || isParticipant;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* May this person seat someone in this conversation - by a mention or by inviting them?
|
|
43
|
+
*
|
|
44
|
+
* In an open room a seat grants nothing (anyone walks in), so anyone may point a colleague at it. In a group
|
|
45
|
+
* the seat is the key, and only the owner hands it out - a member's mention tells, it does not seat. A private
|
|
46
|
+
* room seats nobody: it is the owner's alone. Both seating doors ask this one line.
|
|
47
|
+
*/
|
|
48
|
+
function maySeat(visibility, seatRole) {
|
|
49
|
+
if (isOpenRoom(visibility))
|
|
50
|
+
return true;
|
|
51
|
+
if (visibility === 'group')
|
|
52
|
+
return seatRole === 'owner';
|
|
53
|
+
return false;
|
|
29
54
|
}
|
|
30
55
|
/**
|
|
31
56
|
* 이 사람이 이 대화에 **쓸** 수 있나 — 읽을 수 있는 사람만 쓴다.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-visibility.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-visibility.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;
|
|
1
|
+
{"version":3,"file":"session-visibility.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-visibility.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAsBH,8CAEC;AAQD,gCAEC;AAGD,wCAEC;AASD,0BAIC;AASD,0CAEC;AArDY,QAAA,oBAAoB,GAAiC,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;AAE9F;;;;;;;;;GASG;AACH,SAAgB,iBAAiB,CAAC,WAAmE,EAAE;IACrG,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,UAAU,CAAC,UAAqC;IAC9D,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC,KAAK,MAAM,CAAA;AAC1C,CAAC;AAED,yEAAyE;AACzE,SAAgB,cAAc,CAAC,UAAqC,EAAE,aAAsB;IAC1F,OAAO,UAAU,CAAC,UAAU,CAAC,IAAI,aAAa,CAAA;AAChD,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,OAAO,CAAC,UAAqC,EAAE,QAAmC;IAChG,IAAI,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IACvC,IAAI,UAAU,KAAK,OAAO;QAAE,OAAO,QAAQ,KAAK,OAAO,CAAA;IACvD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,UAAqC,EAAE,aAAsB;IAC3F,OAAO,cAAc,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;AAClD,CAAC","sourcesContent":["/*\n * 새 대화가 공개로 태어나는가 — ADR-0073 판정 2.\n *\n * 판정을 세션을 만드는 자리마다 적으면 한 곳이 빠진 날 그 세션만 다른 규칙으로 산다. 한 줄로 모은다.\n */\n\n/*\n * Three rooms (2026-10-24, the user - ADR-0073 decision 3):\n * open anyone in the domain walks in and writes.\n * group the people the owner brought in, and the owner.\n * private the owner alone. Nobody is seated, and turning a group private sends its members out.\n */\nexport type SessionVisibility = 'open' | 'group' | 'private'\n\nexport const SESSION_VISIBILITIES: readonly SessionVisibility[] = ['open', 'group', 'private']\n\n/**\n * 이렇게 만들어진 세션은 무엇으로 시작하나 — **언제나 비공개.**\n *\n * ── 앵커가 있으면 공개로 태어나던 것을 거둔 이유 (2026-09-30, 제품 오너) ──\n * operato-office 의 대화는 앱 행(`application`)에 매달린다. 그래서 원장 · 급여를 묻는 경영자의 질문이 도메인\n * 안 모든 직원에게 열린 방으로 태어났다. 목록은 그것을 「사업장 공개」 한 줄로만 말해서 공개와 비공개가 한눈에\n * 갈리지 않았다. 앵커는 「무엇에 대한 이야기인가」이지 「누구에게 보이나」가 아니다.\n *\n * 공개는 owner 가 고른다(`setSessionVisibility`) — 화면이 여는 순간 되묻고, 공개 대화는 도크가 다르게 그린다.\n */\nexport function initialVisibility(_session: { anchorType?: string | null; copyOf?: string | null } = {}): SessionVisibility {\n return 'private'\n}\n\n/**\n * Is the room open to everyone in the domain - so no seat needs looking up?\n *\n * Doors skip the participant lookup for an open room. They asked `visibility === 'private'` for it, and a\n * group room is neither: asked that way, a group conversation read like an open one. They ask here.\n */\nexport function isOpenRoom(visibility: string | null | undefined): boolean {\n return (visibility ?? 'open') === 'open'\n}\n\n/** 이 사람이 이 대화를 읽을 수 있나. open 이면 도메인 안 누구나, group · private 이면 앉은 사람만. */\nexport function mayReadSession(visibility: string | null | undefined, isParticipant: boolean): boolean {\n return isOpenRoom(visibility) || isParticipant\n}\n\n/**\n * May this person seat someone in this conversation - by a mention or by inviting them?\n *\n * In an open room a seat grants nothing (anyone walks in), so anyone may point a colleague at it. In a group\n * the seat is the key, and only the owner hands it out - a member's mention tells, it does not seat. A private\n * room seats nobody: it is the owner's alone. Both seating doors ask this one line.\n */\nexport function maySeat(visibility: string | null | undefined, seatRole: string | null | undefined): boolean {\n if (isOpenRoom(visibility)) return true\n if (visibility === 'group') return seatRole === 'owner'\n return false\n}\n\n/**\n * 이 사람이 이 대화에 **쓸** 수 있나 — 읽을 수 있는 사람만 쓴다.\n *\n * 읽는 문(`chatMessages`)과 목록만 막고 쓰는 문을 열어 두면, 비공개 대화의 id 를 가진 사람이 그 안에 말을\n * 넣을 수 있었다. `assistantChat` 에서는 더 나쁘다 — 답이 그 대화의 이력 위에서 만들어지므로, 읽을 수 없는\n * 내용이 답으로 새어 나간다(2026-09-24, operato-ecs 두 사람 시험 7번). 쓰는 문도 같은 관문을 지난다.\n */\nexport function mayWriteSession(visibility: string | null | undefined, isParticipant: boolean): boolean {\n return mayReadSession(visibility, isParticipant)\n}\n"]}
|
|
@@ -4,7 +4,7 @@ export interface WriteGateInput {
|
|
|
4
4
|
visibility?: string | null;
|
|
5
5
|
};
|
|
6
6
|
userId?: string;
|
|
7
|
-
/** Is this user a participant of the session (owner or member)? Asked only for a private one. */
|
|
7
|
+
/** Is this user a participant of the session (owner or member)? Asked only for a group or private one. */
|
|
8
8
|
isParticipant: () => Promise<boolean>;
|
|
9
9
|
t: (key: string, options?: Record<string, unknown>) => string;
|
|
10
10
|
}
|
|
@@ -17,7 +17,7 @@ async function assertMayWriteSession({ session, userId, isParticipant, t }) {
|
|
|
17
17
|
const participant = !!userId && (await isParticipant());
|
|
18
18
|
if (!(0, session_visibility_js_1.mayWriteSession)(visibility, participant)) {
|
|
19
19
|
throw new Error(t('ai-assistant.text.private-session-refused', {
|
|
20
|
-
defaultValue: 'this conversation is private
|
|
20
|
+
defaultValue: 'this conversation is private to the people in it - only they can write here. ask the person who started it to bring you in.'
|
|
21
21
|
}));
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-write-gate.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-write-gate.ts"],"names":[],"mappings":";;AAiBA,sDAaC;AA9BD;;;;;;GAMG;AACH,mEAAyD;AAUlD,KAAK,UAAU,qBAAqB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,EAAkB;IAC/F,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,MAAM,CAAA;IAC/C,0EAA0E;IAC1E,IAAI,UAAU,KAAK,MAAM;QAAE,OAAM;IAEjC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,aAAa,EAAE,CAAC,CAAA;IACvD,IAAI,CAAC,IAAA,uCAAe,EAAC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,CAAC,CAAC,2CAA2C,EAAE;YAC7C,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"session-write-gate.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-write-gate.ts"],"names":[],"mappings":";;AAiBA,sDAaC;AA9BD;;;;;;GAMG;AACH,mEAAyD;AAUlD,KAAK,UAAU,qBAAqB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,EAAkB;IAC/F,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,MAAM,CAAA;IAC/C,0EAA0E;IAC1E,IAAI,UAAU,KAAK,MAAM;QAAE,OAAM;IAEjC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,aAAa,EAAE,CAAC,CAAA;IACvD,IAAI,CAAC,IAAA,uCAAe,EAAC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,CAAC,CAAC,2CAA2C,EAAE;YAC7C,YAAY,EAAE,6HAA6H;SAC5I,CAAC,CACH,CAAA;IACH,CAAC;AACH,CAAC","sourcesContent":["/*\n * The gate every door that writes into a conversation passes - ADR-0073 decision 1, applied to writing.\n *\n * `mayWriteSession` is the rule; this looks up only what the rule needs (whether the writer is in the\n * room, and only for a private conversation) and refuses by a named message. Kept apart from the resolvers\n * so the five write doors call one line and cannot drift.\n */\nimport { mayWriteSession } from './session-visibility.js'\n\nexport interface WriteGateInput {\n session: { id?: string; visibility?: string | null }\n userId?: string\n /** Is this user a participant of the session (owner or member)? Asked only for a group or private one. */\n isParticipant: () => Promise<boolean>\n t: (key: string, options?: Record<string, unknown>) => string\n}\n\nexport async function assertMayWriteSession({ session, userId, isParticipant, t }: WriteGateInput): Promise<void> {\n const visibility = session.visibility ?? 'open'\n /* A public conversation is open to everyone in the domain - no lookup. */\n if (visibility === 'open') return\n\n const participant = !!userId && (await isParticipant())\n if (!mayWriteSession(visibility, participant)) {\n throw new Error(\n t('ai-assistant.text.private-session-refused', {\n defaultValue: 'this conversation is private to the people in it - only they can write here. ask the person who started it to bring you in.'\n })\n )\n }\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChatSessionParticipant } from './chat-session-participant.js';
|
|
2
2
|
export { ChatSessionParticipant };
|
|
3
3
|
export { isOnline, PRESENCE_TTL_MS } from './presence.js';
|
|
4
|
+
export { isSessionParticipant } from './participant-lookup.js';
|
|
4
5
|
export declare const entities: (typeof ChatSessionParticipant)[];
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.entities = exports.PRESENCE_TTL_MS = exports.isOnline = exports.ChatSessionParticipant = void 0;
|
|
3
|
+
exports.entities = exports.isSessionParticipant = exports.PRESENCE_TTL_MS = exports.isOnline = exports.ChatSessionParticipant = void 0;
|
|
4
4
|
const chat_session_participant_js_1 = require("./chat-session-participant.js");
|
|
5
5
|
Object.defineProperty(exports, "ChatSessionParticipant", { enumerable: true, get: function () { return chat_session_participant_js_1.ChatSessionParticipant; } });
|
|
6
6
|
var presence_js_1 = require("./presence.js");
|
|
7
7
|
Object.defineProperty(exports, "isOnline", { enumerable: true, get: function () { return presence_js_1.isOnline; } });
|
|
8
8
|
Object.defineProperty(exports, "PRESENCE_TTL_MS", { enumerable: true, get: function () { return presence_js_1.PRESENCE_TTL_MS; } });
|
|
9
|
+
/* "Is this person in the room" - one lookup for every gate, board-ai's revert included (a `viewer` row is not a seat). */
|
|
10
|
+
var participant_lookup_js_1 = require("./participant-lookup.js");
|
|
11
|
+
Object.defineProperty(exports, "isSessionParticipant", { enumerable: true, get: function () { return participant_lookup_js_1.isSessionParticipant; } });
|
|
9
12
|
exports.entities = [chat_session_participant_js_1.ChatSessionParticipant];
|
|
10
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/index.ts"],"names":[],"mappings":";;;AAAA,+EAAsE;AAE7D,uGAFA,oDAAsB,OAEA;AAC/B,6CAAyD;AAAhD,uGAAA,QAAQ,OAAA;AAAE,8GAAA,eAAe,OAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/index.ts"],"names":[],"mappings":";;;AAAA,+EAAsE;AAE7D,uGAFA,oDAAsB,OAEA;AAC/B,6CAAyD;AAAhD,uGAAA,QAAQ,OAAA;AAAE,8GAAA,eAAe,OAAA;AAClC,0HAA0H;AAC1H,iEAA8D;AAArD,6HAAA,oBAAoB,OAAA;AAEhB,QAAA,QAAQ,GAAG,CAAC,oDAAsB,CAAC,CAAA","sourcesContent":["import { ChatSessionParticipant } from './chat-session-participant.js'\n\nexport { ChatSessionParticipant }\nexport { isOnline, PRESENCE_TTL_MS } from './presence.js'\n/* \"Is this person in the room\" - one lookup for every gate, board-ai's revert included (a `viewer` row is not a seat). */\nexport { isSessionParticipant } from './participant-lookup.js'\n\nexport const entities = [ChatSessionParticipant]\n"]}
|