@things-factory/board-ai 10.1.26 → 10.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/mention-marker.d.ts +1 -11
- package/dist-server/service/mention-marker.js +10 -41
- package/dist-server/service/mention-marker.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/server/service/mention-marker.ts +7 -39
|
@@ -24,17 +24,7 @@ import { type MentionPick } from './mention-resolver';
|
|
|
24
24
|
* 향후 새 종류 (agentId / threadId / 외부 시스템 id 등) 도 동일 형식으로 추가 가능.
|
|
25
25
|
*/
|
|
26
26
|
export declare const MENTION_MARKER_RE: RegExp;
|
|
27
|
-
|
|
28
|
-
* 멘션된 userId 중 *아직 참여자가 아닌* + *자기 자신이 아닌* 신규만 (C4 초대 — 차집합).
|
|
29
|
-
* 순수 — 누구를 새로 참여자 등록할지 판정. (resolver 가 repo 작업과 분리해 테스트 가능.)
|
|
30
|
-
*/
|
|
31
|
-
export declare function newParticipantUserIds(mentioned: ReadonlyArray<string>, existing: ReadonlyArray<string>, selfUserId?: string | null): string[];
|
|
32
|
-
/**
|
|
33
|
-
* 본문에서 `@user{userId:UUID}` 마커의 *userId 집합* 추출 (C4 — 초대 트리거).
|
|
34
|
-
* 같은 user 중복 제거, 순서 보존(첫 등장순). 마커 없으면 빈 배열.
|
|
35
|
-
* 순수 — content 만으로 "이 메시지가 누구를 멘션했나" 판정.
|
|
36
|
-
*/
|
|
37
|
-
export declare function extractMentionUserIds(text: string | null | undefined): string[];
|
|
27
|
+
export { extractMentionUserIds, newParticipantUserIds } from '@things-factory/ai-assistant';
|
|
38
28
|
/**
|
|
39
29
|
* 본문에서 모든 `{key:value}` 인라인 마커 제거. LLM 입력 / textarea 편집 / 평문 표출 등.
|
|
40
30
|
* 토큰 자체 (#component, @user) 는 그대로 유지.
|
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
* 보다 우선) — 렌더링 깨졌을 때 로그 / DB 직접 보고 파악 가능
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.MENTION_MARKER_RE = void 0;
|
|
21
|
-
exports.newParticipantUserIds = newParticipantUserIds;
|
|
22
|
-
exports.extractMentionUserIds = extractMentionUserIds;
|
|
20
|
+
exports.newParticipantUserIds = exports.extractMentionUserIds = exports.MENTION_MARKER_RE = void 0;
|
|
23
21
|
exports.stripMentionRefids = stripMentionRefids;
|
|
24
22
|
exports.injectInlineMarkers = injectInlineMarkers;
|
|
25
23
|
exports.injectMentionRefids = injectMentionRefids;
|
|
@@ -33,45 +31,16 @@ const mention_resolver_1 = require("./mention-resolver");
|
|
|
33
31
|
* 향후 새 종류 (agentId / threadId / 외부 시스템 id 등) 도 동일 형식으로 추가 가능.
|
|
34
32
|
*/
|
|
35
33
|
exports.MENTION_MARKER_RE = /\{[a-zA-Z][a-zA-Z0-9]*:[^}\s]+\}/g;
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (selfUserId)
|
|
43
|
-
have.add(selfUserId);
|
|
44
|
-
const out = [];
|
|
45
|
-
const seen = new Set();
|
|
46
|
-
for (const id of mentioned) {
|
|
47
|
-
if (!id || have.has(id) || seen.has(id))
|
|
48
|
-
continue;
|
|
49
|
-
seen.add(id);
|
|
50
|
-
out.push(id);
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* 본문에서 `@user{userId:UUID}` 마커의 *userId 집합* 추출 (C4 — 초대 트리거).
|
|
56
|
-
* 같은 user 중복 제거, 순서 보존(첫 등장순). 마커 없으면 빈 배열.
|
|
57
|
-
* 순수 — content 만으로 "이 메시지가 누구를 멘션했나" 판정.
|
|
34
|
+
/*
|
|
35
|
+
* 이 둘은 **`@things-factory/ai-assistant` 로 옮겼다**(ADR-0071 결정 1).
|
|
36
|
+
*
|
|
37
|
+
* 멘션이 사람을 불러들이는 일은 보드만의 일이 아니다 — 트윈 협의에서 `@이름` 은 아무도 부르지 않고
|
|
38
|
+
* 있었다. 규칙이 이 제품 안에 있는 한 다른 문은 그것을 쓸 수 없다. 여기서는 이름만 다시 내보내
|
|
39
|
+
* 이 패키지의 부르는 자리가 그대로 돌게 한다.
|
|
58
40
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const re = /\{userId:([^}\s]+)\}/g;
|
|
63
|
-
const seen = new Set();
|
|
64
|
-
const out = [];
|
|
65
|
-
let m;
|
|
66
|
-
while ((m = re.exec(text)) !== null) {
|
|
67
|
-
const id = m[1];
|
|
68
|
-
if (!seen.has(id)) {
|
|
69
|
-
seen.add(id);
|
|
70
|
-
out.push(id);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
41
|
+
var ai_assistant_1 = require("@things-factory/ai-assistant");
|
|
42
|
+
Object.defineProperty(exports, "extractMentionUserIds", { enumerable: true, get: function () { return ai_assistant_1.extractMentionUserIds; } });
|
|
43
|
+
Object.defineProperty(exports, "newParticipantUserIds", { enumerable: true, get: function () { return ai_assistant_1.newParticipantUserIds; } });
|
|
75
44
|
/**
|
|
76
45
|
* 본문에서 모든 `{key:value}` 인라인 마커 제거. LLM 입력 / textarea 편집 / 평문 표출 등.
|
|
77
46
|
* 토큰 자체 (#component, @user) 는 그대로 유지.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mention-marker.js","sourceRoot":"","sources":["../../server/service/mention-marker.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;
|
|
1
|
+
{"version":3,"file":"mention-marker.js","sourceRoot":"","sources":["../../server/service/mention-marker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `#token{refid:N}` inline marker — 채팅 메시지의 단일 source-of-truth.\n *\n * 설계 원칙:\n * - 메시지 영속은 `ChatMessage.content` 한 컬럼에 통합. token-refid 매핑은 본문 안에\n * `#motor1{refid:35}` 형태로 inline 마킹.\n * - 별도 컬럼이 없으니 두 데이터 사이 consistency 깨질 일 자체가 없음.\n * - LLM 한테는 `stripMentionRefids` 로 마커 제거 후 보냄 — 토큰 낭비 / 주의 분산 차단.\n * - 클라이언트 렌더는 `#token{refid:N}` 패턴을 추출해 `<span data-refid=\"N\">#token</span>`\n * 으로 변환 → 클릭 시 deterministic 한 컴포넌트 점프.\n *\n * 마커 형식:\n * - `#token{refid:N}` (N 은 정수)\n * - token 과 `{` 사이 공백 없음\n * - `{refid:N}` 자체는 사람도 디버그 시 읽기 쉬움 (zero-width 문자나 prefix 약어\n * 보다 우선) — 렌더링 깨졌을 때 로그 / DB 직접 보고 파악 가능\n */\n\nimport type { BoardModel } from '@things-factory/board-import'\nimport { findMentions, type MentionPick } from './mention-resolver'\n\n/**\n * 인라인 마커 정규식 — `{key:value}` 형태. 현재 사용:\n * - `{refid:N}` — `#component` 멘션 (things-scene refid)\n * - `{userId:UUID}` — `@user` 멘션 (도메인 사용자)\n * 향후 새 종류 (agentId / threadId / 외부 시스템 id 등) 도 동일 형식으로 추가 가능.\n */\nexport const MENTION_MARKER_RE = /\\{[a-zA-Z][a-zA-Z0-9]*:[^}\\s]+\\}/g\n\n/*\n * 이 둘은 **`@things-factory/ai-assistant` 로 옮겼다**(ADR-0071 결정 1).\n *\n * 멘션이 사람을 불러들이는 일은 보드만의 일이 아니다 — 트윈 협의에서 `@이름` 은 아무도 부르지 않고\n * 있었다. 규칙이 이 제품 안에 있는 한 다른 문은 그것을 쓸 수 없다. 여기서는 이름만 다시 내보내\n * 이 패키지의 부르는 자리가 그대로 돌게 한다.\n */\nexport { extractMentionUserIds, newParticipantUserIds } from '@things-factory/ai-assistant'\n\n\n/**\n * 본문에서 모든 `{key:value}` 인라인 마커 제거. LLM 입력 / textarea 편집 / 평문 표출 등.\n * 토큰 자체 (#component, @user) 는 그대로 유지.\n */\nexport function stripMentionRefids(text: string | null | undefined): string {\n if (!text) return ''\n return text.replace(MENTION_MARKER_RE, '')\n}\n\n/** 토큰 매칭 정규식 빌더 — trigger 별로 build. */\nfunction buildTokenRegex(trigger: string, markerKey: string): RegExp {\n const word =\n '\\\\w\\\\p{Script=Hangul}\\\\p{Script=Hiragana}\\\\p{Script=Katakana}\\\\p{Script=Han}'\n // (앞경계)(트리거)(토큰)(이미 같은 markerKey 의 마커가 붙어있는지 — skip 판정용)\n return new RegExp(\n `(^|[^${word}])(${escapeRegex(trigger)})([${word}-]+)(\\\\{${markerKey}:[^}\\\\s]+\\\\})?`,\n 'gu'\n )\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * 일반화된 inline marker 주입 — trigger 와 markerKey 별로 picks 를 본문에 박음.\n *\n * injectInlineMarkers(text, '#', 'refid', [{ token, value: 35 }])\n * injectInlineMarkers(text, '@', 'userId', [{ token, value: 'uuid' }])\n *\n * 룰:\n * - 정확 token 일치 (popup 명시 선택된 것만 — 자유 입력 토큰은 안 건드림)\n * - 동일 token 여러 번 등장하면 모두 marker 적용\n * - 이미 같은 markerKey 의 marker 가 붙어있으면 그대로 유지 (덮어쓰기 X)\n */\nexport function injectInlineMarkers(\n text: string,\n trigger: string,\n markerKey: string,\n picks: ReadonlyArray<{ token: string; value: string | number }> | null | undefined\n): string {\n if (!text || !picks || picks.length === 0) return text ?? ''\n const map = new Map<string, string | number>()\n for (const p of picks) map.set(p.token, p.value)\n const re = buildTokenRegex(trigger, markerKey)\n return text.replace(re, (match, leading, trig, token, existingMarker) => {\n if (existingMarker) return match\n const value = map.get(token)\n if (value === undefined) return match\n return `${leading}${trig}${token}{${markerKey}:${value}}`\n })\n}\n\n/**\n * `#component` 멘션 picks 를 `{refid:N}` 마커로 주입 (호환 유지).\n */\nexport function injectMentionRefids(\n text: string,\n picks: ReadonlyArray<MentionPick> | null | undefined\n): string {\n return injectInlineMarkers(\n text,\n '#',\n 'refid',\n picks?.map(p => ({ token: p.token, value: p.refid }))\n )\n}\n\n/**\n * `@user` 멘션 picks 를 `{userId:UUID}` 마커로 주입.\n * picks: [{token: 사용자_표시명, value: 사용자_id}]\n */\nexport function injectMentionUserIds(\n text: string,\n picks: ReadonlyArray<{ token: string; userId: string }> | null | undefined\n): string {\n return injectInlineMarkers(\n text,\n '@',\n 'userId',\n picks?.map(p => ({ token: p.token, value: p.userId }))\n )\n}\n\n/**\n * AI 응답 텍스트의 `#token` 들을 currentBoard 에 대해 resolve 해 inline marker 로 주입.\n *\n * - 매칭 룰: `findMentions` (= name/id/type/tag/class/text OR). 다중 일치 시 첫 결과.\n * - 매칭 안 되면 marker 없이 통과 (예: AI 가 patch 로 새로 만든 컴포넌트는 input.currentBoard\n * 에 없으므로 marker 못 박음 — 클라이언트 클릭 시 runtime fallback 으로 해석 가능).\n * - 이미 marker 있으면 그대로.\n */\nexport function injectAiMentionRefids(\n text: string,\n currentBoard: BoardModel | null | undefined\n): string {\n if (!text) return ''\n const matches = findMentions(text, currentBoard ?? null)\n if (matches.length === 0) return text\n\n // token → 첫 매칭 refid 맵\n const tokenToRefid = new Map<string, number>()\n for (const m of matches) {\n if (tokenToRefid.has(m.token)) continue\n if (m.matched.length === 0) continue\n tokenToRefid.set(m.token, m.matched[0].refid)\n }\n if (tokenToRefid.size === 0) return text\n\n // 사용자 picks 와 동일한 injection 룰로 주입.\n const picks = Array.from(tokenToRefid.entries()).map(([token, refid]) => ({ token, refid }))\n return injectMentionRefids(text, picks)\n}\n"]}
|