@composy/chat-core 0.0.1 → 0.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.
- package/README.md +187 -123
- package/dist/index.js +2018 -1035
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1370 -1644
- package/dist/index.min.js.map +1 -0
- package/es/.dts-output-manifest.json +1 -0
- package/es/.dts.tsbuildinfo +1 -0
- package/es/constants/dom.d.ts +3 -19
- package/es/constants/dom.js +4 -4
- package/es/constants/dom.js.map +1 -1
- package/es/core/chat-manager.d.ts +33 -22
- package/es/core/chat-manager.js +174 -50
- package/es/core/chat-manager.js.map +1 -1
- package/es/core/event-emitter.d.ts +0 -1
- package/es/core/event-emitter.js +14 -7
- package/es/core/event-emitter.js.map +1 -1
- package/es/core/mention-engine.d.ts +1 -2
- package/es/core/mention-engine.js +9 -9
- package/es/core/mention-engine.js.map +1 -1
- package/es/core/message-schema.d.ts +19 -0
- package/es/core/message-schema.js +197 -0
- package/es/core/message-schema.js.map +1 -0
- package/es/core/outbox.d.ts +63 -0
- package/es/core/outbox.js +160 -0
- package/es/core/outbox.js.map +1 -0
- package/es/core/transport.d.ts +79 -0
- package/es/core/transport.js +102 -0
- package/es/core/transport.js.map +1 -0
- package/es/index.d.ts +12 -7
- package/es/index.js +167 -36
- package/es/index.js.map +1 -1
- package/es/renderer/dom-renderer.d.ts +16 -4
- package/es/renderer/dom-renderer.js +316 -61
- package/es/renderer/dom-renderer.js.map +1 -1
- package/es/renderer/style-manager.d.ts +14 -38
- package/es/renderer/style-manager.js +92 -63
- package/es/renderer/style-manager.js.map +1 -1
- package/es/theme/theme-manager.d.ts +4 -3
- package/es/theme/theme-manager.js +19 -9
- package/es/theme/theme-manager.js.map +1 -1
- package/es/theme/tokens.d.ts +0 -1
- package/es/theme/tokens.js +92 -92
- package/es/theme/tokens.js.map +1 -1
- package/es/types/index.d.ts +104 -24
- package/es/types/index.js +8 -3
- package/es/types/index.js.map +1 -0
- package/esm/constants/dom.d.ts +3 -19
- package/esm/constants/dom.js +4 -4
- package/esm/constants/dom.js.map +1 -1
- package/esm/core/chat-manager.d.ts +33 -22
- package/esm/core/chat-manager.js +174 -50
- package/esm/core/chat-manager.js.map +1 -1
- package/esm/core/event-emitter.d.ts +0 -1
- package/esm/core/event-emitter.js +14 -7
- package/esm/core/event-emitter.js.map +1 -1
- package/esm/core/mention-engine.d.ts +1 -2
- package/esm/core/mention-engine.js +9 -9
- package/esm/core/mention-engine.js.map +1 -1
- package/esm/core/message-schema.d.ts +19 -0
- package/esm/core/message-schema.js +197 -0
- package/esm/core/message-schema.js.map +1 -0
- package/esm/core/outbox.d.ts +63 -0
- package/esm/core/outbox.js +160 -0
- package/esm/core/outbox.js.map +1 -0
- package/esm/core/transport.d.ts +79 -0
- package/esm/core/transport.js +102 -0
- package/esm/core/transport.js.map +1 -0
- package/esm/index.d.ts +12 -7
- package/esm/index.js +169 -38
- package/esm/index.js.map +1 -1
- package/esm/renderer/dom-renderer.d.ts +16 -4
- package/esm/renderer/dom-renderer.js +316 -61
- package/esm/renderer/dom-renderer.js.map +1 -1
- package/esm/renderer/style-manager.d.ts +14 -38
- package/esm/renderer/style-manager.js +92 -63
- package/esm/renderer/style-manager.js.map +1 -1
- package/esm/theme/theme-manager.d.ts +4 -3
- package/esm/theme/theme-manager.js +19 -9
- package/esm/theme/theme-manager.js.map +1 -1
- package/esm/theme/tokens.d.ts +0 -1
- package/esm/theme/tokens.js +92 -92
- package/esm/theme/tokens.js.map +1 -1
- package/esm/types/index.d.ts +104 -24
- package/esm/types/index.js +8 -3
- package/esm/types/index.js.map +1 -0
- package/lib/constants/dom.cjs +4 -4
- package/lib/constants/dom.cjs.map +1 -1
- package/lib/constants/dom.d.cts +25 -0
- package/lib/constants/dom.d.ts +3 -19
- package/lib/core/chat-manager.cjs +174 -50
- package/lib/core/chat-manager.cjs.map +1 -1
- package/lib/core/chat-manager.d.cts +45 -0
- package/lib/core/chat-manager.d.ts +33 -22
- package/lib/core/event-emitter.cjs +14 -7
- package/lib/core/event-emitter.cjs.map +1 -1
- package/lib/core/event-emitter.d.cts +47 -0
- package/lib/core/event-emitter.d.ts +0 -1
- package/lib/core/mention-engine.cjs +9 -9
- package/lib/core/mention-engine.cjs.map +1 -1
- package/lib/core/mention-engine.d.cts +40 -0
- package/lib/core/mention-engine.d.ts +1 -2
- package/lib/core/message-schema.cjs +202 -0
- package/lib/core/message-schema.cjs.map +1 -0
- package/lib/core/message-schema.d.cts +19 -0
- package/lib/core/message-schema.d.ts +19 -0
- package/lib/core/outbox.cjs +162 -0
- package/lib/core/outbox.cjs.map +1 -0
- package/lib/core/outbox.d.cts +63 -0
- package/lib/core/outbox.d.ts +63 -0
- package/lib/core/transport.cjs +103 -0
- package/lib/core/transport.cjs.map +1 -0
- package/lib/core/transport.d.cts +79 -0
- package/lib/core/transport.d.ts +79 -0
- package/lib/index.cjs +175 -35
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +33 -0
- package/lib/index.d.ts +12 -7
- package/lib/renderer/dom-renderer.cjs +316 -61
- package/lib/renderer/dom-renderer.cjs.map +1 -1
- package/lib/renderer/dom-renderer.d.cts +91 -0
- package/lib/renderer/dom-renderer.d.ts +16 -4
- package/lib/renderer/style-manager.cjs +92 -62
- package/lib/renderer/style-manager.cjs.map +1 -1
- package/lib/renderer/style-manager.d.cts +34 -0
- package/lib/renderer/style-manager.d.ts +14 -38
- package/lib/theme/theme-manager.cjs +19 -9
- package/lib/theme/theme-manager.cjs.map +1 -1
- package/lib/theme/theme-manager.d.cts +34 -0
- package/lib/theme/theme-manager.d.ts +4 -3
- package/lib/theme/tokens.cjs +92 -92
- package/lib/theme/tokens.cjs.map +1 -1
- package/lib/theme/tokens.d.cts +56 -0
- package/lib/theme/tokens.d.ts +0 -1
- package/lib/types/index.cjs +8 -3
- package/lib/types/index.cjs.map +1 -0
- package/lib/types/index.d.cts +284 -0
- package/lib/types/index.d.ts +104 -24
- package/package.json +16 -13
- package/es/constants/dom.d.ts.map +0 -1
- package/es/core/chat-manager.d.ts.map +0 -1
- package/es/core/event-emitter.d.ts.map +0 -1
- package/es/core/mention-engine.d.ts.map +0 -1
- package/es/index.d.ts.map +0 -1
- package/es/renderer/dom-renderer.d.ts.map +0 -1
- package/es/renderer/style-manager.d.ts.map +0 -1
- package/es/theme/theme-manager.d.ts.map +0 -1
- package/es/theme/tokens.d.ts.map +0 -1
- package/es/types/index.d.ts.map +0 -1
- package/esm/constants/dom.d.ts.map +0 -1
- package/esm/core/chat-manager.d.ts.map +0 -1
- package/esm/core/event-emitter.d.ts.map +0 -1
- package/esm/core/mention-engine.d.ts.map +0 -1
- package/esm/index.d.ts.map +0 -1
- package/esm/renderer/dom-renderer.d.ts.map +0 -1
- package/esm/renderer/style-manager.d.ts.map +0 -1
- package/esm/theme/theme-manager.d.ts.map +0 -1
- package/esm/theme/tokens.d.ts.map +0 -1
- package/esm/types/index.d.ts.map +0 -1
- package/lib/constants/dom.d.ts.map +0 -1
- package/lib/core/chat-manager.d.ts.map +0 -1
- package/lib/core/event-emitter.d.ts.map +0 -1
- package/lib/core/mention-engine.d.ts.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/renderer/dom-renderer.d.ts.map +0 -1
- package/lib/renderer/style-manager.d.ts.map +0 -1
- package/lib/theme/theme-manager.d.ts.map +0 -1
- package/lib/theme/tokens.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ChatMessage, MessageStatus } from '../types';
|
|
2
|
+
import type { EventEmitter } from './event-emitter';
|
|
3
|
+
export type MessageMergeResult = 'added' | 'updated' | 'ignored';
|
|
4
|
+
export interface MessageMergeOptions {
|
|
5
|
+
sequence?: number;
|
|
6
|
+
source?: 'local' | 'remote';
|
|
7
|
+
}
|
|
8
|
+
export interface StreamAppendOptions {
|
|
9
|
+
generation?: string;
|
|
10
|
+
sequence?: number;
|
|
11
|
+
}
|
|
12
|
+
/** ChatManager - 聊天消息状态、幂等合并和流式 generation 管理。 */
|
|
13
|
+
export declare class ChatManager {
|
|
14
|
+
private messages;
|
|
15
|
+
private readonly emitter;
|
|
16
|
+
private readonly streams;
|
|
17
|
+
constructor(emitter: EventEmitter, initialMessages?: ChatMessage[]);
|
|
18
|
+
getMessages(): ChatMessage[];
|
|
19
|
+
getMessage(id: string): ChatMessage | undefined;
|
|
20
|
+
getMessageIdByClientMessageId(clientMessageId: string): string | undefined;
|
|
21
|
+
addMessage(message: ChatMessage): void;
|
|
22
|
+
prependMessages(messages: ChatMessage[]): void;
|
|
23
|
+
/** 合并本地 ack 或远端推送,按 id/clientMessageId 去重并拒绝旧 sequence。 */
|
|
24
|
+
upsertMessage(message: ChatMessage, options?: MessageMergeOptions): MessageMergeResult;
|
|
25
|
+
mergeMessages(messages: ChatMessage[], options?: MessageMergeOptions): {
|
|
26
|
+
added: number;
|
|
27
|
+
updated: number;
|
|
28
|
+
ignored: number;
|
|
29
|
+
};
|
|
30
|
+
updateMessage(id: string, updates: Partial<ChatMessage>): void;
|
|
31
|
+
markMessageStatus(id: string, status: MessageStatus): void;
|
|
32
|
+
removeMessage(id: string): void;
|
|
33
|
+
setMessages(messages: ChatMessage[]): void;
|
|
34
|
+
clearMessages(): void;
|
|
35
|
+
beginStreaming(id: string, generation?: string): string | null;
|
|
36
|
+
appendMessageContent(id: string, chunk: string, options?: StreamAppendOptions): boolean;
|
|
37
|
+
finishStreaming(id: string, generation?: string): boolean;
|
|
38
|
+
cancelStreaming(id: string, generation?: string): boolean;
|
|
39
|
+
getStreamGeneration(id: string): string | undefined;
|
|
40
|
+
private findMessageIndex;
|
|
41
|
+
private assertUniqueIds;
|
|
42
|
+
private assertUniqueClientMessageId;
|
|
43
|
+
private emitAdded;
|
|
44
|
+
private emitChanged;
|
|
45
|
+
}
|
|
@@ -1,34 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import type { ChatMessage, MessageStatus } from '../types';
|
|
2
|
+
import type { EventEmitter } from './event-emitter';
|
|
3
|
+
export type MessageMergeResult = 'added' | 'updated' | 'ignored';
|
|
4
|
+
export interface MessageMergeOptions {
|
|
5
|
+
sequence?: number;
|
|
6
|
+
source?: 'local' | 'remote';
|
|
7
|
+
}
|
|
8
|
+
export interface StreamAppendOptions {
|
|
9
|
+
generation?: string;
|
|
10
|
+
sequence?: number;
|
|
11
|
+
}
|
|
12
|
+
/** ChatManager - 聊天消息状态、幂等合并和流式 generation 管理。 */
|
|
9
13
|
export declare class ChatManager {
|
|
10
14
|
private messages;
|
|
11
|
-
private emitter;
|
|
15
|
+
private readonly emitter;
|
|
16
|
+
private readonly streams;
|
|
12
17
|
constructor(emitter: EventEmitter, initialMessages?: ChatMessage[]);
|
|
13
|
-
/** 获取所有消息 */
|
|
14
18
|
getMessages(): ChatMessage[];
|
|
15
|
-
/** 根据 ID 获取消息 */
|
|
16
19
|
getMessage(id: string): ChatMessage | undefined;
|
|
17
|
-
|
|
20
|
+
getMessageIdByClientMessageId(clientMessageId: string): string | undefined;
|
|
18
21
|
addMessage(message: ChatMessage): void;
|
|
19
|
-
/** 在顶部批量插入消息(用于加载更多) */
|
|
20
22
|
prependMessages(messages: ChatMessage[]): void;
|
|
21
|
-
/**
|
|
23
|
+
/** 合并本地 ack 或远端推送,按 id/clientMessageId 去重并拒绝旧 sequence。 */
|
|
24
|
+
upsertMessage(message: ChatMessage, options?: MessageMergeOptions): MessageMergeResult;
|
|
25
|
+
mergeMessages(messages: ChatMessage[], options?: MessageMergeOptions): {
|
|
26
|
+
added: number;
|
|
27
|
+
updated: number;
|
|
28
|
+
ignored: number;
|
|
29
|
+
};
|
|
22
30
|
updateMessage(id: string, updates: Partial<ChatMessage>): void;
|
|
23
|
-
|
|
31
|
+
markMessageStatus(id: string, status: MessageStatus): void;
|
|
24
32
|
removeMessage(id: string): void;
|
|
25
|
-
/** 替换全部消息 */
|
|
26
33
|
setMessages(messages: ChatMessage[]): void;
|
|
27
|
-
/** 清空所有消息 */
|
|
28
34
|
clearMessages(): void;
|
|
29
|
-
|
|
30
|
-
appendMessageContent(id: string, chunk: string):
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
beginStreaming(id: string, generation?: string): string | null;
|
|
36
|
+
appendMessageContent(id: string, chunk: string, options?: StreamAppendOptions): boolean;
|
|
37
|
+
finishStreaming(id: string, generation?: string): boolean;
|
|
38
|
+
cancelStreaming(id: string, generation?: string): boolean;
|
|
39
|
+
getStreamGeneration(id: string): string | undefined;
|
|
40
|
+
private findMessageIndex;
|
|
41
|
+
private assertUniqueIds;
|
|
42
|
+
private assertUniqueClientMessageId;
|
|
43
|
+
private emitAdded;
|
|
44
|
+
private emitChanged;
|
|
33
45
|
}
|
|
34
|
-
//# sourceMappingURL=chat-manager.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* ***********************************
|
|
3
|
-
* @composy/chat-core v0.0.
|
|
3
|
+
* @composy/chat-core v0.0.2 *
|
|
4
4
|
* Built with rollup *
|
|
5
|
-
* Build time: 2024-
|
|
5
|
+
* Build time: 2024-10-12 21:52:18 *
|
|
6
6
|
* Build mode: production *
|
|
7
7
|
* Minified: No *
|
|
8
8
|
* ***********************************
|
|
@@ -20,8 +20,12 @@ var EventEmitter = class {
|
|
|
20
20
|
* @returns 返回一个取消订阅的函数,调用即可移除该监听器
|
|
21
21
|
*/
|
|
22
22
|
on(event, handler) {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
let handlers = this.listeners.get(event);
|
|
24
|
+
if (!handlers) {
|
|
25
|
+
handlers = /* @__PURE__ */ new Set();
|
|
26
|
+
this.listeners.set(event, handlers);
|
|
27
|
+
}
|
|
28
|
+
handlers.add(handler);
|
|
25
29
|
return () => this.off(event, handler);
|
|
26
30
|
}
|
|
27
31
|
/**
|
|
@@ -30,7 +34,10 @@ var EventEmitter = class {
|
|
|
30
34
|
* @param handler - 要移除的处理函数引用(必须与注册时为同一函数引用)
|
|
31
35
|
*/
|
|
32
36
|
off(event, handler) {
|
|
33
|
-
this.listeners.get(event)
|
|
37
|
+
const handlers = this.listeners.get(event);
|
|
38
|
+
if (!handlers) return;
|
|
39
|
+
handlers.delete(handler);
|
|
40
|
+
if (handlers.size === 0) this.listeners.delete(event);
|
|
34
41
|
}
|
|
35
42
|
/**
|
|
36
43
|
* 触发(发射)指定事件,依次调用所有已注册的监听器
|
|
@@ -38,7 +45,7 @@ var EventEmitter = class {
|
|
|
38
45
|
* @param args - 传递给监听器函数的参数列表(剩余参数语法)
|
|
39
46
|
*/
|
|
40
47
|
emit(event, ...args) {
|
|
41
|
-
this.listeners.get(event)
|
|
48
|
+
[...this.listeners.get(event) ?? []].forEach((handler) => {
|
|
42
49
|
try {
|
|
43
50
|
handler(...args);
|
|
44
51
|
} catch (err) {
|
|
@@ -66,7 +73,7 @@ var EventEmitter = class {
|
|
|
66
73
|
* - 若不传参数,则清空所有事件的所有监听器
|
|
67
74
|
*/
|
|
68
75
|
removeAll(event) {
|
|
69
|
-
if (event) this.listeners.delete(event);
|
|
76
|
+
if (event !== void 0) this.listeners.delete(event);
|
|
70
77
|
else this.listeners.clear();
|
|
71
78
|
}
|
|
72
79
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-emitter.cjs","names":[],"sources":["../../src/core/event-emitter.ts"],"sourcesContent":["/**\n * 轻量事件总线\n * 提供发布/订阅模式的事件管理功能,支持事件的注册、注销、触发等操作\n */\n\n// 定义事件处理函数的类型签名:接收任意数量的未知类型参数,无返回值\ntype Handler = (...args: unknown[]) => void
|
|
1
|
+
{"version":3,"file":"event-emitter.cjs","names":[],"sources":["../../src/core/event-emitter.ts"],"sourcesContent":["/**\r\n * 轻量事件总线\r\n * 提供发布/订阅模式的事件管理功能,支持事件的注册、注销、触发等操作\r\n */\r\n\r\n// 定义事件处理函数的类型签名:接收任意数量的未知类型参数,无返回值\r\ntype Handler = (...args: unknown[]) => void\r\n\r\n/**\r\n * EventEmitter - 事件发射器类\r\n * 实现了经典的发布/订阅(Pub/Sub)模式,用于模块间的解耦通信\r\n * 支持:一次性监听、批量移除、自动清理等功能\r\n */\r\nexport class EventEmitter {\r\n // 使用 Map 存储事件名到处理函数集合的映射关系\r\n // 键为事件名称(字符串),值为该事件对应的处理函数 Set 集合\r\n // 使用 Set 而非数组,可自动去重同一处理函数的重复注册\r\n private listeners = new Map<string, Set<Handler>>()\r\n\r\n /**\r\n * 注册事件监听器\r\n * @param event - 要监听的事件名称\r\n * @param handler - 事件触发时执行的回调函数\r\n * @returns 返回一个取消订阅的函数,调用即可移除该监听器\r\n */\r\n on(event: string, handler: Handler): () => void {\r\n let handlers = this.listeners.get(event)\r\n if (!handlers) {\r\n handlers = new Set()\r\n this.listeners.set(event, handlers)\r\n }\r\n handlers.add(handler)\r\n // 返回一个取消订阅函数,方便调用者链式调用后自行移除\r\n return () => this.off(event, handler)\r\n }\r\n\r\n /**\r\n * 移除指定事件的某个监听器\r\n * @param event - 事件名称\r\n * @param handler - 要移除的处理函数引用(必须与注册时为同一函数引用)\r\n */\r\n off(event: string, handler: Handler): void {\r\n // 可选链调用:如果该事件存在监听器集合,则从中删除指定处理函数\r\n // 若事件不存在则安全跳过,不会抛出异常\r\n const handlers = this.listeners.get(event)\r\n if (!handlers) return\r\n handlers.delete(handler)\r\n if (handlers.size === 0) this.listeners.delete(event)\r\n }\r\n\r\n /**\r\n * 触发(发射)指定事件,依次调用所有已注册的监听器\r\n * @param event - 要触发的事件名称\r\n * @param args - 传递给监听器函数的参数列表(剩余参数语法)\r\n */\r\n emit(event: string, ...args: unknown[]): void {\r\n // 获取该事件的监听器集合,若存在则遍历调用每个处理函数\r\n ;[...(this.listeners.get(event) ?? [])].forEach(handler => {\r\n try {\r\n // 使用展开运算符将参数数组传递给处理函数\r\n handler(...args)\r\n } catch (err) {\r\n // 捕获单个处理函数的异常,避免影响其他监听器的执行\r\n // 同时输出错误日志便于调试,标注所属模块为 [Chat]\r\n console.error(`[Chat] Event handler error for \"${event}\":`, err)\r\n }\r\n })\r\n }\r\n\r\n /**\r\n * 注册一次性事件监听器(触发一次后自动移除)\r\n * @param event - 要监听的事件名称\r\n * @param handler - 事件触发时执行的回调函数\r\n * @returns 返回取消订阅的函数,可在触发前手动取消\r\n */\r\n once(event: string, handler: Handler): () => void {\r\n // 创建包装函数:先移除自身,再调用原始处理函数\r\n // 这样保证该监听器只执行一次\r\n const wrapper = (...args: unknown[]) => {\r\n // 先从事件中移除包装函数自身,防止重复触发\r\n this.off(event, wrapper)\r\n // 再执行用户注册的原始处理函数\r\n handler(...args)\r\n }\r\n // 将包装函数注册为普通监听器,复用 on() 的逻辑\r\n return this.on(event, wrapper)\r\n }\r\n\r\n /**\r\n * 批量移除事件监听器\r\n * @param event - 可选的事件名称\r\n * - 若传入事件名,则只移除该事件的所有监听器\r\n * - 若不传参数,则清空所有事件的所有监听器\r\n */\r\n removeAll(event?: string): void {\r\n if (event !== undefined) {\r\n // 移除指定事件的全部监听器(直接删除整个事件条目)\r\n this.listeners.delete(event)\r\n } else {\r\n // 不传参数时,清空整个监听器映射表,移除所有事件\r\n this.listeners.clear()\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;AAaO,IAAM,eAAN,MAAmB;CAAnB,cAAA;EAIL,KAAQ,4BAAY,IAAI,IAA0B;CAAA;;;;;;;CAQlD,GAAG,OAAe,SAA8B;EAC9C,IAAI,WAAW,KAAK,UAAU,IAAI,KAAK;EACvC,IAAI,CAAC,UAAU;GACb,2BAAW,IAAI,IAAI;GACnB,KAAK,UAAU,IAAI,OAAO,QAAQ;EACpC;EACA,SAAS,IAAI,OAAO;EAEpB,aAAa,KAAK,IAAI,OAAO,OAAO;CACtC;;;;;;CAOA,IAAI,OAAe,SAAwB;EAGzC,MAAM,WAAW,KAAK,UAAU,IAAI,KAAK;EACzC,IAAI,CAAC,UAAU;EACf,SAAS,OAAO,OAAO;EACvB,IAAI,SAAS,SAAS,GAAG,KAAK,UAAU,OAAO,KAAK;CACtD;;;;;;CAOA,KAAK,OAAA,GAAkB,MAAuB;EAE3C,CAAC,GAAI,KAAK,UAAU,IAAI,KAAK,KAAK,CAAC,CAAE,CAAA,CAAE,SAAQ,YAAW;GACzD,IAAI;IAEF,QAAQ,GAAG,IAAI;GACjB,SAAS,KAAK;IAGZ,QAAQ,MAAM,mCAAmC,MAAK,KAAM,GAAG;GACjE;EACF,CAAC;CACH;;;;;;;CAQA,KAAK,OAAe,SAA8B;EAGhD,MAAM,WAAU,GAAI,SAAoB;GAEtC,KAAK,IAAI,OAAO,OAAO;GAEvB,QAAQ,GAAG,IAAI;EACjB;EAEA,OAAO,KAAK,GAAG,OAAO,OAAO;CAC/B;;;;;;;CAQA,UAAU,OAAsB;EAC9B,IAAI,UAAU,KAAA,GAEZ,KAAK,UAAU,OAAO,KAAK;OAG3B,KAAK,UAAU,MAAM;CAEzB;AACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量事件总线
|
|
3
|
+
* 提供发布/订阅模式的事件管理功能,支持事件的注册、注销、触发等操作
|
|
4
|
+
*/
|
|
5
|
+
type Handler = (...args: unknown[]) => void;
|
|
6
|
+
/**
|
|
7
|
+
* EventEmitter - 事件发射器类
|
|
8
|
+
* 实现了经典的发布/订阅(Pub/Sub)模式,用于模块间的解耦通信
|
|
9
|
+
* 支持:一次性监听、批量移除、自动清理等功能
|
|
10
|
+
*/
|
|
11
|
+
export declare class EventEmitter {
|
|
12
|
+
private listeners;
|
|
13
|
+
/**
|
|
14
|
+
* 注册事件监听器
|
|
15
|
+
* @param event - 要监听的事件名称
|
|
16
|
+
* @param handler - 事件触发时执行的回调函数
|
|
17
|
+
* @returns 返回一个取消订阅的函数,调用即可移除该监听器
|
|
18
|
+
*/
|
|
19
|
+
on(event: string, handler: Handler): () => void;
|
|
20
|
+
/**
|
|
21
|
+
* 移除指定事件的某个监听器
|
|
22
|
+
* @param event - 事件名称
|
|
23
|
+
* @param handler - 要移除的处理函数引用(必须与注册时为同一函数引用)
|
|
24
|
+
*/
|
|
25
|
+
off(event: string, handler: Handler): void;
|
|
26
|
+
/**
|
|
27
|
+
* 触发(发射)指定事件,依次调用所有已注册的监听器
|
|
28
|
+
* @param event - 要触发的事件名称
|
|
29
|
+
* @param args - 传递给监听器函数的参数列表(剩余参数语法)
|
|
30
|
+
*/
|
|
31
|
+
emit(event: string, ...args: unknown[]): void;
|
|
32
|
+
/**
|
|
33
|
+
* 注册一次性事件监听器(触发一次后自动移除)
|
|
34
|
+
* @param event - 要监听的事件名称
|
|
35
|
+
* @param handler - 事件触发时执行的回调函数
|
|
36
|
+
* @returns 返回取消订阅的函数,可在触发前手动取消
|
|
37
|
+
*/
|
|
38
|
+
once(event: string, handler: Handler): () => void;
|
|
39
|
+
/**
|
|
40
|
+
* 批量移除事件监听器
|
|
41
|
+
* @param event - 可选的事件名称
|
|
42
|
+
* - 若传入事件名,则只移除该事件的所有监听器
|
|
43
|
+
* - 若不传参数,则清空所有事件的所有监听器
|
|
44
|
+
*/
|
|
45
|
+
removeAll(event?: string): void;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* ***********************************
|
|
3
|
-
* @composy/chat-core v0.0.
|
|
3
|
+
* @composy/chat-core v0.0.2 *
|
|
4
4
|
* Built with rollup *
|
|
5
|
-
* Build time: 2024-
|
|
5
|
+
* Build time: 2024-10-12 21:52:18 *
|
|
6
6
|
* Build mode: production *
|
|
7
7
|
* Minified: No *
|
|
8
8
|
* ***********************************
|
|
@@ -21,7 +21,7 @@ var MentionEngine = class {
|
|
|
21
21
|
};
|
|
22
22
|
this.selectedMentions = [];
|
|
23
23
|
this.emitter = emitter;
|
|
24
|
-
if (members) this.members =
|
|
24
|
+
if (members) this.members = members.map((member) => ({ ...member }));
|
|
25
25
|
}
|
|
26
26
|
getState() {
|
|
27
27
|
return {
|
|
@@ -30,10 +30,10 @@ var MentionEngine = class {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
getSelectedMentions() {
|
|
33
|
-
return
|
|
33
|
+
return this.selectedMentions.map((member) => ({ ...member }));
|
|
34
34
|
}
|
|
35
35
|
setMembers(members) {
|
|
36
|
-
this.members =
|
|
36
|
+
this.members = members.map((member) => ({ ...member }));
|
|
37
37
|
if (this.state.active) this.filter(this.state.query);
|
|
38
38
|
}
|
|
39
39
|
handleInput(text, cursorPos) {
|
|
@@ -48,7 +48,7 @@ var MentionEngine = class {
|
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
const query = beforeCursor.slice(atIndex + 1);
|
|
51
|
-
if (
|
|
51
|
+
if (/\s/.test(query)) {
|
|
52
52
|
this.deactivate();
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
@@ -73,7 +73,7 @@ var MentionEngine = class {
|
|
|
73
73
|
select(index) {
|
|
74
74
|
if (index < 0 || index >= this.state.filtered.length) return null;
|
|
75
75
|
const member = this.state.filtered[index];
|
|
76
|
-
if (!this.selectedMentions.
|
|
76
|
+
if (!this.selectedMentions.some((m) => m.id === member.id)) this.selectedMentions.push(member);
|
|
77
77
|
this.emitter.emit("mention-select", member);
|
|
78
78
|
this.deactivate();
|
|
79
79
|
return member;
|
|
@@ -92,7 +92,7 @@ var MentionEngine = class {
|
|
|
92
92
|
parseMentions(text) {
|
|
93
93
|
const mentions = [];
|
|
94
94
|
const sortedMembers = [...this.members].sort((a, b) => b.name.length - a.name.length);
|
|
95
|
-
for (const member of sortedMembers) if (new RegExp(`(^|\\s)@${this.escapeRegex(member.name)}(?=\\s|$|[\uFF0C\u3002\uFF01\uFF1F,.!?])`, "u").test(text) && !mentions.
|
|
95
|
+
for (const member of sortedMembers) if (new RegExp(`(^|\\s)@${this.escapeRegex(member.name)}(?=\\s|$|[\uFF0C\u3002\uFF01\uFF1F,.!?])`, "u").test(text) && !mentions.some((m) => m.id === member.id)) mentions.push({ ...member });
|
|
96
96
|
return mentions;
|
|
97
97
|
}
|
|
98
98
|
resetSelected() {
|
|
@@ -104,7 +104,7 @@ var MentionEngine = class {
|
|
|
104
104
|
filter(query) {
|
|
105
105
|
const normalizedQuery = query.trim().toLowerCase();
|
|
106
106
|
this.state.query = query;
|
|
107
|
-
this.state.filtered = this.members.filter((member) => member.name.toLowerCase().includes(normalizedQuery) || member.id.toLowerCase().includes(normalizedQuery));
|
|
107
|
+
this.state.filtered = this.members.filter((member) => member.name.toLowerCase().includes(normalizedQuery) || member.id.toLowerCase().includes(normalizedQuery)).map((member) => ({ ...member }));
|
|
108
108
|
this.state.activeIndex = 0;
|
|
109
109
|
this.emitter.emit("mention-filter", this.getState());
|
|
110
110
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mention-engine.cjs","names":[],"sources":["../../src/core/mention-engine.ts"],"sourcesContent":["/**\n * @file mention-engine.ts\n * @description mention-engine 模块\n */\n\nimport type { MentionItem } from '../types'
|
|
1
|
+
{"version":3,"file":"mention-engine.cjs","names":[],"sources":["../../src/core/mention-engine.ts"],"sourcesContent":["/**\r\n * @file mention-engine.ts\r\n * @description mention-engine 模块\r\n */\r\n\r\nimport type { MentionItem } from '../types'\r\nimport type { EventEmitter } from './event-emitter'\r\n\r\n/**\r\n * MentionState interface\r\n */\r\nexport interface MentionState {\r\n active: boolean\r\n query: string\r\n filtered: MentionItem[]\r\n activeIndex: number\r\n triggerPosition: number\r\n}\r\n\r\n/**\r\n * MentionEngine 类\r\n */\r\nexport class MentionEngine {\r\n private members: MentionItem[] = []\r\n private emitter: EventEmitter\r\n private state: MentionState = {\r\n active: false,\r\n query: '',\r\n filtered: [],\r\n activeIndex: 0,\r\n triggerPosition: -1,\r\n }\r\n private selectedMentions: MentionItem[] = []\r\n\r\n constructor(emitter: EventEmitter, members?: MentionItem[]) {\r\n this.emitter = emitter\r\n if (members) this.members = members.map(member => ({ ...member }))\r\n }\r\n\r\n getState(): MentionState {\r\n return {\r\n ...this.state,\r\n filtered: [...this.state.filtered],\r\n }\r\n }\r\n\r\n getSelectedMentions(): MentionItem[] {\r\n return this.selectedMentions.map(member => ({ ...member }))\r\n }\r\n\r\n setMembers(members: MentionItem[]): void {\r\n this.members = members.map(member => ({ ...member }))\r\n if (this.state.active) {\r\n this.filter(this.state.query)\r\n }\r\n }\r\n\r\n handleInput(text: string, cursorPos: number): void {\r\n const beforeCursor = text.slice(0, cursorPos)\r\n const atIndex = beforeCursor.lastIndexOf('@')\r\n\r\n if (atIndex === -1) {\r\n this.deactivate()\r\n return\r\n }\r\n\r\n if (atIndex > 0 && !/\\s/.test(beforeCursor[atIndex - 1])) {\r\n this.deactivate()\r\n return\r\n }\r\n\r\n const query = beforeCursor.slice(atIndex + 1)\r\n if (/\\s/.test(query)) {\r\n this.deactivate()\r\n return\r\n }\r\n\r\n this.state.active = true\r\n this.state.triggerPosition = atIndex\r\n this.filter(query)\r\n }\r\n\r\n moveUp(): void {\r\n if (!this.state.active || this.state.filtered.length === 0) return\r\n this.state.activeIndex =\r\n (this.state.activeIndex - 1 + this.state.filtered.length) %\r\n this.state.filtered.length\r\n this.emitter.emit('mention-active-change', this.state.activeIndex)\r\n }\r\n\r\n moveDown(): void {\r\n if (!this.state.active || this.state.filtered.length === 0) return\r\n this.state.activeIndex =\r\n (this.state.activeIndex + 1) % this.state.filtered.length\r\n this.emitter.emit('mention-active-change', this.state.activeIndex)\r\n }\r\n\r\n selectCurrent(): MentionItem | null {\r\n if (!this.state.active || this.state.filtered.length === 0) return null\r\n return this.select(this.state.activeIndex)\r\n }\r\n\r\n select(index: number): MentionItem | null {\r\n if (index < 0 || index >= this.state.filtered.length) return null\r\n const member = this.state.filtered[index]\r\n if (!this.selectedMentions.some(m => m.id === member.id)) {\r\n this.selectedMentions.push(member)\r\n }\r\n this.emitter.emit('mention-select', member)\r\n this.deactivate()\r\n return member\r\n }\r\n\r\n deactivate(): void {\r\n if (!this.state.active) return\r\n this.state = {\r\n active: false,\r\n query: '',\r\n filtered: [],\r\n activeIndex: 0,\r\n triggerPosition: -1,\r\n }\r\n this.emitter.emit('mention-deactivate')\r\n }\r\n\r\n parseMentions(text: string): MentionItem[] {\r\n const mentions: MentionItem[] = []\r\n const sortedMembers = [...this.members].sort(\r\n (a, b) => b.name.length - a.name.length\r\n )\r\n\r\n for (const member of sortedMembers) {\r\n const pattern = new RegExp(\r\n `(^|\\\\s)@${this.escapeRegex(member.name)}(?=\\\\s|$|[,。!?,.!?])`,\r\n 'u'\r\n )\r\n if (pattern.test(text) && !mentions.some(m => m.id === member.id)) {\r\n mentions.push({ ...member })\r\n }\r\n }\r\n\r\n return mentions\r\n }\r\n\r\n resetSelected(): void {\r\n this.selectedMentions = []\r\n }\r\n\r\n getInsertText(member: MentionItem): string {\r\n return `@${member.name} `\r\n }\r\n\r\n private filter(query: string): void {\r\n const normalizedQuery = query.trim().toLowerCase()\r\n this.state.query = query\r\n this.state.filtered = this.members\r\n .filter(\r\n member =>\r\n member.name.toLowerCase().includes(normalizedQuery) ||\r\n member.id.toLowerCase().includes(normalizedQuery)\r\n )\r\n .map(member => ({ ...member }))\r\n this.state.activeIndex = 0\r\n this.emitter.emit('mention-filter', this.getState())\r\n }\r\n\r\n private escapeRegex(str: string): string {\r\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file mention-engine.ts
|
|
3
|
+
* @description mention-engine 模块
|
|
4
|
+
*/
|
|
5
|
+
import type { MentionItem } from '../types';
|
|
6
|
+
import type { EventEmitter } from './event-emitter';
|
|
7
|
+
/**
|
|
8
|
+
* MentionState interface
|
|
9
|
+
*/
|
|
10
|
+
export interface MentionState {
|
|
11
|
+
active: boolean;
|
|
12
|
+
query: string;
|
|
13
|
+
filtered: MentionItem[];
|
|
14
|
+
activeIndex: number;
|
|
15
|
+
triggerPosition: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* MentionEngine 类
|
|
19
|
+
*/
|
|
20
|
+
export declare class MentionEngine {
|
|
21
|
+
private members;
|
|
22
|
+
private emitter;
|
|
23
|
+
private state;
|
|
24
|
+
private selectedMentions;
|
|
25
|
+
constructor(emitter: EventEmitter, members?: MentionItem[]);
|
|
26
|
+
getState(): MentionState;
|
|
27
|
+
getSelectedMentions(): MentionItem[];
|
|
28
|
+
setMembers(members: MentionItem[]): void;
|
|
29
|
+
handleInput(text: string, cursorPos: number): void;
|
|
30
|
+
moveUp(): void;
|
|
31
|
+
moveDown(): void;
|
|
32
|
+
selectCurrent(): MentionItem | null;
|
|
33
|
+
select(index: number): MentionItem | null;
|
|
34
|
+
deactivate(): void;
|
|
35
|
+
parseMentions(text: string): MentionItem[];
|
|
36
|
+
resetSelected(): void;
|
|
37
|
+
getInsertText(member: MentionItem): string;
|
|
38
|
+
private filter;
|
|
39
|
+
private escapeRegex;
|
|
40
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @description mention-engine 模块
|
|
4
4
|
*/
|
|
5
5
|
import type { MentionItem } from '../types';
|
|
6
|
-
import { EventEmitter } from './event-emitter';
|
|
6
|
+
import type { EventEmitter } from './event-emitter';
|
|
7
7
|
/**
|
|
8
8
|
* MentionState interface
|
|
9
9
|
*/
|
|
@@ -38,4 +38,3 @@ export declare class MentionEngine {
|
|
|
38
38
|
private filter;
|
|
39
39
|
private escapeRegex;
|
|
40
40
|
}
|
|
41
|
-
//# sourceMappingURL=mention-engine.d.ts.map
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ***********************************
|
|
3
|
+
* @composy/chat-core v0.0.2 *
|
|
4
|
+
* Built with rollup *
|
|
5
|
+
* Build time: 2024-10-12 21:52:18 *
|
|
6
|
+
* Build mode: production *
|
|
7
|
+
* Minified: No *
|
|
8
|
+
* ***********************************
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
11
|
+
require("../types/index.cjs");
|
|
12
|
+
//#region src/core/message-schema.ts
|
|
13
|
+
function normalizeChatMessage(message) {
|
|
14
|
+
if (!message || typeof message !== "object") throw new TypeError("Chat message must be an object.");
|
|
15
|
+
if (typeof message.id !== "string" || !message.id.trim()) throw new TypeError("Chat message ID must be a non-empty string.");
|
|
16
|
+
if (!Number.isFinite(message.timestamp)) throw new TypeError("Chat message timestamp must be finite.");
|
|
17
|
+
const schemaVersion = message.schemaVersion ?? 1;
|
|
18
|
+
if (schemaVersion > 1) throw new Error(`Unsupported chat message schema version: ${schemaVersion}.`);
|
|
19
|
+
return cloneChatMessage({
|
|
20
|
+
...message,
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
...message.contentBlocks && { contentBlocks: normalizeChatContentBlocks(message.contentBlocks) }
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function normalizeChatContentBlocks(blocks) {
|
|
26
|
+
const normalized = [];
|
|
27
|
+
for (const value of blocks) {
|
|
28
|
+
const block = normalizeContentBlock(value);
|
|
29
|
+
if (block) normalized.push(block);
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
function normalizeChatMessages(messages) {
|
|
34
|
+
const result = [];
|
|
35
|
+
const ids = /* @__PURE__ */ new Set();
|
|
36
|
+
const clientIds = /* @__PURE__ */ new Set();
|
|
37
|
+
for (const message of messages) {
|
|
38
|
+
const normalized = normalizeChatMessage(message);
|
|
39
|
+
if (ids.has(normalized.id)) throw new Error(`Duplicate chat message ID: ${normalized.id}`);
|
|
40
|
+
if (normalized.clientMessageId && clientIds.has(normalized.clientMessageId)) throw new Error(`Duplicate chat client message ID: ${normalized.clientMessageId}`);
|
|
41
|
+
ids.add(normalized.id);
|
|
42
|
+
if (normalized.clientMessageId) clientIds.add(normalized.clientMessageId);
|
|
43
|
+
result.push(normalized);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function cloneChatMessage(message) {
|
|
48
|
+
return {
|
|
49
|
+
...message,
|
|
50
|
+
sender: cloneUser(message.sender),
|
|
51
|
+
...message.image && { image: cloneImage(message.image) },
|
|
52
|
+
...message.file && { file: cloneFile(message.file) },
|
|
53
|
+
...message.mentions && { mentions: message.mentions.map(cloneMention) },
|
|
54
|
+
...message.contentBlocks && { contentBlocks: message.contentBlocks.map(cloneContentBlock) },
|
|
55
|
+
...message.customData && { customData: cloneRecord(message.customData) }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function compareChatMessages(left, right) {
|
|
59
|
+
if (Number.isFinite(left.sequence) && Number.isFinite(right.sequence) && left.sequence !== right.sequence) return left.sequence - right.sequence;
|
|
60
|
+
if (left.timestamp !== right.timestamp) return left.timestamp - right.timestamp;
|
|
61
|
+
return left.id.localeCompare(right.id);
|
|
62
|
+
}
|
|
63
|
+
function cloneContentBlock(block) {
|
|
64
|
+
switch (block.type) {
|
|
65
|
+
case "text": return {
|
|
66
|
+
type: "text",
|
|
67
|
+
text: block.text
|
|
68
|
+
};
|
|
69
|
+
case "mention": return {
|
|
70
|
+
type: "mention",
|
|
71
|
+
id: block.id,
|
|
72
|
+
label: block.label
|
|
73
|
+
};
|
|
74
|
+
case "link": return {
|
|
75
|
+
type: "link",
|
|
76
|
+
url: block.url,
|
|
77
|
+
...block.label && { label: block.label }
|
|
78
|
+
};
|
|
79
|
+
case "code": return {
|
|
80
|
+
type: "code",
|
|
81
|
+
code: block.code,
|
|
82
|
+
...block.language && { language: block.language }
|
|
83
|
+
};
|
|
84
|
+
case "image": return {
|
|
85
|
+
type: "image",
|
|
86
|
+
src: block.src,
|
|
87
|
+
...block.alt && { alt: block.alt },
|
|
88
|
+
...block.width !== void 0 && { width: block.width },
|
|
89
|
+
...block.height !== void 0 && { height: block.height }
|
|
90
|
+
};
|
|
91
|
+
case "file": return {
|
|
92
|
+
type: "file",
|
|
93
|
+
url: block.url,
|
|
94
|
+
name: block.name,
|
|
95
|
+
...block.size !== void 0 && { size: block.size },
|
|
96
|
+
...block.mimeType && { mimeType: block.mimeType }
|
|
97
|
+
};
|
|
98
|
+
case "break": return { type: "break" };
|
|
99
|
+
case "custom": return {
|
|
100
|
+
type: "custom",
|
|
101
|
+
kind: block.kind,
|
|
102
|
+
data: cloneRecord(block.data),
|
|
103
|
+
...block.fallback && { fallback: block.fallback }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function normalizeContentBlock(value) {
|
|
108
|
+
if (!isRecord(value) || typeof value.type !== "string") return null;
|
|
109
|
+
switch (value.type) {
|
|
110
|
+
case "text": return typeof value.text === "string" ? {
|
|
111
|
+
type: "text",
|
|
112
|
+
text: value.text
|
|
113
|
+
} : null;
|
|
114
|
+
case "mention": return typeof value.id === "string" && typeof value.label === "string" ? {
|
|
115
|
+
type: "mention",
|
|
116
|
+
id: value.id,
|
|
117
|
+
label: value.label
|
|
118
|
+
} : null;
|
|
119
|
+
case "link": return typeof value.url === "string" ? {
|
|
120
|
+
type: "link",
|
|
121
|
+
url: value.url,
|
|
122
|
+
...typeof value.label === "string" && { label: value.label }
|
|
123
|
+
} : null;
|
|
124
|
+
case "code": return typeof value.code === "string" ? {
|
|
125
|
+
type: "code",
|
|
126
|
+
code: value.code,
|
|
127
|
+
...typeof value.language === "string" && { language: value.language }
|
|
128
|
+
} : null;
|
|
129
|
+
case "image": return typeof value.src === "string" ? {
|
|
130
|
+
type: "image",
|
|
131
|
+
src: value.src,
|
|
132
|
+
...typeof value.alt === "string" && { alt: value.alt },
|
|
133
|
+
...isPositiveNumber(value.width) && { width: value.width },
|
|
134
|
+
...isPositiveNumber(value.height) && { height: value.height }
|
|
135
|
+
} : null;
|
|
136
|
+
case "file": return typeof value.url === "string" && typeof value.name === "string" ? {
|
|
137
|
+
type: "file",
|
|
138
|
+
url: value.url,
|
|
139
|
+
name: value.name,
|
|
140
|
+
...isPositiveNumber(value.size) && { size: value.size },
|
|
141
|
+
...typeof value.mimeType === "string" && { mimeType: value.mimeType }
|
|
142
|
+
} : null;
|
|
143
|
+
case "break": return { type: "break" };
|
|
144
|
+
case "custom": return typeof value.kind === "string" && isRecord(value.data) ? {
|
|
145
|
+
type: "custom",
|
|
146
|
+
kind: value.kind,
|
|
147
|
+
data: cloneRecord(value.data),
|
|
148
|
+
...typeof value.fallback === "string" && { fallback: value.fallback }
|
|
149
|
+
} : fallbackBlock(value);
|
|
150
|
+
default: return fallbackBlock(value);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function fallbackBlock(value) {
|
|
154
|
+
return typeof value.fallback === "string" ? {
|
|
155
|
+
type: "text",
|
|
156
|
+
text: value.fallback
|
|
157
|
+
} : null;
|
|
158
|
+
}
|
|
159
|
+
function isRecord(value) {
|
|
160
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
function isPositiveNumber(value) {
|
|
163
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
164
|
+
}
|
|
165
|
+
function cloneUser(user) {
|
|
166
|
+
return { ...user };
|
|
167
|
+
}
|
|
168
|
+
function cloneMention(mention) {
|
|
169
|
+
return { ...mention };
|
|
170
|
+
}
|
|
171
|
+
function cloneImage(image) {
|
|
172
|
+
return { ...image };
|
|
173
|
+
}
|
|
174
|
+
function cloneFile(file) {
|
|
175
|
+
return { ...file };
|
|
176
|
+
}
|
|
177
|
+
function cloneRecord(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
178
|
+
if (!value || typeof value !== "object") return value;
|
|
179
|
+
if (seen.has(value)) return seen.get(value);
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
const clone2 = [];
|
|
182
|
+
seen.set(value, clone2);
|
|
183
|
+
value.forEach((item) => clone2.push(cloneRecord(item, seen)));
|
|
184
|
+
return clone2;
|
|
185
|
+
}
|
|
186
|
+
const prototype = Object.getPrototypeOf(value);
|
|
187
|
+
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
188
|
+
const clone = {};
|
|
189
|
+
seen.set(value, clone);
|
|
190
|
+
Object.entries(value).forEach(([key, item]) => {
|
|
191
|
+
clone[key] = cloneRecord(item, seen);
|
|
192
|
+
});
|
|
193
|
+
return clone;
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
exports.cloneChatMessage = cloneChatMessage;
|
|
197
|
+
exports.compareChatMessages = compareChatMessages;
|
|
198
|
+
exports.normalizeChatContentBlocks = normalizeChatContentBlocks;
|
|
199
|
+
exports.normalizeChatMessage = normalizeChatMessage;
|
|
200
|
+
exports.normalizeChatMessages = normalizeChatMessages;
|
|
201
|
+
/*! End of @composy/chat-core | Powered by @composy/builder */
|
|
202
|
+
//# sourceMappingURL=message-schema.cjs.map
|