@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-manager.js","names":[],"sources":["../../src/core/chat-manager.ts"],"sourcesContent":["/**\n * 聊天管理器 - 消息增删改查、状态管理\n */\n\nimport type { ChatMessage } from '../types';\nimport { EventEmitter } from './event-emitter';\n\n/**\n * ChatManager - Chat Manager class\n */\nexport class ChatManager {\n private messages: ChatMessage[] = [];\n private emitter: EventEmitter;\n\n constructor(emitter: EventEmitter, initialMessages?: ChatMessage[]) {\n this.emitter = emitter;\n if (initialMessages?.length) {\n this.messages = [...initialMessages];\n }\n }\n\n /** 获取所有消息 */\n getMessages(): ChatMessage[] {\n return [...this.messages];\n }\n\n /** 根据 ID 获取消息 */\n getMessage(id: string): ChatMessage | undefined {\n return this.messages.find((m) => m.id === id);\n }\n\n /** 添加消息(末尾追加) */\n addMessage(message: ChatMessage): void {\n this.messages.push(message);\n this.emitter.emit('message-added', message);\n this.emitter.emit('messages-changed', this.getMessages());\n }\n\n /** 在顶部批量插入消息(用于加载更多) */\n prependMessages(messages: ChatMessage[]): void {\n this.messages.unshift(...messages);\n this.emitter.emit('messages-prepended', messages);\n this.emitter.emit('messages-changed', this.getMessages());\n }\n\n /** 更新消息 */\n updateMessage(id: string, updates: Partial<ChatMessage>): void {\n const index = this.messages.findIndex((m) => m.id === id);\n if (index === -1) return;\n\n this.messages[index] = { ...this.messages[index], ...updates };\n this.emitter.emit('message-updated', this.messages[index]);\n this.emitter.emit('messages-changed', this.getMessages());\n }\n\n /** 删除消息 */\n removeMessage(id: string): void {\n const index = this.messages.findIndex((m) => m.id === id);\n if (index === -1) return;\n\n const removed = this.messages.splice(index, 1)[0];\n this.emitter.emit('message-removed', removed);\n this.emitter.emit('messages-changed', this.getMessages());\n }\n\n /** 替换全部消息 */\n setMessages(messages: ChatMessage[]): void {\n this.messages = [...messages];\n this.emitter.emit('messages-changed', this.getMessages());\n }\n\n /** 清空所有消息 */\n clearMessages(): void {\n this.messages = [];\n this.emitter.emit('messages-changed', []);\n }\n\n /** 追加流式消息内容 */\n appendMessageContent(id: string, chunk: string): void {\n const index = this.messages.findIndex((m) => m.id === id);\n if (index === -1) return;\n\n const msg = this.messages[index];\n this.messages[index] = {\n ...msg,\n content: (msg.content ?? '') + chunk,\n };\n this.emitter.emit('message-content-appended', id, this.messages[index].content);\n this.emitter.emit('messages-changed', this.getMessages());\n }\n\n /** 完成流式消息 */\n finishStreaming(id: string): void {\n const index = this.messages.findIndex((m) => m.id === id);\n if (index === -1) return;\n\n this.messages[index] = { ...this.messages[index], streaming: false };\n this.emitter.emit('message-updated', this.messages[index]);\n this.emitter.emit('messages-changed', this.getMessages());\n }\n}\n"],"mappings":";;;;;;;;;;AAUO,IAAM,cAAN,MAAkB;CAIvB,YAAY,SAAuB,iBAAiC;EAHpE,KAAQ,WAA0B,CAAC;EAIjC,KAAK,UAAU;EACf,IAAI,iBAAiB,QACnB,KAAK,WAAW,CAAC,GAAG,eAAe;CAEvC;;CAGA,cAA6B;EAC3B,OAAO,CAAC,GAAG,KAAK,QAAQ;CAC1B;;CAGA,WAAW,IAAqC;EAC9C,OAAO,KAAK,SAAS,MAAM,MAAM,EAAE,OAAO,EAAE;CAC9C;;CAGA,WAAW,SAA4B;EACrC,KAAK,SAAS,KAAK,OAAO;EAC1B,KAAK,QAAQ,KAAK,iBAAiB,OAAO;EAC1C,KAAK,QAAQ,KAAK,oBAAoB,KAAK,YAAY,CAAC;CAC1D;;CAGA,gBAAgB,UAA+B;EAC7C,KAAK,SAAS,QAAQ,GAAG,QAAQ;EACjC,KAAK,QAAQ,KAAK,sBAAsB,QAAQ;EAChD,KAAK,QAAQ,KAAK,oBAAoB,KAAK,YAAY,CAAC;CAC1D;;CAGA,cAAc,IAAY,SAAqC;EAC7D,MAAM,QAAQ,KAAK,SAAS,WAAW,MAAM,EAAE,OAAO,EAAE;EACxD,IAAI,UAAU,IAAI;EAElB,KAAK,SAAS,SAAS;GAAE,GAAG,KAAK,SAAS;GAAQ,GAAG;EAAQ;EAC7D,KAAK,QAAQ,KAAK,mBAAmB,KAAK,SAAS,MAAM;EACzD,KAAK,QAAQ,KAAK,oBAAoB,KAAK,YAAY,CAAC;CAC1D;;CAGA,cAAc,IAAkB;EAC9B,MAAM,QAAQ,KAAK,SAAS,WAAW,MAAM,EAAE,OAAO,EAAE;EACxD,IAAI,UAAU,IAAI;EAElB,MAAM,UAAU,KAAK,SAAS,OAAO,OAAO,CAAC,CAAA,CAAE;EAC/C,KAAK,QAAQ,KAAK,mBAAmB,OAAO;EAC5C,KAAK,QAAQ,KAAK,oBAAoB,KAAK,YAAY,CAAC;CAC1D;;CAGA,YAAY,UAA+B;EACzC,KAAK,WAAW,CAAC,GAAG,QAAQ;EAC5B,KAAK,QAAQ,KAAK,oBAAoB,KAAK,YAAY,CAAC;CAC1D;;CAGA,gBAAsB;EACpB,KAAK,WAAW,CAAC;EACjB,KAAK,QAAQ,KAAK,oBAAoB,CAAC,CAAC;CAC1C;;CAGA,qBAAqB,IAAY,OAAqB;EACpD,MAAM,QAAQ,KAAK,SAAS,WAAW,MAAM,EAAE,OAAO,EAAE;EACxD,IAAI,UAAU,IAAI;EAElB,MAAM,MAAM,KAAK,SAAS;EAC1B,KAAK,SAAS,SAAS;GACrB,GAAG;GACH,UAAU,IAAI,WAAW,MAAM;EACjC;EACA,KAAK,QAAQ,KAAK,4BAA4B,IAAI,KAAK,SAAS,MAAK,CAAE,OAAO;EAC9E,KAAK,QAAQ,KAAK,oBAAoB,KAAK,YAAY,CAAC;CAC1D;;CAGA,gBAAgB,IAAkB;EAChC,MAAM,QAAQ,KAAK,SAAS,WAAW,MAAM,EAAE,OAAO,EAAE;EACxD,IAAI,UAAU,IAAI;EAElB,KAAK,SAAS,SAAS;GAAE,GAAG,KAAK,SAAS;GAAQ,WAAW;EAAM;EACnE,KAAK,QAAQ,KAAK,mBAAmB,KAAK,SAAS,MAAM;EACzD,KAAK,QAAQ,KAAK,oBAAoB,KAAK,YAAY,CAAC;CAC1D;AACF"}
|
|
1
|
+
{"version":3,"file":"chat-manager.js","names":["message"],"sources":["../../src/core/chat-manager.ts"],"sourcesContent":["import type { ChatMessage, MessageStatus } from '../types'\nimport type { EventEmitter } from './event-emitter'\nimport {\n cloneChatMessage,\n normalizeChatMessage,\n normalizeChatMessages,\n} from './message-schema'\n\nexport type MessageMergeResult = 'added' | 'updated' | 'ignored'\n\nexport interface MessageMergeOptions {\n sequence?: number\n source?: 'local' | 'remote'\n}\n\nexport interface StreamAppendOptions {\n generation?: string\n sequence?: number\n}\n\ninterface StreamState {\n generation: string\n lastSequence: number\n}\n\n/** ChatManager - 聊天消息状态、幂等合并和流式 generation 管理。 */\nexport class ChatManager {\n private messages: ChatMessage[] = []\n private readonly emitter: EventEmitter\n private readonly streams = new Map<string, StreamState>()\n\n constructor(emitter: EventEmitter, initialMessages?: ChatMessage[]) {\n this.emitter = emitter\n if (initialMessages?.length) {\n this.messages = normalizeChatMessages(initialMessages)\n }\n }\n\n getMessages(): ChatMessage[] {\n return this.messages.map(cloneChatMessage)\n }\n\n getMessage(id: string): ChatMessage | undefined {\n const message = this.messages.find(item => item.id === id)\n return message ? cloneChatMessage(message) : undefined\n }\n\n getMessageIdByClientMessageId(clientMessageId: string): string | undefined {\n return this.messages.find(\n message => message.clientMessageId === clientMessageId\n )?.id\n }\n\n addMessage(message: ChatMessage): void {\n const storedMessage = normalizeChatMessage(message)\n this.assertUniqueIds([storedMessage])\n this.messages.push(storedMessage)\n this.emitAdded(storedMessage)\n }\n\n prependMessages(messages: ChatMessage[]): void {\n const storedMessages = normalizeChatMessages(messages)\n this.assertUniqueIds(storedMessages)\n this.messages.unshift(...storedMessages)\n this.emitter.emit(\n 'messages-prepended',\n storedMessages.map(cloneChatMessage)\n )\n this.emitChanged()\n }\n\n /** 合并本地 ack 或远端推送,按 id/clientMessageId 去重并拒绝旧 sequence。 */\n upsertMessage(\n message: ChatMessage,\n options: MessageMergeOptions = {}\n ): MessageMergeResult {\n const normalized = normalizeChatMessage({\n ...message,\n ...(options.sequence !== undefined && { sequence: options.sequence }),\n })\n const index = this.findMessageIndex(normalized)\n\n if (index < 0) {\n this.messages.push(normalized)\n this.emitAdded(normalized)\n return 'added'\n }\n\n const current = this.messages[index]\n if (\n Number.isFinite(normalized.sequence) &&\n Number.isFinite(current.sequence) &&\n (normalized.sequence as number) <= (current.sequence as number)\n ) {\n return 'ignored'\n }\n\n const merged = normalizeChatMessage({\n ...current,\n ...normalized,\n id: current.id,\n clientMessageId: normalized.clientMessageId ?? current.clientMessageId,\n sequence: normalized.sequence ?? current.sequence,\n })\n this.messages[index] = merged\n this.emitter.emit('message-updated', cloneChatMessage(merged))\n this.emitChanged()\n return 'updated'\n }\n\n mergeMessages(\n messages: ChatMessage[],\n options: MessageMergeOptions = {}\n ): { added: number; updated: number; ignored: number } {\n const result = { added: 0, updated: 0, ignored: 0 }\n for (const message of messages) {\n const outcome = this.upsertMessage(message, options)\n result[outcome] += 1\n }\n return result\n }\n\n updateMessage(id: string, updates: Partial<ChatMessage>): void {\n const index = this.messages.findIndex(message => message.id === id)\n if (index === -1) return\n if (updates.id !== undefined && updates.id !== id) {\n throw new Error('Chat message IDs are immutable.')\n }\n\n const next = normalizeChatMessage({\n ...this.messages[index],\n ...updates,\n id,\n })\n this.assertUniqueClientMessageId(next, id)\n this.messages[index] = next\n this.emitter.emit('message-updated', cloneChatMessage(next))\n this.emitChanged()\n }\n\n markMessageStatus(id: string, status: MessageStatus): void {\n this.updateMessage(id, { status })\n }\n\n removeMessage(id: string): void {\n const index = this.messages.findIndex(message => message.id === id)\n if (index === -1) return\n\n const [removed] = this.messages.splice(index, 1)\n this.streams.delete(id)\n this.emitter.emit('message-removed', cloneChatMessage(removed))\n this.emitChanged()\n }\n\n setMessages(messages: ChatMessage[]): void {\n this.messages = normalizeChatMessages(messages)\n this.streams.clear()\n this.emitChanged()\n }\n\n clearMessages(): void {\n this.messages = []\n this.streams.clear()\n this.emitChanged()\n }\n\n beginStreaming(id: string, generation = createGeneration()): string | null {\n const index = this.messages.findIndex(message => message.id === id)\n if (index === -1) return null\n this.streams.set(id, { generation, lastSequence: -1 })\n this.messages[index] = normalizeChatMessage({\n ...this.messages[index],\n streaming: true,\n })\n this.emitter.emit('message-updated', cloneChatMessage(this.messages[index]))\n this.emitChanged()\n return generation\n }\n\n appendMessageContent(\n id: string,\n chunk: string,\n options: StreamAppendOptions = {}\n ): boolean {\n const stream = this.streams.get(id)\n if (options.generation) {\n if (!stream || stream.generation !== options.generation) return false\n const sequence = options.sequence\n if (\n sequence !== undefined &&\n Number.isFinite(sequence) &&\n sequence <= stream.lastSequence\n ) {\n return false\n }\n if (sequence !== undefined && Number.isFinite(sequence)) {\n stream.lastSequence = sequence\n }\n }\n\n const index = this.messages.findIndex(message => message.id === id)\n if (index === -1) return false\n const message = this.messages[index]\n this.messages[index] = normalizeChatMessage({\n ...message,\n content: (message.content ?? '') + chunk,\n streaming: options.generation ? true : message.streaming,\n })\n this.emitter.emit(\n 'message-content-appended',\n id,\n this.messages[index].content,\n options.generation,\n options.sequence\n )\n this.emitChanged()\n return true\n }\n\n finishStreaming(id: string, generation?: string): boolean {\n const stream = this.streams.get(id)\n if (generation && (!stream || stream.generation !== generation)) {\n return false\n }\n const index = this.messages.findIndex(message => message.id === id)\n if (index === -1) return false\n this.messages[index] = normalizeChatMessage({\n ...this.messages[index],\n streaming: false,\n })\n this.streams.delete(id)\n this.emitter.emit('message-updated', cloneChatMessage(this.messages[index]))\n this.emitChanged()\n return true\n }\n\n cancelStreaming(id: string, generation?: string): boolean {\n const stream = this.streams.get(id)\n if (generation && (!stream || stream.generation !== generation)) {\n return false\n }\n const index = this.messages.findIndex(message => message.id === id)\n if (index === -1) return false\n this.streams.delete(id)\n this.messages[index] = normalizeChatMessage({\n ...this.messages[index],\n streaming: false,\n })\n this.emitter.emit(\n 'stream-cancelled',\n cloneChatMessage(this.messages[index])\n )\n this.emitter.emit('message-updated', cloneChatMessage(this.messages[index]))\n this.emitChanged()\n return true\n }\n\n getStreamGeneration(id: string): string | undefined {\n return this.streams.get(id)?.generation\n }\n\n private findMessageIndex(message: ChatMessage): number {\n const byId = this.messages.findIndex(item => item.id === message.id)\n if (byId >= 0) return byId\n if (!message.clientMessageId) return -1\n return this.messages.findIndex(\n item => item.clientMessageId === message.clientMessageId\n )\n }\n\n private assertUniqueIds(messages: ChatMessage[]): void {\n const ids = new Set(this.messages.map(message => message.id))\n const clientIds = new Set(\n this.messages.flatMap(message =>\n message.clientMessageId ? [message.clientMessageId] : []\n )\n )\n for (const message of messages) {\n if (!message.id || ids.has(message.id)) {\n throw new Error(\n `Chat message ID must be non-empty and unique: ${message.id}`\n )\n }\n if (message.clientMessageId && clientIds.has(message.clientMessageId)) {\n throw new Error(\n `Chat client message ID must be unique: ${message.clientMessageId}`\n )\n }\n ids.add(message.id)\n if (message.clientMessageId) clientIds.add(message.clientMessageId)\n }\n }\n\n private assertUniqueClientMessageId(message: ChatMessage, ownId: string) {\n if (\n message.clientMessageId &&\n this.messages.some(\n item =>\n item.id !== ownId && item.clientMessageId === message.clientMessageId\n )\n ) {\n throw new Error(\n `Chat client message ID must be unique: ${message.clientMessageId}`\n )\n }\n }\n\n private emitAdded(message: ChatMessage): void {\n this.emitter.emit('message-added', cloneChatMessage(message))\n this.emitChanged()\n }\n\n private emitChanged(): void {\n this.emitter.emit('messages-changed', this.getMessages())\n }\n}\n\nfunction createGeneration(): string {\n const cryptoObject = globalThis.crypto\n if (cryptoObject?.randomUUID) return cryptoObject.randomUUID()\n return `stream-${Date.now()}-${Math.random().toString(36).slice(2)}`\n}\n"],"mappings":"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"}
|
|
@@ -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
|
* ***********************************
|
|
@@ -19,8 +19,12 @@ var EventEmitter = class {
|
|
|
19
19
|
* @returns 返回一个取消订阅的函数,调用即可移除该监听器
|
|
20
20
|
*/
|
|
21
21
|
on(event, handler) {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
let handlers = this.listeners.get(event);
|
|
23
|
+
if (!handlers) {
|
|
24
|
+
handlers = /* @__PURE__ */ new Set();
|
|
25
|
+
this.listeners.set(event, handlers);
|
|
26
|
+
}
|
|
27
|
+
handlers.add(handler);
|
|
24
28
|
return () => this.off(event, handler);
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
@@ -29,7 +33,10 @@ var EventEmitter = class {
|
|
|
29
33
|
* @param handler - 要移除的处理函数引用(必须与注册时为同一函数引用)
|
|
30
34
|
*/
|
|
31
35
|
off(event, handler) {
|
|
32
|
-
this.listeners.get(event)
|
|
36
|
+
const handlers = this.listeners.get(event);
|
|
37
|
+
if (!handlers) return;
|
|
38
|
+
handlers.delete(handler);
|
|
39
|
+
if (handlers.size === 0) this.listeners.delete(event);
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
35
42
|
* 触发(发射)指定事件,依次调用所有已注册的监听器
|
|
@@ -37,7 +44,7 @@ var EventEmitter = class {
|
|
|
37
44
|
* @param args - 传递给监听器函数的参数列表(剩余参数语法)
|
|
38
45
|
*/
|
|
39
46
|
emit(event, ...args) {
|
|
40
|
-
this.listeners.get(event)
|
|
47
|
+
[...this.listeners.get(event) ?? []].forEach((handler) => {
|
|
41
48
|
try {
|
|
42
49
|
handler(...args);
|
|
43
50
|
} catch (err) {
|
|
@@ -65,7 +72,7 @@ var EventEmitter = class {
|
|
|
65
72
|
* - 若不传参数,则清空所有事件的所有监听器
|
|
66
73
|
*/
|
|
67
74
|
removeAll(event) {
|
|
68
|
-
if (event) this.listeners.delete(event);
|
|
75
|
+
if (event !== void 0) this.listeners.delete(event);
|
|
69
76
|
else this.listeners.clear();
|
|
70
77
|
}
|
|
71
78
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-emitter.js","names":[],"sources":["../../src/core/event-emitter.ts"],"sourcesContent":["/**\n * 轻量事件总线\n * 提供发布/订阅模式的事件管理功能,支持事件的注册、注销、触发等操作\n */\n\n// 定义事件处理函数的类型签名:接收任意数量的未知类型参数,无返回值\ntype Handler = (...args: unknown[]) => void
|
|
1
|
+
{"version":3,"file":"event-emitter.js","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"}
|
|
@@ -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
|
|
@@ -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,7 +20,7 @@ var MentionEngine = class {
|
|
|
20
20
|
};
|
|
21
21
|
this.selectedMentions = [];
|
|
22
22
|
this.emitter = emitter;
|
|
23
|
-
if (members) this.members =
|
|
23
|
+
if (members) this.members = members.map((member) => ({ ...member }));
|
|
24
24
|
}
|
|
25
25
|
getState() {
|
|
26
26
|
return {
|
|
@@ -29,10 +29,10 @@ var MentionEngine = class {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
getSelectedMentions() {
|
|
32
|
-
return
|
|
32
|
+
return this.selectedMentions.map((member) => ({ ...member }));
|
|
33
33
|
}
|
|
34
34
|
setMembers(members) {
|
|
35
|
-
this.members =
|
|
35
|
+
this.members = members.map((member) => ({ ...member }));
|
|
36
36
|
if (this.state.active) this.filter(this.state.query);
|
|
37
37
|
}
|
|
38
38
|
handleInput(text, cursorPos) {
|
|
@@ -47,7 +47,7 @@ var MentionEngine = class {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
const query = beforeCursor.slice(atIndex + 1);
|
|
50
|
-
if (
|
|
50
|
+
if (/\s/.test(query)) {
|
|
51
51
|
this.deactivate();
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
@@ -72,7 +72,7 @@ var MentionEngine = class {
|
|
|
72
72
|
select(index) {
|
|
73
73
|
if (index < 0 || index >= this.state.filtered.length) return null;
|
|
74
74
|
const member = this.state.filtered[index];
|
|
75
|
-
if (!this.selectedMentions.
|
|
75
|
+
if (!this.selectedMentions.some((m) => m.id === member.id)) this.selectedMentions.push(member);
|
|
76
76
|
this.emitter.emit("mention-select", member);
|
|
77
77
|
this.deactivate();
|
|
78
78
|
return member;
|
|
@@ -91,7 +91,7 @@ var MentionEngine = class {
|
|
|
91
91
|
parseMentions(text) {
|
|
92
92
|
const mentions = [];
|
|
93
93
|
const sortedMembers = [...this.members].sort((a, b) => b.name.length - a.name.length);
|
|
94
|
-
for (const member of sortedMembers) if (new RegExp(`(^|\\s)@${this.escapeRegex(member.name)}(?=\\s|$|[\uFF0C\u3002\uFF01\uFF1F,.!?])`, "u").test(text) && !mentions.
|
|
94
|
+
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 });
|
|
95
95
|
return mentions;
|
|
96
96
|
}
|
|
97
97
|
resetSelected() {
|
|
@@ -103,7 +103,7 @@ var MentionEngine = class {
|
|
|
103
103
|
filter(query) {
|
|
104
104
|
const normalizedQuery = query.trim().toLowerCase();
|
|
105
105
|
this.state.query = query;
|
|
106
|
-
this.state.filtered = this.members.filter((member) => member.name.toLowerCase().includes(normalizedQuery) || member.id.toLowerCase().includes(normalizedQuery));
|
|
106
|
+
this.state.filtered = this.members.filter((member) => member.name.toLowerCase().includes(normalizedQuery) || member.id.toLowerCase().includes(normalizedQuery)).map((member) => ({ ...member }));
|
|
107
107
|
this.state.activeIndex = 0;
|
|
108
108
|
this.emitter.emit("mention-filter", this.getState());
|
|
109
109
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mention-engine.js","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.js","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,19 @@
|
|
|
1
|
+
import type { ChatContentBlock, ChatMessage } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* 归一化消息并复制所有可变字段。
|
|
4
|
+
*
|
|
5
|
+
* 该函数是 transport、outbox、ChatManager 共享的边界:外部数据先经过
|
|
6
|
+
* schema/version 校验,再进入内存状态,避免调用方继续修改内部消息。
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeChatMessage(message: ChatMessage): ChatMessage;
|
|
9
|
+
/**
|
|
10
|
+
* 对来自网络/持久化的 content block 做运行时白名单归一化。
|
|
11
|
+
* 不认识的块优先使用 fallback 文本,否则丢弃,避免将任意对象带入渲染器。
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeChatContentBlocks(blocks: unknown[]): ChatContentBlock[];
|
|
14
|
+
/** 归一化消息集合,并在进入状态层前拒绝重复的消息 ID/client ID。 */
|
|
15
|
+
export declare function normalizeChatMessages(messages: ChatMessage[]): ChatMessage[];
|
|
16
|
+
/** 复制消息,用于事件 payload、outbox 与公开读取,避免可变引用泄漏。 */
|
|
17
|
+
export declare function cloneChatMessage(message: ChatMessage): ChatMessage;
|
|
18
|
+
/** 根据服务端 sequence/timestamp 给出稳定排序值。 */
|
|
19
|
+
export declare function compareChatMessages(left: ChatMessage, right: ChatMessage): number;
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
import "../types/index.js";
|
|
11
|
+
//#region src/core/message-schema.ts
|
|
12
|
+
function normalizeChatMessage(message) {
|
|
13
|
+
if (!message || typeof message !== "object") throw new TypeError("Chat message must be an object.");
|
|
14
|
+
if (typeof message.id !== "string" || !message.id.trim()) throw new TypeError("Chat message ID must be a non-empty string.");
|
|
15
|
+
if (!Number.isFinite(message.timestamp)) throw new TypeError("Chat message timestamp must be finite.");
|
|
16
|
+
const schemaVersion = message.schemaVersion ?? 1;
|
|
17
|
+
if (schemaVersion > 1) throw new Error(`Unsupported chat message schema version: ${schemaVersion}.`);
|
|
18
|
+
return cloneChatMessage({
|
|
19
|
+
...message,
|
|
20
|
+
schemaVersion: 1,
|
|
21
|
+
...message.contentBlocks && { contentBlocks: normalizeChatContentBlocks(message.contentBlocks) }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function normalizeChatContentBlocks(blocks) {
|
|
25
|
+
const normalized = [];
|
|
26
|
+
for (const value of blocks) {
|
|
27
|
+
const block = normalizeContentBlock(value);
|
|
28
|
+
if (block) normalized.push(block);
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
function normalizeChatMessages(messages) {
|
|
33
|
+
const result = [];
|
|
34
|
+
const ids = /* @__PURE__ */ new Set();
|
|
35
|
+
const clientIds = /* @__PURE__ */ new Set();
|
|
36
|
+
for (const message of messages) {
|
|
37
|
+
const normalized = normalizeChatMessage(message);
|
|
38
|
+
if (ids.has(normalized.id)) throw new Error(`Duplicate chat message ID: ${normalized.id}`);
|
|
39
|
+
if (normalized.clientMessageId && clientIds.has(normalized.clientMessageId)) throw new Error(`Duplicate chat client message ID: ${normalized.clientMessageId}`);
|
|
40
|
+
ids.add(normalized.id);
|
|
41
|
+
if (normalized.clientMessageId) clientIds.add(normalized.clientMessageId);
|
|
42
|
+
result.push(normalized);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function cloneChatMessage(message) {
|
|
47
|
+
return {
|
|
48
|
+
...message,
|
|
49
|
+
sender: cloneUser(message.sender),
|
|
50
|
+
...message.image && { image: cloneImage(message.image) },
|
|
51
|
+
...message.file && { file: cloneFile(message.file) },
|
|
52
|
+
...message.mentions && { mentions: message.mentions.map(cloneMention) },
|
|
53
|
+
...message.contentBlocks && { contentBlocks: message.contentBlocks.map(cloneContentBlock) },
|
|
54
|
+
...message.customData && { customData: cloneRecord(message.customData) }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function compareChatMessages(left, right) {
|
|
58
|
+
if (Number.isFinite(left.sequence) && Number.isFinite(right.sequence) && left.sequence !== right.sequence) return left.sequence - right.sequence;
|
|
59
|
+
if (left.timestamp !== right.timestamp) return left.timestamp - right.timestamp;
|
|
60
|
+
return left.id.localeCompare(right.id);
|
|
61
|
+
}
|
|
62
|
+
function cloneContentBlock(block) {
|
|
63
|
+
switch (block.type) {
|
|
64
|
+
case "text": return {
|
|
65
|
+
type: "text",
|
|
66
|
+
text: block.text
|
|
67
|
+
};
|
|
68
|
+
case "mention": return {
|
|
69
|
+
type: "mention",
|
|
70
|
+
id: block.id,
|
|
71
|
+
label: block.label
|
|
72
|
+
};
|
|
73
|
+
case "link": return {
|
|
74
|
+
type: "link",
|
|
75
|
+
url: block.url,
|
|
76
|
+
...block.label && { label: block.label }
|
|
77
|
+
};
|
|
78
|
+
case "code": return {
|
|
79
|
+
type: "code",
|
|
80
|
+
code: block.code,
|
|
81
|
+
...block.language && { language: block.language }
|
|
82
|
+
};
|
|
83
|
+
case "image": return {
|
|
84
|
+
type: "image",
|
|
85
|
+
src: block.src,
|
|
86
|
+
...block.alt && { alt: block.alt },
|
|
87
|
+
...block.width !== void 0 && { width: block.width },
|
|
88
|
+
...block.height !== void 0 && { height: block.height }
|
|
89
|
+
};
|
|
90
|
+
case "file": return {
|
|
91
|
+
type: "file",
|
|
92
|
+
url: block.url,
|
|
93
|
+
name: block.name,
|
|
94
|
+
...block.size !== void 0 && { size: block.size },
|
|
95
|
+
...block.mimeType && { mimeType: block.mimeType }
|
|
96
|
+
};
|
|
97
|
+
case "break": return { type: "break" };
|
|
98
|
+
case "custom": return {
|
|
99
|
+
type: "custom",
|
|
100
|
+
kind: block.kind,
|
|
101
|
+
data: cloneRecord(block.data),
|
|
102
|
+
...block.fallback && { fallback: block.fallback }
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function normalizeContentBlock(value) {
|
|
107
|
+
if (!isRecord(value) || typeof value.type !== "string") return null;
|
|
108
|
+
switch (value.type) {
|
|
109
|
+
case "text": return typeof value.text === "string" ? {
|
|
110
|
+
type: "text",
|
|
111
|
+
text: value.text
|
|
112
|
+
} : null;
|
|
113
|
+
case "mention": return typeof value.id === "string" && typeof value.label === "string" ? {
|
|
114
|
+
type: "mention",
|
|
115
|
+
id: value.id,
|
|
116
|
+
label: value.label
|
|
117
|
+
} : null;
|
|
118
|
+
case "link": return typeof value.url === "string" ? {
|
|
119
|
+
type: "link",
|
|
120
|
+
url: value.url,
|
|
121
|
+
...typeof value.label === "string" && { label: value.label }
|
|
122
|
+
} : null;
|
|
123
|
+
case "code": return typeof value.code === "string" ? {
|
|
124
|
+
type: "code",
|
|
125
|
+
code: value.code,
|
|
126
|
+
...typeof value.language === "string" && { language: value.language }
|
|
127
|
+
} : null;
|
|
128
|
+
case "image": return typeof value.src === "string" ? {
|
|
129
|
+
type: "image",
|
|
130
|
+
src: value.src,
|
|
131
|
+
...typeof value.alt === "string" && { alt: value.alt },
|
|
132
|
+
...isPositiveNumber(value.width) && { width: value.width },
|
|
133
|
+
...isPositiveNumber(value.height) && { height: value.height }
|
|
134
|
+
} : null;
|
|
135
|
+
case "file": return typeof value.url === "string" && typeof value.name === "string" ? {
|
|
136
|
+
type: "file",
|
|
137
|
+
url: value.url,
|
|
138
|
+
name: value.name,
|
|
139
|
+
...isPositiveNumber(value.size) && { size: value.size },
|
|
140
|
+
...typeof value.mimeType === "string" && { mimeType: value.mimeType }
|
|
141
|
+
} : null;
|
|
142
|
+
case "break": return { type: "break" };
|
|
143
|
+
case "custom": return typeof value.kind === "string" && isRecord(value.data) ? {
|
|
144
|
+
type: "custom",
|
|
145
|
+
kind: value.kind,
|
|
146
|
+
data: cloneRecord(value.data),
|
|
147
|
+
...typeof value.fallback === "string" && { fallback: value.fallback }
|
|
148
|
+
} : fallbackBlock(value);
|
|
149
|
+
default: return fallbackBlock(value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function fallbackBlock(value) {
|
|
153
|
+
return typeof value.fallback === "string" ? {
|
|
154
|
+
type: "text",
|
|
155
|
+
text: value.fallback
|
|
156
|
+
} : null;
|
|
157
|
+
}
|
|
158
|
+
function isRecord(value) {
|
|
159
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
160
|
+
}
|
|
161
|
+
function isPositiveNumber(value) {
|
|
162
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
163
|
+
}
|
|
164
|
+
function cloneUser(user) {
|
|
165
|
+
return { ...user };
|
|
166
|
+
}
|
|
167
|
+
function cloneMention(mention) {
|
|
168
|
+
return { ...mention };
|
|
169
|
+
}
|
|
170
|
+
function cloneImage(image) {
|
|
171
|
+
return { ...image };
|
|
172
|
+
}
|
|
173
|
+
function cloneFile(file) {
|
|
174
|
+
return { ...file };
|
|
175
|
+
}
|
|
176
|
+
function cloneRecord(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
177
|
+
if (!value || typeof value !== "object") return value;
|
|
178
|
+
if (seen.has(value)) return seen.get(value);
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
const clone2 = [];
|
|
181
|
+
seen.set(value, clone2);
|
|
182
|
+
value.forEach((item) => clone2.push(cloneRecord(item, seen)));
|
|
183
|
+
return clone2;
|
|
184
|
+
}
|
|
185
|
+
const prototype = Object.getPrototypeOf(value);
|
|
186
|
+
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
187
|
+
const clone = {};
|
|
188
|
+
seen.set(value, clone);
|
|
189
|
+
Object.entries(value).forEach(([key, item]) => {
|
|
190
|
+
clone[key] = cloneRecord(item, seen);
|
|
191
|
+
});
|
|
192
|
+
return clone;
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
export { cloneChatMessage, compareChatMessages, normalizeChatContentBlocks, normalizeChatMessage, normalizeChatMessages };
|
|
196
|
+
/*! End of @composy/chat-core | Powered by @composy/builder */
|
|
197
|
+
//# sourceMappingURL=message-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-schema.js","names":["clone"],"sources":["../../src/core/message-schema.ts"],"sourcesContent":["import type {\n ChatContentBlock,\n ChatMessage,\n ChatUser,\n FileAttachment,\n ImageAttachment,\n MentionItem,\n} from '../types'\nimport { CHAT_MESSAGE_SCHEMA_VERSION } from '../types'\n\n/**\n * 归一化消息并复制所有可变字段。\n *\n * 该函数是 transport、outbox、ChatManager 共享的边界:外部数据先经过\n * schema/version 校验,再进入内存状态,避免调用方继续修改内部消息。\n */\nexport function normalizeChatMessage(message: ChatMessage): ChatMessage {\n if (!message || typeof message !== 'object') {\n throw new TypeError('Chat message must be an object.')\n }\n if (typeof message.id !== 'string' || !message.id.trim()) {\n throw new TypeError('Chat message ID must be a non-empty string.')\n }\n if (!Number.isFinite(message.timestamp)) {\n throw new TypeError('Chat message timestamp must be finite.')\n }\n const schemaVersion = message.schemaVersion ?? CHAT_MESSAGE_SCHEMA_VERSION\n if (schemaVersion > CHAT_MESSAGE_SCHEMA_VERSION) {\n throw new Error(\n `Unsupported chat message schema version: ${schemaVersion}.`\n )\n }\n\n return cloneChatMessage({\n ...message,\n schemaVersion: CHAT_MESSAGE_SCHEMA_VERSION,\n ...(message.contentBlocks && {\n contentBlocks: normalizeChatContentBlocks(message.contentBlocks),\n }),\n })\n}\n\n/**\n * 对来自网络/持久化的 content block 做运行时白名单归一化。\n * 不认识的块优先使用 fallback 文本,否则丢弃,避免将任意对象带入渲染器。\n */\nexport function normalizeChatContentBlocks(\n blocks: unknown[]\n): ChatContentBlock[] {\n const normalized: ChatContentBlock[] = []\n for (const value of blocks) {\n const block = normalizeContentBlock(value)\n if (block) normalized.push(block)\n }\n return normalized\n}\n\n/** 归一化消息集合,并在进入状态层前拒绝重复的消息 ID/client ID。 */\nexport function normalizeChatMessages(messages: ChatMessage[]): ChatMessage[] {\n const result: ChatMessage[] = []\n const ids = new Set<string>()\n const clientIds = new Set<string>()\n for (const message of messages) {\n const normalized = normalizeChatMessage(message)\n if (ids.has(normalized.id)) {\n throw new Error(`Duplicate chat message ID: ${normalized.id}`)\n }\n if (\n normalized.clientMessageId &&\n clientIds.has(normalized.clientMessageId)\n ) {\n throw new Error(\n `Duplicate chat client message ID: ${normalized.clientMessageId}`\n )\n }\n ids.add(normalized.id)\n if (normalized.clientMessageId) clientIds.add(normalized.clientMessageId)\n result.push(normalized)\n }\n return result\n}\n\n/** 复制消息,用于事件 payload、outbox 与公开读取,避免可变引用泄漏。 */\nexport function cloneChatMessage(message: ChatMessage): ChatMessage {\n return {\n ...message,\n sender: cloneUser(message.sender),\n ...(message.image && { image: cloneImage(message.image) }),\n ...(message.file && { file: cloneFile(message.file) }),\n ...(message.mentions && {\n mentions: message.mentions.map(cloneMention),\n }),\n ...(message.contentBlocks && {\n contentBlocks: message.contentBlocks.map(cloneContentBlock),\n }),\n ...(message.customData && {\n customData: cloneRecord(message.customData),\n }),\n }\n}\n\n/** 根据服务端 sequence/timestamp 给出稳定排序值。 */\nexport function compareChatMessages(left: ChatMessage, right: ChatMessage) {\n if (\n Number.isFinite(left.sequence) &&\n Number.isFinite(right.sequence) &&\n left.sequence !== right.sequence\n ) {\n return (left.sequence as number) - (right.sequence as number)\n }\n if (left.timestamp !== right.timestamp)\n return left.timestamp - right.timestamp\n return left.id.localeCompare(right.id)\n}\n\nfunction cloneContentBlock(block: ChatContentBlock): ChatContentBlock {\n switch (block.type) {\n case 'text':\n return { type: 'text', text: block.text }\n case 'mention':\n return { type: 'mention', id: block.id, label: block.label }\n case 'link':\n return {\n type: 'link',\n url: block.url,\n ...(block.label && { label: block.label }),\n }\n case 'code':\n return {\n type: 'code',\n code: block.code,\n ...(block.language && { language: block.language }),\n }\n case 'image':\n return {\n type: 'image',\n src: block.src,\n ...(block.alt && { alt: block.alt }),\n ...(block.width !== undefined && { width: block.width }),\n ...(block.height !== undefined && { height: block.height }),\n }\n case 'file':\n return {\n type: 'file',\n url: block.url,\n name: block.name,\n ...(block.size !== undefined && { size: block.size }),\n ...(block.mimeType && { mimeType: block.mimeType }),\n }\n case 'break':\n return { type: 'break' }\n case 'custom':\n return {\n type: 'custom',\n kind: block.kind,\n data: cloneRecord(block.data),\n ...(block.fallback && { fallback: block.fallback }),\n }\n }\n}\n\nfunction normalizeContentBlock(value: unknown): ChatContentBlock | null {\n if (!isRecord(value) || typeof value.type !== 'string') return null\n switch (value.type) {\n case 'text':\n return typeof value.text === 'string'\n ? { type: 'text', text: value.text }\n : null\n case 'mention':\n return typeof value.id === 'string' && typeof value.label === 'string'\n ? { type: 'mention', id: value.id, label: value.label }\n : null\n case 'link':\n return typeof value.url === 'string'\n ? {\n type: 'link',\n url: value.url,\n ...(typeof value.label === 'string' && { label: value.label }),\n }\n : null\n case 'code':\n return typeof value.code === 'string'\n ? {\n type: 'code',\n code: value.code,\n ...(typeof value.language === 'string' && {\n language: value.language,\n }),\n }\n : null\n case 'image':\n return typeof value.src === 'string'\n ? {\n type: 'image',\n src: value.src,\n ...(typeof value.alt === 'string' && { alt: value.alt }),\n ...(isPositiveNumber(value.width) && { width: value.width }),\n ...(isPositiveNumber(value.height) && { height: value.height }),\n }\n : null\n case 'file':\n return typeof value.url === 'string' && typeof value.name === 'string'\n ? {\n type: 'file',\n url: value.url,\n name: value.name,\n ...(isPositiveNumber(value.size) && { size: value.size }),\n ...(typeof value.mimeType === 'string' && {\n mimeType: value.mimeType,\n }),\n }\n : null\n case 'break':\n return { type: 'break' }\n case 'custom':\n return typeof value.kind === 'string' && isRecord(value.data)\n ? {\n type: 'custom',\n kind: value.kind,\n data: cloneRecord(value.data),\n ...(typeof value.fallback === 'string' && {\n fallback: value.fallback,\n }),\n }\n : fallbackBlock(value)\n default:\n return fallbackBlock(value)\n }\n}\n\nfunction fallbackBlock(\n value: Record<string, unknown>\n): ChatContentBlock | null {\n return typeof value.fallback === 'string'\n ? { type: 'text', text: value.fallback }\n : null\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction isPositiveNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0\n}\n\nfunction cloneUser(user: ChatUser): ChatUser {\n return { ...user }\n}\n\nfunction cloneMention(mention: MentionItem): MentionItem {\n return { ...mention }\n}\n\nfunction cloneImage(image: ImageAttachment): ImageAttachment {\n return { ...image }\n}\n\nfunction cloneFile(file: FileAttachment): FileAttachment {\n return { ...file }\n}\n\nfunction cloneRecord<T>(value: T, seen = new WeakMap<object, unknown>()): T {\n if (!value || typeof value !== 'object') return value\n if (seen.has(value)) return seen.get(value) as T\n if (Array.isArray(value)) {\n const clone: unknown[] = []\n seen.set(value, clone)\n value.forEach(item => clone.push(cloneRecord(item, seen)))\n return clone as T\n }\n\n const prototype = Object.getPrototypeOf(value)\n if (prototype !== Object.prototype && prototype !== null) return value\n const clone: Record<string, unknown> = {}\n seen.set(value, clone)\n Object.entries(value).forEach(([key, item]) => {\n clone[key] = cloneRecord(item, seen)\n })\n return clone as T\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ChatMessage } from '../types';
|
|
2
|
+
/** 离线 outbox 中的一条待发送消息。 */
|
|
3
|
+
export interface ChatOutboxEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
message: ChatMessage;
|
|
6
|
+
enqueuedAt: number;
|
|
7
|
+
attempts: number;
|
|
8
|
+
state: 'pending' | 'failed';
|
|
9
|
+
lastError?: string;
|
|
10
|
+
}
|
|
11
|
+
/** outbox 持久化适配器;可接 IndexedDB、SQLite bridge 或业务缓存。 */
|
|
12
|
+
export interface ChatOutboxStorage {
|
|
13
|
+
load: () => ChatOutboxEntry[] | Promise<ChatOutboxEntry[]>;
|
|
14
|
+
save: (entries: ChatOutboxEntry[]) => void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** 内存适配器,适合默认运行和单元测试。 */
|
|
17
|
+
export declare class MemoryChatOutboxStorage implements ChatOutboxStorage {
|
|
18
|
+
private entries;
|
|
19
|
+
load(): ChatOutboxEntry[];
|
|
20
|
+
save(entries: ChatOutboxEntry[]): void;
|
|
21
|
+
}
|
|
22
|
+
export interface ChatOutboxOptions {
|
|
23
|
+
storage?: ChatOutboxStorage;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
}
|
|
26
|
+
export interface ChatOutboxFlushOptions {
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
maxAttempts?: number;
|
|
29
|
+
concurrency?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ChatOutboxFlushResult {
|
|
32
|
+
sent: string[];
|
|
33
|
+
failed: Array<{
|
|
34
|
+
id: string;
|
|
35
|
+
error: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
remaining: ChatOutboxEntry[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 幂等离线队列。
|
|
41
|
+
*
|
|
42
|
+
* 队列只负责保存和重试,不假设网络协议;发送函数由 ChatTransportController
|
|
43
|
+
* 或业务层注入。每条消息以 clientMessageId 优先作为幂等键,重复 enqueue 不会产生重复项。
|
|
44
|
+
*/
|
|
45
|
+
export declare class ChatOutbox {
|
|
46
|
+
private readonly storage;
|
|
47
|
+
private readonly now;
|
|
48
|
+
private entries;
|
|
49
|
+
private hydrated;
|
|
50
|
+
private writePromise;
|
|
51
|
+
constructor(options?: ChatOutboxOptions);
|
|
52
|
+
hydrate(): Promise<ChatOutboxEntry[]>;
|
|
53
|
+
list(): ChatOutboxEntry[];
|
|
54
|
+
enqueue(message: ChatMessage): Promise<ChatOutboxEntry>;
|
|
55
|
+
remove(id: string): Promise<boolean>;
|
|
56
|
+
retry(id?: string): Promise<void>;
|
|
57
|
+
clear(): Promise<void>;
|
|
58
|
+
flush(send: (message: ChatMessage, options: {
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
clientMessageId: string;
|
|
61
|
+
}) => Promise<unknown>, options?: ChatOutboxFlushOptions): Promise<ChatOutboxFlushResult>;
|
|
62
|
+
private persist;
|
|
63
|
+
}
|